[ 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 ? ' ' . number_format_i18n( $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/ :
$t ) :
if ( ! current_user_can( $t[1] ) ) {
continue;
}
?>
>
' . esc_html( $msg ) . '
';
}
}
/**
* Start of a settings form.
*
* @param string $area Area being saved.
*/
private static function form_open( $area ) {
echo '
' . 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 '';
foreach ( $h as $e ) {
echo ' ';
}
echo '
';
echo '' . esc_html__( 'Show as a table', 'ariaccess' ) . ' ' . esc_html__( 'Day', 'ariaccess' ) . ' ' . esc_html__( 'Must fix', 'ariaccess' ) . ' ' . esc_html__( 'Should review', 'ariaccess' ) . ' ';
foreach ( array_reverse( $h ) as $e ) {
echo '' . 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'] ) ) . ' ';
}
echo '
';
}
/* =====================================================================
* 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 '';
echo paginate_links( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
array(
'base' => add_query_arg( 'paged', '%#%' ),
'format' => '',
'current' => $query['page'],
'total' => $query['pages'],
)
);
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 '';
}
/* =====================================================================
* 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' );
?>
1 ) {
echo '';
echo paginate_links( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
array(
'base' => add_query_arg( 'paged', '%#%' ),
'format' => '',
'current' => $paged,
'total' => $pages,
)
);
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 ' ' . esc_html( $label ) . '
';
}
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' ) );
?>
__( 'Bottom right', 'ariaccess' ),
'bottom-left' => __( 'Bottom left', 'ariaccess' ),
'middle-right' => __( 'Middle right', 'ariaccess' ),
'middle-left' => __( 'Middle left', 'ariaccess' ),
'top-right' => __( 'Top right', 'ariaccess' ),
'top-left' => __( 'Top left', 'ariaccess' ),
);
foreach ( $positions as $val => $label ) {
echo '' . esc_html( $label ) . ' ';
}
?>
>
>
>
>
>
>
$m ) {
echo '
' . esc_html( $m[0] ) . '
';
}
?>
>Norsk bokmål >English
>
>
>
' . 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 ) . '> ' . esc_html( $type->labels->name ) . '
';
}
?>
$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 '';
foreach ( array( 'error', 'warning', 'notice' ) as $type ) {
foreach ( $scan['issues'] as $rule_id => $issue ) {
$rule = AriAccess_Rules::get( $rule_id );
if ( ! $rule || $rule['type'] !== $type || in_array( $rule_id, $dismissed, true ) ) {
continue;
}
echo '' . self::badge( $type ) . ' ' . esc_html( $rule['title'] ) . ' ×' . (int) $issue['count'] . ' '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '' . esc_html( $rule['fix'] ) . '
';
foreach ( $issue['samples'] as $sample ) {
echo '' . esc_html( $sample ) . '';
}
if ( 'error' !== $type && current_user_can( AriAccess_Page_Scan::cap() ) ) {
echo '' . esc_html__( 'Checked: not a problem here', 'ariaccess' ) . '
';
}
echo ' ';
}
}
echo ' ';
}
/* translators: %s: time ago. */
echo '' . esc_html( sprintf( __( 'Checked %s ago.', 'ariaccess' ), human_time_diff( (int) $scan['time'] ) ) ) . '
';
}
echo '' . esc_html__( 'Check now', 'ariaccess' ) . ' ';
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' ) . ' ';
}
}
}