'POST', 'callback' => array( __CLASS__, 'scan_start' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), ) ); register_rest_route( self::NAMESPACE_V1, '/scan/page', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'scan_page' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), 'args' => array( 'page_id' => array( 'type' => 'integer', 'required' => true, ), 'html' => array( 'type' => 'string', 'default' => '', ), 'axe_before' => array( 'type' => array( 'array', 'null' ), 'default' => null, ), 'axe_after' => array( 'type' => array( 'array', 'null' ), 'default' => null, ), 'error' => array( 'type' => 'string', 'default' => '', ), ), ) ); register_rest_route( self::NAMESPACE_V1, '/scan/site', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'scan_site' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), 'args' => array( 'restart' => array( 'type' => 'boolean', 'default' => false, ), ), ) ); register_rest_route( self::NAMESPACE_V1, '/reset', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'reset' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), ) ); register_rest_route( self::NAMESPACE_V1, '/images/(?P\d+)/suggest', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'suggest_image' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), ) ); register_rest_route( self::NAMESPACE_V1, '/images/(?P\d+)', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'save_image' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), 'args' => array( 'alt' => array( 'type' => 'string', 'default' => '', ), 'decorative' => array( 'type' => 'boolean', 'default' => false, ), ), ) ); } /** * Pages for a browser check: the home page and the most recently changed content. * * @return \WP_REST_Response */ public static function scan_start() { $token = Scan_Token::get(); $targets = array( array( 'url' => home_url( '/' ), 'title' => __( 'Home page', 'fixlight' ), 'object_id' => 0, ), ); $front = (int) get_option( 'page_on_front' ); $query = new \WP_Query( array( 'post_type' => Background::post_types(), 'post_status' => 'publish', 'has_password' => false, 'posts_per_page' => 12, 'orderby' => 'modified', 'order' => 'DESC', 'ignore_sticky_posts' => true, 'no_found_rows' => true, 'fields' => 'ids', ) ); foreach ( $query->posts as $post_id ) { $url = get_permalink( $post_id ); if ( (int) $post_id === $front || count( $targets ) >= 12 || ! $url || ! Background::is_own_url( $url ) ) { continue; // The static front page is already in the list as the home page. } $targets[] = array( 'url' => $url, 'title' => get_the_title( $post_id ), 'object_id' => (int) $post_id, ); } $pages = array(); foreach ( $targets as $target ) { $page_id = Store::page_id( $target['url'], wp_strip_all_tags( $target['title'] ), $target['object_id'] ); $base = array( 'fixlight_scan' => $token, 'fixlight_frame' => '1', ); $pages[] = array( 'page_id' => $page_id, 'title' => wp_strip_all_tags( $target['title'] ), 'url' => $target['url'], 'before' => add_query_arg( array_merge( $base, array( 'fixlight_off' => '1', 'fixlight_id' => 'p' . $page_id . 'b', ) ), $target['url'] ), 'after' => add_query_arg( array_merge( $base, array( 'fixlight_id' => 'p' . $page_id . 'a' ) ), $target['url'] ), ); } return rest_ensure_response( array( 'pages' => $pages ) ); } /** * Saves one page's browser check. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response|\WP_Error */ public static function scan_page( \WP_REST_Request $request ) { global $wpdb; $page_id = (int) $request['page_id']; $page = $wpdb->get_row( $wpdb->prepare( 'SELECT id, url, title, object_id FROM %i WHERE id = %d', Store::pages_table(), $page_id ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( ! $page ) { return new \WP_Error( 'fixlight_no_page', __( 'Unknown page.', 'fixlight' ), array( 'status' => 404 ) ); } $html = (string) $request['html']; if ( '' !== $html && strlen( $html ) <= 3 * MB_IN_BYTES ) { $title = 0 === (int) $page['object_id'] ? get_bloginfo( 'name' ) : $page['title'] . ' – ' . get_bloginfo( 'name' ); Store::save_php_results( $page_id, Scanner::check_before_after( $html, Settings::fixes(), Front::context( $title ) ) ); } $before = $request['axe_before']; $after = $request['axe_after']; if ( is_array( $before ) && is_array( $after ) ) { Store::save_axe_results( $page_id, self::clean_violations( $before ), self::clean_violations( $after ) ); } elseif ( '' !== $request['error'] ) { Store::save_error( $page_id, sanitize_text_field( $request['error'] ) ); } return rest_ensure_response( Store::page_scores( $page_id ) ); } /** * Keeps only the fields Fixlight stores from axe results. * * @param array $violations Compact violations from the browser. * @return array */ private static function clean_violations( array $violations ) { $clean = array(); foreach ( array_slice( $violations, 0, 200 ) as $violation ) { if ( ! is_array( $violation ) || empty( $violation['id'] ) ) { continue; } $clean[] = array( 'id' => sanitize_key( $violation['id'] ), 'impact' => isset( $violation['impact'] ) ? sanitize_key( $violation['impact'] ) : 'minor', 'tags' => isset( $violation['tags'] ) && is_array( $violation['tags'] ) ? array_map( 'sanitize_key', array_slice( $violation['tags'], 0, 20 ) ) : array(), 'help' => isset( $violation['help'] ) ? sanitize_text_field( $violation['help'] ) : '', 'helpUrl' => isset( $violation['helpUrl'] ) ? esc_url_raw( $violation['helpUrl'] ) : '', 'count' => isset( $violation['count'] ) ? absint( $violation['count'] ) : 0, 'sample' => isset( $violation['sample'] ) ? sanitize_textarea_field( substr( (string) $violation['sample'], 0, 300 ) ) : '', ); } return $clean; } /** * Runs one batch of the whole-site check and reports progress. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response */ public static function scan_site( \WP_REST_Request $request ) { $state = Background::state(); if ( $request['restart'] || ! $state['running'] ) { Background::start( false ); } $state = Background::run_chunk( 12, 5 ); return rest_ensure_response( array( 'running' => $state['running'], 'done' => $state['done'], 'total' => $state['total'], 'errors' => $state['errors'], 'busy' => ! empty( $state['busy'] ), ) ); } /** * Deletes all scan results. * * @return \WP_REST_Response */ public static function reset() { Store::reset(); delete_option( Background::STATE ); return rest_ensure_response( array( 'ok' => true ) ); } /** * Asks the AI for an image's alt text and keeps it as a suggestion. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response|\WP_Error */ public static function suggest_image( \WP_REST_Request $request ) { $result = Ai::suggest( (int) $request['id'] ); return is_wp_error( $result ) ? $result : rest_ensure_response( $result ); } /** * Saves an image's alt text. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response|\WP_Error */ public static function save_image( \WP_REST_Request $request ) { $result = Images::save( (int) $request['id'], (string) $request['alt'], (bool) $request['decorative'] ); return is_wp_error( $result ) ? $result : rest_ensure_response( $result ); } }