__('Insufficient permissions', 'ability')]); } if (class_exists('ABILITY\Config')) { $runtime_settings = \ABILITY\Config::get_settings(); if (empty($runtime_settings['scan_enabled'])) { wp_send_json_error(['message' => __('Scanning is disabled in ABILITY settings.', 'ability')]); } } $submitted_path = isset($_POST['scan_path']) ? sanitize_text_field(wp_unslash($_POST['scan_path'])) : ''; $scan_path = '' !== $submitted_path ? $submitted_path : ABSPATH; // Run scanner if (class_exists('ABILITY\Scanner')) { $scanner = new \ABILITY\Scanner(); $results = $scanner->scan($scan_path); wp_send_json_success($results); } wp_send_json_error(['message' => __('Scanner not available', 'ability')]); } /** * Save settings. */ public function save_settings() { check_ajax_referer('ability_admin', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Insufficient permissions', 'ability')]); } $settings = (isset($_POST['settings']) && is_array($_POST['settings'])) ? wp_unslash($_POST['settings']) : []; // Auto-update preference is not a parallel Ability setting; it reads // and writes WordPress core's own native per-plugin auto-update state // (the same state the Plugins screen "Enable/Disable auto-updates" // link controls), so there is a single authoritative source of truth. if (array_key_exists('auto_updates_enabled', $settings)) { if (current_user_can('update_plugins')) { $plugin_file = plugin_basename(ABILITY_PATH . 'ability.php'); $enabled = !empty($settings['auto_updates_enabled']); $list = (array) get_site_option('auto_update_plugins', []); if ($enabled) { $list[] = $plugin_file; } else { $list = array_diff($list, [$plugin_file]); } update_site_option('auto_update_plugins', array_values(array_unique($list))); } unset($settings['auto_updates_enabled']); } if (class_exists('ABILITY\Config')) { $sanitized = \ABILITY\Config::sanitize_settings($settings); \ABILITY\Config::update_settings($sanitized); wp_send_json_success([ 'message' => __('Settings saved', 'ability'), 'settings' => \ABILITY\Config::get_settings(), ]); } wp_send_json_error(['message' => __('Failed to save settings', 'ability')]); } /** * Get scan results. */ public function get_scan_results() { check_ajax_referer('ability_admin', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Insufficient permissions', 'ability')]); } global $wpdb; // Sanitize table name - only allow alphanumeric and underscore $table_name = 'ability_scans'; if (!preg_match('/^[a-zA-Z0-9_]+$/', $table_name)) { wp_send_json_error(['message' => __('Invalid table name', 'ability')]); } $table = $wpdb->prefix . $table_name; // Use LIMIT with prepared statement for the limit value // Table name is validated above, so safe to use in query $limit = 100; $results = $wpdb->get_results( $wpdb->prepare("SELECT * FROM `{$table}` ORDER BY created_at DESC LIMIT %d", $limit), ARRAY_A ); wp_send_json_success($results); } /** * Neutralize spreadsheet-formula prefixes in CSV cells. Scan data can * include URLs or error text derived from site/runtime responses, so exported * values should be data when opened in spreadsheet software. */ private function csv_safe_cell($value): string { $value = is_scalar($value) || null === $value ? (string) $value : wp_json_encode($value); if (preg_match('/^[=+\-@]/', ltrim($value))) { return "'" . $value; } return $value; } /** * Export scan results as CSV with standards mapping. */ public function export_scans_csv() { check_ajax_referer('ability_admin', 'nonce'); if (!current_user_can('manage_options')) { wp_die(__('Insufficient permissions', 'ability')); } global $wpdb; $table_name = 'ability_scans'; if (!preg_match('/^[a-zA-Z0-9_]+$/', $table_name)) { wp_die(__('Invalid table name', 'ability')); } $table = $wpdb->prefix . $table_name; $rows = $wpdb->get_results($wpdb->prepare("SELECT * FROM `{$table}` ORDER BY created_at DESC LIMIT %d", 2000), ARRAY_A); header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename=ability-scan-report.csv'); $out = fopen('php://output', 'w'); fputcsv($out, [ 'file_path','issue_type','severity','message','line_number','wcag_sc','technique','standard','section508','tool_version','ruleset_version','created_at' ]); foreach ($rows as $r) { fputcsv($out, array_map([$this, 'csv_safe_cell'], [ $r['file_path'] ?? '', $r['issue_type'] ?? '', $r['severity'] ?? '', $r['message'] ?? '', $r['line_number'] ?? '', $r['wcag_sc'] ?? '', $r['technique'] ?? '', $r['standard'] ?? '', $r['section508'] ?? '', $r['tool_version'] ?? '', $r['ruleset_version'] ?? '', $r['created_at'] ?? '', ])); } fclose($out); exit; } /** * Export scan results as JSON with standards mapping. */ public function export_scans_json() { check_ajax_referer('ability_admin', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Insufficient permissions', 'ability')]); } global $wpdb; $table_name = 'ability_scans'; if (!preg_match('/^[a-zA-Z0-9_]+$/', $table_name)) { wp_send_json_error(['message' => __('Invalid table name', 'ability')]); } $table = $wpdb->prefix . $table_name; $rows = $wpdb->get_results($wpdb->prepare("SELECT * FROM `{$table}` ORDER BY created_at DESC LIMIT %d", 2000), ARRAY_A); $payload = [ 'tool' => 'ability-php-scanner', 'generated_at' => gmdate('c'), 'results' => $rows, ]; header('Content-Type: application/json; charset=utf-8'); header('Content-Disposition: attachment; filename=ability-scan-report.json'); echo wp_json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); exit; } }