PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / export / class-csv.php

class-csv.php in Defender Security – Malware Scanner, Login Security & Firewall 6.3.0, at src/component/export/class-csv.php

135 lines 4.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CSV export component.
4 *
5 * @package WP_Defender\Component\Export
6 */
7
8 declare(strict_types=1);
9
10 namespace WP_Defender\Component\Export;
11
12 use RuntimeException;
13
14 /**
15 * Handles CSV exports and formula injection neutralization.
16 */
17 class Csv {
18
19 /**
20 * Neutralizes an externally influenced string to prevent CSV formula injection.
21 *
22 * Prefixes values whose first effective character (after accounting for leading
23 * whitespace and control characters) is '=', '+', '-', or '@' with an apostrophe.
24 *
25 * @param mixed $value The cell value to sanitize.
26 *
27 * @return mixed
28 */
29 public static function neutralize_cell( $value ) {
30 if ( ! is_string( $value ) ) {
31 return $value;
32 }
33
34 if ( 1 === preg_match( '/^[\s\x00-\x1F\x7F]*[=+@-]/', $value ) ) {
35 return "'" . $value;
36 }
37
38 return $value;
39 }
40
41 /**
42 * Neutralizes all string cells in a CSV row to prevent formula injection.
43 *
44 * @param array $row The row data to neutralize.
45 *
46 * @return array
47 */
48 public static function neutralize_row( array $row ): array {
49 return array_map( array( self::class, 'neutralize_cell' ), $row );
50 }
51
52 /**
53 * Writes a row to an open file pointer as CSV after neutralizing cells for formula injection.
54 *
55 * @param resource $stream The open file pointer.
56 * @param array $fields An array of values.
57 * @param string $separator The field delimiter.
58 * @param string $enclosure The field enclosure.
59 * @param string $escape The escape character. Empty string (default) disables
60 * backslash escaping and uses RFC 4180 quote-doubling instead.
61 *
62 * @return int|false
63 */
64 public static function write_row( $stream, array $fields, string $separator = ',', string $enclosure = '"', string $escape = '' ): bool|int {
65 return fputcsv( $stream, self::neutralize_row( $fields ), $separator, $enclosure, $escape );
66 }
67
68 /**
69 * Exports data as a CSV file to the browser for download.
70 *
71 * @param string $module Module identifier used to build the download filename
72 * (e.g. 'audit-logs' → 'wdf-audit-logs-export-{date}.csv').
73 * @param iterable $rows The rows of data.
74 * @param array $headers Column headers. Pass an empty array (default) to omit the header row.
75 *
76 * @return void
77 * @throws RuntimeException If writing the BOM, header, or any data row to the output stream fails.
78 */
79 public static function to_browser( string $module, iterable $rows, array $headers = array() ): void {
80 $filename = 'wdf-' . $module . '-export-' . wp_date( 'ymdHis' ) . '.csv';
81 $testing = defined( 'WP_DEFENDER_TESTING' ) && WP_DEFENDER_TESTING;
82
83 // Flush and close every active output buffer so the CSV streams directly
84 // to the browser instead of accumulating in memory (shared hosts, caching
85 // plugins, and debug bars all tend to leave OB layers open).
86 // Skipped in test mode so ob_start() capture buffers remain intact.
87 if ( ! $testing ) {
88 while ( ob_get_level() > 0 ) {
89 ob_end_clean();
90 }
91 }
92
93 if ( ! headers_sent() ) {
94 header( 'Content-Type: text/csv; charset=utf-8' );
95 header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $filename ) . '"' );
96 header( 'X-Content-Type-Options: nosniff' );
97 header( 'Cache-Control: no-store, no-cache, must-revalidate, private' );
98 header( 'Pragma: no-cache' );
99 header( 'Expires: 0' );
100 }
101
102 $stream = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
103 if ( false === $stream ) {
104 if ( ! $testing ) {
105 exit();
106 }
107 return;
108 }
109
110 try {
111 // UTF-8 BOM — makes Excel on Windows open the file with the correct
112 // encoding without forcing the user through the import wizard.
113 if ( false === fwrite( $stream, "\xEF\xBB\xBF" ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
114 throw new RuntimeException( 'Failed to write CSV BOM to output stream.' );
115 }
116
117 if ( array() !== $headers && false === self::write_row( $stream, $headers ) ) {
118 throw new RuntimeException( 'Failed to write CSV header row to output stream.' );
119 }
120
121 foreach ( $rows as $row ) {
122 if ( false === self::write_row( $stream, (array) $row ) ) {
123 throw new RuntimeException( 'Failed to write CSV data row to output stream.' );
124 }
125 }
126 } finally {
127 fclose( $stream ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
128 }
129
130 if ( ! $testing ) {
131 exit();
132 }
133 }
134 }
135