active() ) { return; } wp_enqueue_script( 'accesswell-engine', ACCESSWELL_URL . 'assets/engine.js', array(), ACCESSWELL_VERSION, true ); wp_enqueue_script( 'accesswell-overlay', ACCESSWELL_URL . 'assets/overlay.js', array( 'accesswell-engine' ), ACCESSWELL_VERSION, true ); wp_enqueue_style( 'accesswell-overlay', ACCESSWELL_URL . 'assets/overlay.css', array(), ACCESSWELL_VERSION ); $path = self::current_path(); $ignored = array(); foreach ( Accesswell_Settings::ignored() as $key => $item ) { if ( '' === $item['path'] || $item['path'] === $path ) { $ignored[] = array( 'rule' => $item['rule'], 'selector' => $item['selector'], 'everywhere' => '' === $item['path'], ); } } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only switch that opens the panel. $open = isset( $_GET['accesswell'] ) && 'open' === sanitize_key( wp_unslash( $_GET['accesswell'] ) ); wp_localize_script( 'accesswell-engine', 'accesswellData', array( 'rules' => Accesswell_Rules::for_script(), 'text' => Accesswell_Rules::texts(), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'accesswell_front' ), 'postId' => is_singular() ? get_queried_object_id() : 0, 'path' => $path, 'scan' => self::scan_mode(), 'auto' => Accesswell_Settings::get()['auto'], 'open' => $open, 'ignored' => $ignored, 'editUrl' => is_singular() ? (string) get_edit_post_link( get_queried_object_id(), 'raw' ) : '', 'reportUrl' => admin_url( 'tools.php?page=accesswell' ), 'i18n' => array( 'title' => __( 'Accessibility check', 'accesswell' ), 'close' => __( 'Close', 'accesswell' ), 'checking' => __( 'Checking this page...', 'accesswell' ), /* translators: 1: errors, 2: warnings, 3: notices */ 'summary' => __( '%1$s errors, %2$s warnings, %3$s notices', 'accesswell' ), 'none' => __( 'No problems found on this page by the automatic checks. Keyboard use and screen reader output still need a check by a person.', 'accesswell' ), 'all' => __( 'All', 'accesswell' ), 'error' => __( 'Errors', 'accesswell' ), 'warning' => __( 'Warnings', 'accesswell' ), 'notice' => __( 'Notices', 'accesswell' ), 'impact_error' => __( 'Error', 'accesswell' ), 'impact_warning' => __( 'Warning', 'accesswell' ), 'impact_notice' => __( 'Notice', 'accesswell' ), 'show' => __( 'Show on page', 'accesswell' ), 'howToFix' => __( 'How to fix', 'accesswell' ), 'ignoreHere' => __( 'Ignore on this page', 'accesswell' ), 'ignoreAll' => __( 'Ignore on every page', 'accesswell' ), /* translators: %s: number of ignored items */ 'ignoredCount' => __( '%s ignored as reviewed', 'accesswell' ), 'wholePage' => __( 'Applies to the whole page', 'accesswell' ), 'editPage' => __( 'Edit this page', 'accesswell' ), 'report' => __( 'Site report', 'accesswell' ), 'menu' => __( 'Accessibility', 'accesswell' ), /* translators: %s: WCAG success criterion number */ 'wcag' => __( 'WCAG %s', 'accesswell' ), 'saved' => __( 'Result saved to the site report.', 'accesswell' ), ), ) ); } /** * @param WP_Admin_Bar $bar Admin bar. */ public function admin_bar( $bar ) { if ( ! $this->active() || self::scan_mode() ) { return; } $bar->add_node( array( 'id' => 'accesswell', 'title' => '' . esc_html__( 'Accessibility', 'accesswell' ) . ' ', 'href' => '#accesswell', 'meta' => array( 'title' => __( 'Check this page for accessibility problems', 'accesswell' ), ), ) ); } /** * Path of the current page, relative to the site, with its query. * * @return string */ public static function current_path() { $uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/'; $path = (string) wp_parse_url( $uri, PHP_URL_PATH ); // Relative to the home address, so sites in a subfolder work too. $home = trailingslashit( (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ) ); if ( 0 === strpos( trailingslashit( $path ), $home ) ) { $path = '/' . ltrim( substr( $path, strlen( $home ) ), '/' ); } $args = array(); parse_str( (string) wp_parse_url( $uri, PHP_URL_QUERY ), $args ); unset( $args['accesswell'], $args['accesswell_scan'] ); return $args ? $path . '?' . http_build_query( $args ) : $path; } // ------------------------------------------------------------------ // AJAX // ------------------------------------------------------------------ /** * Save the result of checking a page. */ public function ajax_save() { check_ajax_referer( 'accesswell_front', 'nonce' ); if ( ! current_user_can( self::CAPABILITY ) ) { wp_send_json_error( null, 403 ); } $path = isset( $_POST['path'] ) ? self::clean_path( sanitize_text_field( wp_unslash( $_POST['path'] ) ) ) : ''; $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0; $raw = isset( $_POST['issues'] ) ? json_decode( wp_unslash( $_POST['issues'] ), true ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON; every field is cleaned in clean_issues(). if ( '' === $path || ! is_array( $raw ) ) { wp_send_json_error( null, 400 ); } Accesswell_DB::save( home_url( $path ), $post_id, self::clean_issues( $raw ) ); wp_send_json_success(); } /** * Mark an issue as reviewed. */ public function ajax_ignore() { check_ajax_referer( 'accesswell_front', 'nonce' ); if ( ! current_user_can( self::CAPABILITY ) ) { wp_send_json_error( null, 403 ); } $rule = isset( $_POST['rule'] ) ? sanitize_key( wp_unslash( $_POST['rule'] ) ) : ''; $selector = isset( $_POST['selector'] ) ? self::clean_selector( wp_unslash( $_POST['selector'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Cleaned in clean_selector(). $path = isset( $_POST['path'] ) ? self::clean_path( sanitize_text_field( wp_unslash( $_POST['path'] ) ) ) : ''; $snippet = isset( $_POST['snippet'] ) ? self::clean_snippet( wp_unslash( $_POST['snippet'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Cleaned in clean_snippet(). $all = ! empty( $_POST['everywhere'] ); if ( ! Accesswell_Rules::get( $rule ) || '' === $path ) { wp_send_json_error( null, 400 ); } Accesswell_Settings::ignore( array( 'rule' => $rule, 'selector' => $selector, 'path' => $all ? '' : $path, 'snippet' => $snippet, ) ); wp_send_json_success(); } // ------------------------------------------------------------------ // Cleaning data from the browser // ------------------------------------------------------------------ /** * Keep known rules only; impact and WCAG always come from the server. * * @param array $raw Issues from the browser. * @return array */ public static function clean_issues( array $raw ) { $out = array(); foreach ( array_slice( $raw, 0, Accesswell_DB::MAX_ISSUES ) as $issue ) { if ( ! is_array( $issue ) || empty( $issue['rule'] ) ) { continue; } $rule = Accesswell_Rules::get( sanitize_key( $issue['rule'] ) ); if ( ! $rule ) { continue; } $out[] = array( 'rule' => sanitize_key( $issue['rule'] ), 'impact' => $rule['impact'], 'wcag' => $rule['wcag'], 'message' => substr( sanitize_text_field( isset( $issue['message'] ) ? (string) $issue['message'] : '' ), 0, 300 ), 'selector' => self::clean_selector( isset( $issue['selector'] ) ? $issue['selector'] : '' ), 'snippet' => self::clean_snippet( isset( $issue['snippet'] ) ? $issue['snippet'] : '' ), ); } return $out; } /** * @param mixed $selector CSS path. * @return string */ public static function clean_selector( $selector ) { return substr( preg_replace( '/[^\w\s#.:>()\[\]="\'\\\\-]/u', '', (string) $selector ), 0, 500 ); } /** * Markup excerpt, kept as text and always escaped when shown. * * @param mixed $snippet Excerpt. * @return string */ public static function clean_snippet( $snippet ) { $snippet = wp_check_invalid_utf8( (string) $snippet ); $snippet = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $snippet ); return mb_substr( $snippet, 0, 300 ); } /** * A site-relative path; anything else is refused. * * @param string $path Path. * @return string */ public static function clean_path( $path ) { $path = '/' . ltrim( (string) $path, '/' ); if ( preg_match( '#^//|://|[\s<>"]#', $path ) ) { return ''; } return substr( $path, 0, 400 ); } }