string, 'type' => string, 'label' => string ] */ public static function get_scannable_urls(): array { $urls = []; $seen = []; $add = static function ( string $url, string $type, string $label ) use ( &$urls, &$seen ): void { $url = trailingslashit( esc_url_raw( $url ) ); if ( empty( $url ) || isset( $seen[ $url ] ) ) return; $seen[ $url ] = true; $urls[] = compact( 'url', 'type', 'label' ); }; // Homepage $add( home_url( '/' ), 'home', 'Homepage' ); // Blog index (if different from homepage) $blog_page_id = (int) get_option( 'page_for_posts' ); if ( $blog_page_id ) { $add( get_permalink( $blog_page_id ), 'blog', 'Blog Index' ); } // Pages $pages = get_posts( [ 'post_type' => 'page', 'posts_per_page' => -1, 'post_status' => 'publish', 'orderby' => 'menu_order', 'order' => 'ASC', ] ); foreach ( $pages as $page ) { $add( (string) get_permalink( $page ), 'page', get_the_title( $page ) ); } // Posts $posts = get_posts( [ 'post_type' => 'post', 'posts_per_page' => self::MAX_POSTS_PER_TYPE, 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC', ] ); foreach ( $posts as $post ) { $add( (string) get_permalink( $post ), 'post', get_the_title( $post ) ); } // Category & tag archives $categories = get_categories( [ 'hide_empty' => true, 'number' => 20 ] ); foreach ( $categories as $cat ) { $url = get_category_link( $cat->term_id ); if ( $url ) $add( $url, 'category', 'Category: ' . $cat->name ); } // Custom post types $cpts = get_post_types( [ '_builtin' => false, 'public' => true ], 'objects' ); foreach ( $cpts as $cpt ) { if ( $cpt->has_archive ) { $archive_url = get_post_type_archive_link( $cpt->name ); if ( $archive_url ) { $add( $archive_url, 'archive', $cpt->label . ' Archive' ); } } $cpt_posts = get_posts( [ 'post_type' => $cpt->name, 'posts_per_page' => self::MAX_POSTS_PER_TYPE, 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC', ] ); foreach ( $cpt_posts as $cpt_post ) { $add( (string) get_permalink( $cpt_post ), $cpt->name, get_the_title( $cpt_post ) ); } } return $urls; } // ── Storage ─────────────────────────────────────────────── /** * Stores a browser-computed axe-core scan result for $url. * Routes to post meta or a dedicated non-autoloaded option depending on * whether $url maps to a known post/page/CPT. */ public static function store_axe_scan( string $url, array $result ): void { $url = trailingslashit( $url ); $stored = [ 'url' => $url, 'score' => $result['score'] ?? 0, 'grade' => $result['grade'] ?? 'F', 'failed' => $result['failed'] ?? 0, 'issues' => $result['issues'] ?? [], 'scanned_at' => $result['scanned_at'] ?? gmdate( 'c' ), ]; $post_id = url_to_postid( $url ); if ( $post_id > 0 ) { // Post / page / CPT — use post meta (indexed, auto-cleaned with post). update_post_meta( $post_id, self::META_KEY, $stored ); } else { // Non-post URL (homepage, archive, category …). $hash = md5( $url ); update_option( self::OPT_PREFIX . $hash, $stored, false ); // autoload = false // Register the hash in the index so get_page_scans() can find it. $index = get_option( self::OPT_INDEX, [] ); if ( ! in_array( $hash, $index, true ) ) { $index[] = $hash; update_option( self::OPT_INDEX, $index, false ); } } } /** * Returns the full map of per-URL scan summaries, keyed by URL. * Merges post-meta scans and non-post option scans efficiently. * * @return array */ public static function get_page_scans(): array { self::maybe_migrate_legacy(); $scans = []; // ── 1. Post-meta scans (single direct DB query, no full table scan) ── global $wpdb; $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->prepare( "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s", self::META_KEY ) ); foreach ( $rows as $row ) { $data = maybe_unserialize( $row->meta_value ); if ( is_array( $data ) && isset( $data['url'] ) ) { $scans[ $data['url'] ] = $data; } } // ── 2. Non-post scans (index → individual non-autoloaded options) ──── $index = get_option( self::OPT_INDEX, [] ); foreach ( $index as $hash ) { $data = get_option( self::OPT_PREFIX . $hash ); if ( is_array( $data ) && isset( $data['url'] ) ) { $scans[ $data['url'] ] = $data; } } return $scans; } /** * PHP-scanner path (legacy, non-axe): runs AccessMate_Scanner on $url and * stores a summary. Kept for compatibility but axe-core path is preferred. */ public static function scan_page( string $url ): array { $result = AccessMate_Scanner::run( $url ); self::store_axe_scan( $url, [ 'score' => $result['score'] ?? 0, 'grade' => $result['grade'] ?? 'F', 'failed' => $result['failed'] ?? 0, 'issues' => $result['issues'] ?? [], 'scanned_at' => gmdate( 'c' ), ] ); return $result; } // ── Migration ───────────────────────────────────────────── /** * One-time migration: reads the old single-blob `accessmate_page_scans` option * (if present) and redistributes each entry into the new storage layout. * Deletes the legacy option once done. */ private static function maybe_migrate_legacy(): void { $legacy = get_option( self::LEGACY_KEY ); if ( ! is_array( $legacy ) || empty( $legacy ) ) { return; // Already migrated or never existed. } foreach ( $legacy as $url => $data ) { if ( ! is_array( $data ) || empty( $url ) ) continue; self::store_axe_scan( (string) $url, $data ); } delete_option( self::LEGACY_KEY ); } }