[ label, capability ] */ public static function tabs() { $check = AriAccess_Page_Scan::cap(); $tabs = array( 'overview' => array( __( 'Dashboard', 'ariaccess' ), $check ), 'issues' => array( __( 'Issues', 'ariaccess' ), $check ), 'pages' => array( __( 'Page checks', 'ariaccess' ), $check ), 'alt' => array( __( 'Images', 'ariaccess' ), 'upload_files' ), 'fixes' => array( __( 'Fixes', 'ariaccess' ), self::cap() ), 'panel' => array( __( 'Display panel', 'ariaccess' ), self::cap() ), 'statement' => array( __( 'Statement', 'ariaccess' ), self::cap() ), 'settings' => array( __( 'Settings', 'ariaccess' ), self::cap() ), ); /** * Filter the admin tabs. AriAccess Pro adds its own here. * * @param array $tabs slug => [ label, capability ]. */ return apply_filters( 'ariaccess_admin_tabs', $tabs ); } /** * The tab being shown, with old addresses mapped to their new screens. * * @return string */ public static function current_tab() { $tabs = self::tabs(); $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'overview'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $old = array( 'content' => 'issues', 'tools' => 'settings', ); $tab = isset( $old[ $tab ] ) ? $old[ $tab ] : $tab; if ( ! isset( $tabs[ $tab ] ) || ! current_user_can( $tabs[ $tab ][1] ) ) { $tab = ''; foreach ( $tabs as $slug => $t ) { if ( current_user_can( $t[1] ) ) { $tab = $slug; break; } } } return $tab; } /** * Address of a screen. * * @param string $tab Tab slug. * @param array $args Extra query arguments. * @return string */ public static function url( $tab, $args = array() ) { return add_query_arg( array_merge( array( 'page' => self::SLUG, 'tab' => $tab ), $args ), admin_url( 'admin.php' ) ); } /** * Menu. */ public static function menu() { $must = AriAccess_Issues::badge(); /* translators: %s: number of must-fix problems. */ $label = sprintf( _n( '%s must-fix problem', '%s must-fix problems', $must, 'ariaccess' ), number_format_i18n( $must ) ); $badge = $must ? ' ' . esc_html( $label ) . '' : ''; $cap = current_user_can( AriAccess_Page_Scan::cap() ) ? AriAccess_Page_Scan::cap() : 'upload_files'; add_menu_page( 'AriAccess', 'AriAccess' . $badge, $cap, self::SLUG, array( __CLASS__, 'page' ), 'dashicons-universal-access-alt', 81 ); } /** * A page title per screen ("Issues ‹ AriAccess ‹ Site"). * * @param string $title Admin title. * @param string $page_title Page title. * @return string */ public static function admin_title( $title, $page_title ) { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || 'toplevel_page_' . self::SLUG !== $screen->id ) { return $title; } $tabs = self::tabs(); $tab = self::current_tab(); return isset( $tabs[ $tab ] ) ? $tabs[ $tab ][0] . ' ‹ ' . $title : $title; } /** * Settings link on the plugins screen. * * @param array $links Links. * @return array */ public static function action_links( $links ) { array_unshift( $links, '' . esc_html__( 'Open', 'ariaccess' ) . '' ); return $links; } /** * Send the admin to the overview once after activation. */ public static function welcome_redirect() { if ( ! get_transient( 'ariaccess_welcome' ) || wp_doing_ajax() || isset( $_GET['activate-multi'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } delete_transient( 'ariaccess_welcome' ); if ( current_user_can( self::cap() ) ) { wp_safe_redirect( admin_url( 'admin.php?page=' . self::SLUG . '&welcome=1' ) ); exit; } } /** * Assets. * * @param string $hook Screen hook. */ public static function assets( $hook ) { $screen = get_current_screen(); $is_page = 'toplevel_page_' . self::SLUG === $hook; $is_edit = $screen && 'post' === $screen->base && in_array( $screen->post_type, AriAccess_Checker::post_types(), true ); $is_list = $screen && in_array( $screen->base, array( 'edit', 'upload' ), true ); if ( ! $is_page && ! $is_edit && ! $is_list ) { return; } wp_enqueue_style( 'ariaccess-admin', ARIACCESS_URL . 'assets/css/admin.css', array(), ARIACCESS_VERSION ); if ( $is_page || $is_edit ) { wp_enqueue_script( 'ariaccess-admin', ARIACCESS_URL . 'assets/js/admin.js', array(), ARIACCESS_VERSION, true ); wp_add_inline_script( 'ariaccess-admin', 'window.ariaccessAdmin=' . wp_json_encode( array( 'ajax' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'ariaccess' ), 'done' => self::url( self::current_tab() ? self::current_tab() : 'overview', array( 'checked' => 1 ) ), 'pagesDone' => self::url( 'pages' ), 'i18n' => array( /* translators: 1: done, 2: total. */ 'progress' => __( 'Checked %1$d of %2$d…', 'ariaccess' ), 'done' => __( 'Done. Loading the results…', 'ariaccess' ), 'error' => __( 'Something went wrong. Try again.', 'ariaccess' ), 'checking' => __( 'Checking…', 'ariaccess' ), 'pass' => __( 'Pass', 'ariaccess' ), 'fail' => __( 'Fail', 'ariaccess' ), 'dismissed' => __( 'Dismissed.', 'ariaccess' ), 'restored' => __( 'Restored. It is back in the open list.', 'ariaccess' ), 'undo' => __( 'Undo', 'ariaccess' ), 'saved' => __( 'Saved.', 'ariaccess' ), /* translators: 1: page number, 2: total, 3: page title. */ 'pageProgress' => __( 'Checking page %1$d of %2$d: %3$s', 'ariaccess' ), 'pageTimeout' => __( 'This page took too long and was skipped.', 'ariaccess' ), ), ) ) . ';', 'before' ); } } /* ===================================================================== * Page shell * ===================================================================== */ /** * Render the admin page. */ public static function page() { $tabs = self::tabs(); $tab = self::current_tab(); ?>

AriAccess:

' . esc_html( $msg ) . '

'; } } /** * Start of a settings form. * * @param string $area Area being saved. */ private static function form_open( $area ) { echo '
'; echo ''; echo ''; wp_nonce_field( 'ariaccess_save_' . $area ); } /** * Checkbox row. * * @param string $name Field name. * @param bool $value Checked. * @param string $label Label. * @param string $help Help text. */ private static function checkbox( $name, $value, $label, $help = '' ) { $id = 'ariaccess-' . sanitize_html_class( str_replace( array( '[', ']' ), '-', $name ) ); echo '
'; if ( $help ) { echo '

' . wp_kses( $help, array( 'code' => array() ) ) . '

'; } echo '
'; } /** * Severity badge. * * @param string $severity must|review|hand. * @return string */ private static function sev( $severity ) { $labels = AriAccess_Issues::severities(); return '' . esc_html( $labels[ $severity ] ) . ''; } /** * Type badge for content rule types (used by the editor box). * * @param string $type error|warning|notice. * @return string */ private static function badge( $type ) { return self::sev( AriAccess_Issues::from_type( $type ) ); } /* ===================================================================== * Dashboard * ===================================================================== */ /** * Where the site stands on each step of the work, and which step is next. * * @return array[] step => { done, label, detail, action, url, button } */ public static function steps() { $sum = AriAccess_Checker::summary(); $c = AriAccess_Issues::counts(); $scans = AriAccess_Page_Scan::all(); $hand = get_option( self::HANDTEST, array() ); $labels = AriAccess_Rules::standards(); $standard = AriAccess_Settings::get( 'checker', 'standard' ); $top = AriAccess_Issues::top_pages( 1 ); $steps = array( 'standard' => array( 'done' => true, 'label' => __( 'Choose what to measure against', 'ariaccess' ), 'detail' => $labels[ $standard ], 'url' => self::url( 'settings' ), 'button' => __( 'Change', 'ariaccess' ), ), 'content' => array( 'done' => $sum['scanned'] >= $sum['posts'], 'label' => __( 'Check all your content', 'ariaccess' ), /* translators: 1: checked, 2: total. */ 'detail' => sprintf( __( '%1$s of %2$s published items checked.', 'ariaccess' ), number_format_i18n( $sum['scanned'] ), number_format_i18n( $sum['posts'] ) ), 'scan' => true, 'button' => $sum['scanned'] ? __( 'Check all content again', 'ariaccess' ) : __( 'Check all content', 'ariaccess' ), ), 'pages' => array( 'done' => count( $scans ) > 0, 'label' => __( 'Check your key pages', 'ariaccess' ), 'detail' => $scans /* translators: %s: number of pages. */ ? sprintf( _n( '%s page checked. This finds problems in your theme, menus and colours.', '%s pages checked. This finds problems in your theme, menus and colours.', count( $scans ), 'ariaccess' ), number_format_i18n( count( $scans ) ) ) : __( 'Finds problems in your theme, menus, header, footer and colours that a content check cannot see.', 'ariaccess' ), 'url' => self::url( 'pages' ), 'button' => __( 'Check key pages', 'ariaccess' ), ), 'fix' => array( 'done' => ( $sum['scanned'] || $scans ) && 0 === $c['open']['must'], 'label' => __( 'Fix the must-fix problems', 'ariaccess' ), 'detail' => $c['open']['must'] /* translators: 1: problems, 2: pages, 3: page title. */ ? sprintf( _n( '%1$s problem on %2$s page. Start with "%3$s".', '%1$s problems on %2$s pages. Start with "%3$s".', $c['pages_to_fix'], 'ariaccess' ), number_format_i18n( $c['open']['must'] ), number_format_i18n( $c['pages_to_fix'] ), $top ? $top[0]['page'] : '' ) : __( 'None open.', 'ariaccess' ), 'url' => self::url( 'issues', array( 'severity' => 'must', 'group' => 'page', ) ), 'button' => __( 'See must-fix problems', 'ariaccess' ), ), 'hand' => array( 'done' => ! empty( $hand['time'] ), 'label' => __( 'Test by hand with a keyboard and a screen reader', 'ariaccess' ), 'detail' => ! empty( $hand['time'] ) /* translators: %s: date. */ ? sprintf( __( 'Last done %s.', 'ariaccess' ), wp_date( get_option( 'date_format' ), (int) $hand['time'] ) ) : __( 'Automated checks find roughly a third of accessibility problems. Five short tests find much of the rest.', 'ariaccess' ), 'url' => self::url( 'overview' ) . '#ariaccess-handtest', 'button' => __( 'Show the five tests', 'ariaccess' ), ), 'statement' => array( 'done' => '' !== AriAccess_Statement::public_url(), 'label' => __( 'Publish your accessibility statement', 'ariaccess' ), 'detail' => '' !== AriAccess_Statement::public_url() ? __( 'Published.', 'ariaccess' ) : __( 'Tells visitors what works, what does not yet, and how to reach you.', 'ariaccess' ), 'url' => self::url( 'statement' ), 'button' => __( 'Write the statement', 'ariaccess' ), ), ); /** * Filter the dashboard steps. * * @param array $steps Steps. */ return apply_filters( 'ariaccess_steps', $steps ); } /** * Button or link for a step. * * @param array $step Step. * @param bool $primary Primary button. */ private static function step_action( $step, $primary ) { $class = 'button' . ( $primary ? ' button-primary button-hero' : '' ); if ( ! empty( $step['scan'] ) ) { echo ''; } else { echo '' . esc_html( $step['button'] ) . ''; } } /** * Dashboard tab. */ private static function tab_overview() { $steps = self::steps(); $c = AriAccess_Issues::counts(); $next = ''; foreach ( $steps as $key => $step ) { if ( ! $step['done'] ) { $next = $key; break; } } $sev = AriAccess_Issues::severities(); if ( isset( $_GET['welcome'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended echo '

' . esc_html__( 'Welcome to AriAccess', 'ariaccess' ) . '

' . esc_html__( 'Work down the steps below. Each one says what to do and why; the next step is always at the top.', 'ariaccess' ) . '

'; } ?>

    $step ) : ?>

' . esc_html__( 'No page has open must-fix problems.', 'ariaccess' ) . '

'; } else { echo ''; foreach ( $top as $p ) { echo ''; } echo '
' . esc_html__( 'Page', 'ariaccess' ) . '' . esc_html( $sev['must'] ) . '' . esc_html( $sev['review'] ) . '
' . esc_html( $p['page'] ) . '' . esc_html( number_format_i18n( $p['must'] ) ) . '' . esc_html( number_format_i18n( $p['review'] ) ) . '
'; } self::trend(); ?>

' . esc_html__( 'Must-fix problems over time', 'ariaccess' ) . ''; /* translators: 1: number then, 2: date then, 3: number now. */ echo '

' . esc_html( sprintf( __( '%1$s on %2$s, %3$s now.', 'ariaccess' ), number_format_i18n( $first['must'] ), wp_date( get_option( 'date_format' ), strtotime( $first['day'] ) ), number_format_i18n( $last['must'] ) ) ) . '

'; echo ''; echo '
' . esc_html__( 'Show as a table', 'ariaccess' ) . ''; foreach ( array_reverse( $h ) as $e ) { echo ''; } echo '
' . esc_html__( 'Day', 'ariaccess' ) . '' . esc_html__( 'Must fix', 'ariaccess' ) . '' . esc_html__( 'Should review', 'ariaccess' ) . '
' . esc_html( wp_date( get_option( 'date_format' ), strtotime( $e['day'] ) ) ) . '' . esc_html( number_format_i18n( $e['must'] ) ) . '' . esc_html( number_format_i18n( $e['review'] ) ) . '
'; } /* ===================================================================== * Issues * ===================================================================== */ /** * Current issue filters from the address. * * @return array */ private static function issue_args() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only filters. $get = function ( $k ) { return isset( $_GET[ $k ] ) ? sanitize_text_field( wp_unslash( $_GET[ $k ] ) ) : ''; }; $args = array( 'status' => 'dismissed' === $get( 'status' ) ? 'dismissed' : 'open', 'severity' => array_key_exists( $get( 'severity' ), AriAccess_Issues::severities() ) ? $get( 'severity' ) : '', 'source' => in_array( $get( 'source' ), array( 'content', 'page' ), true ) ? $get( 'source' ) : '', 'rule' => preg_match( '/^(axe:)?[a-z0-9_-]+$/', $get( 'rule' ) ) ? $get( 'rule' ) : '', 'search' => mb_substr( $get( 's' ), 0, 100 ), 'group' => 'page' === $get( 'group' ) ? 'page' : 'problem', 'pagekey' => preg_match( '/^(p\d+|u[a-f0-9]{32})$/', $get( 'pagekey' ) ) ? $get( 'pagekey' ) : '', 'page' => max( 1, (int) $get( 'paged' ) ), 'per_page' => self::PER_PAGE, ); // phpcs:enable if ( $args['pagekey'] ) { $args['group'] = 'page'; } return $args; } /** * Issues tab: every open problem from the content check and the page check, in one list. */ private static function tab_issues() { $args = self::issue_args(); $c = AriAccess_Issues::counts(); $sum = AriAccess_Checker::summary(); $query = AriAccess_Issues::query( $args ); $keep = array_filter( array( 'severity' => $args['severity'], 'source' => $args['source'], 's' => $args['search'], 'group' => 'page' === $args['group'] ? 'page' : '', ) ); $sev = AriAccess_Issues::severities(); ?>

' . esc_html__( 'Nothing checked yet. Press "Check all content again" above, then check your key pages.', 'ariaccess' ) . '

'; return; } $items = $query['items']; echo '

'; if ( 'page' === $args['group'] ) { /* translators: 1: pages, 2: problems. */ echo esc_html( sprintf( _n( '%1$s page, %2$s problems', '%1$s pages, %2$s problems', $query['total'], 'ariaccess' ), number_format_i18n( $query['total'] ), number_format_i18n( $query['rows'] ) ) ); } else { /* translators: 1: kinds of problems, 2: problems. */ echo esc_html( sprintf( _n( '%1$s kind of problem, %2$s in all', '%1$s kinds of problems, %2$s in all', $query['total'], 'ariaccess' ), number_format_i18n( $query['total'] ), number_format_i18n( $query['rows'] ) ) ); } echo '

'; if ( ! $items ) { echo '

' . ( 'dismissed' === $args['status'] ? esc_html__( 'Nothing dismissed.', 'ariaccess' ) : esc_html__( 'No open problems match. Automated checks find only part of the problems: also test by hand.', 'ariaccess' ) ) . '

'; return; } echo '
'; foreach ( $items as $i => $group ) { self::issue_group( $group, $args, $args['pagekey'] || ( 1 === count( $items ) ) ); } echo '
'; if ( $query['pages'] > 1 && ! $args['pagekey'] ) { echo ''; } } /** * One group (a problem with its pages, or a page with its problems). * * @param array $g Group. * @param array $args Filters. * @param bool $open Open the details. */ private static function issue_group( $g, $args, $open ) { $by_page = 'page' === $args['group']; $first = $g['rows'][0]; ?>
>

self::ROWS_SHOWN ) { /* translators: 1: shown, 2: total. */ echo '

' . esc_html( sprintf( __( 'Showing %1$s of %2$s. Search for a page, or group by page, to see the rest.', 'ariaccess' ), number_format_i18n( self::ROWS_SHOWN ), number_format_i18n( count( $g['rows'] ) ) ) ) . '

'; } ?>
' . esc_html__( 'Why it matters:', 'ariaccess' ) . ' ' . esc_html( $r['why'] ) . '

'; } if ( '' !== $r['fix'] ) { echo '

' . esc_html__( 'How to fix:', 'ariaccess' ) . ' ' . esc_html( $r['fix'] ) . '

'; } $bits = array(); foreach ( $r['wcag'] as $sc ) { $bits[] = 'WCAG ' . esc_html( $sc ) . ' ' . esc_html__( '(opens in a new tab)', 'ariaccess' ) . ''; } if ( '' !== $r['help'] ) { $bits[] = '' . esc_html__( 'More about this problem', 'ariaccess' ) . ' ' . esc_html__( '(opens in a new tab)', 'ariaccess' ) . ''; } $req = $r['wcag'] ? ( $r['required'] ? __( 'Required by the standard you measure against.', 'ariaccess' ) : __( 'Good practice (not required by the standard you measure against).', 'ariaccess' ) ) : __( 'Good practice.', 'ariaccess' ); echo '

' . implode( ' · ', $bits ) . ( $bits ? ' · ' : '' ) . esc_html( $req ) . ' ' . esc_html( 'page' === $r['source'] ? __( 'Found by the page check.', 'ariaccess' ) : __( 'Found by the content check.', 'ariaccess' ) ) . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Public address of a row's page. * * @param array $r Row. * @return string */ private static function row_url( $r ) { return '' !== $r['url'] ? $r['url'] : (string) get_permalink( $r['post_id'] ); } /** * One row: a problem on a page. * * @param array $r Row. * @param bool $by_page Grouped by page (first column is the problem). * @param array $args Filters. */ private static function issue_row( $r, $by_page, $args ) { $id = 'ariaccess-r-' . md5( $r['key'] ); echo ''; if ( $by_page ) { echo self::sev( $r['severity'] ) . ' ' . esc_html( $r['problem'] ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '
' . esc_html__( 'Why and how to fix', 'ariaccess' ) . ''; self::explain( $r ); echo '
'; } else { echo '' . esc_html( $r['page'] ) . ''; if ( 'page' === $r['source'] ) { echo ' ' . esc_html__( 'page check', 'ariaccess' ) . ''; } } echo ''; /* translators: %s: count. */ echo '' . esc_html( sprintf( _n( '%s place', '%s places', $r['count'], 'ariaccess' ), number_format_i18n( $r['count'] ) ) ) . ' '; foreach ( $r['samples'] as $sample ) { echo '' . esc_html( $sample ) . ' '; } echo ''; $title = $by_page ? $r['problem'] : $r['page']; if ( null !== $r['dismissed'] ) { $reasons = AriAccess_Issues::reasons(); $who = $r['dismissed']['user'] ? get_userdata( (int) $r['dismissed']['user'] ) : false; echo '

' . esc_html( $reasons[ $r['dismissed']['reason'] ] ?? '' ); if ( $r['dismissed']['time'] ) { /* translators: 1: person, 2: date. */ echo '
' . esc_html( sprintf( __( '%1$s, %2$s', 'ariaccess' ), $who ? $who->display_name : __( 'Unknown', 'ariaccess' ), wp_date( get_option( 'date_format' ), (int) $r['dismissed']['time'] ) ) ) . ''; } echo '

'; self::dismiss_form( $r, true, $title ); } else { if ( $r['post_id'] && current_user_can( 'edit_post', $r['post_id'] ) ) { echo '' . esc_html__( 'Edit', 'ariaccess' ) . ' ' . esc_html( $r['page'] ) . ' · '; } echo '' . esc_html__( 'Show on page', 'ariaccess' ) . ' ' . esc_html( $r['page'] ) . ''; self::dismiss_form( $r, false, $title ); } echo ''; } /** * Dismiss (with a reason) or restore form. Works without JavaScript; admin.js sends it in the background. * * @param array $r Row. * @param bool $restore Restore form. * @param string $title What the row is about, for screen readers. */ private static function dismiss_form( $r, $restore, $title ) { if ( 'content' === $r['source'] && ! current_user_can( 'edit_post', $r['post_id'] ) ) { return; } $fid = 'ariaccess-d-' . md5( $r['key'] ); if ( $restore ) { echo '
'; } else { echo '
' . esc_html__( 'Dismiss…', 'ariaccess' ) . ' ' . esc_html( $title ) . ''; echo ''; } echo ''; echo ''; wp_nonce_field( 'ariaccess_save_dismiss', '_wpnonce', false ); if ( $restore ) { echo ''; return; } echo '
'; } /* ===================================================================== * Page checks * ===================================================================== */ /** * Pages worth a page check: home, pages in the menus, the newest post, the statement. At most 12. * * @return array url => title */ public static function key_pages() { $pages = array( home_url( '/' ) => __( 'Home page', 'ariaccess' ) ); foreach ( get_nav_menu_locations() as $menu_id ) { foreach ( (array) wp_get_nav_menu_items( $menu_id ) as $item ) { if ( isset( $item->url ) && 0 === strpos( (string) $item->url, home_url() ) ) { $pages[ $item->url ] = $item->title; } } } if ( count( $pages ) < 6 ) { foreach ( get_posts( array( 'post_type' => 'page', 'posts_per_page' => 6, 'orderby' => 'menu_order', 'order' => 'ASC', ) ) as $p ) { $pages[ (string) get_permalink( $p ) ] = get_the_title( $p ); } } $latest = get_posts( array( 'posts_per_page' => 1 ) ); if ( $latest ) { /* translators: %s: post title. */ $pages[ (string) get_permalink( $latest[0] ) ] = sprintf( __( 'Newest post: %s', 'ariaccess' ), get_the_title( $latest[0] ) ); } $statement = AriAccess_Statement::get(); if ( $statement['page_id'] && 'publish' === get_post_status( $statement['page_id'] ) ) { $pages[ (string) get_permalink( $statement['page_id'] ) ] = __( 'Accessibility statement', 'ariaccess' ); } /** * Filter the pages "Check key pages" checks. * * @param array $pages url => title. */ $pages = apply_filters( 'ariaccess_key_pages', $pages ); $out = array(); $seen = array(); foreach ( $pages as $url => $title ) { $url = (string) strtok( (string) $url, '#' ); $norm = untrailingslashit( $url ); if ( '' !== $url && ! isset( $seen[ $norm ] ) ) { $seen[ $norm ] = true; $out[ $url ] = wp_strip_all_tags( (string) $title ); } } return array_slice( $out, 0, 12, true ); } /** * Page checks tab. */ private static function tab_pages() { $scans = AriAccess_Page_Scan::all(); $keys = self::key_pages(); $sev = AriAccess_Issues::severities(); ?>

' . esc_html__( 'No pages checked yet.', 'ariaccess' ) . '

'; return; } // Count from the same rows as the Issues screen, so "See problems" shows exactly these numbers. $per_page = array(); foreach ( AriAccess_Issues::all_rows() as $r ) { if ( null === $r['dismissed'] ) { if ( ! isset( $per_page[ $r['page_key'] ] ) ) { $per_page[ $r['page_key'] ] = array_fill_keys( array_keys( $sev ), 0 ); } $per_page[ $r['page_key'] ][ $r['severity'] ]++; } } ?>

$scan ) : $post_id = AriAccess_Page_Scan::post_of( $scan ); $pagekey = $post_id ? 'p' . $post_id : 'u' . $hash; $n = isset( $per_page[ $pagekey ] ) ? $per_page[ $pagekey ] : array_fill_keys( array_keys( $sev ), 0 ); $title = '' !== $scan['title'] ? $scan['title'] : $scan['url']; ?>
·
$per, 'paged' => $paged, ) ) ); $images = $q->posts; $total = (int) $q->found_posts; } else { $all = self::filename_alts(); $total = count( $all ); $images = array_map( 'get_post', array_slice( $all, ( $paged - 1 ) * $per, $per ) ); } $missing = AriAccess_Media::missing_count(); $bad = count( self::filename_alts() ); ?>

' . ( 'missing' === $view ? esc_html__( 'Every image in the Media Library has alt text.', 'ariaccess' ) : esc_html__( 'No alt text looks like a file name.', 'ariaccess' ) ) . '

'; return; } self::form_open( 'alt' ); ?>

ID ) ); $parent = $img->post_parent ? get_the_title( $img->post_parent ) : ''; $alt = (string) get_post_meta( $img->ID, '_wp_attachment_image_alt', true ); ?>
ID, array( 96, 96 ), false, array( 'alt' => '' ) ); ?>

1 ) { echo ''; } } /** * Images whose alt text looks like a file name (newest first, at most 2,000 looked at). * * @return int[] */ private static function filename_alts() { static $ids = null; if ( null !== $ids ) { return $ids; } global $wpdb; $rows = $wpdb->get_results( "SELECT p.ID, m.meta_value AS alt, f.meta_value AS file FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} m ON m.post_id = p.ID AND m.meta_key = '_wp_attachment_image_alt' LEFT JOIN {$wpdb->postmeta} f ON f.post_id = p.ID AND f.meta_key = '_wp_attached_file' WHERE p.post_type = 'attachment' AND p.post_mime_type LIKE 'image/%' AND m.meta_value <> '' ORDER BY p.ID DESC LIMIT 2000" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $ids = array(); foreach ( (array) $rows as $r ) { if ( self::alt_looks_bad( $r->alt, (string) $r->file ) ) { $ids[] = (int) $r->ID; } } return $ids; } /** * Fixes tab. */ private static function tab_fixes() { $f = AriAccess_Settings::get( 'fixes' ); self::form_open( 'fixes' ); ?>

maximum-scale and user-scalable=no from the viewport (WCAG 1.4.4).', 'ariaccess' ) ); ?>

__( 'Tell screen reader users ("opens in a new tab")', 'ariaccess' ), 'remove' => __( 'Open them in the same tab instead', 'ariaccess' ), 'off' => __( 'Leave them alone', 'ariaccess' ), ); foreach ( $choices as $val => $label ) { echo '
'; } self::checkbox( 'fixes[read_more]', $f['read_more'], __( 'Add the post title to "Read more" links for screen readers', 'ariaccess' ), __( 'So a list of links reads "Read more: Opening hours in summer", not "Read more" ten times (WCAG 2.4.4).', 'ariaccess' ) ); self::checkbox( 'fixes[strip_titles]', $f['strip_titles'], __( 'Remove redundant title attributes', 'ariaccess' ), __( 'Image titles and link titles that repeat the link text are read twice by screen readers.', 'ariaccess' ) ); self::checkbox( 'fixes[underline_links]', $f['underline_links'], __( 'Underline links in body text', 'ariaccess' ), __( 'Helps when your theme shows links by colour only (WCAG 1.4.1).', 'ariaccess' ) ); ?>

$m ) { echo '
'; } ?>

' . esc_html__( 'Statement page:', 'ariaccess' ) . ' ' . esc_html__( 'Edit', 'ariaccess' ) . ' · ' . esc_html__( 'View', 'ariaccess' ) . ' (' . esc_html( (string) get_post_status( $s['page_id'] ) ) . ')

'; echo '

' . esc_html__( 'Updating the page replaces its text with a fresh version from this form.', 'ariaccess' ) . '

'; } ?>
true ), 'objects' ); self::form_open( 'settings' ); ?>
$label ) : ?>
>

name ) { continue; } echo '
name, $c['post_types'], true ), true, false ) . '>
'; } ?>

7.00:1

$alts[ $one ] ) : array(); } AriAccess_Media::save_alts( $alts ); break; case 'statement': self::must( self::cap() ); $in = isset( $_POST['st'] ) ? wp_unslash( (array) $_POST['st'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitised in save(). $in['standard'] = AriAccess_Settings::get( 'checker', 'standard' ); $action = isset( $_POST['statement_action'] ) ? sanitize_key( wp_unslash( $_POST['statement_action'] ) ) : 'save'; if ( 'prefill' === $action ) { $have = array_filter( array_map( 'trim', explode( "\n", (string) ( $in['known_issues'] ?? '' ) ) ) ); $in['known_issues'] = implode( "\n", array_values( array_unique( array_merge( $have, AriAccess_Issues::statement_lines() ) ) ) ); $flag = 'prefilled'; } AriAccess_Statement::save( $in ); if ( 'draft' === $action || 'publish' === $action ) { AriAccess_Statement::write_page( 'publish' === $action ); } break; case 'import': self::must( self::cap() ); $data = json_decode( isset( $_POST['import'] ) ? wp_unslash( $_POST['import'] ) : '', true ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated in save_all(). if ( ! is_array( $data ) ) { wp_safe_redirect( self::url( 'settings', array( 'import_error' => 1 ) ) ); exit; } AriAccess_Settings::save_all( $data ); AriAccess_Issues::changed(); $tab = 'settings'; break; case 'handtest': self::must( self::cap() ); update_option( self::HANDTEST, array( 'time' => time(), 'user' => get_current_user_id(), ), false ); $tab = 'overview'; break; case 'dismiss': self::must( AriAccess_Page_Scan::cap() ); $key = isset( $_POST['key'] ) ? sanitize_text_field( wp_unslash( $_POST['key'] ) ) : ''; $restore = ! empty( $_POST['restore'] ); $res = AriAccess_Issues::set_dismissed( $key, ! $restore, isset( $_POST['reason'] ) ? sanitize_key( wp_unslash( $_POST['reason'] ) ) : '' ); if ( is_wp_error( $res ) ) { wp_die( esc_html( $res->get_error_message() ), 403 ); } $back = isset( $_POST['back'] ) ? esc_url_raw( wp_unslash( $_POST['back'] ) ) : ''; $back = wp_validate_redirect( $back, self::url( 'issues' ) ); wp_safe_redirect( add_query_arg( $restore ? 'restored' : 'dismissed', 1, $back ) ); exit; case 'settings': self::must( self::cap() ); AriAccess_Settings::save_area( 'checker', isset( $_POST['checker'] ) ? wp_unslash( (array) $_POST['checker'] ) : array() ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitised in save_area(). AriAccess_Settings::save_area( 'general', isset( $_POST['general'] ) ? wp_unslash( (array) $_POST['general'] ) : array() ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitised in save_area(). AriAccess_Issues::changed(); break; case 'fixes': case 'toolbar': self::must( self::cap() ); AriAccess_Settings::save_area( $area, isset( $_POST[ $area ] ) ? wp_unslash( (array) $_POST[ $area ] ) : array() ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitised in save_area(). $tab = 'toolbar' === $area ? 'panel' : $area; break; default: wp_die( esc_html__( 'Unknown request.', 'ariaccess' ) ); } wp_safe_redirect( self::url( $tab, array( $flag => 1 ) ) ); exit; } /* ===================================================================== * AJAX * ===================================================================== */ /** * Check a batch of published content. The issue list is rebuilt once, at the end. */ public static function ajax_scan_batch() { check_ajax_referer( 'ariaccess' ); if ( ! current_user_can( AriAccess_Page_Scan::cap() ) ) { wp_send_json_error( null, 403 ); } $offset = isset( $_POST['offset'] ) ? max( 0, (int) $_POST['offset'] ) : 0; $q = new WP_Query( array( 'post_type' => AriAccess_Checker::post_types(), 'post_status' => 'publish', 'posts_per_page' => 10, 'offset' => $offset, 'orderby' => 'ID', 'order' => 'ASC', 'fields' => 'ids', 'no_found_rows' => false, ) ); AriAccess_Checker::$batch = true; foreach ( $q->posts as $id ) { AriAccess_Checker::scan_post( $id ); } AriAccess_Checker::$batch = false; $done = $offset + count( $q->posts ); if ( $done >= (int) $q->found_posts ) { AriAccess_Issues::changed(); AriAccess_Issues::record(); } wp_send_json_success( array( 'done' => $done, 'total' => (int) $q->found_posts, ) ); } /** * Check one post and return the editor box HTML. */ public static function ajax_scan_post() { check_ajax_referer( 'ariaccess' ); $id = isset( $_POST['post'] ) ? (int) $_POST['post'] : 0; if ( ! $id || ! current_user_can( 'edit_post', $id ) ) { wp_send_json_error( null, 403 ); } AriAccess_Checker::scan_post( $id ); ob_start(); self::box_body( $id ); wp_send_json_success( array( 'html' => ob_get_clean() ) ); } /** * Dismiss (with a reason) or restore one problem. Returns the new counts. */ public static function ajax_dismiss() { check_ajax_referer( 'ariaccess' ); if ( ! current_user_can( AriAccess_Page_Scan::cap() ) ) { wp_send_json_error( null, 403 ); } $key = isset( $_POST['key'] ) ? sanitize_text_field( wp_unslash( $_POST['key'] ) ) : ''; $res = AriAccess_Issues::set_dismissed( $key, empty( $_POST['restore'] ), isset( $_POST['reason'] ) ? sanitize_key( wp_unslash( $_POST['reason'] ) ) : '' ); if ( is_wp_error( $res ) ) { wp_send_json_error( $res->get_error_message(), 403 ); } $c = AriAccess_Issues::counts(); wp_send_json_success( array( 'open' => array_sum( $c['open'] ), 'dismissed' => $c['dismissed'], 'must' => $c['open']['must'], ) ); } /** * Save the alt text of one image. */ public static function ajax_save_alt() { check_ajax_referer( 'ariaccess' ); $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0; if ( ! $id || ! current_user_can( 'upload_files' ) || ! current_user_can( 'edit_post', $id ) ) { wp_send_json_error( null, 403 ); } AriAccess_Media::save_alts( array( $id => isset( $_POST['alt'] ) ? wp_unslash( $_POST['alt'] ) : '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitised in save_alts(). wp_send_json_success(); } /* ===================================================================== * Editor box and list columns * ===================================================================== */ /** * Register the editor box. */ public static function meta_box() { foreach ( AriAccess_Checker::post_types() as $type ) { add_meta_box( 'ariaccess-box', __( 'Accessibility check', 'ariaccess' ), array( __CLASS__, 'box' ), $type, 'side', 'default', array( '__back_compat_meta_box' => false ) ); } } /** * Editor box. * * @param WP_Post $post Post. */ public static function box( $post ) { echo '
'; self::box_body( $post->ID ); echo '
'; } /** * Editor box contents. * * @param int $post_id Post id. */ public static function box_body( $post_id ) { $scan = AriAccess_Checker::get_scan( $post_id ); $dismissed = AriAccess_Checker::dismissed( $post_id ); if ( ! $scan ) { echo '

' . esc_html__( 'Not checked yet. Save the post or press Check now.', 'ariaccess' ) . '

'; } else { $c = AriAccess_Checker::count_types( $scan['issues'], $dismissed ); if ( ! array_sum( $c ) ) { echo '

' . esc_html__( 'No problems found by the automated check.', 'ariaccess' ) . '

'; } else { echo ''; } /* translators: %s: time ago. */ echo '

' . esc_html( sprintf( __( 'Checked %s ago.', 'ariaccess' ), human_time_diff( (int) $scan['time'] ) ) ) . '

'; } echo '

'; if ( 'publish' === get_post_status( $post_id ) ) { echo ' ' . esc_html__( 'Page check', 'ariaccess' ) . ' ' . esc_html__( '(opens in a new tab)', 'ariaccess' ) . ''; } echo '

'; } /** * List table columns. */ public static function columns() { foreach ( AriAccess_Checker::post_types() as $type ) { add_filter( "manage_{$type}_posts_columns", array( __CLASS__, 'add_column' ) ); add_action( "manage_{$type}_posts_custom_column", array( __CLASS__, 'column_value' ), 10, 2 ); } } /** * Add the list column. * * @param array $cols Columns. * @return array */ public static function add_column( $cols ) { $cols['ariaccess'] = '' . esc_html__( 'Accessibility', 'ariaccess' ) . ''; return $cols; } /** * List column value: the most serious open kind, never "OK" while something is open. * * @param string $col Column. * @param int $post_id Post id. */ public static function column_value( $col, $post_id ) { if ( 'ariaccess' !== $col ) { return; } $scan = AriAccess_Checker::get_scan( $post_id ); if ( ! $scan ) { echo '' . esc_html__( 'Not checked', 'ariaccess' ) . ''; return; } $c = AriAccess_Checker::count_types( $scan['issues'], AriAccess_Checker::dismissed( $post_id ) ); if ( $c['error'] ) { /* translators: %s: number. */ echo self::sev( 'must' ) . ' ' . esc_html( sprintf( _n( '%s place', '%s places', $c['error'], 'ariaccess' ), number_format_i18n( $c['error'] ) ) ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } elseif ( $c['warning'] ) { /* translators: %s: number. */ echo self::sev( 'review' ) . ' ' . esc_html( sprintf( _n( '%s place', '%s places', $c['warning'], 'ariaccess' ), number_format_i18n( $c['warning'] ) ) ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } elseif ( $c['notice'] ) { echo self::sev( 'hand' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } else { echo '' . esc_html__( 'No problems found', 'ariaccess' ) . ''; } } }