PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / V3.3.2
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder vV3.3.2
V3.3.2 3.3.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 All 139 releases
bit-form / includes / Core / Form / FormManager.php

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

2,326 lines 88.7 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\Messages\SuccessMessageHandler;
20 use BitCode\BitForm\Core\Util\FieldValueHandler;
21 use BitCode\BitForm\Core\Util\FileHandler;
22 use BitCode\BitForm\Core\Util\FrontendHelpers;
23 use BitCode\BitForm\Core\Util\IpTool;
24 use BitCode\BitForm\Core\Util\Log;
25 use BitCode\BitForm\Core\Util\Translation\FormContentTranslator;
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
38 /**
39 * Object-cache group for translated form_content.
40 */
41 public const TRANSLATION_CACHE_GROUP = 'bitform_translation';
42
43 /**
44 * Stored in place of a password value. Same text the WP User Auth mask always used,
45 * so entries masked before this change stay recognisable.
46 */
47 public const PASSWORD_MASK = '**** (encrypted)';
48
49 /**
50 * Request-scoped memo of translated form_content JSON, keyed "formId|lang".
51 * Static on purpose, unlike $form: the key carries the form id and the
52 * language, so an entry cannot be read back for the wrong form.
53 *
54 * @var array<string,string>
55 */
56 private static $translatedContentCache = [];
57
58 // Per-instance, never static: a static is shared with every subclass and overwritten by the
59 // last-constructed manager, so cached instances would read another form's row.
60 protected $form;
61 protected $formModel;
62 protected $form_id;
63 private $_has_upload;
64 private $_field_label;
65 private $_fields;
66 private $_repeaterFields;
67 private $_work_flows;
68 private $_conf_messages;
69 private $_atomic_class_map;
70 private $_saveFormAsDraft;
71
72 public function __construct($form_id)
73 {
74 $this->form_id = $form_id;
75 $this->formModel = new FormModel();
76
77 $this->form = $this->formModel->get(
78 [
79 'id',
80 'form_content',
81 'form_name',
82 'created_at',
83 'views',
84 'entries',
85 'status',
86 'builder_helper_state',
87 'atomic_class_map',
88 'generated_script_page_ids',
89 ],
90 [
91 'id' => $form_id,
92 ]
93 );
94 if (!is_wp_error($this->form)) {
95 $atomicClassMap = isset($this->form[0]->atomic_class_map) ? $this->form[0]->atomic_class_map : '';
96 $this->_atomic_class_map = json_decode((string) $atomicClassMap);
97 $bfMultipleFormsExists = FrontendHelpers::hasMultipleForms();
98 if ($bfMultipleFormsExists && isset($this->_atomic_class_map->atomic_class_map_with_form_id)) {
99 $this->_atomic_class_map = $this->_atomic_class_map->atomic_class_map_with_form_id;
100 } elseif (isset($this->_atomic_class_map->atomic_class_map)) {
101 $this->_atomic_class_map = $this->_atomic_class_map->atomic_class_map;
102 }
103 } else {
104 // Log the error if needed
105 Log::debug_log('Error fetching form: ' . "Form Id = ($form_id)" . $this->form->get_error_message());
106 }
107 }
108
109 /**
110 * Test hook: clears the request memo of translated form_content.
111 */
112 public static function resetTranslationMemoForTesting()
113 {
114 self::$translatedContentCache = [];
115 }
116
117 // Static method to get the instance of FormManager
118 public static function getInstance($form_id)
119 {
120 // Check if an instance of FormManager is already cached
121 if (!isset(self::$formManagerCache[$form_id])) {
122 // Create and cache the FormManager instance if not found
123 self::$formManagerCache[$form_id] = new self($form_id);
124 }
125
126 // Return the cached instance
127 return self::$formManagerCache[$form_id];
128 }
129
130 public function isExist()
131 {
132 return (!$this->form || is_wp_error($this->form)) ? false : true;
133 }
134
135 public function checkStatus()
136 {
137 // Fail closed for a missing form: $this->form is a WP_Error when the lookup
138 // found nothing, and the unauthenticated submit endpoints call this before
139 // isExist() — indexing it there fatals on any unknown form id.
140 if (!$this->isExist()) {
141 return false;
142 }
143 return '1' === $this->form[0]->status ? true : false;
144 }
145
146 public function getFieldsContent()
147 {
148 // Raw on purpose: FormFieldValidator's allowed-option lookup must compare
149 // against the source-language config whatever the request language is.
150 return $this->form[0]->form_content;
151 }
152
153 /**
154 * form_content JSON with display strings passed through
155 * `bitform_translate_form_string`; the raw string when nothing is hooked.
156 * Never persisted back to the row — stored form_content stays source-language.
157 * AdminFormManager overrides this to always return raw.
158 *
159 * @return string
160 */
161 protected function getEffectiveFormContentJson()
162 {
163 // $this->form is a WP_Error when the form was not found; indexing it here
164 // fataled for callers that pass an unknown or empty form id
165 $raw = $this->isExist() ? ($this->form[0]->form_content ?? '') : '';
166 $raw = is_string($raw) ? $raw : '';
167 if ('' === $raw || !has_filter('bitform_translate_form_string')) {
168 return $raw;
169 }
170
171 $rowId = isset($this->form[0]->id) ? (int) $this->form[0]->id : (int) $this->form_id;
172 $lang = (string) apply_filters('bitform_current_language', '', $rowId);
173 $memoKey = $rowId . '|' . $lang;
174 if (isset(self::$translatedContentCache[$memoKey])) {
175 return self::$translatedContentCache[$memoKey];
176 }
177
178 // Content-addressed, so a form save can never serve a stale entry. Edits on
179 // the translation side do not change the hash — the TTL bounds those.
180 $ttl = (int) apply_filters('bitform_translation_cache_ttl', HOUR_IN_SECONDS, $rowId, $lang);
181 $cacheKey = $ttl > 0 ? "form-{$rowId}-{$lang}-" . md5($raw) : '';
182
183 if ('' !== $cacheKey) {
184 $cached = wp_cache_get($cacheKey, self::TRANSLATION_CACHE_GROUP);
185 if (is_string($cached) && '' !== $cached) {
186 self::$translatedContentCache[$memoKey] = $cached;
187 return $cached;
188 }
189 }
190
191 $translated = $raw;
192 $decoded = Utilities::jsonObj($raw);
193 if ($decoded instanceof stdClass) {
194 FormContentTranslator::translate($decoded, $rowId);
195 $encoded = wp_json_encode($decoded);
196 $translated = is_string($encoded) ? $encoded : $raw;
197 }
198
199 if ('' !== $cacheKey) {
200 wp_cache_set($cacheKey, $translated, self::TRANSLATION_CACHE_GROUP, $ttl);
201 }
202 self::$translatedContentCache[$memoKey] = $translated;
203
204 return $translated;
205 }
206
207 public function getFont()
208 {
209 $atomicClassMap = $this->_atomic_class_map;
210 $font = isset($atomicClassMap->font) ? $atomicClassMap->font : '';
211 return $font;
212 }
213
214 public function getStyle()
215 {
216 $builerState = Utilities::jsonObj($this->isExist() ? ($this->form[0]->builder_helper_state ?? '') : '');
217 $style = '';
218 $themeVars = $builerState->themeVars ?? null;
219 $themeColors = $builerState->themeColors ?? null;
220
221 if (!empty($themeVars)) {
222 $style .= ':root {';
223 foreach ($themeVars->lgLightThemeVars as $key => $value) {
224 $style .= "$key: $value; ";
225 }
226 $style .= '} ';
227 }
228 if (!empty($themeColors)) {
229 $style .= ' :root {';
230 foreach ($themeColors->lightThemeColors as $k => $v) {
231 $style .= "$k:$v; ";
232 }
233 $style .= '} ';
234 }
235
236 $field = $builerState->style->lgLightStyles->fields;
237 foreach ($field as $value) {
238 $classes = $value->classes;
239 foreach ($classes as $key => $value) {
240 $style .= "{$key} {";
241 foreach ($value as $k => $v) {
242 $style .= "$k:$v; ";
243 }
244 $style .= '} ';
245 }
246 }
247 return $style;
248 }
249
250 public function getCustomStyle()
251 {
252 $customCSSPath = BITFORMS_CONTENT_DIR . DIRECTORY_SEPARATOR . 'form-styles' . DIRECTORY_SEPARATOR . "bitform-custom-{$this->form_id}.css";
253 return FileHandler::readFile($customCSSPath);
254 }
255
256 public function getCustomJS()
257 {
258 $customJsPath = BITFORMS_CONTENT_DIR . DIRECTORY_SEPARATOR . 'form-scripts' . DIRECTORY_SEPARATOR . "bitform-custom-{$this->form_id}.js";
259 return FileHandler::readFile($customJsPath);
260 }
261
262 public function getFormContentWithValue($defaultValues = [])
263 {
264 $form_content = Utilities::jsonObj($this->getEffectiveFormContentJson());
265 // this filter just use private purpose
266 if (isset($form_content->fields)) {
267 $form_content->fields = apply_filters('bitform_dynamic_field_filter', $form_content->fields);
268 }
269 if (!is_array($defaultValues) || 0 === count($defaultValues)) {
270 return $form_content;
271 }
272 foreach (($form_content->fields ?? []) as $fieldKey => $fieldDetails) {
273 // $field_name = empty($fieldDetails->lbl) ? null : \preg_replace('/[\`\~\!\@\#\$\'\.\s\?\+\-\*\&\|\/\\!]/', '_', $fieldDetails->lbl);
274 $fieldName = $fieldDetails->fieldName;
275 $defaultValue = isset($defaultValues[$fieldName]) ? $defaultValues[$fieldName] : null;
276 $defaultValue = isset($defaultValues[$fieldKey]) ? $defaultValues[$fieldKey] : $defaultValue;
277 if ((isset($fieldDetails->mul) || 'check' === $fieldDetails->typ) && isset($defaultValue)) {
278 // if (is_array($defaultValue)) {
279 // $fieldDetails->val =
280 // wp_json_encode(
281 // array_map('sanitize_text_field', $defaultValue)
282 // );
283 // } else {
284 // $fieldDetails->val = sanitize_text_field($defaultValue);
285 // }
286 if ((isset($fieldDetails->mul) && true === $fieldDetails->mul) || is_array($defaultValue)) {
287 $fieldDetails->val = wp_json_encode(array_map('sanitize_text_field', $defaultValue));
288 } elseif (!is_array($defaultValue)) {
289 $fieldDetails->val = sanitize_text_field($defaultValue);
290 }
291 } elseif (!is_null($defaultValue)) {
292 $fieldDetails->val = self::sanitizeDefaultValue($defaultValue);
293 }
294 }
295 return $form_content;
296 }
297
298 /**
299 * Normalize a prefill value into the scalar `val` a field can hold.
300 *
301 * Composite fields (address, name, …) arrive as associative arrays, which a
302 * numeric last-index lookup cannot read.
303 *
304 * @param mixed $defaultValue
305 *
306 * @return string
307 */
308 private static function sanitizeDefaultValue($defaultValue)
309 {
310 if (is_string($defaultValue)) {
311 return sanitize_text_field($defaultValue);
312 }
313
314 if (is_scalar($defaultValue)) {
315 return sanitize_text_field((string) $defaultValue);
316 }
317
318 if (is_object($defaultValue)) {
319 $defaultValue = (array) $defaultValue;
320 }
321
322 if (!is_array($defaultValue) || 0 === count($defaultValue)) {
323 return '';
324 }
325
326 // A plain list is a repeated query param — keep the historic "last one wins".
327 if (array_keys($defaultValue) === range(0, count($defaultValue) - 1)) {
328 $last = end($defaultValue);
329 return is_scalar($last) ? sanitize_text_field((string) $last) : (string) wp_json_encode($last);
330 }
331
332 // Keyed (composite) value: keep the whole shape, same as the `mul` branch above.
333 return (string) wp_json_encode(map_deep($defaultValue, 'sanitize_text_field'));
334 }
335
336 public function getFormContent()
337 {
338 $formContent = Utilities::jsonObj($this->getEffectiveFormContentJson());
339 $types = ['check', 'radio', 'select'];
340 $filter = false;
341 foreach (($formContent->fields ?? []) as $field) {
342 if (in_array($field->typ, $types) && property_exists($field, 'customType')) {
343 $filter = true;
344 break; // reduce unnecessary loop
345 }
346 }
347 if (true === $filter) {
348 $updateFields = apply_filters('bitform_dynamic_field_filter', $formContent->fields);
349 $formContent->fields = $updateFields;
350 }
351 return $formContent;
352 }
353
354 /**
355 * Rebase Name/Address and Email/Password confirm child names onto the parent's current name.
356 * Each child stores a full "parent[suffix]" copy, and forms renamed in older builders kept the
357 * old prefix: the child inputs posted under it and the parent's entry value saved empty (or,
358 * for a confirm pair, the confirm value never matched). Confirm parents keep "[primary]".
359 * Runs on save and once per form in FormFallback, not on load.
360 *
361 * @param mixed $fields form_content fields, child objects edited in place
362 *
363 * @return bool true when any child name changed
364 */
365 public static function syncCompositeChildNames($fields)
366 {
367 $changed = false;
368 if (!is_object($fields) && !is_array($fields)) {
369 return $changed;
370 }
371 foreach ($fields as $fieldKey => $field) {
372 if (
373 !is_object($field)
374 || !isset($field->typ, $field->fieldName, $field->childFields)
375 || !is_iterable($field->childFields)
376 ) {
377 continue;
378 }
379 $isComposite = in_array($field->typ, ['name', 'address'], true);
380 // A confirm-enabled Email/Password posts as "<base>[primary]" + "<base>[confirm]"; a renamed
381 // parent left the confirm child under the old base, so the pair never matched on submit.
382 $isConfirmPair = in_array($field->typ, ['email', 'password'], true) && !empty($field->confirm);
383 if (!$isComposite && !$isConfirmPair) {
384 continue;
385 }
386 $parentBase = (string) $field->fieldName;
387 if ($isConfirmPair) {
388 $parentBase = (string) preg_replace('/\[primary\]$/', '', $parentBase);
389 if ('' !== $parentBase && $field->fieldName !== $parentBase . '[primary]') {
390 $field->fieldName = $parentBase . '[primary]';
391 $changed = true;
392 }
393 }
394 foreach ($field->childFields as $childRef) {
395 $childKey = self::childFldKey($childRef);
396 if (!is_string($childKey) || '' === $childKey) {
397 continue;
398 }
399 $child = is_object($fields) ? ($fields->{$childKey} ?? null) : ($fields[$childKey] ?? null);
400 if (!is_object($child) || !isset($child->fieldName)) {
401 continue;
402 }
403 // A corrupted duplicate can list another parent's children; renaming them would break that parent.
404 if (isset($child->parentFieldKey) && (string) $child->parentFieldKey !== (string) $fieldKey) {
405 continue;
406 }
407 $rebased = FieldValueHandler::rebaseChildFieldName($child->fieldName, $parentBase);
408 if ($rebased !== $child->fieldName) {
409 $child->fieldName = $rebased;
410 $changed = true;
411 }
412 }
413 }
414 return $changed;
415 }
416
417 public function getFormInfo()
418 {
419 $formContent = json_decode($this->getEffectiveFormContentJson());
420 $formInfo = isset($formContent->formInfo) ? $formContent->formInfo : null;
421 return $formInfo;
422 }
423
424 public function getFormPermission()
425 {
426 $formContent = json_decode($this->form[0]->form_content);
427 $formPermission = isset($formContent->formPermissions) ? $formContent->formPermissions : null;
428 return $formPermission;
429 }
430
431 public function getFormHelperStates()
432 {
433 $formHelperStates = json_decode($this->form[0]->builder_helper_state);
434 return $formHelperStates;
435 }
436
437 public function getAtomicClsMap()
438 {
439 return $this->_atomic_class_map;
440 }
441
442 private function is_json($str)
443 {
444 $json = json_decode($str);
445 return $json && $str !== $json;
446 }
447
448 public function getFormData($columnName = '')
449 {
450 if (empty($columnName)) {
451 return null;
452 }
453
454 $form = $this->form[0];
455 if (!isset($form->{$columnName})) {
456 return null;
457 }
458
459 $data = $form->{$columnName};
460 if ($this->is_json($data)) {
461 return json_decode($data);
462 }
463
464 return $data;
465 }
466
467 public function getFormName()
468 {
469 return $this->form[0]->form_name;
470 }
471
472 public function getFormLayout()
473 {
474 $formContent = $this->getFormContent();
475 return isset($formContent->layout) ? $formContent->layout : new stdClass();
476 }
477
478 public function getFormNestedLayout()
479 {
480 $formContent = $this->getFormContent();
481 return isset($formContent->nestedLayout) ? $formContent->nestedLayout : new stdClass();
482 }
483
484 private function mergeNestedLayout(&$layout, $nestedLayout)
485 {
486 foreach ($nestedLayout as $key => $brkpnts) {
487 foreach ($brkpnts as $brkpnt=>$nLayout) {
488 $layout->{$brkpnt} = array_merge(isset($layout->{$brkpnt}) ? (array) $layout->{$brkpnt} : [], (array) $nLayout);
489 }
490 }
491 }
492
493 public function flatMultistepFormLayout()
494 {
495 $formLayout = $this->getFormLayout();
496 $multistepLayout = new stdClass();
497 foreach ($formLayout as $stpLayout) {
498 $lyout = $stpLayout->layout;
499
500 foreach ($lyout as $brkpnt=>$fields) {
501 $multistepLayout->{$brkpnt} = array_merge($multistepLayout->{$brkpnt} ?? [], $fields);
502 }
503 }
504
505 return $multistepLayout;
506 }
507
508 public function getFlatenFormLayout()
509 {
510 $layout = $this->getFormLayout();
511 $nestedLayout = $this->getFormNestedLayout();
512 if ('array' === gettype($layout)) {
513 // multi step form layout
514 $layout = $this->flatMultistepFormLayout();
515 }
516 if (!empty((array) $nestedLayout)) {
517 $this->mergeNestedLayout($layout, $nestedLayout);
518 }
519
520 return $layout;
521 }
522
523 /**
524 * Union of field keys referenced by ANY breakpoint (lg/md/sm) of the root
525 * layout (all steps) plus nested layouts of RENDERED containers.
526 *
527 * Nested layout entries are gated by their parent key being in the root
528 * layout — the renderer (FormViewer) only renders nested children of
529 * containers present in the root layout, so a stale nestedLayout entry
530 * (parent removed) must not mark its children as rendered.
531 *
532 * Returns [] when the layout is missing or unparseable — callers MUST
533 * fail closed (validate all fields) on an empty result.
534 *
535 * @return string[]
536 */
537 public function getLayoutFieldKeys()
538 {
539 try {
540 $layout = $this->getFormLayout();
541 $nestedLayout = $this->getFormNestedLayout();
542 if ('array' === gettype($layout)) {
543 // multi step form layout
544 $layout = $this->flatMultistepFormLayout();
545 }
546 } catch (\Throwable $e) {
547 return [];
548 }
549 $rootKeys = self::collectLayoutKeys($layout);
550 if (empty($rootKeys)) {
551 return [];
552 }
553 $keys = array_fill_keys($rootKeys, true);
554 if (is_object($nestedLayout) || is_array($nestedLayout)) {
555 foreach ($nestedLayout as $parentKey => $nLay) {
556 if (!isset($keys[$parentKey])) {
557 continue; // stale entry: container no longer rendered
558 }
559 foreach (self::collectLayoutKeys($nLay) as $nestedKey) {
560 $keys[$nestedKey] = true;
561 }
562 }
563 }
564 return array_keys($keys);
565 }
566
567 /**
568 * Collect field keys from every breakpoint of a layout object.
569 *
570 * @param object $layout layout with ->lg/->md/->sm arrays of {i} items
571 *
572 * @return string[]
573 */
574 private static function collectLayoutKeys($layout)
575 {
576 if (!is_object($layout)) {
577 return [];
578 }
579 $keys = [];
580 foreach (['lg', 'md', 'sm'] as $brkpnt) {
581 if (!isset($layout->{$brkpnt}) || !is_array($layout->{$brkpnt})) {
582 continue;
583 }
584 foreach ($layout->{$brkpnt} as $item) {
585 if (is_object($item) && isset($item->i)) {
586 $keys[$item->i] = true;
587 }
588 }
589 }
590 return array_keys($keys);
591 }
592
593 /**
594 * Extract a child field key from a childFields[] entry (stdClass or array shape).
595 *
596 * @return string|null
597 */
598 private static function childFldKey($child)
599 {
600 if (is_object($child) && isset($child->fldKey)) {
601 return $child->fldKey;
602 }
603 if (is_array($child) && isset($child['fldKey'])) {
604 return $child['fldKey'];
605 }
606 return null;
607 }
608
609 /**
610 * Add childFields of every rendered parent into $renderedKeys (by ref).
611 *
612 * Several field types keep their children flat in `fields` and NEVER in
613 * any layout — Name (first/middle/last), Address (street/city/zip/...),
614 * Email and Password (confirm fields). A child is rendered iff its
615 * parent is, so each rendered parent's childFields[].fldKey must join
616 * the rendered set or their validation would be wrongly skipped.
617 *
618 * Runs to a fixpoint so expansion is safe regardless of field order or
619 * nesting depth.
620 *
621 * @param array $renderedKeys key => true map, mutated in place
622 * @param iterable $fields fields keyed by field key; each field may be
623 * a processed array (getFields()) or raw stdClass
624 */
625 protected static function expandChildFieldKeys(array &$renderedKeys, $fields)
626 {
627 do {
628 $grew = false;
629 foreach ($fields as $key => $field) {
630 if (!isset($renderedKeys[$key])) {
631 continue;
632 }
633 $childFields = null;
634 if (is_object($field) && isset($field->childFields)) {
635 $childFields = $field->childFields;
636 } elseif (is_array($field) && isset($field['childFields'])) {
637 $childFields = $field['childFields'];
638 }
639 if (empty($childFields) || !is_iterable($childFields)) {
640 continue;
641 }
642 foreach ($childFields as $child) {
643 $childKey = self::childFldKey($child);
644 if ($childKey && !isset($renderedKeys[$childKey])) {
645 $renderedKeys[$childKey] = true;
646 $grew = true;
647 }
648 }
649 }
650 } while ($grew);
651 }
652
653 /**
654 * getFields() narrowed to provably-rendered fields: key present in any
655 * breakpoint of any step/nested layout, OR a childField of a rendered
656 * parent (name/address/email/password children live outside layouts),
657 * OR the synthetic GCLID key.
658 *
659 * SECURITY: fail-closed — if the layout yields no keys, ALL fields are
660 * returned (current behavior). Derives exclusively from DB-stored
661 * form_content, never from POST, so submitters cannot influence which
662 * fields are validated.
663 */
664 public function getRenderedFields()
665 {
666 $fields = $this->getFields();
667 try {
668 $renderedKeys = self::renderedKeyMap($this->getFormLayout(), $this->getFormNestedLayout(), $fields);
669 } catch (\Throwable $e) {
670 return $fields; // fail-closed: unusable layout validates all fields
671 }
672 if (null === $renderedKeys) {
673 return $fields;
674 }
675 $rendered = [];
676 foreach ($fields as $key => $field) {
677 // renderedKeys already includes NON_LAYOUT_FIELD_KEYS (GCLID, ...)
678 if (isset($renderedKeys[$key])) {
679 $rendered[$key] = $field;
680 }
681 }
682 return $rendered;
683 }
684
685 /**
686 * Field keys that are legitimately part of a form yet never appear in any
687 * layout — synthetic/system fields the renderer always keeps. They must
688 * never be flagged as orphan (save guard) or dropped from validation
689 * (renderer). Extend this list as new non-layout system fields are added.
690 *
691 * @var string[]
692 */
693 protected const NON_LAYOUT_FIELD_KEYS = ['GCLID'];
694
695 /**
696 * True when $fields (object or array) holds $key.
697 *
698 * @param object|array $fields
699 * @param string $key
700 */
701 private static function fieldExists($fields, $key)
702 {
703 return is_object($fields) ? isset($fields->{$key}) : (is_array($fields) && isset($fields[$key]));
704 }
705
706 /**
707 * Flatten a raw layout into one object unioning lg/md/sm across all steps.
708 * Accepts a single layout object or an array of multi-step entries (each
709 * wrapping its layout in ->layout). Shared by the frontend renderer
710 * (getRenderedFields) and the admin save/import orphan guard
711 * (computeOrphanFieldKeys) so both flatten identically.
712 *
713 * @param array|object $layout
714 *
715 * @return object {lg,md,sm} arrays of {i} items
716 */
717 private static function flattenLayout($layout)
718 {
719 $flat = new stdClass();
720 $flat->lg = [];
721 $flat->md = [];
722 $flat->sm = [];
723 $addLayout = function ($lay) use ($flat) {
724 if (!is_object($lay)) {
725 return;
726 }
727 foreach (['lg', 'md', 'sm'] as $brkpnt) {
728 if (isset($lay->{$brkpnt}) && is_array($lay->{$brkpnt})) {
729 $flat->{$brkpnt} = array_merge($flat->{$brkpnt}, $lay->{$brkpnt});
730 }
731 }
732 };
733 if (is_array($layout)) {
734 // multi-step: each entry wraps its layout in ->layout
735 foreach ($layout as $step) {
736 $addLayout(isset($step->layout) ? $step->layout : $step);
737 }
738 } else {
739 $addLayout($layout);
740 }
741 return $flat;
742 }
743
744 /**
745 * SINGLE SOURCE OF TRUTH for "which field keys the renderer would show":
746 * unions all breakpoints across steps, adds nested-layout children of
747 * RENDERED containers only, then expands childFields of rendered parents
748 * (name/address/email/password children live outside layouts).
749 *
750 * Both getRenderedFields (frontend validation) and computeOrphanFieldKeys
751 * (admin save guard) route through this so the two can never diverge — a
752 * divergence would prune a real field or wrongly validate an orphan.
753 *
754 * @param array|object $layout single layout or array of steps
755 * @param object|null $nestedLayout keyed by parent field key
756 * @param object|array $fields form_content->fields
757 *
758 * @return array<string,true>|null key=>true map, or null when the layout is
759 * unusable (callers MUST fail closed)
760 */
761 protected static function renderedKeyMap($layout, $nestedLayout, $fields)
762 {
763 // Fail closed on a partial/unloaded multi-step layout: a step with no
764 // layout items at all almost always means the layout never finished
765 // loading/syncing (not a real "every field on this step was deleted").
766 // Treating it as usable would flag that step's real fields as orphan.
767 if (is_array($layout)) {
768 if (empty($layout)) {
769 return null;
770 }
771 foreach ($layout as $step) {
772 $stepLayout = is_object($step) && isset($step->layout) ? $step->layout : $step;
773 if (empty(self::collectLayoutKeys(self::flattenLayout($stepLayout)))) {
774 return null;
775 }
776 }
777 }
778 $flat = self::flattenLayout($layout);
779 $rootKeys = self::collectLayoutKeys($flat);
780 if (empty($rootKeys)) {
781 return null;
782 }
783 $renderedKeys = array_fill_keys($rootKeys, true);
784 // nested children count as rendered only when their container is —
785 // matches the renderer, which skips stale nestedLayout entries
786 if (is_object($nestedLayout) || is_array($nestedLayout)) {
787 foreach ($nestedLayout as $parentKey => $nLay) {
788 if (!isset($renderedKeys[$parentKey])) {
789 continue;
790 }
791 foreach (self::collectLayoutKeys($nLay) as $nestedKey) {
792 $renderedKeys[$nestedKey] = true;
793 }
794 }
795 }
796 self::expandChildFieldKeys($renderedKeys, $fields);
797 // synthetic/system fields (e.g. GCLID) live outside every layout; the
798 // renderer always keeps them, so they must never count as orphan
799 foreach (self::NON_LAYOUT_FIELD_KEYS as $sysKey) {
800 if (self::fieldExists($fields, $sysKey)) {
801 $renderedKeys[$sysKey] = true;
802 }
803 }
804 return $renderedKeys;
805 }
806
807 /**
808 * Pure variant of the orphan rule for the admin save/import guard: returns
809 * the keys of $fields absent from every layout (children of rendered
810 * parents excluded). Uses renderedKeyMap — the exact flattening the
811 * renderer uses — so the save guard and the renderer never diverge.
812 *
813 * @param array|object $layout single layout or array of steps ({layout} each)
814 * @param object|null $nestedLayout keyed by parent field key
815 * @param mixed $fields raw form_content->fields (decoded JSON: shape is not guaranteed, hence the runtime guard)
816 *
817 * @return string[]|null orphan keys to drop, or null when the layout is
818 * unusable (fail closed: drop nothing)
819 */
820 public static function computeOrphanFieldKeys($layout, $nestedLayout, $fields)
821 {
822 if (!is_object($fields) && !is_array($fields)) {
823 return null;
824 }
825 $renderedKeys = self::renderedKeyMap($layout, $nestedLayout, $fields);
826 if (null === $renderedKeys) {
827 return null; // unusable layout: fail closed, drop nothing
828 }
829 $orphans = [];
830 foreach ($fields as $key => $field) {
831 if (!isset($renderedKeys[$key])) {
832 $orphans[] = $key;
833 }
834 }
835 return $orphans;
836 }
837
838 public function getFieldsBasedOnLayout()
839 {
840 $layout = $this->getFlatenFormLayout();
841
842 $fieldKeyOrderbasedOnLayout = array_map(function ($fld) {
843 return $fld->i;
844 }, $layout->lg);
845 $orderedFields = [];
846 $fields = $this->getFields();
847
848 foreach ($fieldKeyOrderbasedOnLayout as $key) {
849 if (array_key_exists($key, $fields)) {
850 $orderedFields[$key] = $fields[$key];
851 }
852 }
853
854 foreach ($fields as $k=>$v) {
855 if (!array_key_exists($k, $fieldKeyOrderbasedOnLayout)) {
856 $orderedFields[$k] = $fields[$k];
857 }
858 }
859
860 return $orderedFields;
861 }
862
863 public function getFields()
864 {
865 if (!is_null($this->_fields)) {
866 return $this->_fields;
867 }
868 $form_content = \json_decode($this->form[0]->form_content);
869 $layout = $form_content->layout;
870 $fields = $form_content->fields;
871 $field_details = [];
872 foreach ($fields as $key => $field) {
873 if ('recaptcha' === $field->typ || 'hcaptcha' === $field->typ || 'turnstile' === $field->typ) {
874 continue;
875 }
876 // $field_name = empty($field->lbl) ? null : \preg_replace('/[\`\~\!\@\#\$\'\.\s\?\+\-\*\&\|\/\\\!]/', '_', $field->lbl);
877 $field_type = $field->typ;
878 $field_details[$key]['label'] = !empty($field->lbl) ? $field->lbl : (!empty($field->adminLbl) ? $field->adminLbl : (!empty($field->fieldName) ? $field->fieldName : null));
879 $field_details[$key]['type'] = $field_type;
880 $field_details[$key]['key'] = $key;
881 $field_details[$key]['name'] = isset($field->fieldName) ? $field->fieldName : '';
882 if (isset($field->customType)) {
883 $field_details[$key]['customType'] = $field->customType;
884 }
885 // fields with confirm field
886 if (isset($field->childFields)) {
887 $field_details[$key]['childFields'] = $field->childFields;
888 }
889 if (isset($field->parentFieldKey)) {
890 $field_details[$key]['parentFieldKey'] = $field->parentFieldKey;
891 if (isset($field->isDeactive)) {
892 $field_details[$key]['isDeactive'] = $field->isDeactive;
893 }
894 }
895 if (isset($field->err)) {
896 if (isset($field->err->entryUnique)) {
897 $field_details[$key]['entryUnique'] = $field->err->entryUnique;
898 }
899 if (isset($field->err->userUnique)) {
900 $field_details[$key]['userUnique'] = $field->err->userUnique;
901 }
902 }
903
904 if (isset($field->mul)) {
905 $field_details[$key]['mul'] = $field->mul;
906 }
907 if ('image-select' === $field_type) {
908 // The validator checks submitted choices against these; the render posts val, or lbl without one.
909 $field_details[$key]['options'] = [];
910 foreach ((isset($field->opt) && is_array($field->opt) ? $field->opt : []) as $opt) {
911 if (is_object($opt) && (isset($opt->val) || isset($opt->lbl))) {
912 $field_details[$key]['options'][] = (string) (isset($opt->val) && '' !== $opt->val ? $opt->val : $opt->lbl);
913 }
914 }
915 $field_details[$key]['multiple'] = isset($field->inpType) && 'checkbox' === $field->inpType;
916 }
917 if (in_array($field_type, ['name'])) {
918 $field_details[$key]['label'] = $field->adminLbl ?? $field->lbl;
919 }
920 if ('file-up' === $field_type && isset($field->exts)) {
921 $field_details[$key]['valid']['type'] = $field->exts;
922 }
923 if ('file-up' === $field_type && isset($field->mxUp)) {
924 $field_details[$key]['valid']['upload_size'] = (int) $field->mxUp;
925 }
926 if (isset($field->valid) && !is_null($field->valid)) {
927 if (isset($field->valid->req)) {
928 $field_details[$key]['valid']['req'] = $field->valid->req;
929 }
930 if (isset($field->valid->reqMsg)) {
931 $field_details[$key]['valid']['reqMsg'] = $field->valid->reqMsg;
932 }
933 if (isset($field->valid->typMsg)) {
934 $field_details[$key]['valid']['typMsg'] = $field->valid->typMsg;
935 }
936 if (isset($field->valid->hide)) {
937 $field_details[$key]['valid']['hide'] = $field->valid->hide;
938 }
939 if (isset($field->valid->keepValueWhenHidden)) {
940 $field_details[$key]['valid']['keepValueWhenHidden'] = $field->valid->keepValueWhenHidden;
941 }
942 }
943 $invalidMsg = $this->getInvalidErrorMessage($field);
944 if ('' !== $invalidMsg) {
945 $field_details[$key]['valid']['invalidMsg'] = $invalidMsg;
946 }
947 if ($this->isRepeatedField($key)) {
948 $field_details[$key]['repeated'] = true;
949 }
950 }
951 if ($this->isGCLIDEnabled()) {
952 $field_details['GCLID']['name'] = 'GCLID';
953 $field_details['GCLID']['adminLbl'] = 'GCLID';
954 $field_details['GCLID']['key'] = 'GCLID';
955 $field_details['GCLID']['type'] = 'hidden';
956 }
957 $this->_fields = $field_details;
958 return $field_details;
959 }
960
961 public function getFieldsKey()
962 {
963 $form_content = \json_decode($this->form[0]->form_content);
964 $fields = $form_content->fields;
965 $field_details = [];
966 foreach ($fields as $key => $field) {
967 if ('recaptcha' === $field->typ || 'hcaptcha' === $field->typ || 'turnstile' === $field->typ) {
968 continue;
969 }
970 // $field_name = empty($field->lbl) ? null : \preg_replace('/[\`\~\!\@\#\$\'\.\s\?\+\-\*\&\|\/\\\!]/', '_', $field->lbl);
971 $field_details[$key] = $key;
972 }
973 if ($this->isGCLIDEnabled()) {
974 $field_details['GCLID'] = 'GCLID';
975 }
976 return $field_details;
977 }
978
979 public function getFieldLabel($forQuery = false)
980 {
981 if (!is_null($this->_field_label)) {
982 return $this->_field_label;
983 }
984 $form_content = \json_decode($this->form[0]->form_content);
985 $fields = $form_content->fields;
986 $field_details = [];
987 $fieldCounter = 0;
988 foreach ($fields as $key => $field) {
989 if ('recaptcha' === $field->typ || 'turnstile' === $field->typ || 'html' === $field->typ || 'button' === $field->typ) {
990 continue;
991 }
992 $field_details[$fieldCounter]['name'] = empty($field->lbl) ? null : $field->lbl;
993 $field_details[$fieldCounter]['adminLbl'] = empty($field->adminLbl) ? $field_details[$fieldCounter]['name'] : $field->adminLbl;
994 $field_details[$fieldCounter]['key'] = $key;
995 $field_details[$fieldCounter]['type'] = $field->typ;
996 $fieldCounter += 1;
997 }
998 if ($this->isGCLIDEnabled()) {
999 $field_details[$fieldCounter]['name'] = 'GCLID';
1000 $field_details[$fieldCounter]['adminLbl'] = 'GCLID';
1001 $field_details[$fieldCounter]['key'] = 'GCLID';
1002 $field_details[$fieldCounter]['type'] = 'hidden';
1003 $fieldCounter += 1;
1004 }
1005 if (!$forQuery) {
1006 $field_details = (array) $this->addEntryInfo($field_details, $fieldCounter);
1007 }
1008 $this->_field_label = $field_details;
1009 return $field_details;
1010 }
1011
1012 public function getUploadFields()
1013 {
1014 if (!is_null($this->_has_upload)) {
1015 return $this->_has_upload;
1016 }
1017 $upload_fields = [];
1018 $form_field_details = $this->getFields();
1019 foreach ($form_field_details as $field_name => $__field_detail) {
1020 if (isset($__field_detail['type']) && ('file-up' === $__field_detail['type'] || 'advanced-file-up' === $__field_detail['type'])) {
1021 $upload_fields[] = $field_name;
1022 }
1023 }
1024 $this->_has_upload = $upload_fields;
1025 return $upload_fields;
1026 }
1027
1028 /** Signature pad image types and the extension each is stored with. */
1029 private const SIGNATURE_IMG_TYPES = [
1030 'image/png' => 'png',
1031 'image/jpeg' => 'jpg',
1032 'image/svg+xml' => 'svg',
1033 ];
1034
1035 /**
1036 * Decodes a signature pad data URI into the bytes to store. Run at validation too, so the
1037 * visitor gets an error instead of the entry silently pointing at a placeholder file.
1038 *
1039 * @param mixed $blobLink data URI posted by the signature pad
1040 * @param string $imgType the field's config imgTyp
1041 * @return string file contents; SVG comes back sanitized because it is served from the uploads dir
1042 * @throws \InvalidArgumentException|\RuntimeException when the data is unusable
1043 */
1044 public static function decodeSignatureData($blobLink, $imgType)
1045 {
1046 if (!isset(self::SIGNATURE_IMG_TYPES[$imgType])) {
1047 throw new \InvalidArgumentException('Unsupported signature image type');
1048 }
1049 $parts = is_string($blobLink) ? explode(',', $blobLink, 2) : [];
1050 $decoded_image = 2 === count($parts) ? base64_decode($parts[1], true) : false;
1051 if (false === $decoded_image || '' === $decoded_image) {
1052 throw new \RuntimeException('Invalid or corrupt signature data URI');
1053 }
1054
1055 // An attacker-controlled SVG signature is written to a web-served path, so a raw write is a
1056 // stored-XSS sink. Sanitize with the same enshrined library the upload path uses (FileHandler).
1057 if ('svg' === self::SIGNATURE_IMG_TYPES[$imgType]) {
1058 $clean = (new Sanitizer())->sanitize($decoded_image);
1059 if (false === $clean) {
1060 throw new \RuntimeException('Invalid or unsafe SVG signature data');
1061 }
1062 return $clean;
1063 }
1064
1065 if (false === @getimagesizefromstring($decoded_image)) {
1066 throw new \RuntimeException('Signature data is not an image');
1067 }
1068 return $decoded_image;
1069 }
1070
1071 public function getSignatureFilePath($blobLink, $form_id, $fieldKey, $entry_id, $imgType)
1072 {
1073 try {
1074 $decoded_image = self::decodeSignatureData($blobLink, $imgType);
1075
1076 $_upload_dir = FileHandler::getEntriesFileUploadDir($form_id, $entry_id);
1077 FileHandler::createIndexFile($_upload_dir);
1078 $uniqueId = time() . '-' . bin2hex(\random_bytes(4));
1079 $filename = "{$entry_id}-{$fieldKey}-{$uniqueId}." . self::SIGNATURE_IMG_TYPES[$imgType];
1080 $fullPath = $_upload_dir . DIRECTORY_SEPARATOR . $filename;
1081 if (false === file_put_contents($fullPath, $decoded_image)) {
1082 throw new \RuntimeException("Failed to write image to $fullPath");
1083 }
1084 return $filename;
1085 } catch (\Throwable $e) {
1086 // Data is checked at validation, so only a disk failure lands here; the entry and its
1087 // integrations already exist, so keep the placeholder every reader already skips.
1088 Log::debug_log("[Signature Error] Form: $form_id, Entry: $entry_id, Field: $fieldKey - " . $e->getMessage());
1089 return 'signature-failed.png';
1090 }
1091 }
1092
1093 private function entryInsert($user_details)
1094 {
1095 $formEntryModel = new FormEntryModel();
1096 $entryId = $formEntryModel->insert(
1097 [
1098 'form_id' => $this->form_id,
1099 'user_id' => $user_details['id'],
1100 'user_ip' => $user_details['ip'],
1101 'user_device' => $user_details['device'],
1102 'referer' => $user_details['page'],
1103 'status' => $this->_saveFormAsDraft ? 9 : 1,
1104 'created_at' => $user_details['time'],
1105 ]
1106 );
1107 return $entryId;
1108 }
1109
1110 public function submisionLog($user_details, $entry_id, $type)
1111 {
1112 $formEntryLogModel = new FormEntryLogModel();
1113 $submissionLogData = [
1114 'user_id' => $user_details['id'],
1115 'action_type' => $type, // create, update
1116 'log_type' => 'entry',
1117 'ip' => $user_details['ip'],
1118 'form_entry_id' => $entry_id,
1119 // encoded: wpdb cannot bind an array, so an array here silently stored an
1120 // empty string and every submission lost its device info
1121 'content' => wp_json_encode(['user_device' => $user_details['device']]),
1122 'form_id' => $this->form_id,
1123 'created_at' => $user_details['time'],
1124 ];
1125 $submissionLogData = apply_filters('bitform_filter_submission_log_data', $submissionLogData, $this->form_id, $type);
1126 $logId = $formEntryLogModel->form_log_insert(
1127 $submissionLogData
1128 );
1129 return $logId;
1130 }
1131
1132 private function isArrayAllKeyInt($InputArray)
1133 {
1134 if (!is_array($InputArray)) {
1135 return false;
1136 }
1137
1138 if (count($InputArray) <= 0) {
1139 return true;
1140 }
1141
1142 return array_unique(array_map('is_int', array_keys($InputArray))) === [true];
1143 }
1144
1145 public function formatSubmittedData($submitted_data)
1146 {
1147 $form_content = $this->getFormContent();
1148 $form_fields = $form_content->fields;
1149
1150 foreach ($submitted_data as $key => $value) {
1151 if (!isset($form_fields->{$key})) {
1152 continue;
1153 }
1154 $field_data = $form_fields->{$key};
1155 $field_type = $field_data->typ;
1156 $normalizedParentValue = $this->normalizeSubmittedValue($value);
1157 $parentFieldName = isset($field_data->fieldName) ? $field_data->fieldName : '';
1158 // Confirm child of a repeated email/password never persists — the non-repeated
1159 // path drops it too (the validator collapses the parent to its primary value).
1160 $isRepeatedConfirmComposite = in_array($field_type, ['email', 'password'], true) && $this->isRepeatedField($key);
1161 if (!$isRepeatedConfirmComposite && !empty($field_data->childFields) && is_array($field_data->childFields)) {
1162 foreach ($field_data->childFields as $childFieldRef) {
1163 $childFieldKey = isset($childFieldRef->fldKey) ? $childFieldRef->fldKey : '';
1164 if (empty($childFieldKey) || !isset($form_fields->{$childFieldKey})) {
1165 continue;
1166 }
1167
1168 $childFieldData = $form_fields->{$childFieldKey};
1169 $childFieldName = isset($childFieldData->fieldName) ? $childFieldData->fieldName : '';
1170 $childFieldName = FieldValueHandler::deriveChildName($childFieldName, $parentFieldName);
1171 if (empty($childFieldName)) {
1172 continue;
1173 }
1174
1175 $childValue = FieldValueHandler::extractChildValueFromParentValue($normalizedParentValue, $childFieldName, $childFieldKey);
1176 if (null !== $childValue) {
1177 $submitted_data[$childFieldKey] = $childValue;
1178 }
1179 }
1180 }
1181
1182 if ($this->isRepeatedField($key) && in_array($field_type, ['name', 'address', 'email', 'password'])) {
1183 $normalizedRows = $this->normalizeRepeatedCompositeFieldInput($normalizedParentValue);
1184 if ($isRepeatedConfirmComposite && is_array($normalizedRows)) {
1185 // Keep only the primary value per row, matching the non-repeated behavior
1186 // where a confirm-enabled field collapses to its primary value.
1187 foreach ($normalizedRows as $rowIndex => $rowValue) {
1188 if (is_array($rowValue) && array_key_exists('primary', $rowValue)) {
1189 $normalizedRows[$rowIndex] = $rowValue['primary'];
1190 }
1191 }
1192 }
1193 $submitted_data[$key] = $normalizedRows;
1194 }
1195
1196 if ('select' === $field_type && !empty($field_data->config->multipleSelect)) {
1197 $valueArr = [];
1198 if ($this->isRepeatedField($key) && is_array($normalizedParentValue)) {
1199 foreach ($normalizedParentValue as $index => $v) {
1200 $valueArr[$index] = explode(BITFORMS_BF_SEPARATOR, $v);
1201 }
1202 } else {
1203 $valueArr = explode(BITFORMS_BF_SEPARATOR, (string) $value);
1204 }
1205 $submitted_data[$key] = $valueArr;
1206 }
1207 }
1208 $submitted_data = apply_filters('bitform_filter_format_submitted_data', $submitted_data, $this->form_id);
1209 return $submitted_data;
1210 }
1211
1212 private function normalizeSubmittedValue($value)
1213 {
1214 if (!is_string($value)) {
1215 return $value;
1216 }
1217
1218 $decoded = json_decode($value, true);
1219 return (JSON_ERROR_NONE === json_last_error()) ? $decoded : $value;
1220 }
1221
1222 private function addNewFilePathToFiles($form_id, $entry_id, $file_fields = [])
1223 {
1224 $common_file_path = Helpers::getFullPathWithEncryptedEntryId($form_id, $entry_id);
1225 foreach ($_FILES as $field_key => $file_details) {
1226 if (!($file_fields && in_array($field_key, $file_fields))) {
1227 continue;
1228 }
1229
1230 $isRepeaterFldKey = $this->isRepeatedField($field_key);
1231 if ($isRepeaterFldKey && isset($file_details['new_name'])) {
1232 // If 'new_name' is an array (i.e., for repeated fields)
1233 foreach ($file_details['new_name'] as $slNo => $newFileNamesArray) {
1234 if (is_array($newFileNamesArray)) {
1235 foreach ($newFileNamesArray as $newFileName) {
1236 $filePath = $common_file_path . DIRECTORY_SEPARATOR . $newFileName;
1237 $_FILES[$field_key]['file_path'][$slNo][] = $filePath;
1238 }
1239 } else {
1240 // Generate the file path for each file
1241 $filePath = $common_file_path . DIRECTORY_SEPARATOR . $newFileNamesArray;
1242 $_FILES[$field_key]['file_path'][$slNo] = $filePath;
1243 }
1244 }
1245 } elseif (isset($file_details['new_name'])) {
1246 // If 'new_name' is an array (i.e., for repeated fields)
1247 if (is_array($file_details['new_name'])) {
1248 foreach ($file_details['new_name'] as $slNo => $newFileName) {
1249 // Generate the file path for each file
1250 $filePath = $common_file_path . DIRECTORY_SEPARATOR . $newFileName;
1251 $_FILES[$field_key]['file_path'][$slNo] = $filePath;
1252 }
1253 } else {
1254 $filePath = $common_file_path . DIRECTORY_SEPARATOR . $file_details['new_name'];
1255 $_FILES[$field_key]['file_path'] = $filePath;
1256 }
1257 }
1258 }
1259 }
1260
1261 private function formatRepeateFieldData($submitted_data, $form_fields)
1262 {
1263 $repeaterFields = $this->getRepeaterFields();
1264 foreach ($repeaterFields as $repeaterFldKey => $repeatedFields) {
1265 $repeatIndexes = $submitted_data["{$form_fields[$repeaterFldKey]['name']}-repeat-index"];
1266 $repeatIndexes = explode(',', $repeatIndexes);
1267 foreach ($repeatedFields as $repeatedField) {
1268 $oldFileKey = "{$repeatedField}_old";
1269 if (isset($submitted_data[$oldFileKey]) && is_array($submitted_data[$oldFileKey])) {
1270 $oldFileValues = $submitted_data[$oldFileKey];
1271 $oldFileKeys = array_map('strval', array_keys($oldFileValues));
1272 $repeatIndexKeys = array_map('strval', $repeatIndexes);
1273 $oldFilesUseRepeatIndexes = empty(array_diff($oldFileKeys, $repeatIndexKeys));
1274 $normalizedOldFileValues = [];
1275
1276 foreach ($repeatIndexes as $slNo => $repeatIndex) {
1277 $oldFileSourceIndex = $oldFilesUseRepeatIndexes ? $repeatIndex : $slNo;
1278 if (array_key_exists($oldFileSourceIndex, $oldFileValues)) {
1279 $normalizedOldFileValues[$slNo] = $oldFileValues[$oldFileSourceIndex];
1280 }
1281 }
1282
1283 $submitted_data[$oldFileKey] = $normalizedOldFileValues;
1284 }
1285 }
1286
1287 foreach ($repeatIndexes as $slNo => $repeatIndex) {
1288 foreach ($repeatedFields as $repeatedField) {
1289 if (!isset($submitted_data[$repeatedField][$repeatIndex])) {
1290 continue;
1291 }
1292 if (!isset($submitted_data[$repeaterFldKey][$slNo])) {
1293 $submitted_data[$repeaterFldKey][$slNo] = [];
1294 }
1295 if (!isset($submitted_data[$repeaterFldKey][$slNo][$repeatedField])) {
1296 $submitted_data[$repeaterFldKey][$slNo][$repeatedField] = [];
1297 }
1298 $submitted_data[$repeaterFldKey][$slNo][$repeatedField] = $submitted_data[$repeatedField][$repeatIndex];
1299 }
1300 }
1301 foreach ($repeatedFields as $repeatedField) {
1302 unset($submitted_data[$repeatedField]);
1303 }
1304 unset($submitted_data["{$form_fields[$repeaterFldKey]['name']}-repeat-index"]);
1305 }
1306
1307 return $submitted_data;
1308 }
1309
1310 private function saveEntryMeta($submitted_data, $entry_id)
1311 {
1312 $errorInEntryMetaInsert = false;
1313 $entryMeta = new FormEntryMetaModel();
1314 foreach ($submitted_data as $key => $value) {
1315 $value = $submitted_data[$key];
1316 if (is_string($value)) {
1317 $value = wp_unslash($value);
1318 } elseif ($this->isArrayAllKeyInt($value)) {
1319 $value = wp_json_encode(array_values($value));
1320 } else {
1321 $value = wp_json_encode($value);
1322 }
1323 // Form entry meta insert; meta_key/meta_value required to store dynamic field data per entry.
1324 $status = $entryMeta->insert(
1325 [
1326 'bitforms_form_entry_id' => $entry_id,
1327 'meta_key' => $key,
1328 'meta_value' => $value,
1329 ]
1330 );
1331 if (is_wp_error($status)) {
1332 $errorInEntryMetaInsert = true;
1333 break;
1334 }
1335 }
1336 return $errorInEntryMetaInsert;
1337 }
1338
1339 public function setSaveFormAsDraft()
1340 {
1341 $this->_saveFormAsDraft = true;
1342 }
1343
1344 public function saveFormEntry($submitted_data)
1345 {
1346 // CSRF verified upstream via FrontendFormManager::verifySubmissionNonce() before this method is invoked.
1347 $submitted_data = $this->formatSubmittedData($submitted_data);
1348 $submitted_data = apply_filters('bitform_filter_save_form_entry', $submitted_data, $this->form_id);
1349 $form_content = \json_decode($this->form[0]->form_content);
1350 do_action('bitform_save_entry', $this, $submitted_data, $this->form_id);
1351 $key = null;
1352 $ipTool = new IpTool();
1353 $fileHandler = new FileHandler();
1354 $form_fields = $this->getFields();
1355 $file_fields = $this->getUploadFields();
1356
1357 foreach ($_FILES as $file_name => $file_details) {
1358 if ($file_fields && in_array($file_name, $file_fields)) {
1359 $validation = $fileHandler->validation($file_name, $file_details, $this->form_id);
1360 if (!empty($validation['error_type']) && !empty($validation['message'])) {
1361 return new WP_Error($validation['error_type'], esc_html($validation['message']));
1362 }
1363 }
1364 }
1365 $user_details = $ipTool->getUserDetail();
1366 $user_details = apply_filters('bitform_filter_user_details', $user_details, $this->form_id);
1367 $user_details = apply_filters('bitform_filter_save_entry_user_details', $user_details, $this->form_id);
1368
1369 $form_fields = $this->getFields();
1370 $submitted_data = $this->maskPasswordValues($submitted_data, $form_fields);
1371 $submitted_data = $this->formatRepeateFieldData($submitted_data, $form_fields);
1372 global $wpdb;
1373 // Direct transaction control; no user input involved.
1374 $wpdb->query('START TRANSACTION');
1375 $entry_id = $this->entryInsert($user_details);
1376 $log_id = null;
1377
1378 $GLOBALS['bitform_entry_id'] = $entry_id;
1379
1380 if (is_wp_error($entry_id)) {
1381 return new WP_Error('insert_error', __('Sorry, Error occurred in saving form entry', 'bit-form'));
1382 }
1383 if ($entry_id) {
1384 $log_id = $this->submisionLog($user_details, $entry_id, 'create', $key);
1385 if (is_wp_error($log_id)) {
1386 $wpdb->query('ROLLBACK');
1387 return new WP_Error('error_entry_log', __('Sorry, error occurred in logging form entry', 'bit-form'));
1388 }
1389 }
1390 if ($entry_id) {
1391 $submitted_fields = $this->getFormContentWithValue($submitted_data)->fields;
1392 $workFlowRunHelper = new WorkFlow($this->form_id);
1393
1394 $workFlowreturnedOnSubmit = $workFlowRunHelper->executeOnSubmit(
1395 'create',
1396 $submitted_fields,
1397 $submitted_data,
1398 $entry_id,
1399 $log_id
1400 );
1401
1402 if (!empty($workFlowreturnedOnSubmit['fields'])) {
1403 $submitted_data = $workFlowreturnedOnSubmit['fields'];
1404 }
1405
1406 $file_fields = $this->getUploadFields();
1407 $formFields = $this->getFields();
1408 $submitted_data = FileHandler::tempDirToUploadDir($submitted_data, $formFields, $this->form_id, $entry_id);
1409 $fileHandler = new FileHandler();
1410 foreach ($_FILES as $field_key => $file_details) {
1411 if ($file_fields && in_array($field_key, $file_fields)) {
1412 $fileNames = [];
1413 $repeaterFldKey = $this->isRepeatedField($field_key);
1414 if ($repeaterFldKey) {
1415 foreach ($file_details['name'] as $slNo => $fileName) {
1416 $repeateFileDetails = [
1417 'name' => $file_details['name'][$slNo],
1418 'type' => $file_details['type'][$slNo],
1419 'tmp_name' => $file_details['tmp_name'][$slNo],
1420 'error' => $file_details['error'][$slNo],
1421 'size' => $file_details['size'][$slNo],
1422 ];
1423 $fileNames = $fileHandler->moveUploadedFiles($repeateFileDetails, $this->form_id, $entry_id);
1424 if (!empty($fileNames)) {
1425 $submitted_data[$repeaterFldKey][$slNo - 1][$field_key] = $fileNames;
1426 $_FILES[$field_key]['new_name'][$slNo - 1] = $fileNames;
1427 }
1428 }
1429 } else {
1430 $fileNames = $fileHandler->moveUploadedFiles($file_details, $this->form_id, $entry_id);
1431 if (!empty($fileNames)) {
1432 $submitted_data[$field_key] = $fileNames;
1433 $_FILES[$field_key]['new_name'] = $fileNames;
1434 }
1435 }
1436 }
1437 }
1438
1439 // Get the common path for file storage
1440 $this->addNewFilePathToFiles($this->form_id, $entry_id, $file_fields);
1441
1442 foreach ($form_content->fields as $key => $field) {
1443 /* ======== for Signature field ===========*/
1444 if ('signature' === $field->typ) {
1445 if (isset($submitted_data[$key])) {
1446 $fld_data = $submitted_data[$key];
1447 $img_type = $field->config->imgTyp;
1448 $submitted_data[$key] = $this->getSignatureFilePath($fld_data, $this->form_id, $key, $entry_id, $img_type);
1449 }
1450 }
1451
1452 // for Signature field inside reepater
1453 if ('repeater' === $field->typ) {
1454 $rptr_data = $submitted_data[$key];
1455 $formFields = $form_content->fields;
1456 $this->setSignatureFilePathInRepeater($rptr_data, $key, $formFields, $entry_id, $submitted_data);
1457 }
1458 }
1459
1460 if (!isset($form_content->additional->enabled->submission)) {
1461 $errorInEntryMetaInsert = $this->saveEntryMeta($submitted_data, $entry_id);
1462 if ($errorInEntryMetaInsert) {
1463 do_action('bitform_save_entry_error', $this, $submitted_data, $this->form_id);
1464 $wpdb->query('ROLLBACK');
1465 return new WP_Error('insert_error', __('Sorry, Error occured in saving form entry data', 'bit-form'));
1466 }
1467 do_action('bitform_after_save_entry_success', $this, $submitted_data, $this->form_id, $entry_id);
1468 } else {
1469 $wpdb->query('ROLLBACK');
1470 }
1471 $wpdb->query('COMMIT');
1472 $this->setSubmissionCount();
1473 $workFlowreturnedOnSubmit['entry_id'] = $entry_id;
1474 $workFlowreturnedOnSubmit['fields'] = $submitted_data;
1475 $workFlowreturnedOnSubmit = apply_filters('bitform_filter_return_submit_success', $workFlowreturnedOnSubmit, $this->form_id);
1476
1477 return $workFlowreturnedOnSubmit;
1478 }
1479 }
1480
1481 /**
1482 * The signature file name an entry currently points at, '' when it has none.
1483 */
1484 private function getStoredSignatureFile($entryMeta, $entryID, $fieldKey)
1485 {
1486 $stored = $entryMeta->get(
1487 'meta_value',
1488 [
1489 'bitforms_form_entry_id' => $entryID,
1490 'meta_key' => $fieldKey,
1491 ]
1492 );
1493 if (is_wp_error($stored) || 0 === count($stored)) {
1494 return '';
1495 }
1496 $fileName = trim((string) $stored[0]->meta_value);
1497
1498 // signature-failed.png is a shared placeholder, not this entry's own file.
1499 return 'signature-failed.png' === $fileName ? '' : $fileName;
1500 }
1501
1502 private function setSignatureFilePathInRepeater($repeaterData, $repeaterFieldKey, $formFields, $entry_id, &$submitted_data)
1503 {
1504 foreach ($repeaterData as $rptr_entry_index => $rptr_entries) {
1505 foreach ($rptr_entries as $entry_key => $entry_value) {
1506 if (!isset($formFields->{$entry_key})) {
1507 continue;
1508 }
1509 $rptr_entry_info = $formFields->{$entry_key};
1510
1511 if ('signature' === $rptr_entry_info->typ) {
1512 $imgType = $rptr_entry_info->config->imgTyp;
1513 $signatureImage = $this->getSignatureFilePath($entry_value, $this->form_id, $repeaterFieldKey, $entry_id, $imgType);
1514 $submitted_data[$repeaterFieldKey][$rptr_entry_index][$entry_key] = $signatureImage;
1515 }
1516 }
1517 }
1518 }
1519
1520 /**
1521 * Kept for callers outside this class; masks every password field now, not only on
1522 * forms with a WP User Auth integration.
1523 *
1524 * @param array $updatedValue
1525 * @param array $form_fields
1526 * @return array
1527 */
1528 public function passwordEncrypted($updatedValue, $form_fields)
1529 {
1530 return $this->maskPasswordValues($updatedValue, $form_fields);
1531 }
1532
1533 /**
1534 * Replaces password values with PASSWORD_MASK before anything is stored, logged or
1535 * handed to deferred workflows, emails and integrations. Consumers that need the real
1536 * password (bitform_wp_user_auth, bitform_save_entry) run earlier in the request.
1537 * Runs before formatRepeateFieldData, so repeated rows and an unvalidated
1538 * {primary, confirm} draft value are masked leaf by leaf, keeping their shape.
1539 *
1540 * @param mixed $data Submitted values keyed by field key
1541 * @param mixed $form_fields Fields from getFields()
1542 * @return mixed $data, masked when it is an array
1543 */
1544 public function maskPasswordValues($data, $form_fields)
1545 {
1546 if (!is_array($data) || !is_array($form_fields)) {
1547 return $data;
1548 }
1549 if (!apply_filters('bitform_mask_password_values', true, $this->form_id)) {
1550 return $data;
1551 }
1552 foreach ($this->getPasswordFieldKeys($form_fields) as $fieldKey) {
1553 if (array_key_exists($fieldKey, $data)) {
1554 $data[$fieldKey] = self::maskPasswordValue($data[$fieldKey]);
1555 }
1556 }
1557 return $data;
1558 }
1559
1560 /**
1561 * @param array $form_fields Fields from getFields()
1562 * @return string[]
1563 */
1564 public function getPasswordFieldKeys($form_fields)
1565 {
1566 $keys = [];
1567 foreach ((array) $form_fields as $field) {
1568 if (isset($field['type'], $field['key']) && 'password' === $field['type']) {
1569 $keys[] = $field['key'];
1570 }
1571 }
1572 return $keys;
1573 }
1574
1575 /**
1576 * Password fields that received a new value: not empty and not the mask a prefilled
1577 * form sent back.
1578 *
1579 * @param array $data Submitted values, before masking
1580 * @param string[] $passwordKeys
1581 * @return string[]
1582 */
1583 private static function changedPasswordKeys($data, $passwordKeys)
1584 {
1585 $changed = [];
1586 foreach ($passwordKeys as $fieldKey) {
1587 $value = isset($data[$fieldKey]) ? $data[$fieldKey] : '';
1588 if (is_array($value)) {
1589 $value = isset($value['primary']) ? $value['primary'] : implode('', array_filter($value, 'is_string'));
1590 }
1591 if (is_string($value) && '' !== $value && self::PASSWORD_MASK !== $value) {
1592 $changed[] = $fieldKey;
1593 }
1594 }
1595 return $changed;
1596 }
1597
1598 private static function maskPasswordValue($value)
1599 {
1600 if (is_array($value)) {
1601 return array_map(static function ($item) {
1602 return self::maskPasswordValue($item);
1603 }, $value);
1604 }
1605 return (null === $value || '' === $value) ? $value : self::PASSWORD_MASK;
1606 }
1607
1608 private function normalizeOldFileValues($stored_files, $old_values)
1609 {
1610 $stored_files = is_array($stored_files) ? $stored_files : [];
1611 if (!is_array($old_values)) {
1612 $old_values_string = trim((string) $old_values);
1613 $decoded_old_values = json_decode($old_values_string, true);
1614 $old_values = is_array($decoded_old_values) ? $decoded_old_values : explode(',', $old_values_string);
1615 }
1616
1617 $normalized_values = [];
1618 foreach ($old_values as $value) {
1619 if (!is_string($value) && !is_numeric($value)) {
1620 continue;
1621 }
1622
1623 $trimmed_value = trim((string) $value);
1624 if ('' === $trimmed_value) {
1625 continue;
1626 }
1627
1628 if (in_array($trimmed_value, $stored_files, true)) {
1629 $normalized_values[] = $trimmed_value;
1630 }
1631 }
1632
1633 return array_values(array_unique($normalized_values));
1634 }
1635
1636 public function updateFormEntry($updatedValue, $formID, $entryID)
1637 {
1638 // CSRF / entry-token verified upstream via FrontendFormManager::handleUpdateEntry() before this method is invoked.
1639 $updatedValue = $this->formatSubmittedData($updatedValue);
1640 $updatedValue = apply_filters('bitform_filter_update_form_entry', $updatedValue, $this->form_id);
1641 do_action('bitform_update_entry', $this, $updatedValue, $formID, $entryID);
1642 $form_content = $this->getFormContent();
1643 if (isset($form_content->additional->enabled->submission)) {
1644 // Run workflow but skip DB/meta update
1645 $workFlowRunHelper = new WorkFlow($formID);
1646 $fieldsWithValue = $this->getFormContentWithValue($updatedValue)->fields;
1647 $workFlowreturnedOnSubmit = $workFlowRunHelper->executeOnSubmit(
1648 'edit',
1649 $fieldsWithValue,
1650 $updatedValue,
1651 $entryID,
1652 0
1653 );
1654 if (empty($workFlowreturnedOnSubmit['message'])) {
1655 $workFlowreturnedOnSubmit['message'] = __('Entry update skipped due to submission restriction.', 'bit-form');
1656 }
1657 $workFlowreturnedOnSubmit['entry_id'] = $entryID;
1658 $workFlowreturnedOnSubmit = apply_filters('bitform_filter_return_edit_success', $workFlowreturnedOnSubmit, $this->form_id);
1659 return $workFlowreturnedOnSubmit;
1660 }
1661
1662 $formEntryModel = new FormEntryModel();
1663 $formEntryLogModel = new FormEntryLogModel();
1664 $formOldData = $formEntryLogModel->get_form_value($entryID);
1665 $key = null;
1666 $entryMeta = new FormEntryMetaModel();
1667 $ipTool = new IpTool();
1668 $user_details = $ipTool->getUserDetail();
1669 $user_details = apply_filters('bitform_filter_user_details', $user_details, $this->form_id);
1670 $user_details = apply_filters('bitform_filter_update_entry_user_details', $user_details, $this->form_id);
1671
1672 $form_fields = $this->getFields();
1673
1674 $passwordKeys = $this->getPasswordFieldKeys($form_fields);
1675 $changedPasswordKeys = self::changedPasswordKeys($updatedValue, $passwordKeys);
1676 $updatedValue = $this->maskPasswordValues($updatedValue, $form_fields);
1677 $updatedValue = $this->formatRepeateFieldData($updatedValue, $form_fields);
1678 $field_map = [];
1679 foreach ($formOldData as $index => $data) {
1680 foreach ($form_fields as $field_key => $field) {
1681 if ($data->meta_key === $field['key']) {
1682 $field_map[$field_key] = $field['key'];
1683 }
1684 }
1685 }
1686
1687 $geResult = $formEntryModel->get('status', ['form_id' => $formID, 'id' => $entryID]);
1688 if (is_wp_error($geResult) || empty($geResult)) {
1689 return new WP_Error('empty_form', __('provided form entries does not exists', 'bit-form'));
1690 }
1691 $oldEntry = $geResult[0];
1692 $formEntry = $formEntryModel->update(
1693 [
1694 'status' => ('9' === $oldEntry->status && !$this->_saveFormAsDraft) ? 1 : $oldEntry->status,
1695 'updated_at' => $user_details['time'],
1696 ],
1697 [
1698 'form_id' => $formID,
1699 'id' => $entryID,
1700 ]
1701 );
1702
1703 if (is_wp_error($formEntry) && 'result_empty' !== $formEntry->get_error_code()) {
1704 return new WP_Error('entry_update_failed', __('Sorry, error occurred in updating form entry', 'bit-form'));
1705 }
1706
1707 $log_id = $this->submisionLog($user_details, $entryID, 'update');
1708 $formFields = $this->getFields();
1709 $updatedValue = FileHandler::tempDirToUploadDir($updatedValue, $formFields, $this->form_id, $entryID);
1710 $file_fields = $this->getUploadFields();
1711 if (count($file_fields) > 0) {
1712 $fileHandler = new FileHandler();
1713 foreach ($_FILES as $file_name => $file_details) {
1714 if ($file_fields && in_array($file_name, $file_fields)) {
1715 $validation = $fileHandler->validation($file_name, $file_details, $this->form_id);
1716 if (!empty($validation['error_type']) && !empty($validation['message'])) {
1717 return new WP_Error($validation['error_type'], esc_html($validation['message']));
1718 }
1719 }
1720 }
1721 if (is_object($updatedValue)) {
1722 $updatedValue = (array) $updatedValue;
1723 }
1724 foreach ($file_fields as $field_key) {
1725 $repeaterFldKey = $this->isRepeatedField($field_key);
1726 if (isset($updatedValue[$field_key . '_old'])) {
1727 // Handle file deletion for repeater fields
1728 if ($repeaterFldKey) {
1729 // Form entry meta lookup; meta_key/meta_value query required to retrieve repeater field data by entry.
1730 $repeaterExistData = $entryMeta->get(
1731 'meta_value',
1732 [
1733 'bitforms_form_entry_id' => $entryID,
1734 'meta_key' => $repeaterFldKey,
1735 ]
1736 );
1737 if (!is_wp_error($repeaterExistData)) {
1738 // restructor json
1739 $repeaterExistData = json_decode($repeaterExistData[0]->meta_value, true);
1740 $repeaterExistFiles = [];
1741 $repeaterDeleted_files = [];
1742 $repeaterFiles_old = [];
1743 $submittedRepeaterOldFiles = is_array($updatedValue[$field_key . '_old']) ? $updatedValue[$field_key . '_old'] : [];
1744 foreach ($repeaterExistData as $index => $repeaterRow) {
1745 $repeaterExistFiles[$index] = [];
1746 if (isset($repeaterRow[$field_key]) && !empty($repeaterRow[$field_key]) && is_string($repeaterRow[$field_key])) {
1747 $repeaterExistFiles[$index] = json_decode($repeaterRow[$field_key], true);
1748 }
1749 if (isset($repeaterRow[$field_key]) && !empty($repeaterRow[$field_key]) && is_array($repeaterRow[$field_key])) {
1750 $repeaterExistFiles[$index] = $repeaterRow[$field_key];
1751 }
1752 if (!is_array($repeaterExistFiles[$index])) {
1753 $repeaterExistFiles[$index] = [];
1754 }
1755 $oldFileInputExists = array_key_exists($index, $submittedRepeaterOldFiles);
1756 $repeaterRowExists = $oldFileInputExists || (isset($updatedValue[$repeaterFldKey][$index]) && is_array($updatedValue[$repeaterFldKey][$index]));
1757 $oldFileValues = ($repeaterRowExists && $oldFileInputExists) ? $submittedRepeaterOldFiles[$index] : [];
1758 $repeaterFiles_old[$index] = $this->normalizeOldFileValues($repeaterExistFiles[$index], $oldFileValues);
1759 $repeaterDeleted_files[$index] = array_diff($repeaterExistFiles[$index], $repeaterFiles_old[$index]);
1760 $repeaterFiles_old[$index] = array_values(array_diff($repeaterFiles_old[$index], $repeaterDeleted_files[$index]));
1761 $fileHandler->deleteFiles($formID, $entryID, $repeaterDeleted_files[$index]);
1762 if ($repeaterRowExists) {
1763 if (!isset($updatedValue[$repeaterFldKey][$index]) || !is_array($updatedValue[$repeaterFldKey][$index])) {
1764 $updatedValue[$repeaterFldKey][$index] = [];
1765 }
1766 $updatedValue[$repeaterFldKey][$index][$field_key] = $repeaterFiles_old[$index];
1767 }
1768 }
1769 }
1770 } else {
1771 // Handle file deletion for non-repeater fields; meta_key/meta_value lookup required to identify stored file paths per entry.
1772 $file_exists = $entryMeta->get(
1773 'meta_value',
1774 [
1775 'bitforms_form_entry_id' => $entryID,
1776 'meta_key' => $field_key,
1777 ]
1778 );
1779 if (!is_wp_error($file_exists) && count($file_exists) > 0) {
1780 $files_in_db = json_decode($file_exists[0]->meta_value, true);
1781 if (!is_array($files_in_db)) {
1782 $files_in_db = [];
1783 }
1784 $retained_files = $this->normalizeOldFileValues($files_in_db, empty($updatedValue[$field_key . '_old']) ? [] : $updatedValue[$field_key . '_old']);
1785 $deleted_files = array_diff($files_in_db, $retained_files);
1786 $retained_files = array_values(array_diff($retained_files, $deleted_files));
1787 if (count($deleted_files) > 0) {
1788 $fileHandler->deleteFiles($formID, $entryID, $deleted_files);
1789 }
1790 $updatedValue[$field_key] = $retained_files;
1791 }
1792 }
1793 }
1794 if (!empty($_FILES[$field_key]['name'])) {
1795 if ($repeaterFldKey) {
1796 // Handle repeater field files
1797 $file_details = $_FILES[$field_key];
1798 foreach ($file_details['name'] as $index => $file) {
1799 $old_meta_value = [];
1800 // Retrieve existing old files for this specific repeater index
1801 if (isset($repeaterFiles_old[$index - 1]) && count($repeaterFiles_old[$index - 1]) > 0) {
1802 $old_meta_value = $repeaterFiles_old[$index - 1];
1803 // json format causing issue with repeater file in mail attachment as it's sending broken url(for multistep and abandonment form)
1804 // $updatedValue[$repeaterFldKey][$index - 1][$field_key] = wp_json_encode($old_meta_value);
1805 $updatedValue[$repeaterFldKey][$index - 1][$field_key] = $old_meta_value;
1806 }
1807 $repeateFileDetails = [
1808 'name' => $file_details['name'][$index],
1809 'type' => $file_details['type'][$index],
1810 'tmp_name' => $file_details['tmp_name'][$index],
1811 'error' => $file_details['error'][$index],
1812 'size' => $file_details['size'][$index],
1813 ];
1814 $meta_value = $fileHandler->moveUploadedFiles($repeateFileDetails, $formID, $entryID, $index);
1815 if (!empty($meta_value)) {
1816 $mergedMetaValueWithOld = array_merge($old_meta_value, (array) $meta_value);
1817 // json format causing issue with repeater file in mail attachment as it's sending broken url(for multistep and abandonment form)
1818 // $updatedValue[$repeaterFldKey][$index - 1][$field_key] = wp_json_encode($mergedMetaValueWithOld);
1819 $updatedValue[$repeaterFldKey][$index - 1][$field_key] = $mergedMetaValueWithOld;
1820
1821 $_FILES[$field_key]['new_name'][$index - 1] = $mergedMetaValueWithOld;
1822 // $_FILES[$field_key]['file_path'][$index - 1] = $common_file_path . DIRECTORY_SEPARATOR . $meta_value;
1823 }
1824 }
1825 } else {
1826 // Handle non-repeater field files
1827 $meta_value = $fileHandler->moveUploadedFiles($_FILES[$field_key], $formID, $entryID);
1828 if (!empty($meta_value)) {
1829 $_FILES[$field_key]['new_name'] = $meta_value;
1830 if (isset($updatedValue[$field_key . '_old']) && !is_wp_error($file_exists) && count($file_exists) > 0) {
1831 $meta_value = empty($retained_files) ? $meta_value : array_merge($meta_value, $retained_files);
1832 $updatedValue[$field_key] = $meta_value;
1833 } else {
1834 $updatedValue[$field_key] = $meta_value;
1835 }
1836 }
1837 }
1838 }
1839 }
1840
1841 // Get the common file path to avoid repetitive calculation
1842 $this->addNewFilePathToFiles($formID, $entryID, $file_fields);
1843 }
1844
1845 if (is_object($updatedValue)) {
1846 $updatedValue = (array) $updatedValue;
1847 }
1848 if (isset($updatedValue['_ajax_nonce'], $_REQUEST['g-recaptcha-response']) && sanitize_text_field(wp_unslash($_REQUEST['g-recaptcha-response']))) {
1849 unset($updatedValue['_ajax_nonce'], $_REQUEST['g-recaptcha-response']);
1850 }
1851
1852 $toUpdateValues = [];
1853 foreach ($form_fields as $field) {
1854 if (isset($updatedValue[$field['key']])) {
1855 $toUpdateValues[$field['key']] = $updatedValue[$field['key']];
1856 }
1857 }
1858 // A blank or still-masked password means "keep it": edit forms are never prefilled with it.
1859 foreach (array_diff($passwordKeys, $changedPasswordKeys) as $passwordKey) {
1860 unset($toUpdateValues[$passwordKey]);
1861 }
1862 $form_content = \json_decode($this->form[0]->form_content);
1863
1864 $replacedSignatureFiles = [];
1865
1866 foreach ($form_content->fields as $key => $field) {
1867 if ('signature' === $field->typ) {
1868 $fld_data = isset($updatedValue[$key]) ? $updatedValue[$key] : '';
1869 $img_type = isset($field->config->imgTyp) ? $field->config->imgTyp : 'image/png';
1870 $storedSignature = $this->getStoredSignatureFile($entryMeta, $entryID, $key);
1871 if (is_string($fld_data) && 0 === strpos($fld_data, 'data:')) {
1872 $toUpdateValues[$key] = $this->getSignatureFilePath($fld_data, $this->form_id, $key, $entryID, $img_type);
1873 if ('' !== $storedSignature && $storedSignature !== $toUpdateValues[$key]) {
1874 $replacedSignatureFiles[] = $storedSignature;
1875 }
1876 } elseif (isset($updatedValue[$key . '_old'])) {
1877 // Nothing drawn: `_old` only confirms the stored file was kept, so write that back.
1878 $retained = FieldValueHandler::retainedOldValues($updatedValue, $key);
1879 $keepsStored = '' !== $storedSignature && in_array($storedSignature, $retained, true);
1880 $toUpdateValues[$key] = $keepsStored ? $storedSignature : '';
1881 if (!$keepsStored && '' !== $storedSignature) {
1882 $replacedSignatureFiles[] = $storedSignature;
1883 }
1884 } else {
1885 // No signature and no `_old` marker: leave what is stored alone.
1886 unset($toUpdateValues[$key]);
1887 }
1888 }
1889
1890 // for Signature field inside reepater
1891 if ('repeater' === $field->typ && isset($updatedValue[$key]) && is_array($updatedValue[$key])) {
1892 $rptr_data = $updatedValue[$key];
1893 $formFields = $form_content->fields;
1894 $this->setSignatureFilePathInRepeater($rptr_data, $key, $formFields, $entryID, $toUpdateValues);
1895 }
1896 }
1897
1898 $workFlowRunHelper = new WorkFlow($formID);
1899 $workFlowreturnedOnSubmit = $workFlowRunHelper->executeOnSubmit(
1900 'edit',
1901 $this->getFormContentWithValue($toUpdateValues)->fields,
1902 $toUpdateValues,
1903 $entryID,
1904 $log_id
1905 );
1906
1907 if (!empty($workFlowreturnedOnSubmit['fields'])) {
1908 $updatedValue = $workFlowreturnedOnSubmit['fields'];
1909 }
1910
1911 $formEntryMetaUpdateStatus = $entryMeta->update(
1912 $toUpdateValues,
1913 [
1914 'bitforms_form_entry_id' => $entryID,
1915 ]
1916 );
1917 if (is_wp_error($formEntryMetaUpdateStatus) || isset($newFileInsertStatus) && is_wp_error($newFileInsertStatus)) {
1918 do_action('bitform_update_entry_error', $this, $toUpdateValues, $formEntryMetaUpdateStatus, $this->form_id);
1919 return $formEntryMetaUpdateStatus;
1920 }
1921 // Deleted only now the entry points elsewhere, so a failed update strands nothing.
1922 if (!empty($replacedSignatureFiles)) {
1923 (new FileHandler())->deleteFiles($formID, $entryID, $replacedSignatureFiles);
1924 }
1925 $toUpdateValues = array_merge($formEntryMetaUpdateStatus, ['entry_id' => $entryID]);
1926 do_action('bitform_after_update_entry_success', $this, $toUpdateValues, $formID, $entryID);
1927 if (empty($workFlowreturnedOnSubmit['message'])) {
1928 $workFlowreturnedOnSubmit['message'] = __('Entry Updated Successfully', 'bit-form');
1929 }
1930 $customFieldHandler = new CustomFieldHandler();
1931 $toUpdateValues = $customFieldHandler->updatedData($form_fields, $toUpdateValues);
1932
1933 $workFlowreturnedOnSubmit['updatedData'] = $toUpdateValues;
1934 $counter = 0;
1935 for ($i = 0; $i < count($formOldData); $i++) {
1936 if (array_key_exists($formOldData[$i]->meta_key . '_old', $toUpdateValues)) {
1937 unset($toUpdateValues[$formOldData[$i]->meta_key . '_old']);
1938 }
1939 if (in_array($formOldData[$i]->meta_key, $file_fields)) {
1940 if (
1941 empty($_FILES[$formOldData[$i]->meta_key]['name'])
1942 || (is_array($_FILES[$formOldData[$i]->meta_key]['name'])
1943 && 1 === count($_FILES[$formOldData[$i]->meta_key]['name'])
1944 && empty($_FILES[$formOldData[$i]->meta_key]['name'][0]))
1945 ) {
1946 unset($toUpdateValues[$formOldData[$i]->meta_key]);
1947 continue;
1948 }
1949 if (is_array($_FILES[$formOldData[$i]->meta_key]['name']) && !in_array($_FILES[$formOldData[$i]->meta_key]['name'], json_decode($formOldData[$i]->meta_value))) {
1950 $sanitized_names = array_map('sanitize_file_name', array_map('wp_unslash', (array) $_FILES[$formOldData[$i]->meta_key]['name']));
1951 $key[$i] = '${' . $formOldData[$i]->meta_key . '} file was Updated To ' . wp_json_encode($sanitized_names);
1952 } elseif (!is_array($_FILES[$formOldData[$i]->meta_key]['name']) && !in_array($_FILES[$formOldData[$i]->meta_key]['name'], json_decode($formOldData[$i]->meta_value))) {
1953 $key[$i] = '${' . $formOldData[$i]->meta_key . '} file was Updated To ' . sanitize_file_name(wp_unslash($_FILES[$formOldData[$i]->meta_key]['name']));
1954 }
1955 unset($toUpdateValues[$formOldData[$i]->meta_key]);
1956 } elseif (in_array($formOldData[$i]->meta_key, $passwordKeys, true)) {
1957 // Log that a password changed, never the old or new value.
1958 if (in_array($formOldData[$i]->meta_key, $changedPasswordKeys, true)) {
1959 $key[$i] = '${' . $formOldData[$i]->meta_key . '} was Updated';
1960 }
1961 } elseif (isset($toUpdateValues[$formOldData[$i]->meta_key])) {
1962 if (is_array($toUpdateValues[$formOldData[$i]->meta_key])) {
1963 if (json_decode($formOldData[$i]->meta_value) !== $toUpdateValues[$formOldData[$i]->meta_key]) {
1964 $key[$i] = '${' . $formOldData[$i]->meta_key . '} was Updated From ' . implode(',', json_decode($formOldData[$i]->meta_value)) . ' To ' . implode(',', $toUpdateValues[$formOldData[$i]->meta_key]);
1965 }
1966 } elseif (is_string($toUpdateValues[$formOldData[$i]->meta_key]) && !FieldValueHandler::isEmpty($toUpdateValues[$formOldData[$i]->meta_key])) {
1967 if ($formOldData[$i]->meta_value !== $toUpdateValues[$formOldData[$i]->meta_key]) {
1968 $key[$i] = '${' . $formOldData[$i]->meta_key . '} was Updated' . ($formOldData[$i]->meta_value ? ' From ' . $formOldData[$i]->meta_value : '') . ' To ' . $toUpdateValues[$formOldData[$i]->meta_key];
1969 }
1970 }
1971 }
1972 $counter++;
1973 }
1974
1975 $newField = array_keys(array_diff_key($formEntryMetaUpdateStatus, $field_map));
1976 for ($i = 0; $i < count($newField); $i++) {
1977 if (in_array($newField[$i], $passwordKeys, true)) {
1978 $key[$counter + $i] = '${' . $newField[$i] . '} Updated';
1979 } elseif (is_array($toUpdateValues[$newField[$i]]) && !empty($toUpdateValues[$newField[$i]])) {
1980 $key[$counter + $i] = '${' . $newField[$i] . '} Updated To ' . implode(',', $toUpdateValues[$newField[$i]]);
1981 } elseif (is_string($newField[$i]) && !FieldValueHandler::isEmpty($toUpdateValues[$newField[$i]])) {
1982 $key[$counter + $i] = '${' . $newField[$i] . '} Updated To ' . $toUpdateValues[$newField[$i]];
1983 }
1984 }
1985 if (null !== $key) {
1986 $logUpdate = implode('b::f', (array) $key);
1987 $formEntryLogUpdate = $formEntryLogModel->logUpdate($logUpdate, $log_id);
1988 }
1989 $workFlowreturnedOnSubmit['entry_id'] = $entryID;
1990 $workFlowreturnedOnSubmit = apply_filters('bitform_filter_return_edit_success', $workFlowreturnedOnSubmit, $this->form_id);
1991
1992 return $workFlowreturnedOnSubmit;
1993 }
1994
1995 public function getRepeaterFields()
1996 {
1997 if (!is_null($this->_repeaterFields)) {
1998 return $this->_repeaterFields;
1999 }
2000 $repeaterFields = [];
2001 $form_content = \json_decode($this->form[0]->form_content);
2002 $fields = $form_content->fields;
2003 $nestedLayouts = !empty($form_content->nestedLayout) ? $form_content->nestedLayout : [];
2004 foreach ($nestedLayouts as $fieldKey => $repeatLayout) {
2005 if ('repeater' !== $fields->{$fieldKey}->typ) {
2006 continue;
2007 }
2008 $repeaterFields[$fieldKey] = [];
2009 foreach ($repeatLayout->lg as $fieldLayoutData) {
2010 $repeaterFields[$fieldKey][] = $fieldLayoutData->i;
2011 }
2012 }
2013 $this->_repeaterFields = $repeaterFields;
2014 return $repeaterFields;
2015 }
2016
2017 public function isRepeatedField($fieldKey)
2018 {
2019 $repeatedFields = $this->getRepeaterFields();
2020 foreach ($repeatedFields as $repeaterKey => $repeaterFields) {
2021 if (in_array($fieldKey, $repeaterFields)) {
2022 return $repeaterKey;
2023 }
2024 }
2025 return false;
2026 }
2027
2028 public function getParentRepeaterField($fieldKey)
2029 {
2030 $repeatedFields = $this->getRepeaterFields();
2031 foreach ($repeatedFields as $repeaterKey => $repeaterFields) {
2032 if (in_array($fieldKey, $repeaterFields)) {
2033 return $repeaterKey;
2034 }
2035 }
2036 return null;
2037 }
2038
2039 public function isRepeaterField($fieldKey)
2040 {
2041 $repeatedFields = $this->getRepeaterFields();
2042 if (array_key_exists($fieldKey, $repeatedFields)) {
2043 return true;
2044 }
2045 return false;
2046 }
2047
2048 public function fieldNameReplaceOfPost()
2049 {
2050 // CSRF verified upstream before this method is called; $_POST/$_FILES are being normalized (field key remapping), not reading new user input.
2051 $fields = $this->getFields();
2052 $consumedNames = [];
2053 foreach ($fields as $fieldKey => $fieldData) {
2054 if (array_key_exists('name', $fieldData)) {
2055 $fldName = $fieldData['name'];
2056 $catchChildFldNamePattern = '/\[(.*?)\]/';
2057 // catching the child field name for confirm field, name field's child
2058 // preg_match_all($catchChildFldNamePattern, $fldName, $matches);
2059 $fldName = preg_replace($catchChildFldNamePattern, '', $fldName);
2060 $fldName = str_replace(['.', ' '], '_', $fldName);
2061 if (!empty($fldName)) {
2062 // The second field silently saves nothing; nothing else reports it.
2063 if (isset($consumedNames[$fldName]) && !isset($fieldData['parentFieldKey'])) {
2064 Log::debug_log([
2065 'status' => 'error',
2066 'code' => 'duplicate_field_name',
2067 'message' => "Field {$fieldKey} shares the name attribute \"{$fldName}\" with {$consumedNames[$fldName]}; its submitted value is dropped.",
2068 'form_id' => $this->form_id,
2069 ]);
2070 }
2071 $consumedNames[$fldName] = $fieldKey;
2072 // Multiline Text answers must keep their line breaks; sanitize_text_field() would collapse them to spaces.
2073 $multiline = isset($fieldData['type']) && 'textarea' === $fieldData['type'];
2074 if (array_key_exists($fldName, $_POST)) {
2075 $temp = $this->sanitize_text_recursive($_POST[$fldName], true, $multiline);
2076 unset($_POST[$fldName]);
2077 $_POST[$fieldKey] = $temp;
2078 } elseif (array_key_exists($fieldKey, $_POST)) {
2079 $_POST[$fieldKey] = $this->sanitize_text_recursive($_POST[$fieldKey], true, $multiline);
2080 } elseif (array_key_exists($fldName, $_FILES)) {
2081 $temp = $this->sanitize_text_recursive($_FILES[$fldName], false);
2082 unset($_FILES[$fldName]);
2083 $_FILES[$fieldKey] = $temp;
2084 } elseif (array_key_exists($fieldKey, $_FILES)) {
2085 $_FILES[$fieldKey] = $this->sanitize_text_recursive($_FILES[$fieldKey], false);
2086 }
2087 // Convert _session_id suffix (used by email-otp and similar fields)
2088 if (array_key_exists($fldName . '_session_id', $_POST)) {
2089 $temp = sanitize_text_field(wp_unslash($_POST[$fldName . '_session_id']));
2090 unset($_POST[$fldName . '_session_id']);
2091 $_POST[$fieldKey . '_session_id'] = $temp;
2092 }
2093 }
2094 }
2095 }
2096 }
2097
2098 /**
2099 * The message the field's "Invalid Error Message" setting shows, so the server check says what the browser check says.
2100 *
2101 * @param object $field field config from form_content
2102 * @return string plain text, or '' when the field has no invalid message
2103 */
2104 private function getInvalidErrorMessage($field)
2105 {
2106 if (!isset($field->err->invalid) || !is_object($field->err->invalid)) {
2107 return '';
2108 }
2109 $invalid = $field->err->invalid;
2110 $message = (!empty($invalid->custom) && !empty($invalid->msg)) ? $invalid->msg : ($invalid->dflt ?? '');
2111 return is_string($message) ? trim(wp_strip_all_tags($message)) : '';
2112 }
2113
2114 /**
2115 * @param mixed $input
2116 * @param bool $unslash
2117 * @param bool $multiline keep line breaks (Multiline Text); tags are still stripped
2118 * @return mixed
2119 */
2120 private function sanitize_text_recursive($input, $unslash = true, $multiline = false)
2121 {
2122 if (is_array($input)) {
2123 return array_map(fn ($item) => $this->sanitize_text_recursive($item, $unslash, $multiline), $input);
2124 }
2125
2126 $value = $unslash ? wp_unslash($input) : $input;
2127 return $multiline ? sanitize_textarea_field($value) : sanitize_text_field($value);
2128 }
2129
2130 private function normalizeRepeatedCompositeFieldInput($value)
2131 {
2132 if (!is_array($value) || empty($value)) {
2133 return $value;
2134 }
2135
2136 $hasNestedArray = false;
2137 foreach ($value as $childValues) {
2138 if (!is_array($childValues)) {
2139 return $value;
2140 }
2141 $hasNestedArray = true;
2142 }
2143
2144 if (!$hasNestedArray) {
2145 return $value;
2146 }
2147
2148 $formattedValue = [];
2149 foreach ($value as $childKey => $childValues) {
2150 foreach ($childValues as $repeatIndex => $repeatValue) {
2151 if (!isset($formattedValue[$repeatIndex]) || !is_array($formattedValue[$repeatIndex])) {
2152 $formattedValue[$repeatIndex] = [];
2153 }
2154 $formattedValue[$repeatIndex][$childKey] = $repeatValue;
2155 }
2156 }
2157
2158 return $formattedValue;
2159 }
2160
2161 public function setSubmissionCount($countStep = 1)
2162 {
2163 $update_status = $this->formModel->update(
2164 [
2165 'entries' => intval($this->form[0]->entries) + $countStep,
2166 ],
2167 [
2168 'id' => $this->form_id,
2169 ]
2170 );
2171 }
2172
2173 public function resetSubmissionCount($countStep)
2174 {
2175 $update_status = $this->formModel->update(
2176 [
2177 'entries' => intval($countStep),
2178 ],
2179 [
2180 'id' => $this->form_id,
2181 ]
2182 );
2183 }
2184
2185 public function getCaptchaSettings()
2186 {
2187 $formContents = $this->getFormContent();
2188 $fieldStr = wp_json_encode($formContents->fields);
2189 if (false !== strpos($fieldStr, '"typ":"recaptcha"')) {
2190 return true;
2191 }
2192 }
2193
2194 public function getTurnstileSettings()
2195 {
2196 $formContents = $this->getFormContent();
2197 $fieldStr = wp_json_encode($formContents->fields);
2198 if (false !== strpos($fieldStr, '"typ":"turnstile"')) {
2199 return true;
2200 }
2201 }
2202
2203 public function isFieldTypeExist($fieldType)
2204 {
2205 $formContents = $this->getFormContent();
2206 $fieldStr = wp_json_encode($formContents->fields);
2207 if (false !== strpos($fieldStr, '"typ":"' . $fieldType . '"')) {
2208 return true;
2209 }
2210 }
2211
2212 public function getCaptchaV3Settings()
2213 {
2214 $formContents = $this->getFormContent();
2215 if (!empty($formContents->additional->enabled) && !empty($formContents->additional->enabled->recaptchav3)) {
2216 return $formContents->additional->settings->recaptchav3;
2217 }
2218 return false;
2219 }
2220
2221 // public function getSuccessMessageMarkups() {
2222 // if (is_null($this->_work_flows)) {
2223 // $workFlowManager = new WorkFlowHandler($this->form_id);
2224 // $this->_work_flows = $workFlowManager->getAllworkFlow();
2225 // }
2226
2227 // $ids = [];
2228 // foreach ($this->_work_flows as $msgItem) {
2229 // foreach ($msgItem['conditions'] as $condition) {
2230 // if (isset($condition->actions->success)) {
2231 // foreach ($condition->actions->success as $msg) {
2232 // if ('successMsg' === $msg->type && isset($msg->details->id)) {
2233 // $msgDetailsId = $msg->details->id;
2234 // $idObj = json_decode(stripslashes($msgDetailsId));
2235 // if (is_object($idObj) && !empty($idObj->id)) {
2236 // array_push($ids, $idObj->id);
2237 // }
2238 // }
2239 // }
2240 // }
2241 // if (isset($condition->actions->failure)) {
2242 // $idObj = json_decode(stripslashes($condition->actions->failure));
2243 // if (is_object($idObj) && !empty($idObj->id)) {
2244 // array_push($ids, $idObj->id);
2245 // }
2246 // }
2247 // }
2248 // }
2249 // $ids = array_unique($ids);
2250 // if (is_null($this->_conf_messages)) {
2251 // $successMsgHandler = new SuccessMessageHandler($this->form_id);
2252 // $this->_conf_messages = $successMsgHandler->getMessages($ids);
2253 // }
2254
2255 // $messageMarkups = '';
2256 // if (is_wp_error($this->_conf_messages)) {
2257 // return $messageMarkups;
2258 // }
2259
2260 // foreach ($this->_conf_messages as $key => $msgItem) {
2261 // $messageMarkups .= $this->messageMarkup($msgItem->id);
2262 // }
2263
2264 // return $messageMarkups;
2265 // }
2266
2267 // private function messageMarkup($msgId) {
2268 // return <<<SUCCESSMSG
2269 // <div role="dialog" aria-hidden="true" data-modal-backdrop="true" class="{$this->getAtomicCls("msg-container-{$msgId}")} deactive2 test">
2270 // <div role="button" class="{$this->getAtomicCls("msg-background-{$msgId}")} msg-backdrop">
2271 // <div class="bf-notification-message {$this->getAtomicCls("msg-content-{$msgId}")}">
2272 // <button class="{$this->getAtomicCls("close-{$msgId}")} bf-msg-close" type="button">
2273 // <svg class="{$this->getAtomicCls("close-icn-{$msgId}")}" viewBox="0 0 30 30">
2274 // <line fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x1="4" y1="3.88" x2="26" y2="26.12"></line>
2275 // <line fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x1="26" y1="3.88" x2="4" y2="26.12"></line>
2276 // </svg>
2277 // </button>
2278 // <div class="msg-content"></div>
2279 // </div>
2280 // </div>
2281 // </div>
2282 // SUCCESSMSG;
2283 // }
2284
2285 public function getAtomicCls($element)
2286 {
2287 $atomicClassMap = $this->getAtomicClsMap();
2288 if (is_object($atomicClassMap) && property_exists($atomicClassMap, ".$element")) {
2289 $getAtomicCls = $atomicClassMap->{".$element"};
2290 return implode(' ', $getAtomicCls) . " $element";
2291 }
2292 return $element;
2293 }
2294
2295 public function isGCLIDEnabled()
2296 {
2297 $formContents = $this->getFormContent();
2298 if (isset($formContents->additional->enabled->captureGCLID) && $formContents->additional->enabled->captureGCLID) {
2299 return true;
2300 }
2301 return false;
2302 }
2303
2304 protected function addEntryInfo($field_details, $counter)
2305 {
2306 $infos = [
2307 '__user_id' => __('User', 'bit-form'),
2308 '__entry_status' => __('Status', 'bit-form'),
2309 //'__user_location' => __(''),
2310 '__referer' => __('Refer URL', 'bit-form'),
2311 '__user_device' => __('Device', 'bit-form'),
2312 '__user_ip' => __('IP address', 'bit-form'),
2313 '__created_at' => __('Created Time', 'bit-form'),
2314 '__updated_at' => __('Modified Time', 'bit-form'),
2315 ];
2316 foreach ($infos as $key => $value) {
2317 $field_details[$counter]['name'] = $value;
2318 $field_details[$counter]['key'] = $key;
2319 $field_details[$counter]['type'] = 'sys';
2320 $counter = $counter + 1;
2321 }
2322
2323 return $field_details;
2324 }
2325 }
2326