PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.6
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.6
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.6, at app/Services/Transfer/TransferService.php

623 lines 23.6 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\Framework\Foundation\App;
18 use FluentForm\Framework\Http\Request\File;
19 use FluentForm\Framework\Support\Arr;
20
21 class TransferService
22 {
23 public static function sanitizeImportedMetaValue($metaKey, $metaValue)
24 {
25 if (!is_string($metaValue)) {
26 return $metaValue;
27 }
28
29 if ($metaKey === '_custom_form_css') {
30 return fluentformSanitizeCSS($metaValue);
31 }
32
33 if ($metaKey === '_custom_form_js') {
34 return fluentform_kses_js($metaValue);
35 }
36
37 $decoded = json_decode($metaValue);
38 if (is_array($decoded) || is_object($decoded)) {
39 self::sanitizeJsonNode($decoded);
40 $encoded = wp_json_encode($decoded);
41 return $encoded ?: $metaValue;
42 }
43
44 return wp_kses_post($metaValue);
45 }
46
47 private static function sanitizeJsonNode(&$node)
48 {
49 if (is_array($node)) {
50 foreach ($node as &$value) {
51 self::sanitizeJsonNode($value);
52 }
53 unset($value);
54 } elseif (is_object($node)) {
55 foreach (get_object_vars($node) as $key => $value) {
56 self::sanitizeJsonNode($value);
57 $node->{$key} = $value;
58 }
59 } elseif (is_string($node)) {
60 $node = wp_kses_post($node);
61 }
62 }
63
64 public static function exportForms($formIds)
65 {
66 $result = Form::with(['formMeta'])
67 ->whereIn('id', $formIds)
68 ->get();
69 $forms = [];
70 foreach ($result as $item) {
71 $form = json_decode($item);
72 $formMetaFiltered = array_filter($form->form_meta, function ($item) {
73 return ($item->meta_key !== '_total_views');
74 });
75 $form->metas = $formMetaFiltered;
76 $form->form_fields = json_decode($form->form_fields);
77 $forms[] = $form;
78 }
79
80 $fileName = 'fluentform-export-forms-' . count($forms) . '-' . date('d-m-Y') . '.json';
81
82 header('Content-disposition: attachment; filename=' . $fileName);
83
84 header('Content-type: application/json');
85
86 echo json_encode(array_values($forms)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $forms is escaped before being passed in.
87
88 die();
89 }
90
91 /**
92 * @param File $file The uploaded JSON file
93 * @param bool $applyDefaultStyle Whether to apply default style settings to imported forms
94 * @throws Exception
95 */
96 public static function importForms($file, $applyDefaultStyle = false)
97 {
98 if ($file instanceof File) {
99 $forms = \json_decode($file->getContents(), true);
100 $insertedForms = [];
101 if ($forms && is_array($forms)) {
102 foreach ($forms as $formItem) {
103 $formFields = json_encode([]);
104 if ($fields = Arr::get($formItem, 'form', '')) {
105 $formFields = json_encode($fields);
106 } elseif ($fields = Arr::get($formItem, 'form_fields', '')) {
107 $formFields = json_encode($fields);
108 } else {
109 throw new Exception(esc_html__('You have a faulty JSON file, please export the Fluent Forms again.', 'fluentform'));
110 }
111 $formTitle = sanitize_text_field(Arr::get($formItem, 'title'));
112 $form = [
113 'title' => $formTitle ?: 'Blank Form',
114 'form_fields' => $formFields,
115 'status' => sanitize_text_field(Arr::get($formItem, 'status', 'published')),
116 'has_payment' => sanitize_text_field(Arr::get($formItem, 'has_payment', 0)),
117 'type' => sanitize_text_field(Arr::get($formItem, 'type', 'form')),
118 'created_by' => get_current_user_id(),
119 ];
120
121 if (Arr::get($formItem, 'conditions')) {
122 $form['conditions'] = Arr::get($formItem, 'conditions');
123 }
124
125 if (isset($formItem['appearance_settings'])) {
126 $form['appearance_settings'] = Arr::get($formItem, 'appearance_settings');
127 }
128
129 $formId = Form::insertGetId($form);
130 $insertedForms[$formId] = [
131 'title' => $form['title'],
132 'edit_url' => admin_url('admin.php?page=fluent_forms&route=editor&form_id=' . $formId),
133 ];
134
135 if (isset($formItem['metas'])) {
136 foreach ($formItem['metas'] as $metaData) {
137 $metaKey = sanitize_text_field(Arr::get($metaData, 'meta_key'));
138 $metaValue = Arr::get($metaData, 'value');
139 if ("ffc_form_settings_generated_css" == $metaKey || "ffc_form_settings_meta" == $metaKey) {
140 $metaValue = str_replace('ff_conv_app_' . Arr::get($formItem, 'id'), 'ff_conv_app_' . $formId, $metaValue);
141 }
142 $metaValue = static::sanitizeImportedMetaValue($metaKey, $metaValue);
143 $settings = [
144 'form_id' => $formId,
145 'meta_key' => $metaKey,
146 'value' => $metaValue,
147 ];
148 FormMeta::insert($settings);
149 }
150 } else {
151 $oldKeys = [
152 'formSettings',
153 'notifications',
154 'mailchimp_feeds',
155 'slack',
156 ];
157 foreach ($oldKeys as $key) {
158 if (isset($formItem[$key])) {
159 FormMeta::persist($formId, $key, json_encode(Arr::get($formItem, $key)));
160 }
161 }
162 }
163
164 do_action('fluentform/form_imported', $formId);
165
166 // Apply default style if requested
167 if ($applyDefaultStyle) {
168 do_action('fluentform/inserted_new_form', $formId, $form);
169 }
170 }
171
172 return ([
173 'message' => __('You form has been successfully imported.', 'fluentform'),
174 'inserted_forms' => $insertedForms,
175 ]);
176 }
177 }
178 throw new Exception(esc_html__('You have a faulty JSON file, please export the Fluent Forms again.', 'fluentform'));
179 }
180
181 public static function exportEntries($args)
182 {
183 if (!defined('FLUENTFORM_EXPORTING_ENTRIES')) {
184 define('FLUENTFORM_EXPORTING_ENTRIES', true);
185 }
186
187 $formId = Acl::verifyFormId(Arr::get($args, 'form_id'));
188 Acl::verify('fluentform_entries_viewer', $formId);
189
190 $tableName = Arr::get($args, 'table');
191 try {
192 $form = Form::findOrFail($formId);
193 } catch (Exception $e) {
194 exit('No Form Found');
195 }
196 $type = sanitize_key(Arr::get($args, 'format', 'csv'));
197 if (!in_array($type, ['csv', 'ods', 'xlsx', 'json'])) {
198 exit('Invalid requested format');
199 }
200 if ('json' == $type) {
201 self::exportAsJSON($form, $args);
202 }
203 if (!defined('FLUENTFORM_DOING_CSV_EXPORT')) {
204 define('FLUENTFORM_DOING_CSV_EXPORT', true);
205 }
206 $formInputs = FormFieldsParser::getEntryInputs($form, ['admin_label', 'raw']);
207 $inputLabels = FormFieldsParser::getAdminLabels($form, $formInputs);
208 $selectedLabels = Arr::get($args,'fields_to_export');
209 if (is_string($selectedLabels) && Helper::isJson($selectedLabels)) {
210 $selectedLabels = \json_decode($selectedLabels, true);
211 }
212 $selectedLabels = fluentFormSanitizer($selectedLabels);
213
214 $withNotes = isset($args['with_notes']);
215
216 //filter out unselected fields
217 if (!empty($selectedLabels)) {
218 foreach ($inputLabels as $key => $value) {
219 if (!in_array($key, $selectedLabels) && isset($inputLabels[$key])) {
220 unset($inputLabels[$key]);
221 }
222 }
223 }
224
225 $submissions = self::getSubmissions($args);
226 $submissions = FormDataParser::parseFormEntries($submissions, $form, $formInputs);
227 $parsedShortCodes = [];
228 $exportData = [];
229 $selectedShortcodes = self::getSelectedExportShortcodes($args, $form);
230 $legacyShortcodeHeaders = self::getLegacyExportShortcodeHeaders();
231
232 // Preload notes for all submissions in a single query to avoid N+1
233 $notesMap = [];
234 if ($withNotes && count($submissions)) {
235 $submissionIds = array_map(function ($s) { return is_object($s) ? $s->id : $s['id']; }, $submissions->toArray());
236 $allNotes = SubmissionMeta::whereIn('response_id', $submissionIds)
237 ->where('meta_key', '_notes')
238 ->get();
239 foreach ($allNotes as $note) {
240 $notesMap[$note->response_id][] = $note->value;
241 }
242 }
243
244 foreach ($submissions as $submission) {
245
246 $submission->response = json_decode($submission->response, true);
247
248 $temp = [];
249 foreach ($inputLabels as $field => $label) {
250
251 //format tabular grid data for CSV/XLSV/ODS export
252 if (isset($formInputs[$field]['element']) && "tabular_grid" === $formInputs[$field]['element']) {
253 $gridRawData = Arr::get($submission->response, $field);
254 $content = Helper::getTabularGridFormatValue($gridRawData, Arr::get($formInputs, $field), ' | ');
255 } elseif (isset($formInputs[$field]['element']) && "subscription_payment_component" === $formInputs[$field]['element']) {
256 //resolve plane name for subscription field
257 $planIndex = Arr::get($submission->user_inputs, $field);
258 $planLabel = Arr::get($formInputs, "{$field}.raw.settings.subscription_options.{$planIndex}.name");
259 if ($planLabel) {
260 $content = $planLabel;
261 } else {
262 $content = self::getFieldExportContent($submission, $field);
263 }
264 } else {
265 $content = self::getFieldExportContent($submission, $field);
266 if (Arr::get($formInputs, $field . '.element') === "input_number" && is_numeric($content)) {
267 $content = $content + 0;
268 }
269 }
270 $temp[] = Helper::sanitizeForCSV($content);
271 }
272
273 if (!empty($selectedShortcodes)) {
274 $regularShortcodes = self::getRegularExportShortcodes($selectedShortcodes, $legacyShortcodeHeaders);
275
276 if (!empty($regularShortcodes)) {
277 $parsedShortCodes = ShortCodeParser::parse(
278 $regularShortcodes,
279 $submission->id,
280 $submission->response,
281 $form,
282 false,
283 true
284 );
285 }
286
287 $temp = array_merge(
288 $temp,
289 self::getSelectedShortcodeExportValues(
290 $selectedShortcodes,
291 $parsedShortCodes,
292 $legacyShortcodeHeaders,
293 $submission
294 )
295 );
296 }
297 if ($withNotes) {
298 $noteValues = isset($notesMap[$submission->id]) ? $notesMap[$submission->id] : [];
299 if (!empty($noteValues)) {
300 $temp[] = implode(", ", $noteValues);
301 }
302 }
303
304 $temp = apply_filters('fluentform/export_entry_metadata', $temp, $submission, $form, $args);
305
306 $exportData[] = $temp;
307 }
308
309 $extraLabels = [];
310
311 $extraLabels = self::getSelectedShortcodeExportLabels(
312 $selectedShortcodes,
313 $parsedShortCodes,
314 $legacyShortcodeHeaders
315 );
316
317 $inputLabels = array_merge($inputLabels, $extraLabels);
318 if($withNotes){
319 $inputLabels[] = __('Notes','fluentform');
320 }
321 $inputLabels = apply_filters('fluentform/export_entry_metadata_labels', $inputLabels, $form, $args);
322
323 $data = array_merge([array_values($inputLabels)], $exportData);
324
325 $data = apply_filters('fluentform/export_data', $data, $form, $exportData, $inputLabels);
326 $fileName = self::getReadableExportFileName($form->title);
327 self::downloadOfficeDoc($data, $type, $fileName);
328 }
329
330 private static function getFieldExportContent($submission, $fieldName)
331 {
332 return trim(
333 wp_strip_all_tags(
334 FormDataParser::formatValue(
335 Arr::get($submission->user_inputs, $fieldName)
336 )
337 )
338 );
339 }
340
341 private static function getSelectedExportShortcodes($args, $form)
342 {
343 $selectedShortcodes = fluentFormSanitizer(Arr::get($args, 'shortcodes_to_export', []));
344
345 if (!Arr::has($args, 'shortcodes_to_export_defined') && empty($selectedShortcodes)) {
346 return self::getDefaultExportShortcodes($form);
347 }
348
349 return $selectedShortcodes;
350 }
351
352 private static function getDefaultExportShortcodes($form)
353 {
354 $defaults = [
355 [
356 'label' => __('Submission ID', 'fluentform'),
357 'value' => '{submission.id}',
358 ],
359 [
360 'label' => __('Submission Create Date', 'fluentform'),
361 'value' => '{submission.created_at}',
362 ],
363 [
364 'label' => __('Submission Status', 'fluentform'),
365 'value' => '{submission.status}',
366 ],
367 ];
368
369 if ($form->has_payment) {
370 $defaults[] = [
371 'label' => __('Payment Status', 'fluentform'),
372 'value' => '{payment.payment_status}',
373 ];
374 $defaults[] = [
375 'label' => __('Payment Total', 'fluentform'),
376 'value' => '{payment.payment_total}',
377 ];
378 $defaults[] = [
379 'label' => __('Currency', 'fluentform'),
380 'value' => '{submission.currency}',
381 ];
382 }
383
384 return $defaults;
385 }
386
387 private static function getLegacyExportShortcodeHeaders()
388 {
389 return [
390 '{submission.id}' => 'entry_id',
391 '{submission.status}' => 'entry_status',
392 '{submission.created_at}' => 'created_at',
393 '{payment.payment_status}' => 'payment_status',
394 '{submission.payment_status}' => 'payment_status',
395 '{payment.payment_total}' => 'payment_total',
396 '{submission.payment_total}' => 'payment_total',
397 '{submission.currency}' => 'currency',
398 ];
399 }
400
401 private static function getRegularExportShortcodes($selectedShortcodes, $legacyShortcodeHeaders)
402 {
403 $regularShortcodes = [];
404
405 foreach ($selectedShortcodes as $index => $shortcode) {
406 if (!isset($legacyShortcodeHeaders[Arr::get($shortcode, 'value')])) {
407 $regularShortcodes[$index] = $shortcode;
408 }
409 }
410
411 return $regularShortcodes;
412 }
413
414 private static function getSelectedShortcodeExportValues($selectedShortcodes, $parsedShortCodes, $legacyShortcodeHeaders, $submission)
415 {
416 $values = [];
417
418 foreach ($selectedShortcodes as $index => $shortcode) {
419 $shortcodeValue = Arr::get($shortcode, 'value');
420
421 if (!isset($legacyShortcodeHeaders[$shortcodeValue])) {
422 $values[] = Arr::get($parsedShortCodes, $index . '.value');
423 continue;
424 }
425
426 $values[] = self::getLegacyExportValue($legacyShortcodeHeaders[$shortcodeValue], $submission);
427 }
428
429 return $values;
430 }
431
432 private static function getSelectedShortcodeExportLabels($selectedShortcodes, $parsedShortCodes, $legacyShortcodeHeaders)
433 {
434 $labels = [];
435
436 foreach ($selectedShortcodes as $index => $shortcode) {
437 $shortcodeValue = Arr::get($shortcode, 'value');
438
439 if (isset($legacyShortcodeHeaders[$shortcodeValue])) {
440 $labels[] = $legacyShortcodeHeaders[$shortcodeValue];
441 continue;
442 }
443
444 $labels[] = Arr::get($parsedShortCodes, $index . '.label');
445 }
446
447 return $labels;
448 }
449
450 private static function getLegacyExportValue($header, $submission)
451 {
452 $legacyValueResolvers = [
453 'entry_id' => function ($submission) {
454 return $submission->id ?? '';
455 },
456 'entry_status' => function ($submission) {
457 return $submission->status ?? '';
458 },
459 'created_at' => function ($submission) {
460 return $submission->created_at ?? '';
461 },
462 'payment_status' => function ($submission) {
463 return $submission->payment_status ?? '';
464 },
465 'payment_total' => function ($submission) {
466 return round(($submission->payment_total ?? 0) / 100, 1);
467 },
468 'currency' => function ($submission) {
469 return $submission->currency ?? '';
470 },
471 ];
472
473 if (!isset($legacyValueResolvers[$header])) {
474 return '';
475 }
476
477 return $legacyValueResolvers[$header]($submission);
478 }
479
480 private static function exportAsJSON($form, $args)
481 {
482 $formInputs = FormFieldsParser::getEntryInputs($form, ['admin_label', 'raw']);
483 $submissions = self::getSubmissions($args);
484 $submissions = FormDataParser::parseFormEntries($submissions, $form, $formInputs);
485 foreach ($submissions as $submission) {
486 $submission->response = json_decode($submission->response, true);
487 }
488 self::sendDownloadHeaders('application/json', self::getReadableExportFileName($form->title) . '.json');
489 echo json_encode($submissions); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $submissions is escaped before being passed in.
490 exit();
491 }
492
493 private static function getReadableExportFileName($formTitle)
494 {
495 $sanitizedTitle = sanitize_file_name(wp_strip_all_tags((string) $formTitle));
496
497 if (!$sanitizedTitle) {
498 $sanitizedTitle = 'export';
499 }
500
501 return $sanitizedTitle . '-' . date('Y-m-d');
502 }
503
504 private static function sendDownloadHeaders($contentType, $fileName)
505 {
506 $safeFileName = basename((string) $fileName);
507 $encodedFileName = rawurlencode($safeFileName);
508
509 header('Content-Type: ' . $contentType);
510 header(
511 'Content-Disposition: attachment; ' .
512 'filename="' . $encodedFileName . '"; ' .
513 'filename*=UTF-8\'\'' . $encodedFileName
514 );
515 }
516
517 private static function getSubmissions($args)
518 {
519 $tableName = Arr::get($args, 'table');
520
521 if ($tableName) {
522 $allowedTables = [
523 'fluentform_submissions',
524 'fluentform_draft_submissions',
525 ];
526 if (!in_array($tableName, $allowedTables, true)) {
527 wp_send_json([
528 'message' => __('Invalid table name for export.', 'fluentform')
529 ], 422);
530 }
531 $query = wpFluent()->table($tableName)
532 ->where('form_id', (int) Arr::get($args, 'form_id'))
533 ->orderBy('id', Helper::sanitizeOrderValue(Arr::get($args, 'sort_by', 'DESC')));
534
535 $searchString = Arr::get($args, 'search');
536 if ($searchString) {
537 global $wpdb;
538 $escaped = $wpdb->esc_like($searchString);
539 $query->where(function ($q) use ($escaped) {
540 $q->where('id', 'LIKE', "%{$escaped}%")
541 ->orWhere('response', 'LIKE', "%{$escaped}%");
542 });
543 }
544 } else {
545 $query = (new Submission)->customQuery($args);
546 }
547
548 $entries = fluentFormSanitizer(Arr::get($args, 'entries', []));
549 $query->when(is_array($entries) && (count($entries) > 0), function ($q) use ($entries) {
550 return $q->whereIn('id', $entries);
551 });
552
553 if (Arr::get($args, 'advanced_filter')) {
554 $query = apply_filters('fluentform/apply_entries_advance_filter', $query, $args);
555 }
556
557 return $query->get();
558 }
559
560 private static function downloadOfficeDoc($data, $type = 'csv', $fileName = null)
561 {
562 $data = array_map(function ($item) {
563 return array_map(function ($itemValue) {
564 if (is_array($itemValue)) {
565 return implode(', ', $itemValue);
566 }
567 return $itemValue;
568 }, $item);
569 }, $data);
570 // Load Composer autoloader for OpenSpout
571 require_once FLUENTFORM_DIR_PATH . '/vendor/autoload.php';
572 $fileName = ($fileName) ? $fileName . '.' . $type : 'export-data-' . date('d-m-Y') . '.' . $type;
573
574 // Create writer based on type
575 switch (strtolower($type)) {
576 case 'csv':
577 $writer = \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createCSVWriter();
578 break;
579 case 'xlsx':
580 $writer = \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createXLSXWriter();
581 break;
582 case 'ods':
583 $writer = \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createODSWriter();
584 break;
585 default:
586 throw new \Exception(sprintf('Unsupported file type: %s', esc_html($type)));
587 }
588 $writer->openToBrowser($fileName);
589
590 $rows = self::getOfficeDocRows($data, $type);
591
592 $writer->addRows($rows);
593 $writer->close();
594 die();
595 }
596
597 private static function getOfficeDocRows($data, $type)
598 {
599 if (strtolower($type) !== 'xlsx') {
600 return array_map(function ($rowData) {
601 return \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createRowFromArray($rowData);
602 }, $data);
603 }
604
605 $dateStyle = (new \OpenSpout\Writer\Common\Creator\Style\StyleBuilder())
606 ->setFormat('yyyy-mm-dd hh:mm:ss')
607 ->build();
608
609 return array_map(function ($rowData) use ($dateStyle) {
610 $cells = array_map(function ($cellValue) use ($dateStyle) {
611 if ($cellValue instanceof \DateTimeInterface) {
612 return \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createCell($cellValue, $dateStyle);
613 }
614
615 return \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createCell($cellValue);
616 }, $rowData);
617
618 return \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createRow($cells);
619 }, $data);
620 }
621
622 }
623