PluginProbe ʕ •ᴥ•ʔ
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More / 4.2.0
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More v4.2.0
4.2.0 4.1.0 4.0.9 4.0.8 4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 trunk 1.0.0 2.0.0 2.0.1 2.0.2 2.0.3 3.0 3.0.1 3.0.2 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.2.0 3.2.1 3.2.2 3.2.4 3.2.5 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.4.5 3.4.6 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.6 3.5.7 3.5.8 3.5.9 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1
superb-blocks / src / gutenberg / form / class-form-exporter.php
superb-blocks / src / gutenberg / form Last commit date
class-form-access-control.php 3 weeks ago class-form-captcha-handler.php 2 weeks ago class-form-controller.php 2 days ago class-form-email-config-check.php 3 weeks ago class-form-email-handler.php 2 days ago class-form-encryption.php 3 weeks ago class-form-exporter.php 2 days ago class-form-field-validator.php 3 weeks ago class-form-file-handler.php 2 days ago class-form-google-auth.php 3 weeks ago class-form-integration-handler.php 3 weeks ago class-form-math-parser.php 3 weeks ago class-form-permissions.php 3 weeks ago class-form-registry.php 3 weeks ago class-form-settings.php 3 weeks ago class-form-submission-cpt.php 3 weeks ago class-form-submission-handler.php 2 days ago class-form-zip-exporter.php 2 days ago
class-form-exporter.php
334 lines
1 <?php
2
3 namespace SuperbAddons\Gutenberg\Form;
4
5 defined('ABSPATH') || exit();
6
7 class FormExporter
8 {
9 /**
10 * Fetch every submission matching the filters (or the given IDs) together
11 * with the field metadata the export formats need.
12 *
13 * @param string $form_id
14 * @param array $form_fields Field definitions from form config.
15 * @param bool $include_sensitive Whether to decrypt sensitive fields.
16 * @param string $status Filter by status ('new', 'read', 'spam', or '').
17 * @param string $starred Filter by starred ('1' or '').
18 * @param string $search Search term to filter submissions.
19 * @param string $date_after ISO date string for date range start.
20 * @param string $date_before ISO date string for date range end.
21 * @param bool $pending_delete Whether the form is pending deletion (config unavailable).
22 * @param array|null $ids Restrict to these submission IDs; null applies the filters only.
23 * @return array {
24 * 'submissions' => list of submissions (sensitive fields decrypted when requested),
25 * 'field_order' => field IDs in form order,
26 * 'field_labels' => field ID => label,
27 * 'sensitive_field_ids' => field IDs flagged sensitive,
28 * }
29 */
30 public static function Collect($form_id, $form_fields, $include_sensitive, $status = '', $starred = '', $search = '', $date_after = '', $date_before = '', $pending_delete = false, $ids = null)
31 {
32 // Build field label map and sensitive field list
33 $field_labels = array();
34 $sensitive_field_ids = array();
35 $field_order = array();
36 foreach ($form_fields as $field) {
37 if (!isset($field['fieldId'])) {
38 continue;
39 }
40 $fid = $field['fieldId'];
41 $field_order[] = $fid;
42 $field_labels[$fid] = isset($field['label']) ? $field['label'] : $fid;
43 if (!empty($field['sensitive'])) {
44 $sensitive_field_ids[] = $fid;
45 }
46 }
47
48 // Fetch all matching submissions (no pagination)
49 $all_submissions = array();
50 $page = 1;
51 $batch_size = 100;
52 do {
53 $result = FormSubmissionHandler::GetSubmissions($form_id, $page, $batch_size, $status, $starred, $search, $date_after, $date_before, $ids);
54 if (empty($result['submissions'])) {
55 break;
56 }
57 foreach ($result['submissions'] as $sub) {
58 // When config is gone, derive field order and sensitive IDs from submission data
59 if ($pending_delete && empty($form_fields)) {
60 foreach ($sub['fields'] as $fid => $value) {
61 if (!in_array($fid, $field_order, true)) {
62 $field_order[] = $fid;
63 $field_labels[$fid] = $fid;
64 }
65 if (FormEncryption::IsEncrypted($value) && !in_array($fid, $sensitive_field_ids, true)) {
66 $sensitive_field_ids[] = $fid;
67 }
68 }
69 }
70 // Decrypt sensitive fields if included
71 if ($include_sensitive) {
72 $sub['fields'] = self::DecryptFields($form_fields, $sub['fields'], $pending_delete);
73 }
74 $all_submissions[] = $sub;
75 }
76 $page++;
77 } while (count($result['submissions']) === $batch_size);
78
79 return array(
80 'submissions' => $all_submissions,
81 'field_order' => $field_order,
82 'field_labels' => $field_labels,
83 'sensitive_field_ids' => $sensitive_field_ids,
84 );
85 }
86
87 /**
88 * Field IDs to export as columns: the user's saved preference when given,
89 * otherwise every field, always minus sensitive fields the caller may not see.
90 *
91 * @param array $collected Result of Collect().
92 * @param bool $include_sensitive
93 * @param array|null $export_fields Saved field preference, or null for all fields.
94 * @return array
95 */
96 public static function VisibleFields($collected, $include_sensitive, $export_fields = null)
97 {
98 $field_order = $collected['field_order'];
99 $sensitive_field_ids = $collected['sensitive_field_ids'];
100
101 $visible_fields = array();
102 if ($export_fields !== null) {
103 // Use user's field preference, respecting sensitive field visibility
104 foreach ($export_fields as $fid) {
105 if (!$include_sensitive && in_array($fid, $sensitive_field_ids, true)) {
106 continue;
107 }
108 if (in_array($fid, $field_order, true)) {
109 $visible_fields[] = $fid;
110 }
111 }
112 }
113 if (empty($visible_fields)) {
114 foreach ($field_order as $fid) {
115 if (!$include_sensitive && in_array($fid, $sensitive_field_ids, true)) {
116 continue;
117 }
118 $visible_fields[] = $fid;
119 }
120 }
121 return $visible_fields;
122 }
123
124 /**
125 * Download filename: <form name>-<date>.<extension>.
126 *
127 * @param string $form_id
128 * @param string $extension Without the dot.
129 * @return string
130 */
131 public static function ExportFilename($form_id, $extension)
132 {
133 $form_name = FormRegistry::GetName($form_id);
134 $safe_name = sanitize_file_name($form_name);
135 if (empty($safe_name)) {
136 $safe_name = 'form-export';
137 }
138 return $safe_name . '-' . gmdate('Y-m-d') . '.' . $extension;
139 }
140
141 /**
142 * Write the header row and one row per submission to an open stream.
143 *
144 * @param resource $output
145 * @param array $collected Result of Collect().
146 * @param array $visible_fields Result of VisibleFields().
147 * @param bool $include_notes
148 * @param array|null $folders Submission ID => folder name. When given (ZIP export),
149 * a "Files folder" column maps each row to its files.
150 */
151 public static function WriteCsv($output, $collected, $visible_fields, $include_notes, $folders = null)
152 {
153 $field_labels = $collected['field_labels'];
154
155 // Header row: Date, Status, [Files folder,] then field labels, optionally Notes
156 $header = array('Date', 'Status');
157 if (is_array($folders)) {
158 $header[] = 'Files folder';
159 }
160 foreach ($visible_fields as $fid) {
161 $header[] = isset($field_labels[$fid]) ? $field_labels[$fid] : $fid;
162 }
163 if ($include_notes) {
164 $header[] = 'Notes';
165 }
166 fputcsv($output, $header);
167
168 // Data rows
169 foreach ($collected['submissions'] as $sub) {
170 $row = array();
171
172 // Date
173 $date = isset($sub['date']) ? $sub['date'] : '';
174 if (!empty($date)) {
175 $timestamp = strtotime($date);
176 $row[] = $timestamp !== false ? gmdate('Y-m-d H:i:s', $timestamp) : $date;
177 } else {
178 $row[] = '';
179 }
180
181 // Status
182 $row[] = isset($sub['status']) && $sub['status'] === 'new' ? 'Unread' : 'Read';
183
184 // Files folder
185 if (is_array($folders)) {
186 $sub_id = isset($sub['id']) ? intval($sub['id']) : 0;
187 $row[] = isset($folders[$sub_id]) ? $folders[$sub_id] : '';
188 }
189
190 // Fields
191 $fields = isset($sub['fields']) ? $sub['fields'] : array();
192 foreach ($visible_fields as $fid) {
193 $value = isset($fields[$fid]) ? $fields[$fid] : '';
194 $row[] = self::FormatFieldValue($value);
195 }
196
197 // Notes
198 if ($include_notes) {
199 $sub_id = isset($sub['id']) ? intval($sub['id']) : 0;
200 $notes = $sub_id > 0 ? FormSubmissionHandler::GetNotes($sub_id) : array();
201 $note_texts = array();
202 foreach ($notes as $note) {
203 $note_texts[] = $note['author_name'] . ' (' . $note['date'] . '): ' . $note['text'];
204 }
205 $row[] = implode("\n", $note_texts);
206 }
207
208 fputcsv($output, $row);
209 }
210 }
211
212 /**
213 * The full CSV as a string (with UTF-8 BOM), for embedding in an archive.
214 *
215 * @param array $collected
216 * @param array $visible_fields
217 * @param bool $include_notes
218 * @param array|null $folders See WriteCsv().
219 * @return string
220 */
221 public static function BuildCsvString($collected, $visible_fields, $include_notes, $folders = null)
222 {
223 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- in-memory stream for fputcsv(); no file is touched.
224 $handle = fopen('php://temp', 'w+');
225 self::WriteCsv($handle, $collected, $visible_fields, $include_notes, $folders);
226 rewind($handle);
227 $csv = stream_get_contents($handle);
228 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
229 fclose($handle);
230 return "\xEF\xBB\xBF" . $csv;
231 }
232
233 /**
234 * Generate and stream a CSV export for a form's submissions.
235 *
236 * @param string $form_id
237 * @param array $form_fields Field definitions from form config.
238 * @param bool $include_sensitive Whether to include decrypted sensitive fields.
239 * @param string $status Filter by status ('new', 'read', 'spam', or '').
240 * @param string $starred Filter by starred ('1' or '').
241 * @param string $search Search term to filter submissions.
242 * @param string $date_after ISO date string for date range start.
243 * @param string $date_before ISO date string for date range end.
244 * @param bool $include_notes Whether to add a Notes column.
245 * @param array|null $export_fields Saved field preference, or null for all fields.
246 * @param bool $pending_delete Whether the form is pending deletion (config unavailable).
247 * @return void Streams CSV directly and exits.
248 */
249 public static function Export($form_id, $form_fields, $include_sensitive, $status = '', $starred = '', $search = '', $date_after = '', $date_before = '', $include_notes = false, $export_fields = null, $pending_delete = false)
250 {
251 $collected = self::Collect($form_id, $form_fields, $include_sensitive, $status, $starred, $search, $date_after, $date_before, $pending_delete);
252 $visible_fields = self::VisibleFields($collected, $include_sensitive, $export_fields);
253 $filename = self::ExportFilename($form_id, 'csv');
254
255 // Stream CSV
256 // Disable output buffering to stream directly
257 while (ob_get_level()) {
258 ob_end_clean();
259 }
260
261 header('Content-Type: text/csv; charset=UTF-8');
262 header('Content-Disposition: attachment; filename="' . $filename . '"');
263 header('Cache-Control: no-cache, no-store, must-revalidate');
264 header('Pragma: no-cache');
265 header('Expires: 0');
266
267 // UTF-8 BOM for Excel compatibility. Emitted via echo (same destination as the stream below) so we do not need fwrite, which the plugin checker flags.
268 echo "\xEF\xBB\xBF";
269
270 $output = fopen('php://output', 'w');
271 self::WriteCsv($output, $collected, $visible_fields, $include_notes);
272
273 exit;
274 }
275
276 /**
277 * Format a field value for CSV output.
278 * Arrays (file uploads) are formatted as comma-separated filenames.
279 *
280 * @param mixed $value
281 * @return string
282 */
283 private static function FormatFieldValue($value)
284 {
285 if (is_array($value)) {
286 // File upload field: array of {name, path, type}
287 $names = array();
288 foreach ($value as $file) {
289 if (is_array($file) && isset($file['name'])) {
290 $names[] = $file['name'];
291 }
292 }
293 return implode(', ', $names);
294 }
295 return is_string($value) ? $value : '';
296 }
297
298 /**
299 * Decrypt sensitive fields in submission data.
300 *
301 * @param array $form_fields
302 * @param array $fields
303 * @param bool $pending_delete Whether the form is pending deletion (config unavailable).
304 * @return array
305 */
306 private static function DecryptFields($form_fields, $fields, $pending_delete = false)
307 {
308 if (empty($form_fields) && $pending_delete) {
309 foreach ($fields as $fid => $value) {
310 if (FormEncryption::IsEncrypted($value)) {
311 $decrypted = FormEncryption::Decrypt($value);
312 if ($decrypted !== false) {
313 $fields[$fid] = $decrypted;
314 }
315 }
316 }
317 return $fields;
318 }
319
320 foreach ($form_fields as $field_def) {
321 if (!empty($field_def['sensitive']) && !empty($field_def['fieldId'])) {
322 $sfid = $field_def['fieldId'];
323 if (isset($fields[$sfid]) && is_string($fields[$sfid])) {
324 $decrypted = FormEncryption::Decrypt($fields[$sfid]);
325 if ($decrypted !== false) {
326 $fields[$sfid] = $decrypted;
327 }
328 }
329 }
330 }
331 return $fields;
332 }
333 }
334