Accesswell: site scan, overview, results by page and by rule, * alternative text tools, reviewed (ignored) issues and settings. * * @package Accesswell */ if ( ! defined( 'ABSPATH' ) ) { exit; } class Accesswell_Admin { const SLUG = 'accesswell'; const PER_PAGE = 50; const NOTICE = 'accesswell_notice_'; public function __construct() { add_action( 'admin_menu', array( $this, 'register_page' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); add_action( 'admin_notices', array( $this, 'show_notice' ) ); add_action( 'wp_ajax_accesswell_urls', array( $this, 'ajax_urls' ) ); add_action( 'admin_post_accesswell_alt', array( $this, 'handle_alt' ) ); add_action( 'admin_post_accesswell_fix', array( $this, 'handle_fix' ) ); add_action( 'admin_post_accesswell_fix_all', array( $this, 'handle_fix_all' ) ); add_action( 'admin_post_accesswell_unignore', array( $this, 'handle_unignore' ) ); add_action( 'admin_post_accesswell_settings', array( $this, 'handle_settings' ) ); add_action( 'admin_post_accesswell_clear', array( $this, 'handle_clear' ) ); add_action( 'admin_post_accesswell_export', array( $this, 'handle_export' ) ); add_filter( 'plugin_action_links_' . plugin_basename( ACCESSWELL_FILE ), array( $this, 'action_links' ) ); } public function register_page() { add_management_page( __( 'Accesswell', 'accesswell' ), __( 'Accesswell', 'accesswell' ), Accesswell_Front::CAPABILITY, self::SLUG, array( $this, 'render_page' ) ); } /** * @param string $hook Screen hook. */ public function enqueue( $hook ) { if ( 'tools_page_' . self::SLUG !== $hook ) { return; } wp_enqueue_style( 'accesswell-admin', ACCESSWELL_URL . 'assets/admin.css', array(), ACCESSWELL_VERSION ); wp_enqueue_script( 'accesswell-scan', ACCESSWELL_URL . 'assets/scan.js', array(), ACCESSWELL_VERSION, true ); wp_localize_script( 'accesswell-scan', 'accesswellScan', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'homeUrl' => untrailingslashit( home_url() ), 'urlsNonce' => wp_create_nonce( 'accesswell_urls' ), 'saveNonce' => wp_create_nonce( 'accesswell_front' ), 'i18n' => array( 'starting' => __( 'Finding pages...', 'accesswell' ), /* translators: 1: pages done, 2: total, 3: page address */ 'progress' => __( 'Checking %1$s of %2$s: %3$s', 'accesswell' ), /* translators: %s: page address */ 'timeout' => __( 'Skipped (did not load in time): %s', 'accesswell' ), 'done' => __( 'Scan complete. Loading results...', 'accesswell' ), 'failed' => __( 'The scan stopped because of an error. Try again.', 'accesswell' ), ), ) ); } /** * @param array $links Links. * @return array */ public function action_links( $links ) { array_unshift( $links, sprintf( '%s', esc_url( self::url() ), esc_html__( 'Open Accesswell', 'accesswell' ) ) ); return $links; } /** * @param array $args Query args. * @return string */ public static function url( $args = array() ) { return add_query_arg( array_merge( array( 'page' => self::SLUG ), $args ), admin_url( 'tools.php' ) ); } private static function views() { return array( 'overview' => __( 'Overview', 'accesswell' ), 'pages' => __( 'Pages', 'accesswell' ), 'rules' => __( 'Problems', 'accesswell' ), 'alt' => __( 'Alt text', 'accesswell' ), 'content' => __( 'Images in content', 'accesswell' ), 'ignored' => __( 'Reviewed', 'accesswell' ), 'settings' => __( 'Settings', 'accesswell' ), ); } public function render_page() { if ( ! current_user_can( Accesswell_Front::CAPABILITY ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view switching. $view = isset( $_GET['view'] ) ? sanitize_key( wp_unslash( $_GET['view'] ) ) : 'overview'; $views = self::views(); if ( ! isset( $views[ $view ] ) ) { $view = 'overview'; } $report = new Accesswell_Report(); ?>

totals(); if ( $t['last'] ) { echo esc_html( sprintf( /* translators: %s: time since the last check */ __( 'Last check: %s ago. Pages are also checked while editors browse the site.', 'accesswell' ), human_time_diff( strtotime( $t['last'] . ' UTC' ) ) ) ); } else { esc_html_e( 'No pages checked yet. Scan the site, or open any page and use Accessibility in the admin bar.', 'accesswell' ); } ?>
render_pages( $report ); break; case 'rules': $this->render_rules( $report ); break; case 'alt': $this->render_alt(); break; case 'content': $this->render_content(); break; case 'ignored': $this->render_ignored(); break; case 'settings': $this->render_settings(); break; default: $this->render_overview( $report ); } ?>
totals(); $alt = count( Accesswell_Alt::missing_in_library() ); $gaps = Accesswell_Alt::content_gaps(); $fixable = array_sum( wp_list_pluck( $gaps, 'fixable' ) ); ?>
0 ), array( 'rules', $t['errors'], __( 'Errors', 'accesswell' ), __( 'Fail a WCAG rule', 'accesswell' ), $t['errors'] > 0 ), array( 'rules', $t['warnings'], __( 'Warnings', 'accesswell' ), __( 'Very likely problems', 'accesswell' ), false ), array( 'alt', $alt, __( 'Images without alt text', 'accesswell' ), __( 'In the Media Library', 'accesswell' ), $alt > 0 ), array( 'content', $fixable, __( 'Fixable in one click', 'accesswell' ), __( 'Images in content missing the library alt text', 'accesswell' ), $fixable > 0 ), ); foreach ( $cards as $card ) : ?>
by_rule(), 0, 8, true ); if ( ! $rules ) { return; } ?>

$group ) : ?>
pages(); $this->export_link(); if ( ! $pages ) { echo '

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

'; return; } ?>
|
  • ' . esc_html( $issue['snippet'] ) . '' : ''; ?>
by_rule(); if ( ! $rules ) { echo '

' . esc_html__( 'No problems found on the checked pages.', 'accesswell' ) . '

'; return; } foreach ( $rules as $id => $group ) : ?>

' . esc_html__( 'Images in the Media Library without alternative text. Describe what each image shows, as you would to someone on the phone. Leave decorative images empty. Text saved here is used when the image is inserted from now on; for images already in content, use the "Images in content" tab afterwards.', 'accesswell' ) . '

'; if ( ! $items ) { echo '

' . esc_html__( 'Every image in the Media Library has alternative text.', 'accesswell' ) . '

'; return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only paging. $paged = isset( $_GET['paged'] ) ? max( 1, (int) $_GET['paged'] ) : 1; $total = count( $items ); $items = array_slice( $items, ( $paged - 1 ) * self::PER_PAGE, self::PER_PAGE ); ?>
'' ) ) ); ?>
pagination( $total, $paged, array( 'view' => 'alt' ) ); } private function render_content() { $gaps = Accesswell_Alt::content_gaps(); echo '

' . esc_html__( 'Images inserted into content without alternative text. When the image has alternative text in the Media Library, it can be copied in with one click; WordPress only copies it at the moment an image is inserted, so text added to the library later never reaches existing posts. Each change is saved as a revision.', 'accesswell' ) . '

'; if ( ! $gaps ) { echo '

' . esc_html__( 'Every image in your content has alternative text.', 'accesswell' ) . '

'; return; } $fixable = array_sum( wp_list_pluck( $gaps, 'fixable' ) ); if ( $fixable ) : ?>
()
' . esc_html__( 'Issues a person checked and marked as not a problem, from the page panel. They are left out of all counts. Undo one to see it again.', 'accesswell' ) . '

'; if ( ! $items ) { echo '

' . esc_html__( 'Nothing has been marked as reviewed.', 'accesswell' ) . '

'; return; } ?> $item ) : $rule = Accesswell_Rules::get( $item['rule'] ); $user = get_userdata( (int) $item['user'] ); ?>
' . esc_html( $item['snippet'] ) . '' : ''; ?> ' . esc_html( $item['path'] ) . ''; ?> display_name : '' ) . ', ' . wp_date( get_option( 'date_format' ), (int) $item['time'] ) ); ?>
true ), 'objects' ); $active = Accesswell_Settings::post_types(); $can_save = current_user_can( 'manage_options' ); unset( $public['attachment'] ); if ( ! $can_save ) { echo '

' . esc_html__( 'Only administrators can change these settings.', 'accesswell' ) . '

'; } ?>
__( 'Error', 'accesswell' ), 'warning' => __( 'Warning', 'accesswell' ), 'notice' => __( 'Notice', 'accesswell' ), ); echo '' . esc_html( isset( $labels[ $impact ] ) ? $labels[ $impact ] : $impact ) . ''; } private function export_link() { $url = wp_nonce_url( add_query_arg( 'action', 'accesswell_export', admin_url( 'admin-post.php' ) ), 'accesswell_export' ); echo '

' . esc_html__( 'Download CSV', 'accesswell' ) . '

'; } private function pagination( $total, $paged, $args ) { $pages = (int) ceil( $total / self::PER_PAGE ); if ( $pages < 2 ) { return; } echo '
'; echo wp_kses_post( paginate_links( array( 'base' => add_query_arg( 'paged', '%#%', self::url( $args ) ), 'format' => '', 'current' => $paged, 'total' => $pages, ) ) ); echo '
'; } // ------------------------------------------------------------------ // Actions // ------------------------------------------------------------------ public function ajax_urls() { check_ajax_referer( 'accesswell_urls', 'nonce' ); if ( ! current_user_can( Accesswell_Front::CAPABILITY ) ) { wp_send_json_error( null, 403 ); } wp_send_json_success( Accesswell_Urls::to_scan() ); } public function handle_alt() { check_admin_referer( 'accesswell_alt' ); $this->require_cap(); $alts = isset( $_POST['alt'] ) && is_array( $_POST['alt'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['alt'] ) ) : array(); $result = Accesswell_Alt::save_library( $alts ); $paged = isset( $_POST['paged'] ) ? max( 1, (int) $_POST['paged'] ) : 1; self::notice( 'success', sprintf( /* translators: %s: number of images */ _n( 'Alternative text saved for %s image.', 'Alternative text saved for %s images.', $result['saved'], 'accesswell' ), number_format_i18n( $result['saved'] ) ) ); wp_safe_redirect( self::url( array( 'view' => 'alt' ) + ( $paged > 1 ? array( 'paged' => $paged ) : array() ) ) ); exit; } public function handle_fix() { $post_id = isset( $_GET['post'] ) ? (int) $_GET['post'] : 0; check_admin_referer( 'accesswell_fix_' . $post_id ); $this->require_cap(); $result = Accesswell_Alt::fix_post( $post_id ); if ( is_wp_error( $result ) ) { self::notice( 'error', $result->get_error_message() ); } else { self::notice( 'success', sprintf( /* translators: 1: number of images, 2: post title */ _n( 'Alternative text copied into %1$s image in "%2$s". The previous version is kept as a revision.', 'Alternative text copied into %1$s images in "%2$s". The previous version is kept as a revision.', $result, 'accesswell' ), number_format_i18n( $result ), get_the_title( $post_id ) ) ); } wp_safe_redirect( self::url( array( 'view' => 'content' ) ) ); exit; } public function handle_fix_all() { check_admin_referer( 'accesswell_fix_all' ); $this->require_cap(); $images = 0; $posts = 0; foreach ( Accesswell_Alt::content_gaps() as $gap ) { if ( ! $gap['fixable'] ) { continue; } $result = Accesswell_Alt::fix_post( $gap['id'] ); if ( ! is_wp_error( $result ) && $result ) { $images += $result; ++$posts; } } self::notice( 'success', sprintf( /* translators: 1: number of images, 2: number of posts */ __( 'Alternative text copied into %1$s images in %2$s posts. Each post keeps its previous version as a revision.', 'accesswell' ), number_format_i18n( $images ), number_format_i18n( $posts ) ) ); wp_safe_redirect( self::url( array( 'view' => 'content' ) ) ); exit; } public function handle_unignore() { $key = isset( $_GET['key'] ) ? sanitize_key( wp_unslash( $_GET['key'] ) ) : ''; check_admin_referer( 'accesswell_unignore_' . $key ); $this->require_cap(); Accesswell_Settings::unignore( $key ); self::notice( 'success', __( 'It is counted again.', 'accesswell' ) ); wp_safe_redirect( self::url( array( 'view' => 'ignored' ) ) ); exit; } public function handle_settings() { check_admin_referer( 'accesswell_settings' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'accesswell' ), '', array( 'response' => 403 ) ); } Accesswell_Settings::save( array( 'post_types' => isset( $_POST['post_types'] ) && is_array( $_POST['post_types'] ) ? array_map( 'sanitize_key', wp_unslash( $_POST['post_types'] ) ) : array(), 'max_pages' => isset( $_POST['max_pages'] ) ? (int) $_POST['max_pages'] : 200, 'skip_rules' => isset( $_POST['skip_rules'] ) && is_array( $_POST['skip_rules'] ) ? array_map( 'sanitize_key', wp_unslash( $_POST['skip_rules'] ) ) : array(), 'auto' => ! empty( $_POST['auto'] ), ) ); self::notice( 'success', __( 'Settings saved.', 'accesswell' ) ); wp_safe_redirect( self::url( array( 'view' => 'settings' ) ) ); exit; } public function handle_clear() { check_admin_referer( 'accesswell_clear' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'accesswell' ), '', array( 'response' => 403 ) ); } Accesswell_DB::clear(); self::notice( 'success', __( 'All saved results were cleared.', 'accesswell' ) ); wp_safe_redirect( self::url() ); exit; } /** * Every open issue as CSV. */ public function handle_export() { check_admin_referer( 'accesswell_export' ); $this->require_cap(); $lines = array( array( 'Page', 'Impact', 'WCAG', 'Problem', 'Element' ) ); foreach ( ( new Accesswell_Report() )->pages() as $page ) { foreach ( $page['issues'] as $issue ) { $lines[] = array( $page['path'], $issue['impact'], $issue['wcag'], $issue['message'], $issue['snippet'] ); } } $csv = ''; foreach ( $lines as $line ) { $csv .= implode( ',', array_map( array( __CLASS__, 'csv_cell' ), $line ) ) . "\r\n"; } nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="accesswell-' . gmdate( 'Y-m-d' ) . '.csv"' ); echo "\xEF\xBB\xBF" . $csv; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV download; every cell is quoted by csv_cell(). exit; } /** * @param mixed $value Cell. * @return string */ private static function csv_cell( $value ) { $value = (string) $value; if ( '' !== $value && in_array( $value[0], array( '=', '+', '-', '@', "\t", "\r" ), true ) ) { $value = "'" . $value; } return '"' . str_replace( '"', '""', $value ) . '"'; } private function require_cap() { if ( ! current_user_can( Accesswell_Front::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'accesswell' ), '', array( 'response' => 403 ) ); } } private static function notice( $type, $message ) { set_transient( self::NOTICE . get_current_user_id(), array( 'type' => $type, 'message' => $message, ), MINUTE_IN_SECONDS ); } public function show_notice() { $key = self::NOTICE . get_current_user_id(); $notice = get_transient( $key ); if ( ! is_array( $notice ) ) { return; } delete_transient( $key ); printf( '

%2$s

', esc_attr( 'error' === $notice['type'] ? 'error' : 'success' ), esc_html( $notice['message'] ) ); } }