PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.2.1
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.2.1
V-3.3.0 3.2.2 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 V3.0.3 V3.0.2 -3.0.1 V_3.0.0 1.1.1 1.1.8 1.2 1.3 1.4 1.4.18 1.5.2 1.9 2.0 2.10.0 2.10.1 2.10.2 All 137 releases
bit-form / includes / Core / Form / FormManager.php

FormManager.php in Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder 3.2.1, at includes/Core/Form/FormManager.php

1,924 lines 72.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Get set Form,fields
5 */
6
7 namespace BitCode\BitForm\Core\Form;
8
9 /**
10 * FrontendFormManager class
11 */
12
13 use BitCode\BitForm\Admin\Form\CustomFieldHandler;
14 use BitCode\BitForm\Admin\Form\Helpers;
15 use BitCode\BitForm\Core\Database\FormEntryLogModel;
16 use BitCode\BitForm\Core\Database\FormEntryMetaModel;
17 use BitCode\BitForm\Core\Database\FormEntryModel;
18 use BitCode\BitForm\Core\Database\FormModel;
19 use BitCode\BitForm\Core\Integration\IntegrationHandler;
20 use BitCode\BitForm\Core\Messages\SuccessMessageHandler;
21 use BitCode\BitForm\Core\Util\FieldValueHandler;
22 use BitCode\BitForm\Core\Util\FileHandler;
23 use BitCode\BitForm\Core\Util\FrontendHelpers;
24 use BitCode\BitForm\Core\Util\IpTool;
25 use BitCode\BitForm\Core\Util\Log;
26 use BitCode\BitForm\Core\Util\Utilities;
27 use BitCode\BitForm\Core\WorkFlow\WorkFlow;
28 use BitCode\BitForm\Core\WorkFlow\WorkFlowHandler;
29 use BitCode\BitForm\enshrined\svgSanitize\Sanitizer;
30 use stdClass;
31 use WP_Error;
32
33 class FormManager
34 {
35 // Cache for instances of FormManager by form_id
36 private static $formManagerCache = [];
37 protected static $form;
38 protected $formModel;
39 protected $form_id;
40 private $_has_upload;
41 private $_field_label;
42 private $_fields;
43 private $_repeaterFields;
44 private $_work_flows;
45 private $_conf_messages;
46 private $_atomic_class_map;
47 private $_saveFormAsDraft;
48
49 public function __construct($form_id)
50 {
51 $this->form_id = $form_id;
52 $this->formModel = new FormModel();
53
54 static::$form = $this->formModel->get(
55 [
56 'id',
57 'form_content',
58 'form_name',
59 'created_at',
60 'views',
61 'entries',
62 'status',
63 'builder_helper_state',
64 'atomic_class_map',
65 'generated_script_page_ids',
66 ],
67 [
68 'id' => $form_id,
69 ]
70 );
71 if (!is_wp_error(static::$form)) {
72 $this->_atomic_class_map = json_decode(static::$form[0]->atomic_class_map);
73 $bfMultipleFormsExists = FrontendHelpers::hasMultipleForms();
74 if ($bfMultipleFormsExists && isset($this->_atomic_class_map->atomic_class_map_with_form_id)) {
75 $this->_atomic_class_map = $this->_atomic_class_map->atomic_class_map_with_form_id;
76 } elseif (isset($this->_atomic_class_map->atomic_class_map)) {
77 $this->_atomic_class_map = $this->_atomic_class_map->atomic_class_map;
78 }
79 } else {
80 // Log the error if needed
81 Log::debug_log('Error fetching form: ' . "Form Id = ($form_id)" . static::$form->get_error_message());
82 }
83 }
84
85 // Static method to get the instance of FormManager
86 public static function getInstance($form_id)
87 {
88 // Check if an instance of FormManager is already cached
89 if (!isset(self::$formManagerCache[$form_id])) {
90 // Create and cache the FormManager instance if not found
91 self::$formManagerCache[$form_id] = new self($form_id);
92 }
93
94 // Return the cached instance
95 return self::$formManagerCache[$form_id];
96 }
97
98 public function isExist()
99 {
100 return (!static::$form || is_wp_error(static::$form)) ? false : true;
101 }
102
103 public function checkStatus()
104 {
105 return '1' === static::$form[0]->status ? true : false;
106 }
107
108 public function getFieldsContent()
109 {
110 return self::$form[0]->form_content;
111 }
112
113 public function getFont()
114 {
115 $atomicClassMap = $this->_atomic_class_map;
116 $font = isset($atomicClassMap->font) ? $atomicClassMap->font : '';
117 return $font;
118 }
119
120 public function getStyle()
121 {
122 $builerState = Utilities::jsonObj(static::$form[0]->builder_helper_state ?? '');
123 $style = '';
124 $themeVars = $builerState->themeVars ?? null;
125 $themeColors = $builerState->themeColors ?? null;
126
127 if (!empty($themeVars)) {
128 $style .= ':root {';
129 foreach ($themeVars->lgLightThemeVars as $key => $value) {
130 $style .= "$key: $value; ";
131 }
132 $style .= '} ';
133 }
134 if (!empty($themeColors)) {
135 $style .= ' :root {';
136 foreach ($themeColors->lightThemeColors as $k => $v) {
137 $style .= "$k:$v; ";
138 }
139 $style .= '} ';
140 }
141
142 $field = $builerState->style->lgLightStyles->fields;
143 foreach ($field as $value) {
144 $classes = $value->classes;
145 foreach ($classes as $key => $value) {
146 $style .= "{$key} {";
147 foreach ($value as $k => $v) {
148 $style .= "$k:$v; ";
149 }
150 $style .= '} ';
151 }
152 }
153 return $style;
154 }
155
156 public function getCustomStyle()
157 {
158 $customCSSPath = BITFORMS_CONTENT_DIR . DIRECTORY_SEPARATOR . 'form-styles' . DIRECTORY_SEPARATOR . "bitform-custom-{$this->form_id}.css";
159 return FileHandler::readFile($customCSSPath);
160 }
161
162 public function getCustomJS()
163 {
164 $customJsPath = BITFORMS_CONTENT_DIR . DIRECTORY_SEPARATOR . 'form-scripts' . DIRECTORY_SEPARATOR . "bitform-custom-{$this->form_id}.js";
165 return FileHandler::readFile($customJsPath);
166 }
167
168 public function getFormContentWithValue($defaultValues = [])
169 {
170 $form_content = Utilities::jsonObj(static::$form[0]->form_content ?? '');
171 // this filter just use private purpose
172 if (isset($form_content->fields)) {
173 $form_content->fields = apply_filters('bitform_dynamic_field_filter', $form_content->fields);
174 }
175 if (!is_array($defaultValues) || 0 === count($defaultValues)) {
176 return $form_content;
177 }
178 foreach (($form_content->fields ?? []) as $fieldKey => $fieldDetails) {
179 // $field_name = empty($fieldDetails->lbl) ? null : \preg_replace('/[\`\~\!\@\#\$\'\.\s\?\+\-\*\&\|\/\\!]/', '_', $fieldDetails->lbl);
180 $fieldName = $fieldDetails->fieldName;
181 $defaultValue = isset($defaultValues[$fieldName]) ? $defaultValues[$fieldName] : null;
182 $defaultValue = isset($defaultValues[$fieldKey]) ? $defaultValues[$fieldKey] : $defaultValue;
183 if ((isset($fieldDetails->mul) || 'check' === $fieldDetails->typ) && isset($defaultValue)) {
184 // if (is_array($defaultValue)) {
185 // $fieldDetails->val =
186 // wp_json_encode(
187 // array_map('sanitize_text_field', $defaultValue)
188 // );
189 // } else {
190 // $fieldDetails->val = sanitize_text_field($defaultValue);
191 // }
192 if ((isset($fieldDetails->mul) && true === $fieldDetails->mul) || is_array($defaultValue)) {
193 $fieldDetails->val = wp_json_encode(array_map('sanitize_text_field', $defaultValue));
194 } elseif (!is_array($defaultValue)) {
195 $fieldDetails->val = sanitize_text_field($defaultValue);
196 }
197 } elseif (!is_null($defaultValue)) {
198 $fieldDetails->val = is_string($defaultValue) ?
199 sanitize_text_field($defaultValue) :
200 sanitize_text_field($defaultValue[count($defaultValue) - 1]);
201 }
202 }
203 return $form_content;
204 }
205
206 public function getFormContent()
207 {
208 $formContent = Utilities::jsonObj(static::$form[0]->form_content ?? '');
209 $types = ['check', 'radio', 'select'];
210 $filter = false;
211 foreach (($formContent->fields ?? []) as $field) {
212 if (in_array($field->typ, $types) && property_exists($field, 'customType')) {
213 $filter = true;
214 break; // reduce unnecessary loop
215 }
216 }
217 if (true === $filter) {
218 $updateFields = apply_filters('bitform_dynamic_field_filter', $formContent->fields);
219 $formContent->fields = $updateFields;
220 }
221 return $formContent;
222 }
223
224 public function getFormInfo()
225 {
226 $formContent = json_decode(static::$form[0]->form_content);
227 $formInfo = isset($formContent->formInfo) ? $formContent->formInfo : null;
228 return $formInfo;
229 }
230
231 public function getFormPermission()
232 {
233 $formContent = json_decode(static::$form[0]->form_content);
234 $formPermission = isset($formContent->formPermissions) ? $formContent->formPermissions : null;
235 return $formPermission;
236 }
237
238 public function getFormHelperStates()
239 {
240 $formHelperStates = json_decode(static::$form[0]->builder_helper_state);
241 return $formHelperStates;
242 }
243
244 public function getAtomicClsMap()
245 {
246 return $this->_atomic_class_map;
247 }
248
249 private function is_json($str)
250 {
251 $json = json_decode($str);
252 return $json && $str !== $json;
253 }
254
255 public function getFormData($columnName = '')
256 {
257 if (empty($columnName)) {
258 return null;
259 }
260
261 $form = static::$form[0];
262 if (!isset($form->{$columnName})) {
263 return null;
264 }
265
266 $data = $form->{$columnName};
267 if ($this->is_json($data)) {
268 return json_decode($data);
269 }
270
271 return $data;
272 }
273
274 public function getFormName()
275 {
276 return static::$form[0]->form_name;
277 }
278
279 public function getFormLayout()
280 {
281 $formContent = $this->getFormContent();
282 return $formContent->layout;
283 }
284
285 public function getFormNestedLayout()
286 {
287 $formContent = $this->getFormContent();
288 return $formContent->nestedLayout;
289 }
290
291 private function mergeNestedLayout(&$layout, $nestedLayout)
292 {
293 foreach ($nestedLayout as $key => $brkpnts) {
294 foreach ($brkpnts as $brkpnt=>$nLayout) {
295 $layout->{$brkpnt} = array_merge($layout->{$brkpnt}, $nLayout);
296 }
297 }
298 }
299
300 public function flatMultistepFormLayout()
301 {
302 $formLayout = $this->getFormLayout();
303 $multistepLayout = new stdClass();
304 foreach ($formLayout as $stpLayout) {
305 $lyout = $stpLayout->layout;
306
307 foreach ($lyout as $brkpnt=>$fields) {
308 $multistepLayout->{$brkpnt} = array_merge($multistepLayout->{$brkpnt} ?? [], $fields);
309 }
310 }
311
312 return $multistepLayout;
313 }
314
315 public function getFlatenFormLayout()
316 {
317 $layout = $this->getFormLayout();
318 $nestedLayout = $this->getFormNestedLayout();
319 if ('array' === gettype($layout)) {
320 // multi step form layout
321 $layout = $this->flatMultistepFormLayout();
322 }
323 if (!empty((array) $nestedLayout)) {
324 $this->mergeNestedLayout($layout, $nestedLayout);
325 }
326
327 return $layout;
328 }
329
330 /**
331 * Union of field keys referenced by ANY breakpoint (lg/md/sm) of the root
332 * layout (all steps) plus nested layouts of RENDERED containers.
333 *
334 * Nested layout entries are gated by their parent key being in the root
335 * layout — the renderer (FormViewer) only renders nested children of
336 * containers present in the root layout, so a stale nestedLayout entry
337 * (parent removed) must not mark its children as rendered.
338 *
339 * Returns [] when the layout is missing or unparseable — callers MUST
340 * fail closed (validate all fields) on an empty result.
341 *
342 * @return string[]
343 */
344 public function getLayoutFieldKeys()
345 {
346 try {
347 $layout = $this->getFormLayout();
348 $nestedLayout = $this->getFormNestedLayout();
349 if ('array' === gettype($layout)) {
350 // multi step form layout
351 $layout = $this->flatMultistepFormLayout();
352 }
353 } catch (\Throwable $e) {
354 return [];
355 }
356 $rootKeys = self::collectLayoutKeys($layout);
357 if (empty($rootKeys)) {
358 return [];
359 }
360 $keys = array_fill_keys($rootKeys, true);
361 if (is_object($nestedLayout) || is_array($nestedLayout)) {
362 foreach ($nestedLayout as $parentKey => $nLay) {
363 if (!isset($keys[$parentKey])) {
364 continue; // stale entry: container no longer rendered
365 }
366 foreach (self::collectLayoutKeys($nLay) as $nestedKey) {
367 $keys[$nestedKey] = true;
368 }
369 }
370 }
371 return array_keys($keys);
372 }
373
374 /**
375 * Collect field keys from every breakpoint of a layout object.
376 *
377 * @param object $layout layout with ->lg/->md/->sm arrays of {i} items
378 *
379 * @return string[]
380 */
381 private static function collectLayoutKeys($layout)
382 {
383 if (!is_object($layout)) {
384 return [];
385 }
386 $keys = [];
387 foreach (['lg', 'md', 'sm'] as $brkpnt) {
388 if (!isset($layout->{$brkpnt}) || !is_array($layout->{$brkpnt})) {
389 continue;
390 }
391 foreach ($layout->{$brkpnt} as $item) {
392 if (is_object($item) && isset($item->i)) {
393 $keys[$item->i] = true;
394 }
395 }
396 }
397 return array_keys($keys);
398 }
399
400 /**
401 * Extract a child field key from a childFields[] entry (stdClass or array shape).
402 *
403 * @return string|null
404 */
405 private static function childFldKey($child)
406 {
407 if (is_object($child) && isset($child->fldKey)) {
408 return $child->fldKey;
409 }
410 if (is_array($child) && isset($child['fldKey'])) {
411 return $child['fldKey'];
412 }
413 return null;
414 }
415
416 /**
417 * Add childFields of every rendered parent into $renderedKeys (by ref).
418 *
419 * Several field types keep their children flat in `fields` and NEVER in
420 * any layout — Name (first/middle/last), Address (street/city/zip/...),
421 * Email and Password (confirm fields). A child is rendered iff its
422 * parent is, so each rendered parent's childFields[].fldKey must join
423 * the rendered set or their validation would be wrongly skipped.
424 *
425 * Runs to a fixpoint so expansion is safe regardless of field order or
426 * nesting depth.
427 *
428 * @param array $renderedKeys key => true map, mutated in place
429 * @param iterable $fields fields keyed by field key; each field may be
430 * a processed array (getFields()) or raw stdClass
431 */
432 protected static function expandChildFieldKeys(array &$renderedKeys, $fields)
433 {
434 do {
435 $grew = false;
436 foreach ($fields as $key => $field) {
437 if (!isset($renderedKeys[$key])) {
438 continue;
439 }
440 $childFields = null;
441 if (is_object($field) && isset($field->childFields)) {
442 $childFields = $field->childFields;
443 } elseif (is_array($field) && isset($field['childFields'])) {
444 $childFields = $field['childFields'];
445 }
446 if (empty($childFields) || !is_iterable($childFields)) {
447 continue;
448 }
449 foreach ($childFields as $child) {
450 $childKey = self::childFldKey($child);
451 if ($childKey && !isset($renderedKeys[$childKey])) {
452 $renderedKeys[$childKey] = true;
453 $grew = true;
454 }
455 }
456 }
457 } while ($grew);
458 }
459
460 /**
461 * getFields() narrowed to provably-rendered fields: key present in any
462 * breakpoint of any step/nested layout, OR a childField of a rendered
463 * parent (name/address/email/password children live outside layouts),
464 * OR the synthetic GCLID key.
465 *
466 * SECURITY: fail-closed — if the layout yields no keys, ALL fields are
467 * returned (current behavior). Derives exclusively from DB-stored
468 * form_content, never from POST, so submitters cannot influence which
469 * fields are validated.
470 */
471 public function getRenderedFields()
472 {
473 $fields = $this->getFields();
474 try {
475 $renderedKeys = self::renderedKeyMap($this->getFormLayout(), $this->getFormNestedLayout(), $fields);
476 } catch (\Throwable $e) {
477 return $fields; // fail-closed: unusable layout validates all fields
478 }
479 if (null === $renderedKeys) {
480 return $fields;
481 }
482 $rendered = [];
483 foreach ($fields as $key => $field) {
484 // renderedKeys already includes NON_LAYOUT_FIELD_KEYS (GCLID, ...)
485 if (isset($renderedKeys[$key])) {
486 $rendered[$key] = $field;
487 }
488 }
489 return $rendered;
490 }
491
492 /**
493 * Field keys that are legitimately part of a form yet never appear in any
494 * layout — synthetic/system fields the renderer always keeps. They must
495 * never be flagged as orphan (save guard) or dropped from validation
496 * (renderer). Extend this list as new non-layout system fields are added.
497 *
498 * @var string[]
499 */
500 protected const NON_LAYOUT_FIELD_KEYS = ['GCLID'];
501
502 /**
503 * True when $fields (object or array) holds $key.
504 *
505 * @param object|array $fields
506 * @param string $key
507 */
508 private static function fieldExists($fields, $key)
509 {
510 return is_object($fields) ? isset($fields->{$key}) : (is_array($fields) && isset($fields[$key]));
511 }
512
513 /**
514 * Flatten a raw layout into one object unioning lg/md/sm across all steps.
515 * Accepts a single layout object or an array of multi-step entries (each
516 * wrapping its layout in ->layout). Shared by the frontend renderer
517 * (getRenderedFields) and the admin save/import orphan guard
518 * (computeOrphanFieldKeys) so both flatten identically.
519 *
520 * @param array|object $layout
521 *
522 * @return object {lg,md,sm} arrays of {i} items
523 */
524 private static function flattenLayout($layout)
525 {
526 $flat = new stdClass();
527 $flat->lg = [];
528 $flat->md = [];
529 $flat->sm = [];
530 $addLayout = function ($lay) use ($flat) {
531 if (!is_object($lay)) {
532 return;
533 }
534 foreach (['lg', 'md', 'sm'] as $brkpnt) {
535 if (isset($lay->{$brkpnt}) && is_array($lay->{$brkpnt})) {
536 $flat->{$brkpnt} = array_merge($flat->{$brkpnt}, $lay->{$brkpnt});
537 }
538 }
539 };
540 if (is_array($layout)) {
541 // multi-step: each entry wraps its layout in ->layout
542 foreach ($layout as $step) {
543 $addLayout(isset($step->layout) ? $step->layout : $step);
544 }
545 } else {
546 $addLayout($layout);
547 }
548 return $flat;
549 }
550
551 /**
552 * SINGLE SOURCE OF TRUTH for "which field keys the renderer would show":
553 * unions all breakpoints across steps, adds nested-layout children of
554 * RENDERED containers only, then expands childFields of rendered parents
555 * (name/address/email/password children live outside layouts).
556 *
557 * Both getRenderedFields (frontend validation) and computeOrphanFieldKeys
558 * (admin save guard) route through this so the two can never diverge — a
559 * divergence would prune a real field or wrongly validate an orphan.
560 *
561 * @param array|object $layout single layout or array of steps
562 * @param object|null $nestedLayout keyed by parent field key
563 * @param object|array $fields form_content->fields
564 *
565 * @return array<string,true>|null key=>true map, or null when the layout is
566 * unusable (callers MUST fail closed)
567 */
568 protected static function renderedKeyMap($layout, $nestedLayout, $fields)
569 {
570 // Fail closed on a partial/unloaded multi-step layout: a step with no
571 // layout items at all almost always means the layout never finished
572 // loading/syncing (not a real "every field on this step was deleted").
573 // Treating it as usable would flag that step's real fields as orphan.
574 if (is_array($layout)) {
575 if (empty($layout)) {
576 return null;
577 }
578 foreach ($layout as $step) {
579 $stepLayout = is_object($step) && isset($step->layout) ? $step->layout : $step;
580 if (empty(self::collectLayoutKeys(self::flattenLayout($stepLayout)))) {
581 return null;
582 }
583 }
584 }
585 $flat = self::flattenLayout($layout);
586 $rootKeys = self::collectLayoutKeys($flat);
587 if (empty($rootKeys)) {
588 return null;
589 }
590 $renderedKeys = array_fill_keys($rootKeys, true);
591 // nested children count as rendered only when their container is —
592 // matches the renderer, which skips stale nestedLayout entries
593 if (is_object($nestedLayout) || is_array($nestedLayout)) {
594 foreach ($nestedLayout as $parentKey => $nLay) {
595 if (!isset($renderedKeys[$parentKey])) {
596 continue;
597 }
598 foreach (self::collectLayoutKeys($nLay) as $nestedKey) {
599 $renderedKeys[$nestedKey] = true;
600 }
601 }
602 }
603 self::expandChildFieldKeys($renderedKeys, $fields);
604 // synthetic/system fields (e.g. GCLID) live outside every layout; the
605 // renderer always keeps them, so they must never count as orphan
606 foreach (self::NON_LAYOUT_FIELD_KEYS as $sysKey) {
607 if (self::fieldExists($fields, $sysKey)) {
608 $renderedKeys[$sysKey] = true;
609 }
610 }
611 return $renderedKeys;
612 }
613
614 /**
615 * Pure variant of the orphan rule for the admin save/import guard: returns
616 * the keys of $fields absent from every layout (children of rendered
617 * parents excluded). Uses renderedKeyMap — the exact flattening the
618 * renderer uses — so the save guard and the renderer never diverge.
619 *
620 * @param array|object $layout single layout or array of steps ({layout} each)
621 * @param object|null $nestedLayout keyed by parent field key
622 * @param mixed $fields raw form_content->fields (decoded JSON: shape is not guaranteed, hence the runtime guard)
623 *
624 * @return string[]|null orphan keys to drop, or null when the layout is
625 * unusable (fail closed: drop nothing)
626 */
627 public static function computeOrphanFieldKeys($layout, $nestedLayout, $fields)
628 {
629 if (!is_object($fields) && !is_array($fields)) {
630 return null;
631 }
632 $renderedKeys = self::renderedKeyMap($layout, $nestedLayout, $fields);
633 if (null === $renderedKeys) {
634 return null; // unusable layout: fail closed, drop nothing
635 }
636 $orphans = [];
637 foreach ($fields as $key => $field) {
638 if (!isset($renderedKeys[$key])) {
639 $orphans[] = $key;
640 }
641 }
642 return $orphans;
643 }
644
645 public function getFieldsBasedOnLayout()
646 {
647 $layout = $this->getFlatenFormLayout();
648
649 $fieldKeyOrderbasedOnLayout = array_map(function ($fld) {
650 return $fld->i;
651 }, $layout->lg);
652 $orderedFields = [];
653 $fields = $this->getFields();
654
655 foreach ($fieldKeyOrderbasedOnLayout as $key) {
656 if (array_key_exists($key, $fields)) {
657 $orderedFields[$key] = $fields[$key];
658 }
659 }
660
661 foreach ($fields as $k=>$v) {
662 if (!array_key_exists($k, $fieldKeyOrderbasedOnLayout)) {
663 $orderedFields[$k] = $fields[$k];
664 }
665 }
666
667 return $orderedFields;
668 }
669
670 public function getFields()
671 {
672 if (!is_null($this->_fields)) {
673 return $this->_fields;
674 }
675 $form_content = \json_decode(static::$form[0]->form_content);
676 $layout = $form_content->layout;
677 $fields = $form_content->fields;
678 $field_details = [];
679 foreach ($fields as $key => $field) {
680 if ('recaptcha' === $field->typ || 'hcaptcha' === $field->typ) {
681 continue;
682 }
683 // $field_name = empty($field->lbl) ? null : \preg_replace('/[\`\~\!\@\#\$\'\.\s\?\+\-\*\&\|\/\\\!]/', '_', $field->lbl);
684 $field_type = $field->typ;
685 $field_details[$key]['label'] = !empty($field->lbl) ? $field->lbl : (!empty($field->adminLbl) ? $field->adminLbl : (!empty($field->fieldName) ? $field->fieldName : null));
686 $field_details[$key]['type'] = $field_type;
687 $field_details[$key]['key'] = $key;
688 $field_details[$key]['name'] = isset($field->fieldName) ? $field->fieldName : '';
689 if (isset($field->customType)) {
690 $field_details[$key]['customType'] = $field->customType;
691 }
692 // fields with confirm field
693 if (isset($field->childFields)) {
694 $field_details[$key]['childFields'] = $field->childFields;
695 }
696 if (isset($field->parentFieldKey)) {
697 $field_details[$key]['parentFieldKey'] = $field->parentFieldKey;
698 if (isset($field->isDeactive)) {
699 $field_details[$key]['isDeactive'] = $field->isDeactive;
700 }
701 }
702 if (isset($field->err)) {
703 if (isset($field->err->entryUnique)) {
704 $field_details[$key]['entryUnique'] = $field->err->entryUnique;
705 }
706 if (isset($field->err->userUnique)) {
707 $field_details[$key]['userUnique'] = $field->err->userUnique;
708 }
709 }
710
711 if (isset($field->mul)) {
712 $field_details[$key]['mul'] = $field->mul;
713 }
714 if (in_array($field_type, ['name'])) {
715 $field_details[$key]['label'] = $field->adminLbl ?? $field->lbl;
716 }
717 if ('file-up' === $field_type && isset($field->exts)) {
718 $field_details[$key]['valid']['type'] = $field->exts;
719 }
720 if ('file-up' === $field_type && isset($field->mxUp)) {
721 $field_details[$key]['valid']['upload_size'] = (int) $field->mxUp;
722 }
723 if (isset($field->valid) && !is_null($field->valid)) {
724 if (isset($field->valid->req)) {
725 $field_details[$key]['valid']['req'] = $field->valid->req;
726 }
727 if (isset($field->valid->reqMsg)) {
728 $field_details[$key]['valid']['reqMsg'] = $field->valid->reqMsg;
729 }
730 if (isset($field->valid->typMsg)) {
731 $field_details[$key]['valid']['typMsg'] = $field->valid->typMsg;
732 }
733 if (isset($field->valid->hide)) {
734 $field_details[$key]['valid']['hide'] = $field->valid->hide;
735 }
736 }
737 if ($this->isRepeatedField($key)) {
738 $field_details[$key]['repeated'] = true;
739 }
740 }
741 if ($this->isGCLIDEnabled()) {
742 $field_details['GCLID']['name'] = 'GCLID';
743 $field_details['GCLID']['adminLbl'] = 'GCLID';
744 $field_details['GCLID']['key'] = 'GCLID';
745 $field_details['GCLID']['type'] = 'hidden';
746 }
747 $this->_fields = $field_details;
748 return $field_details;
749 }
750
751 public function getFieldsKey()
752 {
753 $form_content = \json_decode(static::$form[0]->form_content);
754 $fields = $form_content->fields;
755 $field_details = [];
756 foreach ($fields as $key => $field) {
757 if ('recaptcha' === $field->typ || 'hcaptcha' === $field->typ) {
758 continue;
759 }
760 // $field_name = empty($field->lbl) ? null : \preg_replace('/[\`\~\!\@\#\$\'\.\s\?\+\-\*\&\|\/\\\!]/', '_', $field->lbl);
761 $field_details[$key] = $key;
762 }
763 if ($this->isGCLIDEnabled()) {
764 $field_details['GCLID'] = 'GCLID';
765 }
766 return $field_details;
767 }
768
769 public function getFieldLabel($forQuery = false)
770 {
771 if (!is_null($this->_field_label)) {
772 return $this->_field_label;
773 }
774 $form_content = \json_decode(static::$form[0]->form_content);
775 $fields = $form_content->fields;
776 $field_details = [];
777 $fieldCounter = 0;
778 foreach ($fields as $key => $field) {
779 if ('recaptcha' === $field->typ || 'turnstile' === $field->typ || 'html' === $field->typ || 'button' === $field->typ) {
780 continue;
781 }
782 $field_details[$fieldCounter]['name'] = empty($field->lbl) ? null : $field->lbl;
783 $field_details[$fieldCounter]['adminLbl'] = empty($field->adminLbl) ? $field_details[$fieldCounter]['name'] : $field->adminLbl;
784 $field_details[$fieldCounter]['key'] = $key;
785 $field_details[$fieldCounter]['type'] = $field->typ;
786 $fieldCounter += 1;
787 }
788 if ($this->isGCLIDEnabled()) {
789 $field_details[$fieldCounter]['name'] = 'GCLID';
790 $field_details[$fieldCounter]['adminLbl'] = 'GCLID';
791 $field_details[$fieldCounter]['key'] = 'GCLID';
792 $field_details[$fieldCounter]['type'] = 'hidden';
793 $fieldCounter += 1;
794 }
795 if (!$forQuery) {
796 $field_details = (array) $this->addEntryInfo($field_details, $fieldCounter);
797 }
798 $this->_field_label = $field_details;
799 return $field_details;
800 }
801
802 public function getUploadFields()
803 {
804 if (!is_null($this->_has_upload)) {
805 return $this->_has_upload;
806 }
807 $upload_fields = [];
808 $form_field_details = $this->getFields();
809 foreach ($form_field_details as $field_name => $__field_detail) {
810 if (isset($__field_detail['type']) && ('file-up' === $__field_detail['type'] || 'advanced-file-up' === $__field_detail['type'])) {
811 $upload_fields[] = $field_name;
812 }
813 }
814 $this->_has_upload = $upload_fields;
815 return $upload_fields;
816 }
817
818 public function getSignatureFilePath($blobLink, $form_id, $fieldKey, $entry_id, $imgType)
819 {
820 $imgTypes = [
821 'image/png' => 'png',
822 'image/jpeg' => 'jpg',
823 'image/svg+xml' => 'svg',
824 ];
825 try {
826 if (!isset($imgTypes[$imgType])) {
827 throw new \InvalidArgumentException("Unsupported image type: $imgType");
828 }
829 $parts = explode(',', $blobLink, 2);
830 if (2 !== count($parts) || false === ($decoded_image = base64_decode($parts[1]))) {
831 throw new \RuntimeException('Invalid or corrupt signature data URI');
832 }
833
834 // An attacker-controlled SVG signature is written to a web-served path, so a raw write is a
835 // stored-XSS sink. Sanitize with the same enshrined library the upload path uses (FileHandler).
836 if ('svg' === $imgTypes[$imgType]) {
837 $clean = (new Sanitizer())->sanitize($decoded_image);
838 if (false === $clean) {
839 throw new \RuntimeException('Invalid or unsafe SVG signature data');
840 }
841 $decoded_image = $clean;
842 }
843
844 $_upload_dir = FileHandler::getEntriesFileUploadDir($form_id, $entry_id);
845 FileHandler::createIndexFile($_upload_dir);
846 $uniqueId = time() . '-' . bin2hex(\random_bytes(4));
847 $filename = "{$entry_id}-{$fieldKey}-{$uniqueId}.{$imgTypes[$imgType]}";
848 $fullPath = $_upload_dir . DIRECTORY_SEPARATOR . $filename;
849 if (false === file_put_contents($fullPath, $decoded_image)) {
850 throw new \RuntimeException("Failed to write image to $fullPath");
851 }
852 return $filename;
853 } catch (\Throwable $e) {
854 Log::debug_log("[Signature Error] Form: $form_id, Entry: $entry_id, Field: $fieldKey - " . $e->getMessage());
855 return 'signature-failed.png'; // or a default filename if appropriate
856 }
857 }
858
859 private function entryInsert($user_details)
860 {
861 $formEntryModel = new FormEntryModel();
862 $entryId = $formEntryModel->insert(
863 [
864 'form_id' => $this->form_id,
865 'user_id' => $user_details['id'],
866 'user_ip' => $user_details['ip'],
867 'user_device' => $user_details['device'],
868 'referer' => $user_details['page'],
869 'status' => $this->_saveFormAsDraft ? 9 : 1,
870 'created_at' => $user_details['time'],
871 ]
872 );
873 return $entryId;
874 }
875
876 public function submisionLog($user_details, $entry_id, $type)
877 {
878 $formEntryLogModel = new FormEntryLogModel();
879 $submissionLogData = [
880 'user_id' => $user_details['id'],
881 'action_type' => $type, // create, update
882 'log_type' => 'entry',
883 'ip' => $user_details['ip'],
884 'form_entry_id' => $entry_id,
885 'content' => ['user_device' => $user_details['device']],
886 'form_id' => $this->form_id,
887 'created_at' => $user_details['time'],
888 ];
889 $submissionLogData = apply_filters('bitform_filter_submission_log_data', $submissionLogData, $this->form_id, $type);
890 $logId = $formEntryLogModel->form_log_insert(
891 $submissionLogData
892 );
893 return $logId;
894 }
895
896 private function isArrayAllKeyInt($InputArray)
897 {
898 if (!is_array($InputArray)) {
899 return false;
900 }
901
902 if (count($InputArray) <= 0) {
903 return true;
904 }
905
906 return array_unique(array_map('is_int', array_keys($InputArray))) === [true];
907 }
908
909 public function formatSubmittedData($submitted_data)
910 {
911 $form_content = $this->getFormContent();
912 $form_fields = $form_content->fields;
913
914 foreach ($submitted_data as $key => $value) {
915 if (!isset($form_fields->{$key})) {
916 continue;
917 }
918 $field_data = $form_fields->{$key};
919 $field_type = $field_data->typ;
920 $normalizedParentValue = $this->normalizeSubmittedValue($value);
921 $parentFieldName = isset($field_data->fieldName) ? $field_data->fieldName : '';
922 // Confirm child of a repeated email/password never persists — the non-repeated
923 // path drops it too (the validator collapses the parent to its primary value).
924 $isRepeatedConfirmComposite = in_array($field_type, ['email', 'password'], true) && $this->isRepeatedField($key);
925 if (!$isRepeatedConfirmComposite && !empty($field_data->childFields) && is_array($field_data->childFields)) {
926 foreach ($field_data->childFields as $childFieldRef) {
927 $childFieldKey = isset($childFieldRef->fldKey) ? $childFieldRef->fldKey : '';
928 if (empty($childFieldKey) || !isset($form_fields->{$childFieldKey})) {
929 continue;
930 }
931
932 $childFieldData = $form_fields->{$childFieldKey};
933 $childFieldName = isset($childFieldData->fieldName) ? $childFieldData->fieldName : '';
934 $childFieldName = FieldValueHandler::deriveChildName($childFieldName, $parentFieldName);
935 if (empty($childFieldName)) {
936 continue;
937 }
938
939 $childValue = FieldValueHandler::extractChildValueFromParentValue($normalizedParentValue, $childFieldName, $childFieldKey);
940 if (null !== $childValue) {
941 $submitted_data[$childFieldKey] = $childValue;
942 }
943 }
944 }
945
946 if ($this->isRepeatedField($key) && in_array($field_type, ['name', 'address', 'email', 'password'])) {
947 $normalizedRows = $this->normalizeRepeatedCompositeFieldInput($normalizedParentValue);
948 if ($isRepeatedConfirmComposite && is_array($normalizedRows)) {
949 // Keep only the primary value per row, matching the non-repeated behavior
950 // where a confirm-enabled field collapses to its primary value.
951 foreach ($normalizedRows as $rowIndex => $rowValue) {
952 if (is_array($rowValue) && array_key_exists('primary', $rowValue)) {
953 $normalizedRows[$rowIndex] = $rowValue['primary'];
954 }
955 }
956 }
957 $submitted_data[$key] = $normalizedRows;
958 }
959
960 if ('select' === $field_type && !empty($field_data->config->multipleSelect)) {
961 $valueArr = [];
962 if ($this->isRepeatedField($key) && is_array($normalizedParentValue)) {
963 foreach ($normalizedParentValue as $index => $v) {
964 $valueArr[$index] = explode(BITFORMS_BF_SEPARATOR, $v);
965 }
966 } else {
967 $valueArr = explode(BITFORMS_BF_SEPARATOR, (string) $value);
968 }
969 $submitted_data[$key] = $valueArr;
970 }
971 }
972 $submitted_data = apply_filters('bitform_filter_format_submitted_data', $submitted_data, $this->form_id);
973 return $submitted_data;
974 }
975
976 private function normalizeSubmittedValue($value)
977 {
978 if (!is_string($value)) {
979 return $value;
980 }
981
982 $decoded = json_decode($value, true);
983 return (JSON_ERROR_NONE === json_last_error()) ? $decoded : $value;
984 }
985
986 private function addNewFilePathToFiles($form_id, $entry_id, $file_fields = [])
987 {
988 $common_file_path = Helpers::getFullPathWithEncryptedEntryId($form_id, $entry_id);
989 foreach ($_FILES as $field_key => $file_details) {
990 if (!($file_fields && in_array($field_key, $file_fields))) {
991 continue;
992 }
993
994 $isRepeaterFldKey = $this->isRepeatedField($field_key);
995 if ($isRepeaterFldKey && isset($file_details['new_name'])) {
996 // If 'new_name' is an array (i.e., for repeated fields)
997 foreach ($file_details['new_name'] as $slNo => $newFileNamesArray) {
998 if (is_array($newFileNamesArray)) {
999 foreach ($newFileNamesArray as $newFileName) {
1000 $filePath = $common_file_path . DIRECTORY_SEPARATOR . $newFileName;
1001 $_FILES[$field_key]['file_path'][$slNo][] = $filePath;
1002 }
1003 } else {
1004 // Generate the file path for each file
1005 $filePath = $common_file_path . DIRECTORY_SEPARATOR . $newFileNamesArray;
1006 $_FILES[$field_key]['file_path'][$slNo] = $filePath;
1007 }
1008 }
1009 } elseif (isset($file_details['new_name'])) {
1010 // If 'new_name' is an array (i.e., for repeated fields)
1011 if (is_array($file_details['new_name'])) {
1012 foreach ($file_details['new_name'] as $slNo => $newFileName) {
1013 // Generate the file path for each file
1014 $filePath = $common_file_path . DIRECTORY_SEPARATOR . $newFileName;
1015 $_FILES[$field_key]['file_path'][$slNo] = $filePath;
1016 }
1017 } else {
1018 $filePath = $common_file_path . DIRECTORY_SEPARATOR . $file_details['new_name'];
1019 $_FILES[$field_key]['file_path'] = $filePath;
1020 }
1021 }
1022 }
1023 }
1024
1025 private function formatRepeateFieldData($submitted_data, $form_fields)
1026 {
1027 $repeaterFields = $this->getRepeaterFields();
1028 foreach ($repeaterFields as $repeaterFldKey => $repeatedFields) {
1029 $repeatIndexes = $submitted_data["{$form_fields[$repeaterFldKey]['name']}-repeat-index"];
1030 $repeatIndexes = explode(',', $repeatIndexes);
1031 foreach ($repeatedFields as $repeatedField) {
1032 $oldFileKey = "{$repeatedField}_old";
1033 if (isset($submitted_data[$oldFileKey]) && is_array($submitted_data[$oldFileKey])) {
1034 $oldFileValues = $submitted_data[$oldFileKey];
1035 $oldFileKeys = array_map('strval', array_keys($oldFileValues));
1036 $repeatIndexKeys = array_map('strval', $repeatIndexes);
1037 $oldFilesUseRepeatIndexes = empty(array_diff($oldFileKeys, $repeatIndexKeys));
1038 $normalizedOldFileValues = [];
1039
1040 foreach ($repeatIndexes as $slNo => $repeatIndex) {
1041 $oldFileSourceIndex = $oldFilesUseRepeatIndexes ? $repeatIndex : $slNo;
1042 if (array_key_exists($oldFileSourceIndex, $oldFileValues)) {
1043 $normalizedOldFileValues[$slNo] = $oldFileValues[$oldFileSourceIndex];
1044 }
1045 }
1046
1047 $submitted_data[$oldFileKey] = $normalizedOldFileValues;
1048 }
1049 }
1050
1051 foreach ($repeatIndexes as $slNo => $repeatIndex) {
1052 foreach ($repeatedFields as $repeatedField) {
1053 if (!isset($submitted_data[$repeatedField][$repeatIndex])) {
1054 continue;
1055 }
1056 if (!isset($submitted_data[$repeaterFldKey][$slNo])) {
1057 $submitted_data[$repeaterFldKey][$slNo] = [];
1058 }
1059 if (!isset($submitted_data[$repeaterFldKey][$slNo][$repeatedField])) {
1060 $submitted_data[$repeaterFldKey][$slNo][$repeatedField] = [];
1061 }
1062 $submitted_data[$repeaterFldKey][$slNo][$repeatedField] = $submitted_data[$repeatedField][$repeatIndex];
1063 }
1064 }
1065 foreach ($repeatedFields as $repeatedField) {
1066 unset($submitted_data[$repeatedField]);
1067 }
1068 unset($submitted_data["{$form_fields[$repeaterFldKey]['name']}-repeat-index"]);
1069 }
1070
1071 return $submitted_data;
1072 }
1073
1074 private function saveEntryMeta($submitted_data, $entry_id)
1075 {
1076 $errorInEntryMetaInsert = false;
1077 $entryMeta = new FormEntryMetaModel();
1078 foreach ($submitted_data as $key => $value) {
1079 $value = $submitted_data[$key];
1080 if (is_string($value)) {
1081 $value = wp_unslash($value);
1082 } elseif ($this->isArrayAllKeyInt($value)) {
1083 $value = wp_json_encode(array_values($value));
1084 } else {
1085 $value = wp_json_encode($value);
1086 }
1087 // Form entry meta insert; meta_key/meta_value required to store dynamic field data per entry.
1088 $status = $entryMeta->insert(
1089 [
1090 'bitforms_form_entry_id' => $entry_id,
1091 'meta_key' => $key,
1092 'meta_value' => $value,
1093 ]
1094 );
1095 if (is_wp_error($status)) {
1096 $errorInEntryMetaInsert = true;
1097 break;
1098 }
1099 }
1100 return $errorInEntryMetaInsert;
1101 }
1102
1103 public function setSaveFormAsDraft()
1104 {
1105 $this->_saveFormAsDraft = true;
1106 }
1107
1108 public function saveFormEntry($submitted_data)
1109 {
1110 // CSRF verified upstream via FrontendFormManager::verifySubmissionNonce() before this method is invoked.
1111 $submitted_data = $this->formatSubmittedData($submitted_data);
1112 $submitted_data = apply_filters('bitform_filter_save_form_entry', $submitted_data, $this->form_id);
1113 $form_content = \json_decode(static::$form[0]->form_content);
1114 do_action('bitform_save_entry', $this, $submitted_data, $this->form_id);
1115 $key = null;
1116 $ipTool = new IpTool();
1117 $fileHandler = new FileHandler();
1118 $form_fields = $this->getFields();
1119 $file_fields = $this->getUploadFields();
1120
1121 foreach ($_FILES as $file_name => $file_details) {
1122 if ($file_fields && in_array($file_name, $file_fields)) {
1123 $validation = $fileHandler->validation($file_name, $file_details, $this->form_id);
1124 if (!empty($validation['error_type']) && !empty($validation['message'])) {
1125 return new WP_Error($validation['error_type'], esc_html($validation['message']));
1126 }
1127 }
1128 }
1129 $user_details = $ipTool->getUserDetail();
1130 $user_details = apply_filters('bitform_filter_user_details', $user_details, $this->form_id);
1131 $user_details = apply_filters('bitform_filter_save_entry_user_details', $user_details, $this->form_id);
1132
1133 $form_fields = $this->getFields();
1134 $submitted_data = $this->passwordEncrypted($submitted_data, $form_fields);
1135 $submitted_data = $this->formatRepeateFieldData($submitted_data, $form_fields);
1136 global $wpdb;
1137 // Direct transaction control; no user input involved.
1138 $wpdb->query('START TRANSACTION');
1139 $entry_id = $this->entryInsert($user_details);
1140 $log_id = null;
1141
1142 $GLOBALS['bitform_entry_id'] = $entry_id;
1143
1144 if (is_wp_error($entry_id)) {
1145 return new WP_Error('insert_error', __('Sorry, Error occurred in saving form entry', 'bit-form'));
1146 }
1147 if ($entry_id) {
1148 $log_id = $this->submisionLog($user_details, $entry_id, 'create', $key);
1149 if (is_wp_error($log_id)) {
1150 $wpdb->query('ROLLBACK');
1151 return new WP_Error('error_entry_log', __('Sorry, error occurred in logging form entry', 'bit-form'));
1152 }
1153 }
1154 if ($entry_id) {
1155 $submitted_fields = $this->getFormContentWithValue($submitted_data)->fields;
1156 $workFlowRunHelper = new WorkFlow($this->form_id);
1157
1158 $workFlowreturnedOnSubmit = $workFlowRunHelper->executeOnSubmit(
1159 'create',
1160 $submitted_fields,
1161 $submitted_data,
1162 $entry_id,
1163 $log_id
1164 );
1165
1166 if (!empty($workFlowreturnedOnSubmit['fields'])) {
1167 $submitted_data = $workFlowreturnedOnSubmit['fields'];
1168 }
1169
1170 $file_fields = $this->getUploadFields();
1171 $formFields = $this->getFields();
1172 $submitted_data = FileHandler::tempDirToUploadDir($submitted_data, $formFields, $this->form_id, $entry_id);
1173 $fileHandler = new FileHandler();
1174 foreach ($_FILES as $field_key => $file_details) {
1175 if ($file_fields && in_array($field_key, $file_fields)) {
1176 $fileNames = [];
1177 $repeaterFldKey = $this->isRepeatedField($field_key);
1178 if ($repeaterFldKey) {
1179 foreach ($file_details['name'] as $slNo => $fileName) {
1180 $repeateFileDetails = [
1181 'name' => $file_details['name'][$slNo],
1182 'type' => $file_details['type'][$slNo],
1183 'tmp_name' => $file_details['tmp_name'][$slNo],
1184 'error' => $file_details['error'][$slNo],
1185 'size' => $file_details['size'][$slNo],
1186 ];
1187 $fileNames = $fileHandler->moveUploadedFiles($repeateFileDetails, $this->form_id, $entry_id);
1188 if (!empty($fileNames)) {
1189 $submitted_data[$repeaterFldKey][$slNo - 1][$field_key] = $fileNames;
1190 $_FILES[$field_key]['new_name'][$slNo - 1] = $fileNames;
1191 }
1192 }
1193 } else {
1194 $fileNames = $fileHandler->moveUploadedFiles($file_details, $this->form_id, $entry_id);
1195 if (!empty($fileNames)) {
1196 $submitted_data[$field_key] = $fileNames;
1197 $_FILES[$field_key]['new_name'] = $fileNames;
1198 }
1199 }
1200 }
1201 }
1202
1203 // Get the common path for file storage
1204 $this->addNewFilePathToFiles($this->form_id, $entry_id, $file_fields);
1205
1206 foreach ($form_content->fields as $key => $field) {
1207 /* ======== for Signature field ===========*/
1208 if ('signature' === $field->typ) {
1209 if (isset($submitted_data[$key])) {
1210 $fld_data = $submitted_data[$key];
1211 $img_type = $field->config->imgTyp;
1212 $submitted_data[$key] = $this->getSignatureFilePath($fld_data, $this->form_id, $key, $entry_id, $img_type);
1213 }
1214 }
1215
1216 // for Signature field inside reepater
1217 if ('repeater' === $field->typ) {
1218 $rptr_data = $submitted_data[$key];
1219 $formFields = $form_content->fields;
1220 $this->setSignatureFilePathInRepeater($rptr_data, $key, $formFields, $entry_id, $submitted_data);
1221 }
1222 }
1223
1224 if (!isset($form_content->additional->enabled->submission)) {
1225 $errorInEntryMetaInsert = $this->saveEntryMeta($submitted_data, $entry_id);
1226 if ($errorInEntryMetaInsert) {
1227 do_action('bitform_save_entry_error', $this, $submitted_data, $this->form_id);
1228 $wpdb->query('ROLLBACK');
1229 return new WP_Error('insert_error', __('Sorry, Error occured in saving form entry data', 'bit-form'));
1230 }
1231 do_action('bitform_after_save_entry_success', $this, $submitted_data, $this->form_id, $entry_id);
1232 } else {
1233 $wpdb->query('ROLLBACK');
1234 }
1235 $wpdb->query('COMMIT');
1236 $this->setSubmissionCount();
1237 $workFlowreturnedOnSubmit['entry_id'] = $entry_id;
1238 $workFlowreturnedOnSubmit['fields'] = $submitted_data;
1239 $workFlowreturnedOnSubmit = apply_filters('bitform_filter_return_submit_success', $workFlowreturnedOnSubmit, $this->form_id);
1240
1241 return $workFlowreturnedOnSubmit;
1242 }
1243 }
1244
1245 private function setSignatureFilePathInRepeater($repeaterData, $repeaterFieldKey, $formFields, $entry_id, &$submitted_data)
1246 {
1247 foreach ($repeaterData as $rptr_entry_index => $rptr_entries) {
1248 foreach ($rptr_entries as $entry_key => $entry_value) {
1249 if (!isset($formFields->{$entry_key})) {
1250 continue;
1251 }
1252 $rptr_entry_info = $formFields->{$entry_key};
1253
1254 if ('signature' === $rptr_entry_info->typ) {
1255 $imgType = $rptr_entry_info->config->imgTyp;
1256 $signatureImage = $this->getSignatureFilePath($entry_value, $this->form_id, $repeaterFieldKey, $entry_id, $imgType);
1257 $submitted_data[$repeaterFieldKey][$rptr_entry_index][$entry_key] = $signatureImage;
1258 }
1259 }
1260 }
1261 }
1262
1263 public function passwordEncrypted($updatedValue, $form_fields)
1264 {
1265 $integrationHandler = new IntegrationHandler($this->form_id);
1266 $formIntegrations = $integrationHandler->getAllIntegration('wp_user_auth', 'wp_auth', 1);
1267 if (!isset($formIntegrations->errors['result_empty'])) {
1268 foreach ($form_fields as $field) {
1269 if (array_key_exists($field['key'], $updatedValue) && 'password' === $field['type']) {
1270 $updatedValue[$field['key']] = '**** (encrypted)';
1271 }
1272 }
1273 }
1274 return $updatedValue;
1275 }
1276
1277 private function normalizeOldFileValues($stored_files, $old_values)
1278 {
1279 $stored_files = is_array($stored_files) ? $stored_files : [];
1280 if (!is_array($old_values)) {
1281 $old_values_string = trim((string) $old_values);
1282 $decoded_old_values = json_decode($old_values_string, true);
1283 $old_values = is_array($decoded_old_values) ? $decoded_old_values : explode(',', $old_values_string);
1284 }
1285
1286 $normalized_values = [];
1287 foreach ($old_values as $value) {
1288 if (!is_string($value) && !is_numeric($value)) {
1289 continue;
1290 }
1291
1292 $trimmed_value = trim((string) $value);
1293 if ('' === $trimmed_value) {
1294 continue;
1295 }
1296
1297 if (in_array($trimmed_value, $stored_files, true)) {
1298 $normalized_values[] = $trimmed_value;
1299 }
1300 }
1301
1302 return array_values(array_unique($normalized_values));
1303 }
1304
1305 public function updateFormEntry($updatedValue, $formID, $entryID)
1306 {
1307 // CSRF / entry-token verified upstream via FrontendFormManager::handleUpdateEntry() before this method is invoked.
1308 $updatedValue = $this->formatSubmittedData($updatedValue);
1309 $updatedValue = apply_filters('bitform_filter_update_form_entry', $updatedValue, $this->form_id);
1310 do_action('bitform_update_entry', $this, $updatedValue, $formID, $entryID);
1311 $form_content = $this->getFormContent();
1312 if (isset($form_content->additional->enabled->submission)) {
1313 // Run workflow but skip DB/meta update
1314 $workFlowRunHelper = new WorkFlow($formID);
1315 $fieldsWithValue = $this->getFormContentWithValue($updatedValue)->fields;
1316 $workFlowreturnedOnSubmit = $workFlowRunHelper->executeOnSubmit(
1317 'edit',
1318 $fieldsWithValue,
1319 $updatedValue,
1320 $entryID,
1321 0
1322 );
1323 if (empty($workFlowreturnedOnSubmit['message'])) {
1324 $workFlowreturnedOnSubmit['message'] = __('Entry update skipped due to submission restriction.', 'bit-form');
1325 }
1326 $workFlowreturnedOnSubmit['entry_id'] = $entryID;
1327 $workFlowreturnedOnSubmit = apply_filters('bitform_filter_return_edit_success', $workFlowreturnedOnSubmit, $this->form_id);
1328 return $workFlowreturnedOnSubmit;
1329 }
1330
1331 $formEntryModel = new FormEntryModel();
1332 $formEntryLogModel = new FormEntryLogModel();
1333 $formOldData = $formEntryLogModel->get_form_value($entryID);
1334 $key = null;
1335 $entryMeta = new FormEntryMetaModel();
1336 $ipTool = new IpTool();
1337 $user_details = $ipTool->getUserDetail();
1338 $user_details = apply_filters('bitform_filter_user_details', $user_details, $this->form_id);
1339 $user_details = apply_filters('bitform_filter_update_entry_user_details', $user_details, $this->form_id);
1340
1341 $form_fields = $this->getFields();
1342
1343 $updatedValue = $this->passwordEncrypted($updatedValue, $form_fields);
1344 $updatedValue = $this->formatRepeateFieldData($updatedValue, $form_fields);
1345 $field_map = [];
1346 foreach ($formOldData as $index => $data) {
1347 foreach ($form_fields as $field_key => $field) {
1348 if ($data->meta_key === $field['key']) {
1349 $field_map[$field_key] = $field['key'];
1350 }
1351 }
1352 }
1353 $geResult = $formEntryModel->get('status', ['id' => $entryID]);
1354 if (is_wp_error($geResult) || empty($geResult)) {
1355 return new WP_Error('empty_form', __('provided form entries does not exists', 'bit-form'));
1356 }
1357 $oldEntry = $geResult[0];
1358 $formEntry = $formEntryModel->update(
1359 [
1360 'status' => ('9' === $oldEntry->status && !$this->_saveFormAsDraft) ? 1 : $oldEntry->status,
1361 'updated_at' => $user_details['time'],
1362 ],
1363 [
1364 'form_id' => $formID,
1365 'id' => $entryID,
1366 ]
1367 );
1368 $log_id = null;
1369 if ($formEntry) {
1370 $log_id = $this->submisionLog($user_details, $entryID, 'update');
1371 }
1372
1373 if (is_wp_error($formEntry) || !$formEntry) {
1374 return new WP_Error('empty_form', __('provided form entries does not exists', 'bit-form'));
1375 }
1376 $formFields = $this->getFields();
1377 $updatedValue = FileHandler::tempDirToUploadDir($updatedValue, $formFields, $this->form_id, $entryID);
1378 $file_fields = $this->getUploadFields();
1379 if (count($file_fields) > 0) {
1380 $fileHandler = new FileHandler();
1381 foreach ($_FILES as $file_name => $file_details) {
1382 if ($file_fields && in_array($file_name, $file_fields)) {
1383 $validation = $fileHandler->validation($file_name, $file_details, $this->form_id);
1384 if (!empty($validation['error_type']) && !empty($validation['message'])) {
1385 return new WP_Error($validation['error_type'], esc_html($validation['message']));
1386 }
1387 }
1388 }
1389 if (is_object($updatedValue)) {
1390 $updatedValue = (array) $updatedValue;
1391 }
1392 foreach ($file_fields as $field_key) {
1393 $repeaterFldKey = $this->isRepeatedField($field_key);
1394 if (isset($updatedValue[$field_key . '_old'])) {
1395 // Handle file deletion for repeater fields
1396 if ($repeaterFldKey) {
1397 // Form entry meta lookup; meta_key/meta_value query required to retrieve repeater field data by entry.
1398 $repeaterExistData = $entryMeta->get(
1399 'meta_value',
1400 [
1401 'bitforms_form_entry_id' => $entryID,
1402 'meta_key' => $repeaterFldKey,
1403 ]
1404 );
1405 if (!is_wp_error($repeaterExistData)) {
1406 // restructor json
1407 $repeaterExistData = json_decode($repeaterExistData[0]->meta_value, true);
1408 $repeaterExistFiles = [];
1409 $repeaterDeleted_files = [];
1410 $repeaterFiles_old = [];
1411 $submittedRepeaterOldFiles = is_array($updatedValue[$field_key . '_old']) ? $updatedValue[$field_key . '_old'] : [];
1412 foreach ($repeaterExistData as $index => $repeaterRow) {
1413 $repeaterExistFiles[$index] = [];
1414 if (isset($repeaterRow[$field_key]) && !empty($repeaterRow[$field_key]) && is_string($repeaterRow[$field_key])) {
1415 $repeaterExistFiles[$index] = json_decode($repeaterRow[$field_key], true);
1416 }
1417 if (isset($repeaterRow[$field_key]) && !empty($repeaterRow[$field_key]) && is_array($repeaterRow[$field_key])) {
1418 $repeaterExistFiles[$index] = $repeaterRow[$field_key];
1419 }
1420 if (!is_array($repeaterExistFiles[$index])) {
1421 $repeaterExistFiles[$index] = [];
1422 }
1423 $oldFileInputExists = array_key_exists($index, $submittedRepeaterOldFiles);
1424 $repeaterRowExists = $oldFileInputExists || (isset($updatedValue[$repeaterFldKey][$index]) && is_array($updatedValue[$repeaterFldKey][$index]));
1425 $oldFileValues = ($repeaterRowExists && $oldFileInputExists) ? $submittedRepeaterOldFiles[$index] : [];
1426 $repeaterFiles_old[$index] = $this->normalizeOldFileValues($repeaterExistFiles[$index], $oldFileValues);
1427 $repeaterDeleted_files[$index] = array_diff($repeaterExistFiles[$index], $repeaterFiles_old[$index]);
1428 $repeaterFiles_old[$index] = array_values(array_diff($repeaterFiles_old[$index], $repeaterDeleted_files[$index]));
1429 $fileHandler->deleteFiles($formID, $entryID, $repeaterDeleted_files[$index]);
1430 if ($repeaterRowExists) {
1431 if (!isset($updatedValue[$repeaterFldKey][$index]) || !is_array($updatedValue[$repeaterFldKey][$index])) {
1432 $updatedValue[$repeaterFldKey][$index] = [];
1433 }
1434 $updatedValue[$repeaterFldKey][$index][$field_key] = $repeaterFiles_old[$index];
1435 }
1436 }
1437 }
1438 } else {
1439 // Handle file deletion for non-repeater fields; meta_key/meta_value lookup required to identify stored file paths per entry.
1440 $file_exists = $entryMeta->get(
1441 'meta_value',
1442 [
1443 'bitforms_form_entry_id' => $entryID,
1444 'meta_key' => $field_key,
1445 ]
1446 );
1447 if (!is_wp_error($file_exists) && count($file_exists) > 0) {
1448 $files_in_db = json_decode($file_exists[0]->meta_value, true);
1449 if (!is_array($files_in_db)) {
1450 $files_in_db = [];
1451 }
1452 $retained_files = $this->normalizeOldFileValues($files_in_db, empty($updatedValue[$field_key . '_old']) ? [] : $updatedValue[$field_key . '_old']);
1453 $deleted_files = array_diff($files_in_db, $retained_files);
1454 $retained_files = array_values(array_diff($retained_files, $deleted_files));
1455 if (count($deleted_files) > 0) {
1456 $fileHandler->deleteFiles($formID, $entryID, $deleted_files);
1457 }
1458 $updatedValue[$field_key] = $retained_files;
1459 }
1460 }
1461 }
1462 if (!empty($_FILES[$field_key]['name'])) {
1463 if ($repeaterFldKey) {
1464 // Handle repeater field files
1465 $file_details = $_FILES[$field_key];
1466 foreach ($file_details['name'] as $index => $file) {
1467 $old_meta_value = [];
1468 // Retrieve existing old files for this specific repeater index
1469 if (isset($repeaterFiles_old[$index - 1]) && count($repeaterFiles_old[$index - 1]) > 0) {
1470 $old_meta_value = $repeaterFiles_old[$index - 1];
1471 // json format causing issue with repeater file in mail attachment as it's sending broken url(for multistep and abandonment form)
1472 // $updatedValue[$repeaterFldKey][$index - 1][$field_key] = wp_json_encode($old_meta_value);
1473 $updatedValue[$repeaterFldKey][$index - 1][$field_key] = $old_meta_value;
1474 }
1475 $repeateFileDetails = [
1476 'name' => $file_details['name'][$index],
1477 'type' => $file_details['type'][$index],
1478 'tmp_name' => $file_details['tmp_name'][$index],
1479 'error' => $file_details['error'][$index],
1480 'size' => $file_details['size'][$index],
1481 ];
1482 $meta_value = $fileHandler->moveUploadedFiles($repeateFileDetails, $formID, $entryID, $index);
1483 if (!empty($meta_value)) {
1484 $mergedMetaValueWithOld = array_merge($old_meta_value, (array) $meta_value);
1485 // json format causing issue with repeater file in mail attachment as it's sending broken url(for multistep and abandonment form)
1486 // $updatedValue[$repeaterFldKey][$index - 1][$field_key] = wp_json_encode($mergedMetaValueWithOld);
1487 $updatedValue[$repeaterFldKey][$index - 1][$field_key] = $mergedMetaValueWithOld;
1488
1489 $_FILES[$field_key]['new_name'][$index - 1] = $mergedMetaValueWithOld;
1490 // $_FILES[$field_key]['file_path'][$index - 1] = $common_file_path . DIRECTORY_SEPARATOR . $meta_value;
1491 }
1492 }
1493 } else {
1494 // Handle non-repeater field files
1495 $meta_value = $fileHandler->moveUploadedFiles($_FILES[$field_key], $formID, $entryID);
1496 if (!empty($meta_value)) {
1497 $_FILES[$field_key]['new_name'] = $meta_value;
1498 if (isset($updatedValue[$field_key . '_old']) && !is_wp_error($file_exists) && count($file_exists) > 0) {
1499 $meta_value = empty($retained_files) ? $meta_value : array_merge($meta_value, $retained_files);
1500 $updatedValue[$field_key] = $meta_value;
1501 } else {
1502 $updatedValue[$field_key] = $meta_value;
1503 }
1504 }
1505 }
1506 }
1507 }
1508
1509 // Get the common file path to avoid repetitive calculation
1510 $this->addNewFilePathToFiles($formID, $entryID, $file_fields);
1511 }
1512
1513 if (is_object($updatedValue)) {
1514 $updatedValue = (array) $updatedValue;
1515 }
1516 if (isset($updatedValue['_ajax_nonce'], $_REQUEST['g-recaptcha-response']) && sanitize_text_field(wp_unslash($_REQUEST['g-recaptcha-response']))) {
1517 unset($updatedValue['_ajax_nonce'], $_REQUEST['g-recaptcha-response']);
1518 }
1519
1520 $toUpdateValues = [];
1521 foreach ($form_fields as $field) {
1522 if (isset($updatedValue[$field['key']])) {
1523 $toUpdateValues[$field['key']] = $updatedValue[$field['key']];
1524 }
1525 }
1526 $form_content = \json_decode(static::$form[0]->form_content);
1527
1528 foreach ($form_content->fields as $key => $field) {
1529 if ('signature' === $field->typ) {
1530 $fld_data = $updatedValue[$key];
1531 $img_type = $field->config->imgTyp;
1532 $toUpdateValues[$key] = $this->getSignatureFilePath($fld_data, $this->form_id, $key, $entryID, $img_type);
1533 }
1534
1535 // for Signature field inside reepater
1536 if ('repeater' === $field->typ) {
1537 $rptr_data = $updatedValue[$key];
1538 $formFields = $form_content->fields;
1539 $this->setSignatureFilePathInRepeater($rptr_data, $key, $formFields, $entryID, $toUpdateValues);
1540 }
1541 }
1542
1543 $workFlowRunHelper = new WorkFlow($formID);
1544 $workFlowreturnedOnSubmit = $workFlowRunHelper->executeOnSubmit(
1545 'edit',
1546 $this->getFormContentWithValue($toUpdateValues)->fields,
1547 $toUpdateValues,
1548 $entryID,
1549 $log_id
1550 );
1551
1552 if (!empty($workFlowreturnedOnSubmit['fields'])) {
1553 $updatedValue = $workFlowreturnedOnSubmit['fields'];
1554 }
1555
1556 $formEntryMetaUpdateStatus = $entryMeta->update(
1557 $toUpdateValues,
1558 [
1559 'bitforms_form_entry_id' => $entryID,
1560 ]
1561 );
1562 if (is_wp_error($formEntryMetaUpdateStatus) || isset($newFileInsertStatus) && is_wp_error($newFileInsertStatus)) {
1563 do_action('bitform_update_entry_error', $this, $toUpdateValues, $formEntryMetaUpdateStatus, $this->form_id);
1564 return $formEntryMetaUpdateStatus;
1565 }
1566 $toUpdateValues = array_merge($formEntryMetaUpdateStatus, ['entry_id' => $entryID]);
1567 do_action('bitform_after_update_entry_success', $this, $toUpdateValues, $formID, $entryID);
1568 if (empty($workFlowreturnedOnSubmit['message'])) {
1569 $workFlowreturnedOnSubmit['message'] = __('Entry Updated Successfully', 'bit-form');
1570 }
1571 $customFieldHandler = new CustomFieldHandler();
1572 $toUpdateValues = $customFieldHandler->updatedData($form_fields, $toUpdateValues);
1573
1574 $workFlowreturnedOnSubmit['updatedData'] = $toUpdateValues;
1575 $counter = 0;
1576 for ($i = 0; $i < count($formOldData); $i++) {
1577 if (array_key_exists($formOldData[$i]->meta_key . '_old', $toUpdateValues)) {
1578 unset($toUpdateValues[$formOldData[$i]->meta_key . '_old']);
1579 }
1580 if (in_array($formOldData[$i]->meta_key, $file_fields)) {
1581 if (
1582 empty($_FILES[$formOldData[$i]->meta_key]['name'])
1583 || (is_array($_FILES[$formOldData[$i]->meta_key]['name'])
1584 && 1 === count($_FILES[$formOldData[$i]->meta_key]['name'])
1585 && empty($_FILES[$formOldData[$i]->meta_key]['name'][0]))
1586 ) {
1587 unset($toUpdateValues[$formOldData[$i]->meta_key]);
1588 continue;
1589 }
1590 if (is_array($_FILES[$formOldData[$i]->meta_key]['name']) && !in_array($_FILES[$formOldData[$i]->meta_key]['name'], json_decode($formOldData[$i]->meta_value))) {
1591 $sanitized_names = array_map('sanitize_file_name', array_map('wp_unslash', (array) $_FILES[$formOldData[$i]->meta_key]['name']));
1592 $key[$i] = '${' . $formOldData[$i]->meta_key . '} file was Updated To ' . wp_json_encode($sanitized_names);
1593 } elseif (!is_array($_FILES[$formOldData[$i]->meta_key]['name']) && !in_array($_FILES[$formOldData[$i]->meta_key]['name'], json_decode($formOldData[$i]->meta_value))) {
1594 $key[$i] = '${' . $formOldData[$i]->meta_key . '} file was Updated To ' . sanitize_file_name(wp_unslash($_FILES[$formOldData[$i]->meta_key]['name']));
1595 }
1596 unset($toUpdateValues[$formOldData[$i]->meta_key]);
1597 } elseif (isset($toUpdateValues[$formOldData[$i]->meta_key])) {
1598 if (is_array($toUpdateValues[$formOldData[$i]->meta_key])) {
1599 if (json_decode($formOldData[$i]->meta_value) !== $toUpdateValues[$formOldData[$i]->meta_key]) {
1600 $key[$i] = '${' . $formOldData[$i]->meta_key . '} was Updated From ' . implode(',', json_decode($formOldData[$i]->meta_value)) . ' To ' . implode(',', $toUpdateValues[$formOldData[$i]->meta_key]);
1601 }
1602 } elseif (is_string($toUpdateValues[$formOldData[$i]->meta_key]) && !FieldValueHandler::isEmpty($toUpdateValues[$formOldData[$i]->meta_key])) {
1603 if ($formOldData[$i]->meta_value !== $toUpdateValues[$formOldData[$i]->meta_key]) {
1604 $key[$i] = '${' . $formOldData[$i]->meta_key . '} was Updated' . ($formOldData[$i]->meta_value ? ' From ' . $formOldData[$i]->meta_value : '') . ' To ' . $toUpdateValues[$formOldData[$i]->meta_key];
1605 }
1606 }
1607 }
1608 $counter++;
1609 }
1610
1611 $newField = array_keys(array_diff_key($formEntryMetaUpdateStatus, $field_map));
1612 for ($i = 0; $i < count($newField); $i++) {
1613 if (is_array($toUpdateValues[$newField[$i]]) && !empty($toUpdateValues[$newField[$i]])) {
1614 $key[$counter + $i] = '${' . $newField[$i] . '} Updated To ' . implode(',', $toUpdateValues[$newField[$i]]);
1615 } elseif (is_string($newField[$i]) && !FieldValueHandler::isEmpty($toUpdateValues[$newField[$i]])) {
1616 $key[$counter + $i] = '${' . $newField[$i] . '} Updated To ' . $toUpdateValues[$newField[$i]];
1617 }
1618 }
1619 if (null !== $key) {
1620 $logUpdate = implode('b::f', (array) $key);
1621 $formEntryLogUpdate = $formEntryLogModel->logUpdate($logUpdate, $log_id);
1622 }
1623 $workFlowreturnedOnSubmit['entry_id'] = $entryID;
1624 $workFlowreturnedOnSubmit = apply_filters('bitform_filter_return_edit_success', $workFlowreturnedOnSubmit, $this->form_id);
1625
1626 return $workFlowreturnedOnSubmit;
1627 }
1628
1629 public function getRepeaterFields()
1630 {
1631 if (!is_null($this->_repeaterFields)) {
1632 return $this->_repeaterFields;
1633 }
1634 $repeaterFields = [];
1635 $form_content = \json_decode(static::$form[0]->form_content);
1636 $fields = $form_content->fields;
1637 $nestedLayouts = !empty($form_content->nestedLayout) ? $form_content->nestedLayout : [];
1638 foreach ($nestedLayouts as $fieldKey => $repeatLayout) {
1639 if ('repeater' !== $fields->{$fieldKey}->typ) {
1640 continue;
1641 }
1642 $repeaterFields[$fieldKey] = [];
1643 foreach ($repeatLayout->lg as $fieldLayoutData) {
1644 $repeaterFields[$fieldKey][] = $fieldLayoutData->i;
1645 }
1646 }
1647 $this->_repeaterFields = $repeaterFields;
1648 return $repeaterFields;
1649 }
1650
1651 public function isRepeatedField($fieldKey)
1652 {
1653 $repeatedFields = $this->getRepeaterFields();
1654 foreach ($repeatedFields as $repeaterKey => $repeaterFields) {
1655 if (in_array($fieldKey, $repeaterFields)) {
1656 return $repeaterKey;
1657 }
1658 }
1659 return false;
1660 }
1661
1662 public function getParentRepeaterField($fieldKey)
1663 {
1664 $repeatedFields = $this->getRepeaterFields();
1665 foreach ($repeatedFields as $repeaterKey => $repeaterFields) {
1666 if (in_array($fieldKey, $repeaterFields)) {
1667 return $repeaterKey;
1668 }
1669 }
1670 return null;
1671 }
1672
1673 public function isRepeaterField($fieldKey)
1674 {
1675 $repeatedFields = $this->getRepeaterFields();
1676 if (array_key_exists($fieldKey, $repeatedFields)) {
1677 return true;
1678 }
1679 return false;
1680 }
1681
1682 public function fieldNameReplaceOfPost()
1683 {
1684 // CSRF verified upstream before this method is called; $_POST/$_FILES are being normalized (field key remapping), not reading new user input.
1685 $fields = $this->getFields();
1686 foreach ($fields as $fieldKey => $fieldData) {
1687 if (array_key_exists('name', $fieldData)) {
1688 $fldName = $fieldData['name'];
1689 $catchChildFldNamePattern = '/\[(.*?)\]/';
1690 // catching the child field name for confirm field, name field's child
1691 // preg_match_all($catchChildFldNamePattern, $fldName, $matches);
1692 $fldName = preg_replace($catchChildFldNamePattern, '', $fldName);
1693 $fldName = str_replace(['.', ' '], '_', $fldName);
1694 if (!empty($fldName)) {
1695 if (array_key_exists($fldName, $_POST)) {
1696 $temp = $this->sanitize_text_recursive($_POST[$fldName]);
1697 unset($_POST[$fldName]);
1698 $_POST[$fieldKey] = $temp;
1699 } elseif (array_key_exists($fieldKey, $_POST)) {
1700 $_POST[$fieldKey] = $this->sanitize_text_recursive($_POST[$fieldKey]);
1701 } elseif (array_key_exists($fldName, $_FILES)) {
1702 $temp = $this->sanitize_text_recursive($_FILES[$fldName], false);
1703 unset($_FILES[$fldName]);
1704 $_FILES[$fieldKey] = $temp;
1705 } elseif (array_key_exists($fieldKey, $_FILES)) {
1706 $_FILES[$fieldKey] = $this->sanitize_text_recursive($_FILES[$fieldKey], false);
1707 }
1708 // Convert _session_id suffix (used by email-otp and similar fields)
1709 if (array_key_exists($fldName . '_session_id', $_POST)) {
1710 $temp = sanitize_text_field(wp_unslash($_POST[$fldName . '_session_id']));
1711 unset($_POST[$fldName . '_session_id']);
1712 $_POST[$fieldKey . '_session_id'] = $temp;
1713 }
1714 }
1715 }
1716 }
1717 }
1718
1719 private function sanitize_text_recursive($input, $unslash = true)
1720 {
1721 if (is_array($input)) {
1722 return array_map(fn ($item) => $this->sanitize_text_recursive($item, $unslash), $input);
1723 }
1724
1725 return sanitize_text_field($unslash ? wp_unslash($input) : $input);
1726 }
1727
1728 private function normalizeRepeatedCompositeFieldInput($value)
1729 {
1730 if (!is_array($value) || empty($value)) {
1731 return $value;
1732 }
1733
1734 $hasNestedArray = false;
1735 foreach ($value as $childValues) {
1736 if (!is_array($childValues)) {
1737 return $value;
1738 }
1739 $hasNestedArray = true;
1740 }
1741
1742 if (!$hasNestedArray) {
1743 return $value;
1744 }
1745
1746 $formattedValue = [];
1747 foreach ($value as $childKey => $childValues) {
1748 foreach ($childValues as $repeatIndex => $repeatValue) {
1749 if (!isset($formattedValue[$repeatIndex]) || !is_array($formattedValue[$repeatIndex])) {
1750 $formattedValue[$repeatIndex] = [];
1751 }
1752 $formattedValue[$repeatIndex][$childKey] = $repeatValue;
1753 }
1754 }
1755
1756 return $formattedValue;
1757 }
1758
1759 public function setSubmissionCount($countStep = 1)
1760 {
1761 $update_status = $this->formModel->update(
1762 [
1763 'entries' => intval(static::$form[0]->entries) + $countStep,
1764 ],
1765 [
1766 'id' => $this->form_id,
1767 ]
1768 );
1769 }
1770
1771 public function resetSubmissionCount($countStep)
1772 {
1773 $update_status = $this->formModel->update(
1774 [
1775 'entries' => intval($countStep),
1776 ],
1777 [
1778 'id' => $this->form_id,
1779 ]
1780 );
1781 }
1782
1783 public function getCaptchaSettings()
1784 {
1785 $formContents = $this->getFormContent();
1786 $fieldStr = wp_json_encode($formContents->fields);
1787 if (false !== strpos($fieldStr, '"typ":"recaptcha"')) {
1788 return true;
1789 }
1790 }
1791
1792 public function getTurnstileSettings()
1793 {
1794 $formContents = $this->getFormContent();
1795 $fieldStr = wp_json_encode($formContents->fields);
1796 if (false !== strpos($fieldStr, '"typ":"turnstile"')) {
1797 return true;
1798 }
1799 }
1800
1801 public function isFieldTypeExist($fieldType)
1802 {
1803 $formContents = $this->getFormContent();
1804 $fieldStr = wp_json_encode($formContents->fields);
1805 if (false !== strpos($fieldStr, '"typ":"' . $fieldType . '"')) {
1806 return true;
1807 }
1808 }
1809
1810 public function getCaptchaV3Settings()
1811 {
1812 $formContents = $this->getFormContent();
1813 if (!empty($formContents->additional->enabled) && !empty($formContents->additional->enabled->recaptchav3)) {
1814 return $formContents->additional->settings->recaptchav3;
1815 }
1816 return false;
1817 }
1818
1819 // public function getSuccessMessageMarkups() {
1820 // if (is_null($this->_work_flows)) {
1821 // $workFlowManager = new WorkFlowHandler($this->form_id);
1822 // $this->_work_flows = $workFlowManager->getAllworkFlow();
1823 // }
1824
1825 // $ids = [];
1826 // foreach ($this->_work_flows as $msgItem) {
1827 // foreach ($msgItem['conditions'] as $condition) {
1828 // if (isset($condition->actions->success)) {
1829 // foreach ($condition->actions->success as $msg) {
1830 // if ('successMsg' === $msg->type && isset($msg->details->id)) {
1831 // $msgDetailsId = $msg->details->id;
1832 // $idObj = json_decode(stripslashes($msgDetailsId));
1833 // if (is_object($idObj) && !empty($idObj->id)) {
1834 // array_push($ids, $idObj->id);
1835 // }
1836 // }
1837 // }
1838 // }
1839 // if (isset($condition->actions->failure)) {
1840 // $idObj = json_decode(stripslashes($condition->actions->failure));
1841 // if (is_object($idObj) && !empty($idObj->id)) {
1842 // array_push($ids, $idObj->id);
1843 // }
1844 // }
1845 // }
1846 // }
1847 // $ids = array_unique($ids);
1848 // if (is_null($this->_conf_messages)) {
1849 // $successMsgHandler = new SuccessMessageHandler($this->form_id);
1850 // $this->_conf_messages = $successMsgHandler->getMessages($ids);
1851 // }
1852
1853 // $messageMarkups = '';
1854 // if (is_wp_error($this->_conf_messages)) {
1855 // return $messageMarkups;
1856 // }
1857
1858 // foreach ($this->_conf_messages as $key => $msgItem) {
1859 // $messageMarkups .= $this->messageMarkup($msgItem->id);
1860 // }
1861
1862 // return $messageMarkups;
1863 // }
1864
1865 // private function messageMarkup($msgId) {
1866 // return <<<SUCCESSMSG
1867 // <div role="dialog" aria-hidden="true" data-modal-backdrop="true" class="{$this->getAtomicCls("msg-container-{$msgId}")} deactive2 test">
1868 // <div role="button" class="{$this->getAtomicCls("msg-background-{$msgId}")} msg-backdrop">
1869 // <div class="bf-notification-message {$this->getAtomicCls("msg-content-{$msgId}")}">
1870 // <button class="{$this->getAtomicCls("close-{$msgId}")} bf-msg-close" type="button">
1871 // <svg class="{$this->getAtomicCls("close-icn-{$msgId}")}" viewBox="0 0 30 30">
1872 // <line fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x1="4" y1="3.88" x2="26" y2="26.12"></line>
1873 // <line fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x1="26" y1="3.88" x2="4" y2="26.12"></line>
1874 // </svg>
1875 // </button>
1876 // <div class="msg-content"></div>
1877 // </div>
1878 // </div>
1879 // </div>
1880 // SUCCESSMSG;
1881 // }
1882
1883 public function getAtomicCls($element)
1884 {
1885 $atomicClassMap = $this->getAtomicClsMap();
1886 if (is_object($atomicClassMap) && property_exists($atomicClassMap, ".$element")) {
1887 $getAtomicCls = $atomicClassMap->{".$element"};
1888 return implode(' ', $getAtomicCls) . " $element";
1889 }
1890 return $element;
1891 }
1892
1893 public function isGCLIDEnabled()
1894 {
1895 $formContents = $this->getFormContent();
1896 if (isset($formContents->additional->enabled->captureGCLID) && $formContents->additional->enabled->captureGCLID) {
1897 return true;
1898 }
1899 return false;
1900 }
1901
1902 protected function addEntryInfo($field_details, $counter)
1903 {
1904 $infos = [
1905 '__user_id' => __('User', 'bit-form'),
1906 '__entry_status' => __('Status', 'bit-form'),
1907 //'__user_location' => __(''),
1908 '__referer' => __('Refer URL', 'bit-form'),
1909 '__user_device' => __('Device', 'bit-form'),
1910 '__user_ip' => __('IP address', 'bit-form'),
1911 '__created_at' => __('Created Time', 'bit-form'),
1912 '__updated_at' => __('Modified Time', 'bit-form'),
1913 ];
1914 foreach ($infos as $key => $value) {
1915 $field_details[$counter]['name'] = $value;
1916 $field_details[$counter]['key'] = $key;
1917 $field_details[$counter]['type'] = 'sys';
1918 $counter = $counter + 1;
1919 }
1920
1921 return $field_details;
1922 }
1923 }
1924