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 );
}
}