true ), 'names' ); foreach ( $types as $type ) { add_filter( "manage_{$type}_posts_columns", array( $this, 'add_column' ) ); add_action( "manage_{$type}_posts_custom_column", array( $this, 'render_column' ), 10, 2 ); add_filter( "manage_edit-{$type}_sortable_columns", array( $this, 'make_sortable' ) ); } } /** * Inserts the "A11y" column right before the Date column when present. * * @param array $columns Existing columns. * @return array */ public function add_column( array $columns ): array { $new = array(); foreach ( $columns as $key => $label ) { if ( 'date' === $key ) { $new['acceguru_issues'] = esc_html__( 'A11y', 'acceguru' ); } $new[ $key ] = $label; } if ( ! isset( $new['acceguru_issues'] ) ) { $new['acceguru_issues'] = esc_html__( 'A11y', 'acceguru' ); } return $new; } /** * Renders the column cell for a single post. * * @param string $column Column key. * @param int $post_id Post ID. */ public function render_column( string $column, int $post_id ): void { if ( 'acceguru_issues' !== $column ) { return; } $summary = get_post_meta( $post_id, self::META_KEY, true ); if ( ! is_array( $summary ) ) { echo '-'; return; } if ( ! Acceguru_Scanner::summary_is_current( $summary ) ) { echo '↻'; return; } $counts = $this->visible_issue_counts( $post_id, $summary ); $total = $counts['total']; $by_impact = $counts['by_impact']; $worst = $this->worst_impact( $by_impact ); if ( 0 === $total ) { printf( '%s 0', esc_attr__( 'No automated violations found in last scan', 'acceguru' ), '✓' // ✓ ); return; } $tooltip_parts = array(); foreach ( array( 'critical', 'serious', 'moderate', 'minor' ) as $level ) { $count = isset( $by_impact[ $level ] ) ? (int) $by_impact[ $level ] : 0; if ( $count > 0 ) { $tooltip_parts[] = Acceguru_Admin::impact_label( $level ) . ': ' . $count; } } $view_url = (string) get_permalink( $post_id ); $href = '' !== $view_url ? add_query_arg( 'acceguru', 'open', $view_url ) : ''; printf( '%4$s', esc_attr( $worst ), '' !== $href ? ' href="' . esc_url( $href ) . '" target="_blank" rel="noopener noreferrer"' : '', ' title="' . esc_attr( implode( ' · ', $tooltip_parts ) ) . '"', esc_html( (string) (int) $total ) ); } /** * Counts issues that remain visible after Free and add-on filters. * * Falls back to the saved summary when an older scan has no detailed issue * list, so a page with legacy results is never shown as clean by mistake. * * @param int $post_id Post being rendered. * @param array $summary Saved scan summary. * @return array{total:int,by_impact:array} */ private function visible_issue_counts( int $post_id, array $summary ): array { $issues = get_post_meta( $post_id, '_acceguru_scan_results', true ); if ( ! is_array( $issues ) || empty( $issues ) ) { return array( 'total' => isset( $summary['total_issues'] ) ? (int) $summary['total_issues'] : 0, 'by_impact' => isset( $summary['by_impact'] ) && is_array( $summary['by_impact'] ) ? $summary['by_impact'] : array(), ); } $issues = Acceguru_Scanner::filter_issues( $issues, $post_id ); $issues = array_values( array_filter( $issues, static fn( array $issue ): bool => '' !== (string) ( $issue['rule_id'] ?? '' ) ) ); $by_impact = array( 'critical' => 0, 'serious' => 0, 'moderate' => 0, 'minor' => 0, ); foreach ( $issues as $issue ) { $impact = (string) ( $issue['impact'] ?? 'minor' ); if ( isset( $by_impact[ $impact ] ) ) { ++$by_impact[ $impact ]; } } return array( 'total' => count( $issues ), 'by_impact' => $by_impact, ); } /** * Marks the column sortable. * * @param array $columns Sortable columns. * @return array */ public function make_sortable( array $columns ): array { $columns['acceguru_issues'] = 'acceguru_issues'; return $columns; } /** * Orders the list table by the numeric issue count when the A11y column * header is clicked. * * @param WP_Query $query Query object. */ public function handle_sort( WP_Query $query ): void { if ( ! is_admin() || ! $query->is_main_query() ) { return; } if ( 'acceguru_issues' !== $query->get( 'orderby' ) ) { return; } // Order by the plain numeric count, never by the serialized summary: // sorting that blob as a string compares the digits as text, so a page // with 19 issues would land below one with 2. The OR/NOT EXISTS pair // keeps never-scanned posts in the list instead of filtering them out. $query->set( 'meta_query', array( 'relation' => 'OR', array( 'key' => self::COUNT_KEY, 'compare' => 'EXISTS', ), array( 'key' => self::COUNT_KEY, 'compare' => 'NOT EXISTS', ), ) ); $query->set( 'orderby', 'meta_value_num' ); } /** * Returns the worst impact level present in the per-impact breakdown. * * @param array $by_impact Counts per impact level. */ private function worst_impact( array $by_impact ): string { foreach ( array( 'critical', 'serious', 'moderate', 'minor' ) as $level ) { if ( ! empty( $by_impact[ $level ] ) ) { return $level; } } return 'minor'; } }