'aardwolf_sc_export', 'format' => $format, ), admin_url( 'admin-post.php' ) ), 'aardwolf_sc_export', '_aardwolf_nonce' ); } /** * Handle the export request. */ public function handle() { if ( ! current_user_can( AARDWOLF_SC_CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to export results.', 'aardwolf-security-scanner' ), '', array( 'response' => 403 ) ); } check_admin_referer( 'aardwolf_sc_export', '_aardwolf_nonce' ); $payload = Aardwolf_Scanner::get_last_scan(); if ( empty( $payload ) || empty( $payload['results'] ) ) { wp_die( esc_html__( 'There is no scan to export yet. Run a scan first.', 'aardwolf-security-scanner' ) ); } $format = isset( $_GET['format'] ) ? sanitize_key( wp_unslash( $_GET['format'] ) ) : 'csv'; if ( 'pdf' === $format || 'html' === $format ) { $this->render_html_report( $payload ); } else { $this->render_csv( $payload ); } exit; } /** * Stream the results as a CSV download. * * @param array $payload Scan payload. */ protected function render_csv( $payload ) { $filename = 'aardwolf-security-scan-' . gmdate( 'Y-m-d', (int) ( $payload['timestamp'] ?? time() ) ) . '.csv'; nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); $rows = array( array( 'Category', 'Check', 'Status', 'Severity', 'Summary', 'Recommended fix', 'Reference' ), ); foreach ( $payload['results'] as $r ) { $rows[] = array( $r['category'], $r['label'], Aardwolf_Admin::status_label( $r['status'] ), Aardwolf_Admin::severity_label( $r['severity'] ), $this->clean( $r['summary'] ), $this->clean( $r['remediation'] ), $r['reference'], ); } $lines = array(); foreach ( $rows as $row ) { $lines[] = implode( ',', array_map( array( $this, 'csv_field' ), $row ) ); } // UTF-8 BOM so spreadsheet apps read accented characters correctly, // then CRLF-separated rows per RFC 4180. This is a file download, not // HTML, so the payload is not HTML-escaped. echo "\xEF\xBB\xBF" . implode( "\r\n", $lines ) . "\r\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV file download (text/csv), not HTML; fields are CSV-quoted via csv_field(). HTML escaping would corrupt the data. } /** * Quote and escape a single CSV field per RFC 4180. * * @param string $value Raw field value. * @return string */ protected function csv_field( $value ) { return '"' . str_replace( '"', '""', (string) $value ) . '"'; } /** * Output a self-contained, print-ready HTML report. * * @param array $payload Scan payload. */ protected function render_html_report( $payload ) { $summary = isset( $payload['summary'] ) ? $payload['summary'] : array(); $score = isset( $summary['score'] ) ? (int) $summary['score'] : 0; $grade = isset( $summary['grade'] ) ? $summary['grade'] : ''; $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); $site_url = home_url( '/' ); $scanned_at = isset( $payload['timestamp'] ) ? wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) $payload['timestamp'] ) : ''; // Group by category. $grouped = array(); foreach ( $payload['results'] as $r ) { $grouped[ $r['category'] ][] = $r; } nocache_headers(); header( 'Content-Type: text/html; charset=utf-8' ); // Register the report stylesheet and let WordPress print the tag, so the // asset goes through wp_enqueue_style() rather than a hard-coded . wp_register_style( 'aardwolf-sc-report', AARDWOLF_SC_URL . 'assets/css/report.css', array(), AARDWOLF_SC_VERSION ); wp_enqueue_style( 'aardwolf-sc-report' ); // The report is a complete standalone document, so we echo raw markup // here rather than going through the admin template system. All dynamic // values are escaped. ?>