PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.14
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.14
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Services / Transfer / TransferService.php

TransferService.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.14, at app/Services/Transfer/TransferService.php

801 lines 31.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\App\Services\Transfer;
4
5 defined('ABSPATH') or die;
6
7 use Exception;
8 use FluentForm\App\Helpers\Helper;
9 use FluentForm\App\Models\Form;
10 use FluentForm\App\Models\FormMeta;
11 use FluentForm\App\Models\Submission;
12 use FluentForm\App\Models\SubmissionMeta;
13 use FluentForm\App\Modules\Acl\Acl;
14 use FluentForm\App\Modules\Form\FormDataParser;
15 use FluentForm\App\Modules\Form\FormFieldsParser;
16 use FluentForm\App\Services\FormBuilder\ShortCodeParser;
17 use FluentForm\App\Services\FormBuilder\DateConfigPolicy;
18 use FluentForm\Framework\Foundation\App;
19 use FluentForm\Framework\Http\Request\File;
20 use FluentForm\Framework\Support\Arr;
21
22 class TransferService
23 {
24 public static function sanitizeImportedMetaValue($metaKey, $metaValue)
25 {
26 if (!is_string($metaValue)) {
27 return $metaValue;
28 }
29
30 if ($metaKey === '_custom_form_css') {
31 return fluentformSanitizeCSS($metaValue);
32 }
33
34 if ($metaKey === '_custom_form_js') {
35 return fluentform_kses_js($metaValue);
36 }
37
38 $decoded = json_decode($metaValue);
39 if (is_array($decoded) || is_object($decoded)) {
40 self::sanitizeJsonNode($decoded);
41 $encoded = wp_json_encode($decoded);
42 return $encoded ?: $metaValue;
43 }
44
45 return wp_kses_post($metaValue);
46 }
47
48 private static function sanitizeJsonNode(&$node)
49 {
50 if (is_array($node)) {
51 foreach ($node as $key => &$value) {
52 if ('attributes' === $key) {
53 $value = self::dropEventHandlerAttributeKeys($value);
54 }
55 $value = self::sanitizeAttributeControlSetting($key, $value);
56 self::sanitizeJsonNode($value);
57 }
58 unset($value);
59 } elseif (is_object($node)) {
60 foreach (get_object_vars($node) as $key => $value) {
61 if ('attributes' === $key) {
62 $value = self::dropEventHandlerAttributeKeys($value);
63 }
64 $value = self::sanitizeAttributeControlSetting($key, $value);
65 self::sanitizeJsonNode($value);
66 $node->{$key} = $value;
67 }
68 } elseif (is_string($node)) {
69 $node = wp_kses_post($node);
70 }
71 }
72
73 private static function sanitizeAttributeControlSetting($key, $value)
74 {
75 if ('max_repeat_field' === $key) {
76 return is_scalar($value) && '' !== trim((string) $value) ? absint($value) : '';
77 }
78
79 if ('display_mode' === $key) {
80 $mode = is_scalar($value) ? sanitize_key((string) $value) : '';
81 return in_array($mode, ['accordion', 'tabs'], true) ? $mode : 'accordion';
82 }
83
84 if ('display_type' === $key) {
85 return is_scalar($value) ? sanitize_html_class((string) $value) : '';
86 }
87
88 if ('subscription_options' === $key && is_array($value)) {
89 foreach ($value as &$option) {
90 if (!is_array($option)) {
91 continue;
92 }
93
94 foreach (['name', 'user_input_label'] as $labelKey) {
95 if (!array_key_exists($labelKey, $option)) {
96 continue;
97 }
98
99 $label = $option[$labelKey];
100 $option[$labelKey] = is_scalar($label) ? fluentform_sanitize_html((string) $label) : '';
101 }
102 }
103 unset($option);
104
105 return $value;
106 }
107
108 if ('pricing_options' !== $key || !is_array($value)) {
109 return $value;
110 }
111
112 foreach ($value as &$option) {
113 if (!is_array($option)) {
114 continue;
115 }
116
117 if (array_key_exists('label', $option)) {
118 $label = $option['label'];
119 $option['label'] = is_scalar($label) ? fluentform_sanitize_html((string) $label) : '';
120 }
121
122 if (array_key_exists('image', $option)) {
123 $image = $option['image'];
124 $option['image'] = is_scalar($image) ? esc_url_raw((string) $image) : '';
125 }
126 }
127 unset($option);
128
129 return $value;
130 }
131
132 /**
133 * kses cleans string values only, so an `onfocus` KEY survives an import untouched.
134 * Shares the Helper rule so the two write paths cannot drift apart.
135 */
136 private static function dropEventHandlerAttributeKeys($attributes)
137 {
138 if (!is_array($attributes) && !is_object($attributes)) {
139 return $attributes;
140 }
141
142 $keys = is_object($attributes)
143 ? array_keys(get_object_vars($attributes))
144 : array_keys($attributes);
145
146 foreach ($keys as $key) {
147 if (Helper::isSafeAttributeKey($key)) {
148 continue;
149 }
150
151 if (is_object($attributes)) {
152 unset($attributes->{$key});
153 } else {
154 unset($attributes[$key]);
155 }
156 }
157
158 return $attributes;
159 }
160
161 public static function exportForms($formIds)
162 {
163 $result = Form::with(['formMeta'])
164 ->whereIn('id', $formIds)
165 ->get();
166 $forms = [];
167 foreach ($result as $item) {
168 $form = json_decode($item);
169 $formMetaFiltered = array_filter($form->form_meta, function ($item) {
170 return ($item->meta_key !== '_total_views');
171 });
172 $form->metas = $formMetaFiltered;
173 $form->form_fields = json_decode($form->form_fields);
174 $forms[] = $form;
175 }
176
177 $fileName = 'fluentform-export-forms-' . count($forms) . '-' . date('d-m-Y') . '.json';
178
179 header('Content-disposition: attachment; filename=' . $fileName);
180
181 header('Content-type: application/json');
182
183 echo json_encode(array_values($forms)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $forms is escaped before being passed in.
184
185 die();
186 }
187
188 /**
189 * Build the notice shown when imported custom JS/CSS or executable date
190 * configuration was skipped because the importer lacks unfiltered_html. Returns an empty string when nothing was skipped.
191 *
192 * @param int $skippedForms
193 * @param int $totalForms
194 * @return string
195 */
196 protected static function restrictedCodeNotice($skippedForms, $totalForms)
197 {
198 if (!$skippedForms) {
199 return '';
200 }
201
202 if ($totalForms < 2) {
203 return __('Custom JS, CSS and advanced date configuration were not imported because your account cannot add custom code. Ask an administrator to add it.', 'fluentform');
204 }
205
206 return sprintf(
207 /* translators: 1: number of forms whose custom code was skipped, 2: total number of imported forms */
208 __('Custom JS, CSS and advanced date configuration were not imported for %1$d of %2$d forms because your account cannot add custom code. Ask an administrator to add it.', 'fluentform'),
209 $skippedForms,
210 $totalForms
211 );
212 }
213
214 /**
215 * @param File $file The uploaded JSON file
216 * @param bool $applyDefaultStyle Whether to apply default style settings to imported forms
217 * @throws Exception
218 */
219 public static function importForms($file, $applyDefaultStyle = false)
220 {
221 if ($file instanceof File) {
222 $forms = \json_decode($file->getContents(), true);
223 $insertedForms = [];
224 $restrictedCodeForms = 0;
225 if ($forms && is_array($forms)) {
226 foreach ($forms as $formItem) {
227 $formFields = json_encode([]);
228 if ($fields = Arr::get($formItem, 'form', '')) {
229 $formFields = json_encode($fields);
230 } elseif ($fields = Arr::get($formItem, 'form_fields', '')) {
231 $formFields = json_encode($fields);
232 } else {
233 throw new Exception(esc_html__('You have a faulty JSON file, please export the Fluent Forms again.', 'fluentform'));
234 }
235
236 // SECURITY (FINDING-07): the editor save path routes form_fields through
237 // Updater::sanitizeFields (skipped only for unfiltered_html users), but import
238 // stored them verbatim, so an importer without unfiltered_html could plant
239 // stored XSS (e.g. a field label of <img onerror=...>). Apply the same
240 // recursive HTML sanitizer used for imported meta values unless the importer
241 // may author raw HTML.
242 $droppedDateConfigs = 0;
243 if (!fluentformCanUnfilteredHTML()) {
244 $decodedFields = json_decode($formFields, true);
245 if (is_array($decodedFields)) {
246 self::sanitizeJsonNode($decodedFields);
247 $decodedFields['fields'] = DateConfigPolicy::dropExecutableConfigs(
248 Arr::get($decodedFields, 'fields', []),
249 $droppedDateConfigs
250 );
251 $formFields = wp_json_encode($decodedFields) ?: $formFields;
252 }
253 }
254
255 $formTitle = sanitize_text_field(Arr::get($formItem, 'title'));
256 $form = [
257 'title' => $formTitle ?: 'Blank Form',
258 'form_fields' => $formFields,
259 'status' => sanitize_text_field(Arr::get($formItem, 'status', 'published')),
260 'has_payment' => sanitize_text_field(Arr::get($formItem, 'has_payment', 0)),
261 'type' => sanitize_text_field(Arr::get($formItem, 'type', 'form')),
262 'created_by' => get_current_user_id(),
263 ];
264
265 if (Arr::get($formItem, 'conditions')) {
266 $form['conditions'] = Arr::get($formItem, 'conditions');
267 }
268
269 if (isset($formItem['appearance_settings'])) {
270 $form['appearance_settings'] = Arr::get($formItem, 'appearance_settings');
271 }
272
273 $formId = Form::insertGetId($form);
274 $insertedForms[$formId] = [
275 'title' => $form['title'],
276 'edit_url' => admin_url('admin.php?page=fluent_forms&route=editor&form_id=' . $formId),
277 ];
278
279 $skippedCustomCode = $droppedDateConfigs > 0;
280
281 if (isset($formItem['metas'])) {
282 foreach ($formItem['metas'] as $metaData) {
283 $metaKey = sanitize_text_field(Arr::get($metaData, 'meta_key'));
284 $metaValue = Arr::get($metaData, 'value');
285 // SECURITY (FINDING-08): Customizer::store() refuses to save custom
286 // JS/CSS without unfiltered_html; import must honor the same boundary.
287 // Sanitizing _custom_form_js via fluentform_kses_js is insufficient
288 // because the value is JS *code* executed inside a <script> block (kses
289 // only strips <script> tags), so skip these keys entirely for importers
290 // who cannot author raw JS/CSS.
291 if (
292 in_array($metaKey, ['_custom_form_js', '_custom_form_css'], true)
293 && !fluentformCanUnfilteredHTML()
294 ) {
295 $skippedCustomCode = true;
296 continue;
297 }
298 if ('ffc_form_settings_generated_css' == $metaKey || 'ffc_form_settings_meta' == $metaKey) {
299 $metaValue = str_replace('ff_conv_app_' . Arr::get($formItem, 'id'), 'ff_conv_app_' . $formId, $metaValue);
300 }
301 $metaValue = static::sanitizeImportedMetaValue($metaKey, $metaValue);
302 $settings = [
303 'form_id' => $formId,
304 'meta_key' => $metaKey,
305 'value' => $metaValue,
306 ];
307 FormMeta::insert($settings);
308 }
309 } else {
310 $oldKeys = [
311 'formSettings',
312 'notifications',
313 'mailchimp_feeds',
314 'slack',
315 ];
316 foreach ($oldKeys as $key) {
317 if (isset($formItem[$key])) {
318 FormMeta::persist($formId, $key, json_encode(Arr::get($formItem, $key)));
319 }
320 }
321 }
322
323 if ($skippedCustomCode) {
324 $restrictedCodeForms++;
325 }
326
327 do_action('fluentform/form_imported', $formId);
328
329 // Apply default style if requested
330 if ($applyDefaultStyle) {
331 do_action('fluentform/inserted_new_form', $formId, $form);
332 }
333 }
334
335 return ([
336 'message' => __('You form has been successfully imported.', 'fluentform'),
337 'inserted_forms' => $insertedForms,
338 'restricted_code_notice' => static::restrictedCodeNotice($restrictedCodeForms, count($insertedForms)),
339 ]);
340 }
341 }
342 throw new Exception(esc_html__('You have a faulty JSON file, please export the Fluent Forms again.', 'fluentform'));
343 }
344
345 public static function exportEntries($args)
346 {
347 if (!defined('FLUENTFORM_EXPORTING_ENTRIES')) {
348 define('FLUENTFORM_EXPORTING_ENTRIES', true);
349 }
350
351 $formId = Acl::verifyFormId(Arr::get($args, 'form_id'));
352 Acl::verify('fluentform_entries_viewer', $formId);
353
354 $tableName = Arr::get($args, 'table');
355 try {
356 $form = Form::findOrFail($formId);
357 } catch (Exception $e) {
358 exit('No Form Found');
359 }
360 $type = sanitize_key(Arr::get($args, 'format', 'csv'));
361 if (!in_array($type, ['csv', 'ods', 'xlsx', 'json'])) {
362 exit('Invalid requested format');
363 }
364 if ('json' == $type) {
365 self::exportAsJSON($form, $args);
366 }
367 if (!defined('FLUENTFORM_DOING_CSV_EXPORT')) {
368 define('FLUENTFORM_DOING_CSV_EXPORT', true);
369 }
370 $formInputs = FormFieldsParser::getEntryInputs($form, ['admin_label', 'raw']);
371 $inputLabels = FormFieldsParser::getAdminLabels($form, $formInputs);
372 $selectedLabels = Arr::get($args, 'fields_to_export');
373 if (is_string($selectedLabels) && Helper::isJson($selectedLabels)) {
374 $selectedLabels = \json_decode($selectedLabels, true);
375 }
376 $selectedLabels = fluentFormSanitizer($selectedLabels);
377
378 $withNotes = isset($args['with_notes']);
379
380 //filter out unselected fields
381 if (!empty($selectedLabels)) {
382 foreach ($inputLabels as $key => $value) {
383 if (!in_array($key, $selectedLabels) && isset($inputLabels[$key])) {
384 unset($inputLabels[$key]);
385 }
386 }
387 }
388
389 $submissions = self::getSubmissions($args);
390 $submissions = FormDataParser::parseFormEntries($submissions, $form, $formInputs);
391 $parsedShortCodes = [];
392 $exportData = [];
393 $selectedShortcodes = self::getSelectedExportShortcodes($args, $form);
394 $legacyShortcodeHeaders = self::getLegacyExportShortcodeHeaders();
395
396 // Preload notes for all submissions in a single query to avoid N+1
397 $notesMap = [];
398 if ($withNotes && count($submissions)) {
399 $submissionIds = array_map(function ($s) {
400 return is_object($s) ? $s->id : $s['id'];
401 }, $submissions->toArray());
402 $allNotes = SubmissionMeta::whereIn('response_id', $submissionIds)
403 ->where('meta_key', '_notes')
404 ->get();
405 foreach ($allNotes as $note) {
406 $notesMap[$note->response_id][] = $note->value;
407 }
408 }
409
410 foreach ($submissions as $submission) {
411
412 $submission->response = json_decode($submission->response, true);
413
414 $temp = [];
415 foreach ($inputLabels as $field => $label) {
416
417 //format tabular grid data for CSV/XLSV/ODS export
418 if (isset($formInputs[$field]['element']) && 'tabular_grid' === $formInputs[$field]['element']) {
419 $gridRawData = Arr::get($submission->response, $field);
420 $content = Helper::getTabularGridFormatValue($gridRawData, Arr::get($formInputs, $field), ' | ');
421 } elseif (isset($formInputs[$field]['element']) && 'subscription_payment_component' === $formInputs[$field]['element']) {
422 //resolve plane name for subscription field
423 $planIndex = Arr::get($submission->user_inputs, $field);
424 $planLabel = Arr::get($formInputs, "{$field}.raw.settings.subscription_options.{$planIndex}.name");
425 if ($planLabel) {
426 $content = $planLabel;
427 } else {
428 $content = self::getFieldExportContent($submission, $field);
429 }
430 } else {
431 $content = self::getFieldExportContent($submission, $field);
432 if (Arr::get($formInputs, $field . '.element') === 'input_number' && is_numeric($content)) {
433 $content = $content + 0;
434 }
435 }
436 $temp[] = Helper::sanitizeForCSV($content);
437 }
438
439 if (!empty($selectedShortcodes)) {
440 $regularShortcodes = self::getRegularExportShortcodes($selectedShortcodes, $legacyShortcodeHeaders);
441
442 if (!empty($regularShortcodes)) {
443 $parsedShortCodes = ShortCodeParser::parse(
444 $regularShortcodes,
445 $submission->id,
446 $submission->response,
447 $form,
448 false,
449 true
450 );
451 }
452
453 // SECURITY (FINDING-17): shortcode-export values (which include submitter-controlled
454 // {inputs.*} content) bypassed the CSV formula guard applied to regular columns.
455 // Sanitize each so a leading = - + @ etc. cannot execute when opened in a spreadsheet.
456 $shortcodeValues = self::getSelectedShortcodeExportValues(
457 $selectedShortcodes,
458 $parsedShortCodes,
459 $legacyShortcodeHeaders,
460 $submission
461 );
462 $shortcodeValues = array_map(function ($v) {
463 return is_scalar($v) ? Helper::sanitizeForCSV((string) $v) : $v;
464 }, $shortcodeValues);
465 $temp = array_merge($temp, $shortcodeValues);
466 }
467 if ($withNotes) {
468 $noteValues = isset($notesMap[$submission->id]) ? $notesMap[$submission->id] : [];
469 if (!empty($noteValues)) {
470 // SECURITY (FINDING-17): notes are submitter-influenceable and were exported raw.
471 $temp[] = Helper::sanitizeForCSV(implode(", ", $noteValues));
472 }
473 }
474
475 $temp = apply_filters('fluentform/export_entry_metadata', $temp, $submission, $form, $args);
476
477 $exportData[] = $temp;
478 }
479
480 $extraLabels = [];
481
482 $extraLabels = self::getSelectedShortcodeExportLabels(
483 $selectedShortcodes,
484 $parsedShortCodes,
485 $legacyShortcodeHeaders
486 );
487
488 $inputLabels = array_merge($inputLabels, $extraLabels);
489 if ($withNotes) {
490 $inputLabels[] = __('Notes', 'fluentform');
491 }
492 $inputLabels = apply_filters('fluentform/export_entry_metadata_labels', $inputLabels, $form, $args);
493
494 // SECURITY (FINDING-17): sanitize the header row too — field/shortcode labels can start with
495 // a formula lead character (=, +, -, @) and were exported unguarded.
496 $headerRow = array_map(function ($v) {
497 return is_scalar($v) ? Helper::sanitizeForCSV((string) $v) : $v;
498 }, array_values($inputLabels));
499 $data = array_merge([$headerRow], $exportData);
500
501 $data = apply_filters('fluentform/export_data', $data, $form, $exportData, $inputLabels);
502 $fileName = self::getReadableExportFileName($form->title);
503 self::downloadOfficeDoc($data, $type, $fileName);
504 }
505
506 private static function getFieldExportContent($submission, $fieldName)
507 {
508 return trim(
509 wp_strip_all_tags(
510 FormDataParser::formatValue(
511 Arr::get($submission->user_inputs, $fieldName)
512 )
513 )
514 );
515 }
516
517 private static function getSelectedExportShortcodes($args, $form)
518 {
519 $selectedShortcodes = fluentFormSanitizer(Arr::get($args, 'shortcodes_to_export', []));
520
521 if (!Arr::has($args, 'shortcodes_to_export_defined') && empty($selectedShortcodes)) {
522 return self::getDefaultExportShortcodes($form);
523 }
524
525 return $selectedShortcodes;
526 }
527
528 private static function getDefaultExportShortcodes($form)
529 {
530 $defaults = [
531 [
532 'label' => __('Submission ID', 'fluentform'),
533 'value' => '{submission.id}',
534 ],
535 [
536 'label' => __('Submission Create Date', 'fluentform'),
537 'value' => '{submission.created_at}',
538 ],
539 [
540 'label' => __('Submission Status', 'fluentform'),
541 'value' => '{submission.status}',
542 ],
543 ];
544
545 if ($form->has_payment) {
546 $defaults[] = [
547 'label' => __('Payment Status', 'fluentform'),
548 'value' => '{payment.payment_status}',
549 ];
550 $defaults[] = [
551 'label' => __('Payment Total', 'fluentform'),
552 'value' => '{payment.payment_total}',
553 ];
554 $defaults[] = [
555 'label' => __('Currency', 'fluentform'),
556 'value' => '{submission.currency}',
557 ];
558 }
559
560 return $defaults;
561 }
562
563 private static function getLegacyExportShortcodeHeaders()
564 {
565 return [
566 '{submission.id}' => 'entry_id',
567 '{submission.status}' => 'entry_status',
568 '{submission.created_at}' => 'created_at',
569 '{payment.payment_status}' => 'payment_status',
570 '{submission.payment_status}' => 'payment_status',
571 '{payment.payment_total}' => 'payment_total',
572 '{submission.payment_total}' => 'payment_total',
573 '{submission.currency}' => 'currency',
574 ];
575 }
576
577 private static function getRegularExportShortcodes($selectedShortcodes, $legacyShortcodeHeaders)
578 {
579 $regularShortcodes = [];
580
581 foreach ($selectedShortcodes as $index => $shortcode) {
582 if (!isset($legacyShortcodeHeaders[Arr::get($shortcode, 'value')])) {
583 $regularShortcodes[$index] = $shortcode;
584 }
585 }
586
587 return $regularShortcodes;
588 }
589
590 private static function getSelectedShortcodeExportValues($selectedShortcodes, $parsedShortCodes, $legacyShortcodeHeaders, $submission)
591 {
592 $values = [];
593
594 foreach ($selectedShortcodes as $index => $shortcode) {
595 $shortcodeValue = Arr::get($shortcode, 'value');
596
597 if (!isset($legacyShortcodeHeaders[$shortcodeValue])) {
598 $values[] = Arr::get($parsedShortCodes, $index . '.value');
599 continue;
600 }
601
602 $values[] = self::getLegacyExportValue($legacyShortcodeHeaders[$shortcodeValue], $submission);
603 }
604
605 return $values;
606 }
607
608 private static function getSelectedShortcodeExportLabels($selectedShortcodes, $parsedShortCodes, $legacyShortcodeHeaders)
609 {
610 $labels = [];
611
612 foreach ($selectedShortcodes as $index => $shortcode) {
613 $shortcodeValue = Arr::get($shortcode, 'value');
614
615 if (isset($legacyShortcodeHeaders[$shortcodeValue])) {
616 $labels[] = $legacyShortcodeHeaders[$shortcodeValue];
617 continue;
618 }
619
620 $labels[] = Arr::get($parsedShortCodes, $index . '.label');
621 }
622
623 return $labels;
624 }
625
626 private static function getLegacyExportValue($header, $submission)
627 {
628 $legacyValueResolvers = [
629 'entry_id' => function ($submission) {
630 return $submission->id ?? '';
631 },
632 'entry_status' => function ($submission) {
633 return $submission->status ?? '';
634 },
635 'created_at' => function ($submission) {
636 return $submission->created_at ?? '';
637 },
638 'payment_status' => function ($submission) {
639 return $submission->payment_status ?? '';
640 },
641 'payment_total' => function ($submission) {
642 return round(($submission->payment_total ?? 0) / 100, 1);
643 },
644 'currency' => function ($submission) {
645 return $submission->currency ?? '';
646 },
647 ];
648
649 if (!isset($legacyValueResolvers[$header])) {
650 return '';
651 }
652
653 return $legacyValueResolvers[$header]($submission);
654 }
655
656 private static function exportAsJSON($form, $args)
657 {
658 $formInputs = FormFieldsParser::getEntryInputs($form, ['admin_label', 'raw']);
659 $submissions = self::getSubmissions($args);
660 $submissions = FormDataParser::parseFormEntries($submissions, $form, $formInputs);
661 foreach ($submissions as $submission) {
662 $submission->response = json_decode($submission->response, true);
663 }
664 self::sendDownloadHeaders('application/json', self::getReadableExportFileName($form->title) . '.json');
665 echo json_encode($submissions); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $submissions is escaped before being passed in.
666 exit();
667 }
668
669 private static function getReadableExportFileName($formTitle)
670 {
671 $sanitizedTitle = sanitize_file_name(wp_strip_all_tags((string) $formTitle));
672
673 if (!$sanitizedTitle) {
674 $sanitizedTitle = 'export';
675 }
676
677 return $sanitizedTitle . '-' . date('Y-m-d');
678 }
679
680 private static function sendDownloadHeaders($contentType, $fileName)
681 {
682 $safeFileName = basename((string) $fileName);
683 $encodedFileName = rawurlencode($safeFileName);
684
685 header('Content-Type: ' . $contentType);
686 header(
687 'Content-Disposition: attachment; ' .
688 'filename="' . $encodedFileName . '"; ' .
689 'filename*=UTF-8\'\'' . $encodedFileName
690 );
691 }
692
693 private static function getSubmissions($args)
694 {
695 $tableName = Arr::get($args, 'table');
696
697 if ($tableName) {
698 $allowedTables = [
699 'fluentform_submissions',
700 'fluentform_draft_submissions',
701 ];
702 if (!in_array($tableName, $allowedTables, true)) {
703 wp_send_json([
704 'message' => __('Invalid table name for export.', 'fluentform'),
705 ], 422);
706 }
707 $query = wpFluent()->table($tableName)
708 ->where('form_id', (int) Arr::get($args, 'form_id'))
709 ->orderBy('id', Helper::sanitizeOrderValue(Arr::get($args, 'sort_by', 'DESC')));
710
711 $searchString = Arr::get($args, 'search');
712 if ($searchString) {
713 global $wpdb;
714 $escaped = $wpdb->esc_like($searchString);
715 $query->where(function ($q) use ($escaped) {
716 $q->where('id', 'LIKE', "%{$escaped}%")
717 ->orWhere('response', 'LIKE', "%{$escaped}%");
718 });
719 }
720 } else {
721 $query = (new Submission())->customQuery($args);
722 }
723
724 $entries = fluentFormSanitizer(Arr::get($args, 'entries', []));
725 $query->when(is_array($entries) && (count($entries) > 0), function ($q) use ($entries) {
726 return $q->whereIn('id', $entries);
727 });
728
729 if (Arr::get($args, 'advanced_filter')) {
730 $query = apply_filters('fluentform/apply_entries_advance_filter', $query, $args);
731 }
732
733 return $query->get();
734 }
735
736 private static function downloadOfficeDoc($data, $type = 'csv', $fileName = null)
737 {
738 $data = array_map(function ($item) {
739 return array_map(function ($itemValue) {
740 if (is_array($itemValue)) {
741 $itemValue = implode(', ', $itemValue);
742 }
743
744 return is_string($itemValue)
745 ? Helper::sanitizeForCSV($itemValue)
746 : $itemValue;
747 }, $item);
748 }, $data);
749 // Load Composer autoloader for OpenSpout
750 require_once FLUENTFORM_DIR_PATH . '/vendor/autoload.php';
751 $fileName = ($fileName) ? $fileName . '.' . $type : 'export-data-' . date('d-m-Y') . '.' . $type;
752
753 // Create writer based on type
754 switch (strtolower($type)) {
755 case 'csv':
756 $writer = \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createCSVWriter();
757 break;
758 case 'xlsx':
759 $writer = \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createXLSXWriter();
760 break;
761 case 'ods':
762 $writer = \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createODSWriter();
763 break;
764 default:
765 throw new \Exception(sprintf('Unsupported file type: %s', esc_html($type)));
766 }
767 $writer->openToBrowser($fileName);
768
769 $rows = self::getOfficeDocRows($data, $type);
770
771 $writer->addRows($rows);
772 $writer->close();
773 die();
774 }
775
776 private static function getOfficeDocRows($data, $type)
777 {
778 if (strtolower($type) !== 'xlsx') {
779 return array_map(function ($rowData) {
780 return \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createRowFromArray($rowData);
781 }, $data);
782 }
783
784 $dateStyle = (new \OpenSpout\Writer\Common\Creator\Style\StyleBuilder())
785 ->setFormat('yyyy-mm-dd hh:mm:ss')
786 ->build();
787
788 return array_map(function ($rowData) use ($dateStyle) {
789 $cells = array_map(function ($cellValue) use ($dateStyle) {
790 if ($cellValue instanceof \DateTimeInterface) {
791 return \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createCell($cellValue, $dateStyle);
792 }
793
794 return \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createCell($cellValue);
795 }, $rowData);
796
797 return \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createRow($cells);
798 }, $data);
799 }
800 }
801