PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.4.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.4.0
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
suredonation / inc / import-export / csv-exporter.php

csv-exporter.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.4.0, at inc/import-export/csv-exporter.php

109 lines 3.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 primitives shared by the Import & Export feature.
4 *
5 * Centralizes the two low-level concerns every CSV export needs:
6 * - CSV formula-injection escaping (so a malicious donor-supplied value
7 * cannot execute when an admin opens the file in a spreadsheet).
8 * - Building a CSV string from an array of rows via an in-memory stream.
9 *
10 * Per-entity column/query definitions (donations, donors) live in the
11 * exporters that call these helpers; this class is format mechanics only.
12 *
13 * @package SureDonation
14 * @since 1.3.0
15 */
16
17 namespace SureDonation\Inc\Import_Export;
18
19 // Exit if accessed directly.
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit;
22 }
23
24 /**
25 * Shared CSV export helper.
26 *
27 * @since 1.3.0
28 */
29 class Csv_Exporter {
30
31 /**
32 * Characters that, when leading a cell, can trigger formula/DDE execution
33 * in Excel, Google Sheets, and LibreOffice. Includes `|` and `%` which
34 * trigger DDE in some Excel locales beyond the standard formula prefixes.
35 *
36 * @var array<int, string>
37 * @since 1.3.0
38 */
39 const FORMULA_PREFIXES = [ '=', '+', '-', '@', '|', '%', "\t", "\r" ];
40
41 /**
42 * Escape a single cell value against CSV formula injection.
43 *
44 * Leading whitespace is stripped before inspecting the first character —
45 * Excel still interprets " =cmd|..." as a formula even with leading
46 * spaces. When the (trimmed) value begins with a dangerous character the
47 * original value is prefixed with a single quote, which neutralizes the
48 * formula while remaining human-readable.
49 *
50 * @param mixed $value Raw cell value.
51 * @return string Escaped cell value.
52 * @since 1.3.0
53 */
54 public static function escape_cell( $value ) {
55 $value = is_scalar( $value ) ? (string) $value : '';
56 $stripped = ltrim( $value );
57
58 if ( '' !== $stripped && in_array( $stripped[0], self::FORMULA_PREFIXES, true ) ) {
59 return "'" . $value;
60 }
61
62 return $value;
63 }
64
65 /**
66 * Build a CSV string from an array of rows.
67 *
68 * Every cell is passed through escape_cell() so callers cannot forget the
69 * injection guard. The first row is typically the header labels (escaping
70 * static labels is a harmless no-op). Uses php://temp — an in-memory
71 * stream that spills to a temp file only if the payload is large — so
72 * nothing is written to a web-reachable path.
73 *
74 * @param array<int, array<int, mixed>> $rows Rows, each an ordered array of cell values.
75 * @return string CSV content, or an empty string on failure / no rows.
76 * @since 1.3.0
77 */
78 public static function build( array $rows ) {
79 if ( empty( $rows ) ) {
80 return '';
81 }
82
83 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Writing to an in-memory stream, not the filesystem.
84 $stream = fopen( 'php://temp', 'r+' );
85 if ( false === $stream ) {
86 return '';
87 }
88
89 foreach ( $rows as $row ) {
90 if ( ! is_array( $row ) ) {
91 continue;
92 }
93 $escaped = array_map( [ self::class, 'escape_cell' ], $row );
94 // Explicit empty $escape (RFC-4180): omitting it applies PHP's
95 // non-standard "\" escaping, which corrupts export->re-import
96 // round-trips for values ending in a backslash and is deprecated
97 // as of PHP 8.4. Valid on the plugin's PHP 7.4+ floor.
98 fputcsv( $stream, $escaped, ',', '"', '' );
99 }
100
101 rewind( $stream );
102 $csv = stream_get_contents( $stream );
103 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing an in-memory stream.
104 fclose( $stream );
105
106 return is_string( $csv ) ? $csv : '';
107 }
108 }
109