PluginProbe
FireBox – WooCommerce Popup Builder, Exit Intent Popup, Email Optin & Cart Abandonment / 3.1.12
FireBox – WooCommerce Popup Builder, Exit Intent Popup, Email Optin & Cart Abandonment v3.1.12
3.1.13 3.1.12 3.1.11 3.1.10 3.1.9 3.1.8 3.1.7 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 All 122 releases
firebox / Inc / Core / Helpers / Form / Form.php

Form.php in FireBox – WooCommerce Popup Builder, Exit Intent Popup, Email Optin & Cart Abandonment 3.1.12, at Inc/Core/Helpers/Form/Form.php

733 lines 17.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package FireBox
4 * @version 3.1.12 Free
5 *
6 * @author FirePlugins <info@fireplugins.com>
7 * @link https://www.fireplugins.com
8 * @copyright Copyright © 2026 FirePlugins All Rights Reserved
9 * @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
10 */
11
12 namespace FireBox\Core\Helpers\Form;
13
14 if (!defined('ABSPATH'))
15 {
16 exit; // Exit if accessed directly.
17 }
18
19 use \FireBox\Core\Helpers\BoxHelper;
20 use \FPFramework\Helpers\Plugins\FireBox\Form as FrameworkFireBoxForm;
21
22 class Form
23 {
24 /**
25 * Returns a list of forms with form id => form title key,value pair.
26 *
27 * @return array
28 */
29 public static function getParsedForms()
30 {
31 // cache key
32 $hash = md5('FireBox\Core\Helpers\Form::getParsedForms');
33
34 // check cache
35 if ($forms = wp_cache_get($hash, 'firebox'))
36 {
37 return $forms;
38 }
39
40 $forms = self::getForms();
41
42 $parsed = [];
43
44 foreach ($forms as $key => $value)
45 {
46 $parsed[$value['id']] = $value['name'];
47 }
48
49 // set cache
50 wp_cache_set($hash, $parsed, 'firebox', HOUR_IN_SECONDS);
51
52 return $parsed;
53 }
54
55 public static function getCampaignForms($campaigns)
56 {
57 $forms = [];
58
59 // Find forms
60 foreach ($campaigns as $id => $title)
61 {
62 if (!has_block('firebox/form', $id))
63 {
64 continue;
65 }
66
67 $campaign_modified_gmt = get_post_modified_time('U', true, $id);
68 $campaign_gmt = get_post_time('U', true, $id);
69 $campaign_status = get_post_status($id);
70
71 $blocks = parse_blocks(get_the_content(null, false, $id));
72
73 foreach ($blocks as $key => $block)
74 {
75 if (isset($block['innerBlocks']))
76 {
77 foreach ($block['innerBlocks'] as $innerBlock)
78 {
79 // Find form block
80 if (!$form_block = FrameworkFireBoxForm::findRecursiveForm($innerBlock))
81 {
82 continue;
83 }
84
85 $atts = isset($form_block['attrs']) ? $form_block['attrs'] : false;
86 if (!$atts)
87 {
88 continue;
89 }
90
91 $block_unique_id = isset($atts['uniqueId']) ? $atts['uniqueId'] : false;
92 if (!$block_unique_id)
93 {
94 continue;
95 }
96
97 $forms[] = [
98 'id' => $block_unique_id,
99 'campaign_id' => $id,
100 'block' => $form_block,
101 'created_at' => $campaign_modified_gmt ? $campaign_modified_gmt : $campaign_gmt,
102 'state' => $campaign_status === 'publish' ? '1' : '0',
103 'name' => $title . ' (' . $id . ')'
104 ];
105 }
106 }
107
108 if ($block['blockName'] !== 'firebox/form')
109 {
110 continue;
111 }
112
113 $atts = isset($block['attrs']) ? $block['attrs'] : false;
114 if (!$atts)
115 {
116 continue;
117 }
118
119 $block_unique_id = isset($atts['uniqueId']) ? $atts['uniqueId'] : false;
120 if (!$block_unique_id)
121 {
122 continue;
123 }
124
125 $forms[] = [
126 'id' => $block_unique_id,
127 'campaign_id' => $id,
128 'block' => $block,
129 'created_at' => $campaign_modified_gmt ? $campaign_modified_gmt : $campaign_gmt,
130 'state' => $campaign_status === 'publish' ? '1' : '0',
131 'name' => $title . ' (' . $id . ')'
132 ];
133 }
134 }
135
136 return $forms;
137 }
138
139 /**
140 * Gets all forms.
141 *
142 * @return array
143 */
144 public static function getForms()
145 {
146 // check cache — avoids re-parsing every campaign's block content on every call
147 // (e.g. on every frontend form submission via getFormByID())
148 if (($forms = wp_cache_get(self::FORMS_CACHE_KEY, 'firebox')) !== false)
149 {
150 return $forms;
151 }
152
153 // Get all popups
154 $popups_data = BoxHelper::getAllBoxes(['publish', 'draft']);
155 $campaigns = BoxHelper::produceKeyValueBoxes($popups_data->posts);
156
157 $forms = self::getCampaignForms($campaigns);
158
159 wp_cache_set(self::FORMS_CACHE_KEY, $forms, 'firebox', HOUR_IN_SECONDS);
160
161 return $forms;
162 }
163
164 /**
165 * The cache key used to store the result of getForms().
166 *
167 * @var string
168 */
169 const FORMS_CACHE_KEY = 'firebox_forms_all';
170
171 /**
172 * Clears the cached getForms() result. Called whenever a campaign is saved so
173 * getFormByID() doesn't serve a stale form list.
174 *
175 * @return void
176 */
177 public static function clearFormsCache()
178 {
179 wp_cache_delete(self::FORMS_CACHE_KEY, 'firebox');
180 }
181
182 /**
183 * Returns all forms in a campaign id, campaign label format.
184 *
185 * @return array
186 */
187 public static function getPublishedForms()
188 {
189 // Get all popups
190 $popups_data = BoxHelper::getAllBoxes(['publish']);
191 $campaigns = BoxHelper::produceKeyValueBoxes($popups_data->posts);
192
193 return $campaigns;
194 }
195
196 /**
197 * Validates the form.
198 *
199 * @param array $form_fields
200 * @param array $fields_values
201 *
202 * @return array
203 */
204 public static function validate($form_fields = [], &$fields_values = [])
205 {
206 // Submitted values must be an array; coerce so a caller passing a scalar
207 // (e.g. a malformed request) doesn't fatal on the array operations below.
208 if (!is_array($fields_values))
209 {
210 $fields_values = [];
211 }
212
213 if (!$form_fields || !$fields_values)
214 {
215 return false;
216 }
217
218 $validation = [];
219
220 // Check honeypot
221 if (isset($fields_values['hnpt']) && !empty($fields_values['hnpt']))
222 {
223 return [
224 'error' => true,
225 'message' => firebox()->_('FB_HONEYPOT_FIELD_TRIGGERED')
226 ];
227 }
228
229 // Remove honeypot field
230 unset($fields_values['hnpt']);
231
232 $error_msgs = [];
233
234 // Validate fields
235 foreach ($form_fields as $index => $field)
236 {
237 $field->setData($fields_values);
238
239 $field_name = $field->getOptionValue('name');
240 $field_value = isset($fields_values[$field_name]) ? $fields_values[$field_name] : '';
241
242 // If it's not required and the value is empty, we don't need to validate
243 if (!$field->isRequired() && empty($field_value))
244 {
245 unset($fields_values[$field_name]);
246 continue;
247 }
248
249 // Validate class
250 if (!$field->validate($field_value))
251 {
252 $validation_message = $field->getValidationMessage();
253 $error_msgs[] = $field->getLabel() . ': ' . $validation_message;
254
255 $validation[] = [
256 'name' => $field_name,
257 'label' => $field->getLabel(),
258 'type' => $field->getOptionValue('type'),
259 'validation_message' => $validation_message
260 ];
261 }
262
263 $fields_values[$field_name] = $field_value;
264 }
265
266 /**
267 * Remove any fields that are empty, after they have been validated.
268 *
269 * For example, fields such as Captcha fields shouldn't be saved.
270 */
271 $fields_values = array_filter($fields_values);
272
273 return $validation ? ['error' => $validation, 'message' => implode('<br />', $error_msgs)] : $form_fields;
274 }
275
276 /**
277 * Finds all supported blocks recursively.
278 *
279 * @param array $block
280 * @param array $supported_blocks
281 *
282 * @return array
283 */
284 private static function findRecursiveBlocks($block, $supported_blocks)
285 {
286 $matching_blocks = [];
287
288 if (in_array($block['blockName'], $supported_blocks))
289 {
290 $matching_blocks[] = $block;
291 }
292
293 if (!empty($block['innerBlocks']))
294 {
295 foreach ($block['innerBlocks'] as $innerBlockItem)
296 {
297 $innerBlocks = self::findRecursiveBlocks($innerBlockItem, $supported_blocks);
298
299 if (!empty($innerBlocks))
300 {
301 $matching_blocks = array_merge($matching_blocks, $innerBlocks);
302 }
303 }
304 }
305
306 return $matching_blocks;
307 }
308
309 /**
310 * Return the form fields.
311 *
312 * @param array $blocks
313 *
314 * @return array
315 */
316 public static function getFormFields($blocks = [])
317 {
318 if (!$blocks)
319 {
320 return [];
321 }
322
323 // Find all supported fields
324 $supported_blocks = self::getSupportedBlocks();
325
326 $form_fields = [];
327
328 // Find form blocks
329 foreach ($blocks as $key => $block)
330 {
331 // Find supported block
332 if (!$found_blocks = self::findRecursiveBlocks($block, $supported_blocks))
333 {
334 continue;
335 }
336
337 foreach ($found_blocks as $_block)
338 {
339 $field_payload = [
340 'id' => isset($_block['attrs']['uniqueId']) ? $_block['attrs']['uniqueId'] : '',
341 'label' => Field::getFieldLabel($_block),
342 'type' => Field::getFieldType($_block['blockName']),
343 'name' => Field::getFieldName($_block)
344 ];
345 $final_field_payload = array_merge($field_payload, $_block['attrs']);
346
347 /**
348 * This is nuts.
349 *
350 * WordPress doesn't provide us directly with the block default attribute values if the post is saved without editing the block.
351 *
352 * So we have to manually set the default values for specific fields.
353 */
354 if (in_array($final_field_payload['type'], ['dropdown', 'radio', 'checkbox']))
355 {
356 if (!isset($final_field_payload['choices']))
357 {
358 $final_field_payload['choices'] = [
359 [
360 'default' => false,
361 'value' => 1,
362 'label' => 'Choice 1',
363 'image' => ''
364 ],
365 [
366 'default' => false,
367 'value' => 2,
368 'label' => 'Choice 2',
369 'image' => ''
370 ],
371 [
372 'default' => false,
373 'value' => 3,
374 'label' => 'Choice 3',
375 'image' => ''
376 ]
377 ];
378 }
379 }
380 else if ($final_field_payload['type'] === 'rating')
381 {
382 // Add icon
383 if (!isset($final_field_payload['icon']))
384 {
385 $final_field_payload['icon'] = 'star';
386 }
387
388 // Add size
389 if (!isset($final_field_payload['size']))
390 {
391 $final_field_payload['size'] = 24;
392 }
393
394 // Add maxRating
395 if (!isset($final_field_payload['maxRating']))
396 {
397 $final_field_payload['maxRating'] = 5;
398 }
399
400 // Add halfRatings
401 if (!isset($final_field_payload['halfRatings']))
402 {
403 $final_field_payload['halfRatings'] = false;
404 }
405
406 // Add selectedColor
407 if (!isset($final_field_payload['selectedColor']))
408 {
409 $final_field_payload['selectedColor'] = '#f6cc01';
410 }
411
412 // Add unselectedColor
413 if (!isset($final_field_payload['unselectedColor']))
414 {
415 $final_field_payload['unselectedColor'] = '#bdbdbd';
416 }
417 }
418
419 if (!$class = Field::getFieldClass($final_field_payload))
420 {
421 continue;
422 }
423
424 $form_fields[] = $class;
425 }
426 }
427
428 return $form_fields;
429 }
430
431 /**
432 * Returns the form given its ID.
433 *
434 * @param string $form_id The form ID.
435 * @param bool $only_inputs If true, fields that doesn't have an input element such as HTML and reCAPTCHA, won't be returned.
436 *
437 * @return array
438 */
439 public static function getFormByID($form_id = null, $only_inputs = false)
440 {
441 if (!$form_id)
442 {
443 return;
444 }
445
446 $forms = self::getForms();
447
448 $form = current(array_filter($forms, function($form) use ($form_id) {
449 return $form['id'] === $form_id;
450 })) ?: false;
451
452 if (!$form)
453 {
454 return;
455 }
456
457 $fields = self::getFormFields($form['block']['innerBlocks']);
458
459 foreach ($fields as $index => $field)
460 {
461 if ($only_inputs && $field->getOptionValue('name') === '')
462 {
463 unset($fields[$index]);
464 }
465 }
466
467 $form['fields'] = $fields;
468
469 return $form;
470 }
471
472 /**
473 * Returns the supported blocks.
474 *
475 * @param bool $clean Whether to return only the name of the field without the prefix "firebox/"
476 *
477 * @return array
478 */
479 public static function getSupportedBlocks($clean = false)
480 {
481 static $blocks = null;
482
483 if ($blocks === null)
484 {
485 $blocks = array_diff(scandir(FBOX_PLUGIN_DIR . 'Inc/Core/Form/Fields/Fields'), ['index.php', '.', '..', '.DS_Store']);
486 }
487
488 $data = [];
489
490 foreach ($blocks as $key => $name)
491 {
492 // Strip the trailing ".php" extension only
493 $name = preg_replace('/\.php$/', '', $name);
494
495 $data[] = (!$clean ? 'firebox/' : '') . strtolower($name);
496 }
497
498 return $data;
499 }
500
501 /**
502 * Store submission.
503 *
504 * @param string $form_id
505 * @param array $form_settings
506 * @param array $valid_fields
507 * @param array $fields_values
508 * @param bool $save
509 *
510 * @return array
511 */
512 public static function storeSubmission($form_id, $form_settings, $valid_fields, $fields_values, $save = true)
513 {
514 $submissionDefaultState = isset($form_settings['attrs']['submissionDefaultState']) ? $form_settings['attrs']['submissionDefaultState'] : '1';
515
516 if (!$submission_data = Submission::create($form_id, $submissionDefaultState, $save))
517 {
518 return false;
519 }
520
521 if (!$submission_meta_data = SubmissionMeta::create($submission_data['id'], $fields_values, $save))
522 {
523 return false;
524 }
525
526 return self::prepare($submission_data, $valid_fields, $fields_values);
527 }
528
529 /**
530 * Prepare fields.
531 *
532 * @param array $submission
533 * @param array $valid_fields
534 * @param array $fields_values
535 *
536 * @return array
537 */
538 private static function prepare($submission, $valid_fields, $fields_values)
539 {
540 $prepared_data = $submission;
541 $prepared_data['prepared_fields'] = [];
542
543 foreach ($valid_fields as $key => $field)
544 {
545 $field_name = $field->getOptionValue('name');
546
547 // Skip fields with no name like reCAPTCHA, HTML e.t.c
548 if (!$field_name)
549 {
550 continue;
551 }
552
553 $field_id = $field->getOptionValue('id');
554 $field_value = isset($fields_values[$field_id]) ? $fields_values[$field_id] : '';
555
556 $field->setValue($field_value);
557
558 $prepared_data['prepared_fields'][$field_name] = [
559 'class' => $field,
560 'submitted_value' => $field_value,
561 'value' => $field->prepareValue($field_value),
562 'value_html' => $field->prepareValueHTML($field_value),
563 'value_raw' => $field->prepareRawValue($field_value)
564 ];
565 }
566
567 return $prepared_data;
568 }
569
570 /**
571 * Ensure the popup has unique Form IDs.
572 *
573 * @param string $content
574 *
575 * @return void
576 */
577 public static function ensureUniqueFormIDs(&$content)
578 {
579 // Get forms
580 $forms = self::getForms();
581
582 // Get form IDs in content
583 $pattern = '/wp:firebox\/form {"uniqueId":"(.*?)"/';
584
585 // Find matches
586 preg_match_all($pattern, $content, $matches);
587
588 // Ensure we have at least one form in the popup
589 if (!isset($matches[1]) || empty($matches[1]))
590 {
591 return;
592 }
593
594 $old_form_ids_in_popup = $matches[1];
595 $new_form_ids_in_popup = [];
596
597 // Find new IDs
598 foreach ($old_form_ids_in_popup as $key => $id)
599 {
600 while (true)
601 {
602 $form = array_filter($forms, function($form_item) use ($id) {
603 return $id === $form_item['id'];
604 });
605 $form_id = reset($form);
606
607 // Form ID is unique
608 if (!$form_id)
609 {
610 $new_form_ids_in_popup[] = $id;
611 break;
612 }
613
614 // Form ID is not unique, generate new
615 $id = md5(uniqid());
616 $id = substr($id, 0, 12);
617
618 // Add dash after 6th character
619 $id = substr_replace($id, '-', 6, 0);
620
621 // Add dash after 10th character
622 $id = substr_replace($id, '-', 11, 0);
623 }
624 }
625
626 if (count($old_form_ids_in_popup) !== count($new_form_ids_in_popup))
627 {
628 return;
629 }
630
631 // Replace old IDs with new IDs
632 foreach ($old_form_ids_in_popup as $index => $id)
633 {
634 // Replace unique ID
635 $content = str_replace('"uniqueId":"' . $id . '"', '"uniqueId":"' . $new_form_ids_in_popup[$index] . '"', $content);
636
637 // Replace all other instances
638 $content = str_replace('form-' . $id, 'form-' . $new_form_ids_in_popup[$index], $content);
639 }
640 }
641
642 /**
643 * Replaces Smart Tags.
644 *
645 * @param array $attrs
646 * @param array $fields_values
647 * @param array $submission
648 *
649 * @return void
650 */
651 public static function replaceSmartTags(&$attrs, $fields_values, $submission)
652 {
653 // Replace Smart Tags
654 $tags = new \FPFramework\Base\SmartTags\SmartTags();
655
656 // register FB Smart Tags
657 $tags->register('\FireBox\Core\Form\SmartTags', FBOX_BASE_FOLDER . '/Inc/Core/Form/SmartTags', [
658 'field_values' => $fields_values,
659 'submission' => $submission
660 ]);
661
662 $attrs = $tags->replace($attrs);
663 }
664
665 /**
666 * Returns the submission action.
667 *
668 * @param array $attrs
669 *
670 * @return array
671 */
672 public static function getSubmissionAction($attrs)
673 {
674 $action = isset($attrs['submissionAction']) ? $attrs['submissionAction'] : 'message';
675
676 $payload = [
677 'action' => $action,
678 /**
679 * The success message is rendered as HTML by the frontend and, because Smart
680 * Tags have already been substituted, may contain submitted values. kses keeps
681 * the formatting an admin configured while dropping scripts and event handlers.
682 */
683 'message' => $action === 'message' ? (isset($attrs['messageAfterSuccess']) ? \wp_kses_post(\wpautop($attrs['messageAfterSuccess'])) : 'Thanks for contacting us! We will get in touch with you shortly.') : '',
684 'resetForm' => isset($attrs['resetForm']) ? $attrs['resetForm'] : true,
685 'hideForm' => isset($attrs['hideForm']) ? $attrs['hideForm'] : true
686 ];
687
688 if ($action === 'redirect')
689 {
690 /**
691 * Smart Tags have already been substituted into $attrs by this point, so a
692 * template like "https://example.com/thanks?ref={field.source}" puts submitted
693 * data into the redirect target. Restrict it to an http(s) URL so a crafted
694 * submission cannot turn it into a javascript: or data: URL.
695 */
696 $url = isset($attrs['redirectURL']) && is_scalar($attrs['redirectURL'])
697 ? trim((string) $attrs['redirectURL'])
698 : '';
699
700 $payload['redirectURL'] = $url === '' ? '' : esc_url_raw($url, ['http', 'https']);
701 }
702
703 return $payload;
704 }
705
706 public static function getSubmissions($form_id = null)
707 {
708 if (!$form_id)
709 {
710 return;
711 }
712
713 $data = [
714 'where' => [
715 'form_id' => " = '" . sanitize_key($form_id) . "'"
716 ],
717 'limit' => 1000
718 ];
719
720 $submissions = firebox()->tables->submission->getResults($data, true);
721
722 // Fetch all submission meta in chunked IN() queries instead of one query per submission.
723 $submission_ids = wp_list_pluck($submissions, 'id');
724 $meta_by_submission = SubmissionMeta::getMetaForSubmissions($submission_ids);
725
726 foreach ($submissions as &$submission)
727 {
728 $submission->meta = isset($meta_by_submission[$submission->id]) ? $meta_by_submission[$submission->id] : [];
729 }
730
731 return $submissions;
732 }
733 }