WP_REST_Server::READABLE, 'permission_callback' => fn() => current_user_can( 'manage_options' ), 'callback' => [ self::class, 'handle_get_settings' ], ]); // POST /wp-json/accessmate/v1/settings register_rest_route( self::$ns, '/settings', [ 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => fn() => current_user_can( 'manage_options' ), 'callback' => [ self::class, 'handle_save_settings' ], 'args' => [ 'ai_provider' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => 'none' ], 'ai_key' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => '' ], 'ai_model' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => '' ], 'scan_schedule' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => 'weekly' ], ], ]); // POST /wp-json/accessmate/v1/ai/proxy register_rest_route( self::$ns, '/ai/proxy', [ 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => fn() => current_user_can( 'manage_options' ), 'callback' => [ self::class, 'handle_ai_proxy' ], 'args' => [ 'task' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'required' => true ], 'params' => [ 'type' => 'object', 'default' => [] ], ], ]); // GET /wp-json/accessmate/v1/scan-history register_rest_route( self::$ns, '/scan-history', [ 'methods' => WP_REST_Server::READABLE, 'permission_callback' => fn() => current_user_can( 'manage_options' ), 'callback' => [ self::class, 'handle_scan_history' ], ]); // GET /wp-json/accessmate/v1/widget-config register_rest_route( self::$ns, '/widget-config', [ 'methods' => WP_REST_Server::READABLE, 'permission_callback' => '__return_true', // public — widget reads this on front end 'callback' => [ self::class, 'handle_get_widget_config' ], ]); // POST /wp-json/accessmate/v1/widget-config register_rest_route( self::$ns, '/widget-config', [ 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => fn() => current_user_can( 'manage_options' ), 'callback' => [ self::class, 'handle_save_widget_config' ], 'args' => [ 'enabled' => [ 'type' => 'boolean', 'default' => true ], 'color' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_hex_color', 'default' => '#3858e9' ], 'position' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => 'bottom-right', 'enum' => [ 'top-left', 'top-right', 'bottom-left', 'bottom-right' ], ], 'features' => [ 'type' => 'array', 'default' => [] ], ], ]); // POST /wp-json/accessmate/v1/scan register_rest_route( self::$ns, '/scan', [ 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => fn() => current_user_can( 'manage_options' ), 'callback' => [ self::class, 'handle_scan' ], 'args' => [ 'url' => [ 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', 'default' => '', ], ], ]); // GET /wp-json/accessmate/v1/issues register_rest_route( self::$ns, '/issues', [ 'methods' => WP_REST_Server::READABLE, 'permission_callback' => fn() => current_user_can( 'manage_options' ), 'callback' => [ self::class, 'handle_issues' ], ]); // GET /wp-json/accessmate/v1/report register_rest_route( self::$ns, '/report', [ 'methods' => WP_REST_Server::READABLE, 'permission_callback' => fn() => current_user_can( 'manage_options' ), 'callback' => [ self::class, 'handle_report' ], ]); // POST /wp-json/accessmate/v1/axe-scan — store browser-computed axe-core results register_rest_route( self::$ns, '/axe-scan', [ 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => fn() => current_user_can( 'manage_options' ), 'callback' => [ self::class, 'handle_axe_scan' ], 'args' => [ 'url' => [ 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', 'required' => true ], 'score' => [ 'type' => 'integer', 'default' => 0 ], 'grade' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => 'F' ], 'passed' => [ 'type' => 'integer', 'default' => 0 ], 'failed' => [ 'type' => 'integer', 'default' => 0 ], 'issues' => [ 'type' => 'array', 'default' => [] ], ], ]); // GET /wp-json/accessmate/v1/scannable-urls register_rest_route( self::$ns, '/scannable-urls', [ 'methods' => WP_REST_Server::READABLE, 'permission_callback' => fn() => current_user_can( 'manage_options' ), 'callback' => [ self::class, 'handle_scannable_urls' ], ]); // POST /wp-json/accessmate/v1/scan-page register_rest_route( self::$ns, '/scan-page', [ 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => fn() => current_user_can( 'manage_options' ), 'callback' => [ self::class, 'handle_scan_page' ], 'args' => [ 'url' => [ 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', 'required' => true, ], ], ]); // GET /wp-json/accessmate/v1/page-scans register_rest_route( self::$ns, '/page-scans', [ 'methods' => WP_REST_Server::READABLE, 'permission_callback' => fn() => current_user_can( 'manage_options' ), 'callback' => [ self::class, 'handle_page_scans' ], ]); } public static function handle_scan( WP_REST_Request $request ): WP_REST_Response { $url = $request->get_param( 'url' ); if ( empty( $url ) ) { $url = get_site_url(); } // Basic URL validation — must belong to this site or be an absolute URL. if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) { return new WP_REST_Response( [ 'error' => __( 'Invalid URL provided.', 'accessmate' ) ], 400 ); } $result = AccessMate_Scanner::run( $url ); $status = isset( $result['error'] ) ? 422 : 200; return new WP_REST_Response( $result, $status ); } public static function handle_issues( WP_REST_Request $request ): WP_REST_Response { $scan = get_option( 'accessmate_last_scan', [] ); // Always return an array — never null. $issues = $scan['issues'] ?? []; return new WP_REST_Response( $issues, 200 ); } public static function handle_report( WP_REST_Request $request ): WP_REST_Response { $report = AccessMate_Report::get(); return new WP_REST_Response( $report, 200 ); } public static function handle_get_settings( WP_REST_Request $request ): WP_REST_Response { return new WP_REST_Response( AccessMate_Settings::get(), 200 ); } public static function handle_save_settings( WP_REST_Request $request ): WP_REST_Response { $provider = $request->get_param( 'ai_provider' ); $raw_key = $request->get_param( 'ai_key' ); $model = $request->get_param( 'ai_model' ); $schedule = $request->get_param( 'scan_schedule' ); $valid_providers = array_merge( [ 'none' ], array_keys( AccessMate_Providers::all() ) ); $valid_schedules = [ 'daily', 'weekly', 'monthly' ]; if ( ! in_array( $provider, $valid_providers, true ) ) { return new WP_REST_Response( [ 'error' => __( 'Invalid provider.', 'accessmate' ) ], 400 ); } if ( ! in_array( $schedule, $valid_schedules, true ) ) { return new WP_REST_Response( [ 'error' => __( 'Invalid schedule.', 'accessmate' ) ], 400 ); } AccessMate_Settings::save( $provider, $raw_key, $model, $schedule ); return new WP_REST_Response( [ 'saved' => true ], 200 ); } public static function handle_scan_history( WP_REST_Request $request ): WP_REST_Response { return new WP_REST_Response( AccessMate_Report::get_history(), 200 ); } public static function handle_get_widget_config( WP_REST_Request $request ): WP_REST_Response { $config = get_option( 'accessmate_widget_config', AccessMate_Widget_Injector::defaults() ); return new WP_REST_Response( $config, 200 ); } public static function handle_save_widget_config( WP_REST_Request $request ): WP_REST_Response { $allowed_features = [ 'font', 'contrast', 'dyslexia', 'grayscale', 'reading-guide', 'animations', 'line-height', 'cursor', 'links', 'screen-mask' ]; $features = array_values( array_filter( array_map( 'sanitize_text_field', (array) $request->get_param( 'features' ) ), fn( $f ) => in_array( $f, $allowed_features, true ) )); update_option( 'accessmate_widget_config', [ 'enabled' => (bool) $request->get_param( 'enabled' ), 'color' => sanitize_hex_color( $request->get_param( 'color' ) ) ?? '#3858e9', 'position' => sanitize_text_field( $request->get_param( 'position' ) ), 'features' => $features, 'hide_powered_by' => (bool) $request->get_param( 'hide_powered_by' ), ]); return new WP_REST_Response( [ 'saved' => true ], 200 ); } public static function handle_axe_scan( WP_REST_Request $request ): WP_REST_Response { $url = $request->get_param( 'url' ); $score = (int) $request->get_param( 'score' ); $grade = $request->get_param( 'grade' ); $passed = (int) $request->get_param( 'passed' ); $failed = (int) $request->get_param( 'failed' ); // Sanitise the issues array coming from the browser. $clean_issues = []; $allowed_severities = [ 'critical', 'warning', 'notice' ]; foreach ( (array) $request->get_param( 'issues' ) as $iss ) { if ( ! is_array( $iss ) ) continue; $sev = sanitize_text_field( $iss['severity'] ?? 'notice' ); // Validate help_url: must be a Deque University or W3C URL. $raw_help_url = $iss['help_url'] ?? ''; $allowed_hosts = [ 'dequeuniversity.com', 'www.w3.org', 'www.dequeuniversity.com' ]; $parsed_host = wp_parse_url( $raw_help_url, PHP_URL_HOST ); $safe_help_url = in_array( $parsed_host, $allowed_hosts, true ) ? esc_url_raw( $raw_help_url ) : ''; $clean_issues[] = [ 'id' => sanitize_text_field( $iss['id'] ?? '' ), 'rule' => sanitize_text_field( $iss['rule'] ?? '' ), 'wcag' => sanitize_text_field( $iss['wcag'] ?? '' ), 'severity' => in_array( $sev, $allowed_severities, true ) ? $sev : 'notice', 'title' => sanitize_text_field( $iss['title'] ?? '' ), 'description' => sanitize_text_field( $iss['description'] ?? '' ), 'element' => wp_kses_post( $iss['element'] ?? '' ), 'selector' => sanitize_text_field( $iss['selector'] ?? '' ), 'count' => (int) ( $iss['count'] ?? 1 ), 'fixed' => false, 'ai_fixable' => (bool) ( $iss['ai_fixable'] ?? false ), 'help_url' => $safe_help_url, ]; } $result = [ 'url' => $url, 'scanned_at' => current_time( 'c' ), 'score' => $score, 'grade' => $grade, 'issues' => $clean_issues, 'passed' => $passed, 'failed' => $failed, ]; // Persist as last scan (Dashboard) and per-page scan (PageScanner). update_option( 'accessmate_last_scan', $result ); AccessMate_Site_Scanner::store_axe_scan( $url, $result ); AccessMate_Scanner::append_history( $result ); return new WP_REST_Response( [ 'saved' => true ], 200 ); } public static function handle_scannable_urls( WP_REST_Request $request ): WP_REST_Response { $urls = AccessMate_Site_Scanner::get_scannable_urls(); $search = strtolower( trim( (string) $request->get_param( 'search' ) ) ); if ( $search !== '' ) { $urls = array_values( array_filter( $urls, static function ( $item ) use ( $search ) { return str_contains( strtolower( $item['label'] ), $search ) || str_contains( strtolower( $item['url'] ), $search ); } ) ); } return new WP_REST_Response( $urls, 200 ); } public static function handle_scan_page( WP_REST_Request $request ): WP_REST_Response { $url = $request->get_param( 'url' ); if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) { return new WP_REST_Response( [ 'error' => __( 'Invalid URL.', 'accessmate' ) ], 400 ); } $result = AccessMate_Site_Scanner::scan_page( $url ); $status = isset( $result['error'] ) ? 422 : 200; return new WP_REST_Response( $result, $status ); } public static function handle_page_scans( WP_REST_Request $request ): WP_REST_Response { $scans = AccessMate_Site_Scanner::get_page_scans(); // URL-keyed associative array $search = strtolower( trim( (string) $request->get_param( 'search' ) ) ); if ( $search !== '' ) { $scans = array_filter( $scans, static function ( $scan ) use ( $search ) { return str_contains( strtolower( $scan['url'] ?? '' ), $search ); } ); } // Sort by most issues first — preserve URL keys so response serialises as a JSON object. uasort( $scans, static fn( $a, $b ) => ( $b['failed'] ?? 0 ) <=> ( $a['failed'] ?? 0 ) ); return new WP_REST_Response( $scans, 200 ); } public static function handle_ai_proxy( WP_REST_Request $request ): WP_REST_Response { $task = $request->get_param( 'task' ); $params = (array) ( $request->get_param( 'params' ) ?? [] ); // Sanitise each param value individually. $params = array_map( 'sanitize_text_field', $params ); $result = AccessMate_AI_Proxy::call( $task, $params ); $status = isset( $result['error'] ) ? 400 : 200; return new WP_REST_Response( $result, $status ); } }