posts)) return; $ids = array(); $urls = array(); foreach ($wp_query->posts as $p) { if (!is_object($p) || !isset($p->ID)) continue; $canon = AllAccessible_UrlCanonicalizer::for_post((int) $p->ID); if ($canon === '') continue; $ids[] = (int) $p->ID; $urls[] = $canon; } if (empty($ids)) return; $client = AllAccessible_ApiClient::get_instance(); $bulk = $client->get_pages_audit_bulk(false, $ids, array(), $urls); if (self::$url_map === null) self::$url_map = array(); if (!is_wp_error($bulk) && is_array($bulk) && !empty($bulk['pages'])) { foreach ($bulk['pages'] as $row) { if (!empty($row['post_id'])) { self::$url_map['post:' . (int) $row['post_id']] = $row; } $url = isset($row['page_url']) ? (string) $row['page_url'] : ''; if ($url !== '') { self::$url_map[self::normalize_url($url)] = $row; } } } } /** * AJAX diagnosis handler */ public static function ajax_diagnose() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'forbidden'), 403); } check_ajax_referer('aacb_diag_lookup', '_wpnonce'); $post_id = isset($_POST['post_id']) ? (int) $_POST['post_id'] : 0; if ($post_id <= 0) { wp_send_json_error(array('message' => 'post_id required'), 400); } $post = get_post($post_id); $raw_permalink = $post ? (string) get_permalink($post_id) : ''; $canonical = $raw_permalink !== '' ? AllAccessible_UrlCanonicalizer::canonicalize($raw_permalink) : ''; $client = AllAccessible_ApiClient::get_instance(); $response = $client->get_pages_audit_bulk( true, // force refresh — diagnostics must bypass cache array($post_id), array(), $canonical !== '' ? array($canonical) : array() ); $row_match = null; if (!is_wp_error($response) && is_array($response) && !empty($response['pages'])) { foreach ($response['pages'] as $row) { $row_post_id = isset($row['post_id']) ? (int) $row['post_id'] : 0; $row_url = isset($row['page_url']) ? (string) $row['page_url'] : ''; $by_post_id = ($row_post_id === $post_id); $by_url = ($row_url !== '' && $row_url === $canonical); if ($by_post_id || $by_url) { $row_match = array( 'matched_by' => $by_post_id ? 'post_id' : 'canonical_url', 'row_post_id' => $row_post_id ?: null, 'row_page_url' => $row_url, 'row_score' => $row['accessibility_score'] ?? null, 'row_audit_status' => $row['audit_status'] ?? null, 'row_last_scan' => $row['last_scan'] ?? null, ); break; } } } wp_send_json_success(array( 'inputs' => array( 'post_id' => $post_id, 'post_status' => $post ? $post->post_status : 'not_found', 'post_type' => $post ? $post->post_type : null, 'raw_permalink' => $raw_permalink, 'canonical_url' => $canonical, 'site_url_option' => get_option('aacb_siteID'), 'account_id' => get_option('aacb_accountID'), ), 'api_request' => array( 'endpoint' => '/integrations/v1/audits/by-subdomain', 'post_ids' => array($post_id), 'page_urls' => $canonical !== '' ? array($canonical) : array(), ), 'api_response' => array( 'is_wp_error' => is_wp_error($response), 'error' => is_wp_error($response) ? $response->get_error_message() : null, 'pages_count' => (!is_wp_error($response) && is_array($response) && isset($response['pages'])) ? count($response['pages']) : 0, 'audited_count' => (!is_wp_error($response) && is_array($response)) ? ($response['audited_count'] ?? null) : null, 'pages_total' => (!is_wp_error($response) && is_array($response)) ? ($response['pages_count'] ?? null) : null, 'first_3_rows' => (!is_wp_error($response) && is_array($response) && !empty($response['pages'])) ? array_slice($response['pages'], 0, 3) : array(), ), 'lookup_result' => $row_match ?: array('matched_by' => 'none', 'reason' => 'no row returned for this post_id or URL'), 'expected_match_key' => 'post:' . $post_id . ' OR ' . $canonical, )); } public static function register() { foreach (array('post', 'page') as $type) { add_filter("manage_{$type}_posts_columns", array(__CLASS__, 'add_column')); add_action("manage_{$type}_posts_custom_column", array(__CLASS__, 'render_column'), 10, 2); } // Inline CSS for badge + spinner + cell wrapper. add_action('admin_head-edit.php', array(__CLASS__, 'print_styles')); // Async swap-in script — replaces server-rendered spinners with // real scores once the batch endpoint responds. Page paints // instantly; scores arrive shortly after. add_action('admin_footer-edit.php', array(__CLASS__, 'print_async_script')); // Batch endpoint backing the async swap. add_action('wp_ajax_aacb_batch_scores', array(__CLASS__, 'ajax_batch_scores')); // Diagnostic AJAX handler — Account → Advanced tab. add_action('wp_ajax_aacb_diag_lookup', array(__CLASS__, 'ajax_diagnose')); // Flush every plugin transient — same panel. add_action('wp_ajax_aacb_flush_caches', array(__CLASS__, 'ajax_flush_caches')); } /** * AJAX handler */ public static function ajax_flush_caches() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'forbidden'), 403); } check_ajax_referer('aacb_flush_caches', '_wpnonce'); AllAccessible_ApiClient::get_instance()->flush_all_caches(); wp_send_json_success(array('message' => 'caches flushed')); } /** * Hook into `the_posts` once per WP_Query on edit.php screens. * * @param array $posts Raw posts WP_Query just returned. * @param WP_Query $query The query instance. * @return array Posts unchanged. */ public static function prefetch_for_visible_posts($posts, $query) { if (!is_admin() || empty($posts)) return $posts; if (!$query || $query->get('post_type') === '' || !is_array($posts)) return $posts; global $pagenow; if ($pagenow !== 'edit.php') return $posts; if (!self::is_eligible()) return $posts; $ids = array(); $urls = array(); foreach ($posts as $p) { if (!is_object($p) || !isset($p->ID)) continue; $canon = AllAccessible_UrlCanonicalizer::for_post((int) $p->ID); if ($canon === '') continue; $ids[] = (int) $p->ID; $urls[] = $canon; } if (empty($ids)) return $posts; $client = AllAccessible_ApiClient::get_instance(); $bulk = $client->get_pages_audit_bulk(false, $ids, array(), $urls); if (self::$url_map === null) self::$url_map = array(); if (!is_wp_error($bulk) && is_array($bulk) && !empty($bulk['pages'])) { foreach ($bulk['pages'] as $row) { if (!empty($row['post_id'])) { self::$url_map['post:' . (int) $row['post_id']] = $row; } $url = isset($row['page_url']) ? (string) $row['page_url'] : ''; if ($url !== '') { self::$url_map[self::normalize_url($url)] = $row; } } } if (AllAccessible_Debug::is_enabled()) { self::$debug_payload = array( 'requested_ids' => $ids, 'requested_urls' => $urls, 'response_meta' => is_wp_error($bulk) ? array('error' => $bulk->get_error_message()) : array( 'pages_returned' => isset($bulk['pages']) ? count($bulk['pages']) : 0, 'audited_count' => $bulk['audited_count'] ?? null, 'pages_total' => $bulk['pages_count'] ?? null, ), 'map_keys' => array_keys(self::$url_map), ); add_action('admin_footer', array(__CLASS__, 'print_debug_console')); } return $posts; } /** * Per-request debug payload captured during prefetch. */ private static ?array $debug_payload = null; /** * Print a console.log dump */ public static function print_debug_console() { if (self::$debug_payload === null) return; $payload = self::$debug_payload; // Build per-row lookup result so user sees post → resolved row // (or "missed") inline with the request/response context. $row_lookups = array(); foreach ((array) $payload['requested_ids'] as $i => $pid) { $permalink = isset($payload['requested_urls'][$i]) ? $payload['requested_urls'][$i] : ''; $row = self::lookup_post((int) $pid); $row_lookups[] = array( 'post_id' => (int) $pid, 'canonical' => $permalink, 'matched' => $row !== null, 'matched_by' => $row !== null ? (isset(self::$url_map['post:' . (int) $pid]) ? 'post_id' : 'canonical_url') : null, 'score' => $row['accessibility_score'] ?? null, 'audit_status' => $row['audit_status'] ?? null, 'last_audit_id' => $row['last_audit_id'] ?? null, ); } $payload['row_lookups'] = $row_lookups; AllAccessible_Debug::console('PostListColumn', $payload); } public static function add_column(array $columns): array { // Skip the column on tiers that have no audit data to render. if (!self::is_eligible()) return $columns; // Insert before the date column for visual proximity with other // freshness signals. Falls back to appending if 'date' isn't set. if (!isset($columns['date'])) { $columns['aacb_a11y'] = __('Accessibility', 'allaccessible'); return $columns; } $out = array(); foreach ($columns as $k => $v) { if ($k === 'date') { $out['aacb_a11y'] = __('Accessibility', 'allaccessible'); } $out[$k] = $v; } return $out; } public static function render_column($column_name, $post_id) { if ($column_name !== 'aacb_a11y') return; if (!self::is_eligible()) { echo '—'; return; } $canonical = AllAccessible_UrlCanonicalizer::for_post((int) $post_id); $post = get_post((int) $post_id); $non_public_statuses = array('draft', 'pending', 'auto-draft', 'future', 'private'); $is_draft_like = $post && in_array($post->post_status, $non_public_statuses, true); $preview_url = ''; if ($is_draft_like && function_exists('get_preview_post_link')) { $preview_url = (string) get_preview_post_link($post); if ($preview_url !== '') { $preview_url = add_query_arg('aa_force_audit', 'draft_preview', $preview_url); } } printf( '' . '' . '' . '%s' . '' . '', (int) $post_id, esc_attr($canonical), esc_attr($post ? $post->post_status : ''), esc_attr($preview_url), esc_attr__('Loading accessibility score', 'allaccessible'), esc_attr__('Loading', 'allaccessible'), esc_html__('Loading…', 'allaccessible') ); $debug_opts = get_option('aacb_options', array()); if (!empty($debug_opts['debug_mode'])) { printf( "\n\n", (int) $post_id, esc_attr($canonical) ); } } /** * AJAX batch handler */ public static function ajax_batch_scores() { if (!current_user_can('edit_posts')) { wp_send_json_error(array('message' => 'forbidden'), 403); } check_ajax_referer('aacb_batch_scores', '_wpnonce'); $raw_ids = isset($_POST['post_ids']) ? (array) $_POST['post_ids'] : array(); $ids = array(); foreach ($raw_ids as $pid) { $n = (int) $pid; if ($n > 0) $ids[] = $n; } $ids = array_values(array_unique($ids)); if (count($ids) > 200) $ids = array_slice($ids, 0, 200); if (empty($ids)) { wp_send_json_success(array('rows' => new \stdClass())); } $urls = array(); foreach ($ids as $pid) { $canon = AllAccessible_UrlCanonicalizer::for_post($pid); if ($canon !== '') $urls[] = $canon; } $client = AllAccessible_ApiClient::get_instance(); $bulk = $client->get_pages_audit_bulk(false, $ids, array(), $urls); $out = array(); if (!is_wp_error($bulk) && is_array($bulk) && !empty($bulk['pages'])) { // Map by post_id first (preferred). Index by canonical URL too // so rows that came back URL-matched still resolve. $by_post = array(); $by_url = array(); foreach ($bulk['pages'] as $row) { if (!empty($row['post_id'])) $by_post[(int) $row['post_id']] = $row; if (!empty($row['page_url'])) $by_url[(string) $row['page_url']] = $row; } foreach ($ids as $pid) { $row = $by_post[$pid] ?? null; if ($row === null) { $canon = AllAccessible_UrlCanonicalizer::for_post($pid); if ($canon !== '' && isset($by_url[$canon])) $row = $by_url[$canon]; } if ($row !== null) { $breakdown = isset($row['score_breakdown']) && is_array($row['score_breakdown']) ? $row['score_breakdown'] : array(); $out[(string) $pid] = array( 'score' => $row['accessibility_score'] ?? null, 'audit_status' => $row['audit_status'] ?? null, 'last_scan' => $row['last_scan'] ?? null, 'audit_id' => $row['last_audit_id'] ?? null, // KAN-28: pass breakdown so JS can render // "+N potential" subline when applicable. 'potential' => $breakdown['potential'] ?? null, 'raw' => $breakdown['raw'] ?? null, 'pending_count' => isset($breakdown['manifest_pending']) ? (int) $breakdown['manifest_pending'] : 0, ); } } } wp_send_json_success(array('rows' => (object) $out)); } /** * Print the small JS that drives the async score-load on edit.php. */ public static function print_async_script() { $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!$screen || $screen->base !== 'edit') return; if (!self::is_eligible()) return; ?> get_subscription_tier(); if ($tier === 'free' || $tier === 'legacy') return false; $opts = $client->get_site_options(); if (is_wp_error($opts) || !is_object($opts)) return false; $features = isset($opts->features) && is_array($opts->features) ? $opts->features : array(); // image_alt_text_manager comes with the audit pipeline — using it // as a proxy for "this account has per-page audit data". return in_array('agentic_fixes_approve', $features, true) || in_array('image_alt_text_manager', $features, true); } /** * One-shot bulk fetch + permalink-keyed map. */ private static function lookup_post(int $post_id): ?array { if (self::$url_map === null) self::$url_map = array(); // Prefer indexed post_id lookup when available. $by_id = 'post:' . (int) $post_id; if (isset(self::$url_map[$by_id])) return self::$url_map[$by_id]; // URL fallback. $permalink = get_permalink($post_id); if (!$permalink) return null; $key = self::normalize_url($permalink); return self::$url_map[$key] ?? null; } /** * Thin wrapper — delegates to the central UrlCanonicalizer so every * plugin file produces identical keys for the same URL. */ private static function normalize_url(string $url): string { return AllAccessible_UrlCanonicalizer::canonicalize($url); } }