| Server IP : 52.25.153.185 / Your IP : 216.73.217.131 Web Server : Apache System : Linux ip-172-26-6-158 5.10.0-35-cloud-amd64 #1 SMP Debian 5.10.237-1 (2025-05-19) x86_64 User : daemon ( 1) PHP Version : 8.1.10 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /bitnami/wordpress/wp-content/plugins/allaccessible/inc/ |
Upload File : |
<?php
/**
* WordPress All Posts / All Pages "Accessibility" column.
*
* @package AllAccessible
* @since 2.1.1
*/
if (!defined('ABSPATH')) {
exit;
}
final class AllAccessible_PostListColumn {
/**
* In-memory per-request map: permalink → bulk-row data.
*/
private static ?array $url_map = null;
/**
* Lazy backstop.
*/
private static bool $lazy_prefetch_attempted = false;
private static function lazy_prefetch_fallback() {
if (self::$lazy_prefetch_attempted) return;
self::$lazy_prefetch_attempted = true;
global $wp_query;
if (!$wp_query || empty($wp_query->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 '<span style="color:#999;">—</span>';
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(
'<span class="aacb-col-cell" data-post-id="%d" data-canonical="%s" data-post-status="%s" data-preview-url="%s">'
. '<span class="aacb-col-pending" title="%s" aria-label="%s">'
. '<span class="aacb-col-pending-dot" aria-hidden="true"></span>'
. '<span class="aacb-col-pending-text">%s</span>'
. '</span>'
. '</span>',
(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<!-- aacb-debug post_id=%d canonical=%s render=async-placeholder -->\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;
?>
<script>
(function() {
var cells = document.querySelectorAll('.aacb-col-cell[data-post-id]');
if (!cells.length) return;
var ids = [];
cells.forEach(function(c) {
var pid = parseInt(c.getAttribute('data-post-id'), 10);
if (pid > 0) ids.push(pid);
});
if (!ids.length) return;
var data = new FormData();
data.append('action', 'aacb_batch_scores');
data.append('_wpnonce', <?php echo wp_json_encode(wp_create_nonce('aacb_batch_scores')); ?>);
ids.forEach(function(id) { data.append('post_ids[]', String(id)); });
fetch(ajaxurl, { method: 'POST', body: data, credentials: 'same-origin' })
.then(function(r) { return r.json(); })
.then(function(resp) {
var rows = (resp && resp.success && resp.data && resp.data.rows) ? resp.data.rows : {};
cells.forEach(function(c) {
var pid = c.getAttribute('data-post-id');
var row = rows[pid];
c.innerHTML = renderCell(row, c);
});
})
.catch(function() {
});
function renderCell(row, cell) {
if (!row || row.score === null || row.score === undefined) {
var status = (cell && cell.getAttribute('data-post-status')) || '';
var nonPublic = ['draft','pending','auto-draft','future','private'];
if (nonPublic.indexOf(status) !== -1) {
// Draft-like: the anonymous crawler can't reach it.
var preview = (cell && cell.getAttribute('data-preview-url')) || '';
var label = '<?php echo esc_js(__('Will scan on publish', 'allaccessible')); ?>';
var tip = '<?php echo esc_js(__('Drafts are not reachable by the public crawler. Publish to scan automatically, or preview to scan now.', 'allaccessible')); ?>';
var html = '<span class="aacb-col-draft" title="' + tip + '">'
+ '<span class="aacb-col-draft-text" style="color:#6b7280;">' + label + '</span>'
+ '</span>';
if (preview) {
var link = '<?php echo esc_js(__('Preview & scan', 'allaccessible')); ?>';
html += '<br><a href="' + preview + '" target="_blank" rel="noopener" class="aacb-col-draft-link" style="font-size:11px;">' + link + ' ↗</a>';
}
return html;
}
return '<span class="aacb-col-pending" title="<?php echo esc_js(__('Awaiting scan — AllAccessible will populate this on the next crawl', 'allaccessible')); ?>">'
+ '<span class="aacb-col-pending-dot" aria-hidden="true"></span>'
+ '<span class="aacb-col-pending-text"><?php echo esc_js(__('Awaiting scan', 'allaccessible')); ?></span>'
+ '</span>';
}
var score = parseInt(row.score, 10);
var color = score > 85 ? '#16a34a' : (score > 70 ? '#f59e0b' : '#dc2626');
// Status surfaced via title only when stale/failed — fresh
// is the default expectation, no need to label it.
var status = (row.audit_status || '').toLowerCase();
var title = '';
if (status === 'stale') title = '<?php echo esc_js(__('Score is stale — page hasn\'t been scanned recently', 'allaccessible')); ?>';
else if (status === 'failed') title = '<?php echo esc_js(__('Last scan failed', 'allaccessible')); ?>';
else title = '<?php echo esc_js(__('Accessibility score', 'allaccessible')); ?>';
var html = '<span class="aacb-col-score" style="color:' + color + ';" title="' + title + '">' + score + '</span>'
+ '<span class="aacb-col-score-suffix">/100</span>';
// "↑ X potential" when manifest fixes are pending
// approval. Subtle, single-line, links the customer's
// attention to the Agentic Fixes queue.
var potential = row.potential !== null && row.potential !== undefined ? parseInt(row.potential, 10) : null;
if (potential !== null && potential > score) {
html += '<div class="aacb-col-potential" title="<?php echo esc_js(__('Approve pending fixes to reach this score', 'allaccessible')); ?>">'
+ '↑ ' + potential + '% potential'
+ '</div>';
}
if (row.last_scan) {
var ts = new Date(row.last_scan);
if (!isNaN(ts.getTime())) {
var diff = Math.floor((Date.now() - ts.getTime()) / 1000);
var when = diff < 60 ? 'just now'
: diff < 3600 ? Math.floor(diff/60) + 'm ago'
: diff < 86400 ? Math.floor(diff/3600) + 'h ago'
: Math.floor(diff/86400) + 'd ago';
html += '<div class="aacb-col-when" title="' + row.last_scan + '">' + when + '</div>';
}
}
return html;
}
})();
</script>
<?php
}
public static function print_styles() {
?>
<style>
.column-aacb_a11y { width: 110px; }
.aacb-col-cell { display: inline-block; line-height: 1.3; }
.aacb-col-score {
font-size: 16px;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.aacb-col-score-suffix {
font-size: 11px;
color: #6b7280;
margin-left: 1px;
}
.aacb-col-when {
margin-top: 2px;
font-size: 12px;
color: #6b7280;
}
.aacb-col-potential {
margin-top: 2px;
font-size: 11px;
color: #6366f1;
font-weight: 500;
}
.aacb-col-pending {
display: inline-flex;
align-items: center;
gap: 6px;
color: #6b7280;
font-size: 12px;
}
.aacb-col-pending-dot {
display: inline-block;
width: 10px;
height: 10px;
border: 2px solid #d1d5db;
border-top-color: #6366f1;
border-radius: 50%;
animation: aacb-spin 0.9s linear infinite;
}
@keyframes aacb-spin {
to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.aacb-col-pending-dot { animation: none; border-top-color: #d1d5db; }
}
</style>
<?php
}
/* ─── helpers ──────────────────────────────────────────────────── */
/**
* Tier gate.
*/
private static function is_eligible(): bool {
if (!get_option('aacb_accountID')) return false;
if (!class_exists('AllAccessible_ApiClient')) return false;
$client = AllAccessible_ApiClient::get_instance();
$tier = (string) $client->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);
}
}