register_ajax_handlers(); } /** * Summary Ajax * * @return void * * - '-1' means that nonce could not be varified * - '-2' means that the post ID was not specified * - '-3' means that there isn't any summary data to return * - '-5' means that the user does not have permission to view this information for this post */ public function summary() { // nonce security. if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_REQUEST['nonce'] ) ), 'ajax-nonce' ) ) { wp_send_json_error( new \WP_Error( '-1', __( 'Permission Denied', 'accessibility-checker' ) ) ); } if ( ! isset( $_REQUEST['post_id'] ) ) { wp_send_json_error( new \WP_Error( '-2', __( 'The post ID was not set', 'accessibility-checker' ) ) ); } if ( ! current_user_can( 'edit_post', (int) $_REQUEST['post_id'] ) ) { wp_send_json_error( new \WP_Error( '-5', __( 'You do not have permission to view this information for this post.', 'accessibility-checker' ) ) ); } $html = []; $html['content'] = ''; $post_id = (int) $_REQUEST['post_id']; $summary = ( new Summary_Generator( $post_id ) )->generate_summary(); $simplified_summary_text = ''; $simplified_summary_prompt = get_option( 'edac_simplified_summary_prompt' ); $simplified_summary = get_post_meta( $post_id, '_edac_simplified_summary', true ) ? get_post_meta( $post_id, '_edac_simplified_summary', true ) : ''; $simplified_summary_grade = 0; if ( class_exists( 'DaveChild\TextStatistics\TextStatistics' ) ) { $text_statistics = new \DaveChild\TextStatistics\TextStatistics(); $simplified_summary_grade = (int) floor( $text_statistics->fleschKincaidGradeLevel( $simplified_summary ) ); } $simplified_summary_grade_failed = ( $simplified_summary_grade > 9 ) ? true : false; $simplified_summary_text = esc_html__( 'A Simplified summary has not been included for this content.', 'accessibility-checker' ); if ( 'none' !== $simplified_summary_prompt ) { if ( $summary['content_grade'] <= 9 ) { $simplified_summary_text = esc_html__( 'Your content has a reading level at or below 9th grade and does not require a simplified summary.', 'accessibility-checker' ); } elseif ( $summary['simplified_summary'] ) { if ( $simplified_summary_grade_failed ) { $simplified_summary_text = esc_html__( 'The reading level of the simplified summary is too high.', 'accessibility-checker' ); } else { $simplified_summary_text = esc_html__( 'A simplified summary has been included for this content.', 'accessibility-checker' ); } } } $html['content'] .= '
' . edac_icon( edac_get_readability_panel_icon( (int) $summary['content_grade'] > 0, (int) $summary['content_grade'], (int) $summary['content_grade'] > 9, $simplified_summary, $simplified_summary_grade, $simplified_summary_grade_failed ) ) . '
' . $summary['readability'] . '
' . esc_html__( 'Reading Level', 'accessibility-checker' ) . '
' . $simplified_summary_text . '
'; $html['content'] .= '
' . PHP_EOL; $html['content'] .= sprintf( /* translators: 1: opening anchor tag, 2: closing anchor tag with arrow icon and screen reader text */ __( '* True accessibility requires manual testing in addition to automated scans. %1$sLearn how to manually test for accessibility%2$s', 'accessibility-checker' ), '', '' . __( ', opens a new window', 'accessibility-checker' ) . '' ) . PHP_EOL; $html['content'] .= '
' . PHP_EOL; if ( ! $html ) { wp_send_json_error( new \WP_Error( '-3', __( 'No summary to return', 'accessibility-checker' ) ) ); } wp_send_json_success( wp_json_encode( $html ) ); } /** * Details Ajax * * @return void * * - '-1' means that nonce could not be varified * - '-2' means that the post ID was not specified * - '-3' means that the table name is not valid * - '-4' means that there isn't any details to return * - '-5' means that the user does not have permission to view this information for this post */ public function details() { if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_REQUEST['nonce'] ) ), 'ajax-nonce' ) ) { wp_send_json_error( new \WP_Error( '-1', __( 'Permission Denied', 'accessibility-checker' ) ) ); } if ( ! isset( $_REQUEST['post_id'] ) ) { wp_send_json_error( new \WP_Error( '-2', __( 'The post ID was not set', 'accessibility-checker' ) ) ); } if ( ! current_user_can( 'edit_post', (int) $_REQUEST['post_id'] ) ) { wp_send_json_error( new \WP_Error( '-5', __( 'You do not have permission to view this information for this post.', 'accessibility-checker' ) ) ); } $html = ''; global $wpdb; $table_name = edac_get_valid_table_name( $wpdb->prefix . 'accessibility_checker' ); $postid = (int) $_REQUEST['post_id']; $siteid = get_current_blog_id(); // Send error if table name is not valid. if ( ! $table_name ) { wp_send_json_error( new \WP_Error( '-3', __( 'Invalid table name', 'accessibility-checker' ) ) ); } $rules = edac_register_rules(); if ( $rules ) { // if ANWW is active remove link_blank for details meta box. if ( defined( 'ANWW_VERSION' ) ) { $rules = edac_remove_element_with_value( $rules, 'slug', 'link_blank' ); } // separate rule types. $passed_rules = []; $error_rules = edac_remove_element_with_value( $rules, 'rule_type', 'warning' ); $warning_rules = edac_remove_element_with_value( $rules, 'rule_type', 'error' ); // add count, unset passed error rules and add passed rules to array. if ( $error_rules ) { foreach ( $error_rules as $key => $error_rule ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for interacting with custom database, safe variable used for table name, caching not required for one time operation. $count = count( $wpdb->get_results( $wpdb->prepare( 'SELECT id, postid, object, ruletype, ignre, ignre_user, ignre_date, ignre_comment FROM %i where postid = %d and rule = %s and siteid = %d and ignre = %d', $table_name, $postid, $error_rule['slug'], $siteid, 0 ), ARRAY_A ) ); if ( $count ) { $error_rules[ $key ]['count'] = $count; } else { $error_rule['count'] = 0; $passed_rules[] = $error_rule; unset( $error_rules[ $key ] ); } } } // add count, unset passed warning rules and add passed rules to array. if ( $warning_rules ) { foreach ( $warning_rules as $key => $error_rule ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for interacting with custom database, safe variable used for table name, caching not required for one time operation. $count = count( $wpdb->get_results( $wpdb->prepare( 'SELECT id, postid, object, ruletype, ignre, ignre_user, ignre_date, ignre_comment FROM %i where postid = %d and rule = %s and siteid = %d and ignre = %d', $table_name, $postid, $error_rule['slug'], $siteid, 0 ), ARRAY_A ) ); if ( $count ) { $warning_rules[ $key ]['count'] = $count; } else { $error_rule['count'] = 0; $passed_rules[] = $error_rule; unset( $warning_rules[ $key ] ); } } } } // shared comparator: sort by severity (ascending, 1=critical first), then count (descending). $sort_by_severity_then_count = function ( $a, $b ) { $severity_a = isset( $a['severity'] ) ? (int) $a['severity'] : PHP_INT_MAX; $severity_b = isset( $b['severity'] ) ? (int) $b['severity'] : PHP_INT_MAX; if ( $severity_a !== $severity_b ) { return $severity_a - $severity_b; } return (int) $b['count'] - (int) $a['count']; }; usort( $error_rules, $sort_by_severity_then_count ); usort( $warning_rules, $sort_by_severity_then_count ); // sort passed rules array by severity (ascending, 1=critical first), then title. usort( $passed_rules, function ( $a, $b ) { $severity_a = isset( $a['severity'] ) ? (int) $a['severity'] : PHP_INT_MAX; $severity_b = isset( $b['severity'] ) ? (int) $b['severity'] : PHP_INT_MAX; if ( $severity_a !== $severity_b ) { return $severity_a - $severity_b; } return strcmp( $b['title'], $a['title'] ); } ); // merge rule arrays together. $rules = array_merge( $error_rules, $warning_rules, $passed_rules ); if ( $rules ) { /** * Filters if a user can ignore issues. * * @since 1.4.0 * * @allowed bool True if allowed, false if not */ $ignore_permission = apply_filters( 'edac_ignore_permission', true ); $severity_map = [ 1 => [ 'label' => __( 'Critical', 'accessibility-checker' ), 'class' => 'severity-critical', ], 2 => [ 'label' => __( 'High', 'accessibility-checker' ), 'class' => 'severity-high', ], 3 => [ 'label' => __( 'Medium', 'accessibility-checker' ), 'class' => 'severity-medium', ], 4 => [ 'label' => __( 'Low', 'accessibility-checker' ), 'class' => 'severity-low', ], ]; foreach ( $rules as $rule ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for interacting with custom database, safe variable used for table name, caching not required for one time operation. $results = $wpdb->get_results( $wpdb->prepare( 'SELECT id, postid, object, ruletype, ignre, ignre_user, ignre_date, ignre_comment, ignre_reason, ignre_global, landmark, landmark_selector FROM %i where postid = %d and rule = %s and siteid = %d', $table_name, $postid, $rule['slug'], $siteid ), ARRAY_A ); $count_classes = ( 'error' === $rule['rule_type'] ) ? ' edac-details-rule-count-error' : ' edac-details-rule-count-warning'; $count_classes .= ( 0 !== $rule['count'] ) ? ' active' : ''; $count_ignored = 0; $ignores = array_column( $results, 'ignre' ); if ( $ignores ) { foreach ( $ignores as $ignore ) { if ( true === (bool) $ignore ) { ++$count_ignored; } } } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for interacting with custom database, safe variable used for table name, caching not required for one time operation. $expand_rule = count( $wpdb->get_results( $wpdb->prepare( 'SELECT id FROM %i where postid = %d and rule = %s and siteid = %d', $table_name, $postid, $rule['slug'], $siteid ), ARRAY_A ) ); $tool_tip_link = edac_link_wrapper( $rule['info_url'], 'frontend-highlighter', $rule['slug'], false ); $severity_badge = ''; if ( ! empty( $rule['severity'] ) && isset( $severity_map[ (int) $rule['severity'] ] ) ) { $sev = $severity_map[ (int) $rule['severity'] ]; $severity_badge = '' . esc_html( $sev['label'] ) . ''; } $html .= '
'; $html .= '
'; $icon_name = ( 0 === $rule['count'] ) ? 'check' : ( ( 'error' === $rule['rule_type'] ) ? 'error' : 'warning' ); $html .= '

'; $html .= edac_icon( $icon_name ); $html .= ' ' . esc_html( $rule['title'] ); $html .= ' ' . $rule['count'] . '' . esc_html__( ' total', 'accessibility-checker' ) . ''; if ( $count_ignored > 0 ) { $html .= '' . $count_ignored . ' ' . esc_html( _n( 'Dismissed Issue', 'Dismissed Issues', $count_ignored, 'accessibility-checker' ) ) . ''; } $html .= $severity_badge; $html .= '

'; $html .= ''; $html .= ( $expand_rule ) ? '' : ''; $html .= '
'; if ( $results ) { $html .= '
'; $fixes_for_item = []; if ( isset( $rule['fixes'] ) && current_user_can( apply_filters( 'edac_filter_settings_capability', 'manage_options' ) ) ) { foreach ( $rule['fixes'] as $fix_slug ) { $fixes_for_item[] = FixesManager::get_instance()->get_fix( $fix_slug ); } $controls_id = 'edac-fix-modal-' . $rule['slug'] . '__' . implode( '__', $rule['fixes'] ); ob_start(); // NOTE: This is markup to be cloned into a thickbox modal. It gets cloned from the inner div. ?>
%s %s

', esc_html__( 'These settings enable global fixes across your entire site.', 'accessibility-checker' ), esc_html__( 'Pages may need to be resaved or a full site scan run to see fixes reflected in reports.', 'accessibility-checker' ) ) ?>
$fix ) : ?>

get_nicename() ); ?>

get_fields_array() as $name => $field ) { $field['name'] = $name; $field['location'] = 'details-panel'; FixesPage::{$field['type']}( $field ); } ?>
'; foreach ( $results as $row ) { $id = (int) $row['id']; $ignore = (int) $row['ignre']; $ignore_class = $ignore ? ' active' : ''; $ignore_label = $ignore ? __( 'Dismissed', 'accessibility-checker' ) : __( 'Dismiss', 'accessibility-checker' ); $ignore_global = (int) $row['ignre_global']; $ignore_reason = isset( $row['ignre_reason'] ) ? sanitize_text_field( $row['ignre_reason'] ) : ''; // check for images and svgs in object code. $media = edac_parse_html_for_media( $row['object'] ); $object_img = $media['img']; $object_svg = $media['svg']; $html .= '

' . sprintf( /* translators: %d: issue ID number */ esc_html__( 'Issue ID %d', 'accessibility-checker' ), $id ) . '

'; $html .= '
'; $html .= '
'; $html .= '' . esc_html( $row['object'] ) . ''; $html .= '
'; $html .= '
'; if ( $object_img ) { $html .= '' . esc_attr(
								sprintf(
									/* translators: %d: issue ID number */
									__( 'image for issue %d', 'accessibility-checker' ),
									$id
								)
							) . ''; } elseif ( $object_svg ) { $html .= $object_svg; } $html .= '
'; $html .= '
'; $landmark = isset( $row['landmark'] ) ? $row['landmark'] : ''; $landmark_selector = isset( $row['landmark_selector'] ) ? $row['landmark_selector'] : ''; $html .= edac_generate_landmark_link( $landmark, $landmark_selector, $postid ); $html .= '
'; $html .= '
'; if ( ! isset( $rule['viewable'] ) || $rule['viewable'] ) { $post_view_link = apply_filters( 'edac_get_origin_url_for_virtual_page', get_the_permalink( $postid ), $postid ); $url = add_query_arg( [ 'edac' => $id, 'edac_nonce' => wp_create_nonce( 'edac_highlight' ), ], $post_view_link ); // Translators: %d is the issue ID. $aria_label = sprintf( __( 'View Issue ID %d on website, opens a new window', 'accessibility-checker' ), $id ); $html .= '' . __( 'View on page', 'accessibility-checker' ) . ''; } if ( true === $ignore_permission ) { $html .= ''; } if ( ! empty( $fixes_for_item ) ) { $html .= sprintf( '', esc_attr( $controls_id ), esc_attr( __( 'Fix: ', 'accessibility-checker' ) . $fixes_for_item[0]->get_nicename() ), esc_html__( 'Fix', 'accessibility-checker' ) ); } $html .= '
'; $html .= IgnoreUI::render_ignore_panel( [ 'issue_id' => $id, 'is_ignored' => (bool) $ignore, 'ignore_user' => (int) $row['ignre_user'], 'ignore_date' => $row['ignre_date'] ?? '', 'ignore_comment' => $row['ignre_comment'] ?? '', 'ignore_reason' => $ignore_reason, 'ignore_global' => $ignore_global, 'ignore_type' => $rule['rule_type'], 'ignore_permission' => $ignore_permission, ] ); $html .= '
'; } $html .= '
'; } $html .= ''; } } if ( ! $html ) { wp_send_json_error( new \WP_Error( '-4', __( 'No details to return', 'accessibility-checker' ) ) ); } wp_send_json_success( wp_json_encode( $html ) ); } /** * Readability Ajax * * @return void * * - '-1' means that nonce could not be varified * - '-2' means that the post ID was not specified * - '-3' means that there isn't any readability data to return * - '-5' means that the user does not have permission to view this information for this post */ public function readability() { if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_REQUEST['nonce'] ) ), 'ajax-nonce' ) ) { wp_send_json_error( new \WP_Error( '-1', __( 'Permission Denied', 'accessibility-checker' ) ) ); } if ( ! isset( $_REQUEST['post_id'] ) ) { wp_send_json_error( new \WP_Error( '-2', __( 'The post ID was not set', 'accessibility-checker' ) ) ); } if ( ! current_user_can( 'edit_post', (int) $_REQUEST['post_id'] ) ) { wp_send_json_error( new \WP_Error( '-5', __( 'You do not have permission to view this information for this post.', 'accessibility-checker' ) ) ); } $post_id = (int) $_REQUEST['post_id']; $html = ''; $simplified_summary = get_post_meta( $post_id, '_edac_simplified_summary', true ) ? get_post_meta( $post_id, '_edac_simplified_summary', true ) : ''; $simplified_summary_position = get_option( 'edac_simplified_summary_position', $default = false ); $content_post = get_post( $post_id ); $content = $content_post->post_content; $content = apply_filters( 'the_content', $content ); /** * Filter the content used for reading grade readability analysis. * * @since 1.4.0 * * @param string $content The content to be filtered. * @param int $post_id The post ID. */ $content = apply_filters( 'edac_filter_readability_content', $content, $post_id ); $content = wp_filter_nohtml_kses( $content ); $content = str_replace( ']]>', ']]>', $content ); // get readability metadata and determine if a simplified summary is required. $edac_summary = get_post_meta( $post_id, '_edac_summary', true ); $post_grade_readability = ( isset( $edac_summary['readability'] ) ) ? $edac_summary['readability'] : 0; $post_grade = (int) filter_var( $post_grade_readability, FILTER_SANITIZE_NUMBER_INT ); $post_grade_failed = ( $post_grade < 9 ) ? false : true; $simplified_summary_grade = 0; if ( class_exists( 'DaveChild\TextStatistics\TextStatistics' ) ) { $text_statistics = new \DaveChild\TextStatistics\TextStatistics(); $simplified_summary_grade = (int) floor( $text_statistics->fleschKincaidGradeLevel( $simplified_summary ) ); } $simplified_summary_grade_failed = ( $simplified_summary_grade > 9 ) ? true : false; $simplified_summary_prompt = get_option( 'edac_simplified_summary_prompt' ); $html .= ''; if ( ( $post_grade_failed || 'always' === $simplified_summary_prompt ) && ( 'none' !== $simplified_summary_prompt ) ) { $html .= '

'; } $html .= ' ' . esc_html__( 'Learn more about improving readability and simplified summary requirements', 'accessibility-checker' ) . '' . __( ', opens a new window', 'accessibility-checker' ) . ''; if ( ! $html ) { wp_send_json_error( new \WP_Error( '-3', __( 'No readability data to return', 'accessibility-checker' ) ) ); } wp_send_json_success( wp_json_encode( $html ) ); } /** * Insert ignore data into database * * Note: There is a new dismiss-issue rest endpoint that covers this functionality * now and should be used going forward. This ajax was updated to support dismiss * reasons to align with the new endpoint and allow other things to continue to * work here gracefully. * * This should be removed in a future release. * * @return void * * - '-1' means that nonce could not be varified * - '-2' means that there isn't any ignore data to return */ public function add_ignore() { // nonce security. if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'ajax-nonce' ) ) { wp_send_json_error( new \WP_Error( '-1', __( 'Permission Denied', 'accessibility-checker' ) ) ); } global $wpdb; $table_name = $wpdb->prefix . 'accessibility_checker'; $raw_ids = isset( $_REQUEST['ids'] ) ? (array) wp_unslash( $_REQUEST['ids'] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitization handled below. $ids = array_map( function ( $value ) { return (int) $value; }, $raw_ids ); // Sanitizing array elements to integers. $action = isset( $_REQUEST['ignore_action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ignore_action'] ) ) : ''; $type = isset( $_REQUEST['ignore_type'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ignore_type'] ) ) : ''; $siteid = get_current_blog_id(); $ignre = ( 'enable' === $action ) ? 1 : 0; $ignre_user = ( 'enable' === $action ) ? get_current_user_id() : null; $ignre_user_info = ( 'enable' === $action ) ? get_userdata( $ignre_user ) : ''; $ignre_username = ( 'enable' === $action ) ? $ignre_user_info->user_login : ''; $ignre_date = ( 'enable' === $action ) ? edac_get_current_utc_datetime() : null; $ignre_date_formatted = ( 'enable' === $action ) ? edac_format_datetime_from_utc( $ignre_date ) : ''; $ignre_comment = ( 'enable' === $action && isset( $_REQUEST['comment'] ) ) ? sanitize_textarea_field( wp_unslash( $_REQUEST['comment'] ) ) : null; $ignre_reason = ( 'enable' === $action && isset( $_REQUEST['reason'] ) ) ? sanitize_text_field( wp_unslash( $_REQUEST['reason'] ) ) : null; $ignore_global = ( 'enable' === $action && isset( $_REQUEST['ignore_global'] ) ) ? sanitize_textarea_field( wp_unslash( $_REQUEST['ignore_global'] ) ) : 0; // If largeBatch is set and 'true', we need to perform an update using the 'object' // instead of IDs. It is a much less efficient query than by IDs - but many IDs run // into request size limits which caused this to not function at all. if ( isset( $_REQUEST['largeBatch'] ) && 'true' === $_REQUEST['largeBatch'] ) { // Get the 'object' from the first id. $first_id = $ids[0]; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to get the latest value, not a cached value. $object = $wpdb->get_var( $wpdb->prepare( 'SELECT object FROM %i WHERE id = %d', $table_name, $first_id ) ); if ( ! $object ) { wp_send_json_error( new \WP_Error( '-2', __( 'No ignore data to return', 'accessibility-checker' ) ) ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Safe variable used for table name, caching not required for one time operation. $wpdb->query( $wpdb->prepare( 'UPDATE %i SET ignre = %d, ignre_user = %d, ignre_date = %s, ignre_comment = %s, ignre_reason = %s, ignre_global = %d WHERE siteid = %d and object = %s', $table_name, $ignre, $ignre_user, $ignre_date, $ignre_comment, $ignre_reason, $ignore_global, $siteid, $object ) ); } else { // For small batches of IDs, we can just loop through. foreach ( $ids as $id ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Safe variable used for table name, caching not required for one time operation. $wpdb->query( $wpdb->prepare( 'UPDATE %i SET ignre = %d, ignre_user = %d, ignre_date = %s, ignre_comment = %s, ignre_reason = %s, ignre_global = %d WHERE siteid = %d and id = %d', $table_name, $ignre, $ignre_user, $ignre_date, $ignre_comment, $ignre_reason, $ignore_global, $siteid, $id ) ); } } $data = [ 'ids' => $ids, 'action' => $action, 'type' => $type, 'user' => $ignre_username, 'date' => $ignre_date_formatted, ]; if ( ! $data ) { wp_send_json_error( new \WP_Error( '-2', __( 'No ignore data to return', 'accessibility-checker' ) ) ); } wp_send_json_success( wp_json_encode( $data ) ); } /** * Handle AJAX request to dismiss Welcome CTA * * @return void */ public function dismiss_welcome_cta() { update_user_meta( get_current_user_id(), 'edac_welcome_cta_dismissed', true ); wp_send_json( 'success' ); } /** * Handle AJAX request to dismiss dashboard CTA * * @return void */ public function dismiss_dashboard_cta() { update_user_meta( get_current_user_id(), 'edac_dashboard_cta_dismissed', true ); wp_send_json( 'success' ); } }