PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.6.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.6.1
1.6.1 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.6.1, at inc/import-export/csv-exporter.php

139 lines 4.5 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 * Undo escape_cell() on a value read back from a CSV.
67 *
68 * The inverse of the guard above, so an export -> import round trip returns
69 * the value the donor actually wrote. Without it a comment that began with
70 * a formula character comes back one apostrophe longer every trip, and that
71 * text is published on the campaign page.
72 *
73 * Only strips a leading apostrophe when the character after it is one this
74 * class would have escaped — so a value the donor genuinely began with an
75 * apostrophe ("'til next year") is left alone.
76 *
77 * @param string $value Raw cell value from the CSV.
78 * @return string Value without the injected guard.
79 * @since 1.6.0
80 */
81 public static function unescape_cell( $value ) {
82 if ( ! is_string( $value ) || 2 > strlen( $value ) || "'" !== $value[0] ) {
83 return is_string( $value ) ? $value : '';
84 }
85
86 $stripped = ltrim( substr( $value, 1 ) );
87
88 if ( '' !== $stripped && in_array( $stripped[0], self::FORMULA_PREFIXES, true ) ) {
89 return substr( $value, 1 );
90 }
91
92 return $value;
93 }
94
95 /**
96 * Build a CSV string from an array of rows.
97 *
98 * Every cell is passed through escape_cell() so callers cannot forget the
99 * injection guard. The first row is typically the header labels (escaping
100 * static labels is a harmless no-op). Uses php://temp — an in-memory
101 * stream that spills to a temp file only if the payload is large — so
102 * nothing is written to a web-reachable path.
103 *
104 * @param array<int, array<int, mixed>> $rows Rows, each an ordered array of cell values.
105 * @return string CSV content, or an empty string on failure / no rows.
106 * @since 1.3.0
107 */
108 public static function build( array $rows ) {
109 if ( empty( $rows ) ) {
110 return '';
111 }
112
113 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Writing to an in-memory stream, not the filesystem.
114 $stream = fopen( 'php://temp', 'r+' );
115 if ( false === $stream ) {
116 return '';
117 }
118
119 foreach ( $rows as $row ) {
120 if ( ! is_array( $row ) ) {
121 continue;
122 }
123 $escaped = array_map( [ self::class, 'escape_cell' ], $row );
124 // Explicit empty $escape (RFC-4180): omitting it applies PHP's
125 // non-standard "\" escaping, which corrupts export->re-import
126 // round-trips for values ending in a backslash and is deprecated
127 // as of PHP 8.4. Valid on the plugin's PHP 7.4+ floor.
128 fputcsv( $stream, $escaped, ',', '"', '' );
129 }
130
131 rewind( $stream );
132 $csv = stream_get_contents( $stream );
133 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing an in-memory stream.
134 fclose( $stream );
135
136 return is_string( $csv ) ? $csv : '';
137 }
138 }
139