PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / V-3.3.0
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder vV-3.3.0
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 2.10.1 All 138 releases
bit-form / includes / Core / Util / FieldValueHandler.php

FieldValueHandler.php in Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder V-3.3.0, at includes/Core/Util/FieldValueHandler.php

1,244 lines 42.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace BitCode\BitForm\Core\Util;
4
5 use BitCode\BitForm\Admin\Form\Helpers;
6 use BitCode\BitForm\Core\Form\FormManager;
7
8 final class FieldValueHandler
9 {
10 public static function replaceFieldWithValue($stringToReplaceField, $fieldValues, $formID = null, $stripShortcodesFromValues = false)
11 {
12 if (empty($stringToReplaceField)) {
13 return $stringToReplaceField;
14 }
15 if (!is_string($stringToReplaceField)) {
16 $stringToReplaceField = wp_json_encode($stringToReplaceField);
17 }
18 $fieldValues = $formID ? self::sortValueBasedOnLayout($formID, $fieldValues) : $fieldValues;
19
20 if ($formID) {
21 $stringToReplaceField = self::replaceValueOfBf_all_data($stringToReplaceField, $fieldValues, $formID);
22 $stringToReplaceField = self::replaceRepeaterFieldValue($stringToReplaceField, $fieldValues, $formID);
23 }
24
25 $stringToReplaceField = self::replaceSmartTagWithValue($stringToReplaceField);
26
27 $fieldPattern = '/\${\w[^ ${}]*}/';
28
29 preg_match_all($fieldPattern, $stringToReplaceField, $matchedField);
30 if (empty($matchedField)) {
31 return $stringToReplaceField;
32 }
33 $uniqueFieldsInStr = array_unique($matchedField[0]);
34 foreach ($uniqueFieldsInStr as $key => $value) {
35 $fieldName = substr($value, 2, strlen($value) - 3);
36 $fieldValue = null;
37 if (isset($fieldValues[$fieldName])) {
38 $targetFieldValue = isset($fieldValues[$fieldName]['value']) ? $fieldValues[$fieldName]['value'] : $fieldValues[$fieldName];
39 if ('array' === gettype($targetFieldValue) || 'object' === gettype($targetFieldValue)) {
40 foreach (self::stripMetaSubfields((array) $targetFieldValue) as $singleTargetVal) {
41 if (isset($fieldValue)) {
42 if (is_numeric($fieldValue) && is_numeric($singleTargetVal)) {
43 $fieldValue = $fieldValue + $singleTargetVal;
44 } else {
45 $fieldValue = "$fieldValue, $singleTargetVal";
46 }
47 } else {
48 $fieldValue = $singleTargetVal;
49 }
50 }
51 // $fieldValue = wp_json_encode($targetFieldValue);
52 } else {
53 $fieldValue = strval($targetFieldValue);
54 }
55 // Neutralize shortcodes in low-trust submitted values before they are merged into a
56 if ($stripShortcodesFromValues && is_string($fieldValue)) {
57 $fieldValue = strip_shortcodes($fieldValue);
58 }
59 $stringToReplaceField = str_replace($value, $fieldValue, $stringToReplaceField);
60 } else {
61 $stringToReplaceField = str_replace($value, '', $stringToReplaceField);
62 }
63 }
64
65 // check if the string is a function like : "${_bf_calc(${b27-5}*10)}"
66 // TO DO: Implement the function properly
67 // if (self::isFunction($stringToReplaceField)) {
68 // $functionName = self::getFunctionName($stringToReplaceField);
69
70 // switch ($functionName) {
71 // case '_bf_calc':
72 // return self::getFunctionParameter($stringToReplaceField);
73 // case '_bf_count':
74 // return self::getCountValue($stringToReplaceField);
75 // default:
76 // return 0;
77 // }
78 // }
79 return $stringToReplaceField;
80 }
81
82 public static function replaceBackBtnWithPrevPageUrl($stringToReplaceField)
83 {
84 $prevPageUrl = isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : home_url();
85 preg_match_all('/\$?\{back_to_view\}/', $stringToReplaceField, $matches);
86 $matched = $matches[0];
87
88 if (empty($matched) || !$prevPageUrl) {
89 return $stringToReplaceField;
90 }
91
92 foreach ($matched as $m) {
93 $stringToReplaceField = str_replace($m, $prevPageUrl, $stringToReplaceField);
94 }
95 return $stringToReplaceField;
96 }
97
98 /**
99 * Summary of getCountValue - get the count value from the function string "${_bf_count(item-1, item-2)}" => 2
100 *
101 * @param string $functionString
102 * @return int
103 */
104 private static function getCountValue(string $functionString): int
105 {
106 $options = self::getFunctionParameter($functionString);
107 $option = explode(',', $options);
108 return count($option);
109 }
110
111 /**
112 * Summary of getFunctionParameter - get the function parameter from the function string "${_bf_calc(2*10)}" => 2*10
113 *
114 * @param string $functionString
115 * @return string
116 */
117 private static function getFunctionParameter(string $functionString): string
118 {
119 $regexPattern = '/\(([^)]*)\)/';
120 preg_match($regexPattern, $functionString, $matches);
121 return $matches[1];
122 }
123
124 /**
125 * Summary of isFunction - check if the string is a function "${_bf_calc(${b27-5}*10)}" or not "${_bf_date}"
126 *
127 * @param string $functionString
128 * @return bool true if the string is a function else false
129 */
130 private static function isFunction(string $functionString): bool
131 {
132 $regexPattern = '/\([^)]*\)/';
133 return preg_match($regexPattern, $functionString);
134 }
135
136 /**
137 * Summary of getFunctionName - get the function name from the function string "${_bf_calc(${b27-5}*10)}" => _bf_calc
138 *
139 * @param string $functionString
140 * @return string
141 */
142 private static function getFunctionName(string $functionString): string
143 {
144 $regexPattern = '/\b([a-zA-Z_][a-zA-Z0-9_]*)\(/';
145 preg_match($regexPattern, $functionString, $matches);
146 return $matches[1];
147 }
148
149 public static function validateMailArry($emailAddresses, $fieldValues)
150 {
151 if (!is_array($emailAddresses)) {
152 return [FieldValueHandler::replaceFieldWithValue($emailAddresses, $fieldValues)];
153 }
154 foreach ($emailAddresses as $key => $email) {
155 if (!is_email($email)) {
156 $email = FieldValueHandler::replaceFieldWithValue($email, $fieldValues);
157 if (is_email($email)) {
158 $emailAddresses[$key] = $email;
159 }
160 }
161 }
162 return $emailAddresses;
163 }
164
165 public static function replaceSmartTagWithValue($contentWithSmartTag)
166 {
167 $fieldPattern = '/(\${_[^{]*?)(?=\})}/';
168 $matchPattern = preg_match_all($fieldPattern, $contentWithSmartTag, $matchedField);
169 if (!$matchPattern) {
170 return $contentWithSmartTag;
171 }
172
173 $ajaxRequest = false;
174 // Read-only action name check for routing; CSRF verified upstream in the form submission flow via verifySubmissionNonce().
175 if (isset($_REQUEST['action']) && 'bitforms_trigger_workflow' === sanitize_text_field(wp_unslash($_REQUEST['action']))) {
176 $ajaxRequest = true;
177 }
178
179 foreach (array_unique($matchedField[0]) as $value) {
180 $fieldName = trim(substr($value, 2, strlen($value) - 3));
181
182 $matches = preg_match('/\("*([^\)]+"*)\)/', $value, $matchCustomFormat);
183
184 $customValue = '';
185 if ($matches) {
186 $removeQuote = ["'", '"'];
187 $customValue = str_replace($removeQuote, '', $matchCustomFormat[1]);
188 $fieldName = str_replace($matchCustomFormat[0], '', $fieldName);
189 }
190
191 $tagFieldValues = SmartTags::getSmartTagValue($fieldName, $ajaxRequest, $customValue);
192
193 $contentWithSmartTag = str_replace($value, $tagFieldValues, $contentWithSmartTag);
194 }
195 return $contentWithSmartTag;
196 }
197
198 public static function isEmpty($val)
199 {
200 if (empty($val) && !in_array($val, ['0', 0, 0.0], true)) {
201 return true;
202 }
203 return false;
204 }
205
206 /**
207 * Values a field kept on an entry edit, posted as `<fieldKey>_old` instead of resubmitted.
208 *
209 * @param mixed $postData submitted data, keyed by field key
210 * @param string $fieldKey
211 *
212 * @return array retained values, empty when the field kept nothing
213 */
214 public static function retainedOldValues($postData, $fieldKey)
215 {
216 if (!is_array($postData) || !isset($postData[$fieldKey . '_old'])) {
217 return [];
218 }
219 return self::flattenOldValues($postData[$fieldKey . '_old']);
220 }
221
222 private static function flattenOldValues($value)
223 {
224 if (is_object($value)) {
225 $value = (array) $value;
226 }
227 if (!is_array($value)) {
228 if (!is_string($value) && !is_numeric($value)) {
229 return [];
230 }
231 $value = trim((string) $value);
232 if ('' === $value) {
233 return [];
234 }
235 // A repeater posts one JSON list per row, so a list can arrive nested.
236 $decoded = json_decode($value, true);
237 if (!is_array($decoded)) {
238 $retained = [];
239 foreach (explode(',', $value) as $item) {
240 $item = trim($item);
241 if ('' !== $item) {
242 $retained[] = $item;
243 }
244 }
245 return $retained;
246 }
247 $value = $decoded;
248 }
249
250 $retained = [];
251 foreach ($value as $item) {
252 $retained = array_merge($retained, self::flattenOldValues($item));
253 }
254 return $retained;
255 }
256
257 public static function formatFieldValueForMail($fields, $fieldValues = [])
258 {
259 $formattedFldValues = $fieldValues;
260 $file_upload_types = Helpers::$file_upload_types;
261 $repeated_array_type_data_fields = Helpers::$repeated_array_type_data_fields;
262 foreach ($fields as $fldKey => $fldData) {
263 if (in_array($fldData->typ, $file_upload_types)) {
264 continue;
265 }
266 if (is_array($fieldValues) && array_key_exists($fldKey, $fieldValues)) {
267 $value = $fieldValues[$fldKey];
268 // if (is_array($value)) {
269 // $formattedFldValues[$fldKey] = htmlspecialchars(implode(', ', $value));
270 // } else {
271 // $formattedFldValues[$fldKey] = htmlspecialchars($value);
272 // }
273
274 // TODO: this code are temporary commented, need to change and remove the comment
275
276 // if (is_array($value)) {
277 // $arrValue = '';
278 // foreach ($value as $v) {
279 // if (is_array($v)) {
280 // foreach ($v as $k1 => $v1) {
281 // if (array_key_exists($k1, $repeaterFieldKey)) {
282 // $oldValue = $repeaterFieldKey[$k1];
283 // if (is_array($v1) && in_array($fields->{$k1}->typ, $repeated_array_type_data_fields)) {
284 // $newValues = '[' . implode(', ', $v1) . '] ';
285 // if (!preg_match('/\[.*\]/', $oldValue)) {
286 // $oldValue = '[' . $oldValue . '] ';
287 // }
288 // } else {
289 // $newValues = $v1;
290 // }
291 // $repeaterFieldKey[$k1] = $oldValue . ', ' . $newValues;
292 // } else {
293 // if (!empty($v1) && is_array($v1)) {
294 // $repeaterFieldKey[$k1] = htmlspecialchars(implode(', ', $v1));
295 // } else {
296 // $repeaterFieldKey[$k1] = htmlspecialchars($v1);
297 // }
298 // }
299 // }
300 // } else {
301 // $arrValue .= $v . ', ';
302 // }
303 // }
304 // $formattedFldValues[$fldKey] = htmlspecialchars(rtrim($arrValue, ', '));
305 // $arrValue = '';
306 // } else {
307 // $formattedFldValues[$fldKey] = htmlspecialchars($value);
308 // }
309 if ('textarea' === $fldData->typ) {
310 $formattedFldValues[$fldKey] = nl2br(htmlspecialchars($value));
311 }
312 if ('date' === $fldData->typ && !empty($value)) {
313 $formattedFldValues[$fldKey] = date_i18n(get_option('date_format'), strtotime(htmlspecialchars($value)));
314 }
315 }
316 }
317
318 $merge_values = array_merge($fieldValues, $formattedFldValues);
319 // $merge_values = array_merge($merge_values, $repeaterFieldKey);
320
321 return $merge_values;
322 }
323
324 public static function changeHrefPathInHTMLString($html_body, $path)
325 {
326 if (empty($html_body) || empty($path)) {
327 return $html_body;
328 }
329
330 return preg_replace_callback(
331 '/<a\s+[^>]*href=[\'"]([^\'"]+)[\'"][^>]*>/i',
332 function ($matches) use ($path) {
333 $href = $matches[1];
334
335 if (filter_var($href, FILTER_VALIDATE_URL)) {
336 return $matches[0];
337 }
338
339 if (preg_match('/^(mailto:|tel:|javascript:|#)/i', $href)) {
340 return $matches[0];
341 }
342 if (preg_match('/\$?\{back_to_view\}/', $matches[0])) {
343 return $matches[0];
344 }
345
346 $fullPath = rtrim($path, '/') . '/' . ltrim($href, '/');
347
348 return str_replace(
349 $href,
350 htmlspecialchars($fullPath, ENT_QUOTES),
351 $matches[0]
352 );
353 },
354 $html_body
355 );
356 }
357
358 public static function changeImagePathInHTMLString($html_body, $path)
359 {
360 if (empty($html_body) || empty($path)) {
361 return $html_body;
362 }
363
364 $allowedMimeTypes = [
365 'jpg' => ['image/jpeg', 'image/pjpeg'],
366 'jpeg' => ['image/jpeg', 'image/pjpeg'],
367 'png' => ['image/png'],
368 'svg' => ['image/svg+xml']
369 ];
370
371 return preg_replace_callback(
372 '/<img\s+[^>]*src=[\'"]([^\'"]*)[\'"][^>]*>/i',
373 function ($matches) use ($path, $allowedMimeTypes) {
374 $src = $matches[1];
375
376 // Already-embedded inline images (cid:) must be left untouched.
377 if (0 === stripos($src, 'cid:')) {
378 return $matches[0];
379 }
380
381 if (filter_var($src, FILTER_VALIDATE_URL)) {
382 return $matches[0];
383 }
384
385 if (!trim($src)) {
386 return '';
387 }
388
389 $fullPath = rtrim($path, '/') . '/' . ltrim($src, '/');
390
391 $extension = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));
392
393 if (!preg_match('/^(http|https):\/\//', $fullPath)) {
394 if (!file_exists($fullPath) || !isset($allowedMimeTypes[$extension])) {
395 Log::debug_log([
396 'status' => 'error',
397 'code' => 'file_not_found',
398 'message' => "File not found or unsupported file type: $fullPath",
399 ]);
400 return '';
401 } else {
402 Log::debug_log([
403 'status' => 'success',
404 'code' => 'file_found',
405 'message' => "File Found => location: {$fullPath}"
406 ]);
407 }
408 $mimeType = mime_content_type($fullPath);
409 if (!in_array($mimeType, $allowedMimeTypes[$extension], true)) {
410 Log::debug_log([
411 'status' => 'error',
412 'code' => 'unsupported_file_type',
413 'message' => "Unsupported file type: $mimeType for file: $fullPath",
414 ]);
415 return '';
416 }
417 }
418
419 return str_replace($src, htmlspecialchars($fullPath, ENT_QUOTES), $matches[0]);
420 },
421 $html_body
422 );
423 }
424
425 public static function sortValueBasedOnLayout($formId, $fieldValues)
426 {
427 $formManager = FormManager::getInstance($formId);
428 $formLayout = $formManager->getFlatenFormLayout(); // returns all layouts (lg, md, sm)
429 // A form saved without a layout (or a minimal/legacy form_content) has no ->lg
430 $lgLayout = isset($formLayout->lg) ? (array) $formLayout->lg : [];
431 $fieldKeyOrderbasedOnLayout = array_filter(array_map(function ($fld) {
432 return isset($fld->i) ? $fld->i : null;
433 }, $lgLayout), function ($key) {
434 return !is_null($key);
435 });
436 $ordered = [];
437
438 foreach ($fieldKeyOrderbasedOnLayout as $key) {
439 if (array_key_exists($key, $fieldValues)) {
440 $ordered[$key] = $fieldValues[$key];
441 }
442 }
443
444 foreach ($fieldValues as $k=>$v) {
445 if (!array_key_exists($k, $fieldKeyOrderbasedOnLayout)) {
446 $ordered[$k] = $fieldValues[$k];
447 }
448 }
449 return $ordered;
450 }
451
452 public static function replaceValueOfBf_all_data($stringToReplaceField, $fieldValues, $formId)
453 {
454 // $pattern = '/\$\{bf_all_data\}/'; // Corrected escaping
455 $pattern = '/\$\{bf_all_data(?:\.onlyValues)?\}/'; // Corrected escaping
456
457 preg_match_all($pattern, $stringToReplaceField, $matches);
458 $matchesArray = $matches[0] ?? [];
459 if (count($matchesArray) > 0) {
460 $formManager = FormManager::getInstance($formId);
461 $formFields = $formManager->getFields();
462 $orderedFormFields = $formManager->getFieldsBasedOnLayout(); // ordered form fields based on layout(lg) order
463 foreach ($matchesArray as $match) {
464 switch ($match) {
465 case '${bf_all_data}':
466 $fieldValues = self::bindFormData($orderedFormFields, $fieldValues, $formId);
467 $table = self::generateTable($fieldValues, $orderedFormFields, $formId);
468 $stringToReplaceField = str_replace('${bf_all_data}', $table, $stringToReplaceField);
469 break;
470
471 case '${bf_all_data.onlyValues}':
472 $fieldValues = self::bindFormData($orderedFormFields, $fieldValues, $formId, true);
473 $table = self::generateTable($fieldValues, $orderedFormFields, $formId);
474 $stringToReplaceField = str_replace('${bf_all_data.onlyValues}', $table, $stringToReplaceField);
475 break;
476 default:
477 Log::debug_log([
478 'status' => 'error',
479 'code' => 'unknown_placeholder',
480 'message' => "Unknown placeholder: $match",
481 ]);
482 break;
483 }
484 }
485 }
486 return $stringToReplaceField;
487 }
488
489 /**
490 * Ensures an <img> tag with a style attribute exists in the input.
491 *
492 * Behavior:
493 * - If the input is just an image filename (e.g., "1.png"), returns a complete <img> tag with the default style.
494 * - If the input is HTML with <img> tags:
495 * - If any <img> has a style, returns the HTML as-is.
496 * - If <img> exists without style, adds the default style to the first one found.
497 * - If no <img> tag or image file is found, returns the input unchanged.
498 *
499 * @param string $input Image filename or HTML string.
500 * @param string $defaultStyle Optional. The CSS style to apply if missing. Default: 'max-width: 100%; height: auto;'.
501 *
502 * @return string Modified HTML string with styled <img> tag if needed.
503 */
504 private static function ensureImgWithStyle($input, $defaultStyle = 'max-width: 100%; height: auto;')
505 {
506 $imgTagWithStylePattern = '/<img\b[^>]*\bstyle\s*=\s*["\'][^"\']*["\'][^>]*>/i';
507 $imgTagPattern = '/<img\b[^>]*>/i';
508 $filePattern = '/\.(jpg|jpeg|png|gif|webp)$/i';
509
510 if (preg_match($filePattern, trim($input)) && !preg_match('/<img\b/i', $input)) {
511 return '<img src="' . htmlspecialchars(trim($input)) . '" style="' . $defaultStyle . '" />';
512 }
513
514 if (preg_match($imgTagWithStylePattern, $input)) {
515 // <img> already has style, return as is
516 return $input;
517 } elseif (preg_match($imgTagPattern, $input, $match)) {
518 // <img> without style, add style
519 $updatedImg = preg_replace('/<img\b(.*?)(\/?)>/i', '<img$1 style="' . $defaultStyle . '" $2>', $match[0]);
520 return str_replace($match[0], $updatedImg, $input);
521 } else {
522 // No <img> tag found, return input
523 return $input;
524 }
525 }
526
527 private static function orderRepeaterData($repeaterFldKey, $repeaterData, $formId)
528 {
529 if (!$formId) {
530 return $repeaterData;
531 }
532
533 $formManager = FormManager::getInstance($formId);
534 $nestedLayout = $formManager->getFormNestedLayout();
535 $repeaterLayout = $nestedLayout->{$repeaterFldKey}->lg;
536 $orderedFldKey = array_map(function ($fld) {
537 return $fld->i;
538 }, $repeaterLayout);
539
540 $orderedRepeaterData = [];
541 foreach ($repeaterData as $rptr) {
542 $orderedFlds = [];
543 foreach ($orderedFldKey as $k) {
544 if (array_key_exists($k, $rptr)) {
545 $orderedFlds[$k] = $rptr[$k];
546 }
547 }
548
549 foreach ($rptr as $ky => $v) {
550 if (!array_key_exists($ky, $orderedFldKey)) {
551 $orderedFlds[$ky] = $v;
552 }
553 }
554
555 $orderedRepeaterData[] = $orderedFlds;
556 }
557 return $orderedRepeaterData;
558 }
559
560 private static function bindFormData($formFields, $formData, $formId, $isOnlyValues = false)
561 {
562 $entryID = isset($formData['entry_id']) ? $formData['entry_id'] : null;
563 return array_reduce(array_keys($formFields), function ($filteredData, $key) use ($formFields, $formData, $isOnlyValues, $formId) {
564 $field = $formFields[$key];
565
566 $fieldNewData = $filteredData;
567
568 $ignoreFields = ['button', 'recaptcha', 'html', 'divider', 'spacer', 'section', 'turnstile', 'hcaptcha', 'image'];
569
570 $arrayValueFldType = ['check', 'select', 'image-select'];
571
572 if (in_array($field['type'], $ignoreFields)) {
573 return $fieldNewData;
574 }
575
576 // Skip processing for hidden or empty fields only when $isOnlyValues is true
577 if ($isOnlyValues) {
578 // Check if the value is strictly an empty string or null, but allow 0
579 if (!isset($formData[$key]) || '' === $formData[$key] || null === $formData[$key]) {
580 return $fieldNewData;
581 }
582
583 if (isset($field['valid']['hide']) && $field['valid']['hide']) {
584 return $fieldNewData;
585 }
586 }
587
588 if (isset($formData[$key]) && !array_key_exists('parentFieldKey', $field)) {
589 if ('repeater' === $field['type']) {
590 $repeater_data = is_string($formData[$key]) ? json_decode($formData[$key], true) : $formData[$key];
591 // ordering repeater field according to nested repeater layout
592 $repeater_data = self::orderRepeaterData($key, $repeater_data, $formId);
593 if ($isOnlyValues) {
594 $repeater_data = array_filter($repeater_data, function ($sub) {
595 return array_filter($sub, fn ($value) => '' !== $value);
596 });
597 }
598 $fieldNewData[$key] = $repeater_data;
599 } elseif ('signature' === $field['type']) {
600 if ('signature-failed.png' !== $formData[$key]) {
601 $file_path = strpos($formData[$key], '/') ? $formData[$key] : $formData[$key];
602 $newPath = $file_path;
603 $fieldNewData[$key] = self::ensureImgWithStyle($newPath, 'max-width: 100%; height: auto;');
604 }
605 } elseif (in_array($field['type'], $arrayValueFldType)) {
606 $v = is_string($formData[$key]) ? json_decode($formData[$key], true) : $formData[$key];
607 $fieldNewData[$key] = $v && is_array($v) ? implode(', ', $v) : $formData[$key];
608 } else {
609 $fieldNewData[$key] = $formData[$key];
610 }
611 }
612
613 return $fieldNewData;
614 }, []);
615 }
616
617 private static function generateTable($fieldValues, $formFields, $formId = null)
618 {
619 if (empty($fieldValues)) {
620 Log::debug_log([
621 'status' => 'error',
622 'code' => 'no_fields_found',
623 'type' => 'bf_all_data',
624 'message' => 'No fields found for bf_all_data',
625 'fields' => $fieldValues,
626 'formFields' => $formFields,
627 ]);
628 return '<p>No data available.</p>';
629 }
630
631 $table = "<table style='font-family: arial, sans-serif; border-collapse: collapse; width: 100%;'>";
632
633 foreach ($fieldValues as $fk => $value) {
634 $value = self::decodeIfJson($value);
635 $fieldName = self::getLabel($formFields, $fk) ?? $fk;
636 $fieldType = $formFields[$fk]['type'];
637 $table .= "<tr>
638 <td style='border: 1px solid #dddddd; text-align: left; padding: 8px; font-weight: bold;'>{$fieldName}</td>
639 <td style='border: 1px solid #dddddd; text-align: left; padding: 8px;'>";
640
641 if (is_array($value)) {
642 if ('repeater' === $fieldType) {
643 $table .= "<table style='width: 100%; border-collapse: collapse;'>";
644
645 $subKeys = self::repeaterColumnKeys($value, $fk, $formId);
646
647 $table .= '<tr>';
648 foreach ($subKeys as $subKey) {
649 $subLabel = self::getLabel($formFields, $subKey) ?? $subKey;
650 $table .= "<th style='border: 1px solid #dddddd; padding: 8px; background-color: #f2f2f2;'>" . $subLabel . '</th>';
651 }
652 $table .= '</tr>';
653
654 foreach ($value as $row) {
655 if (!is_array($row)) {
656 continue;
657 }
658 $table .= '<tr>';
659 // Walk the shared column list so a row missing a conditionally hidden
660 // sub-field still lines up with the header.
661 foreach ($subKeys as $subKey) {
662 $subValue = array_key_exists($subKey, $row) ? $row[$subKey] : '';
663 $subFieldType = self::getFldType($subKey, $formFields);
664 if (is_array($subValue)) {
665 if (self::isCompositeFieldType($subFieldType)) {
666 $subValue = self::joinCompositeFieldValue($subValue, $subFieldType);
667 } else {
668 $subValue = self::unorderedAnchorListMarkup($subValue);
669 }
670
671 // $subValue = implode(', ', array_map(function ($v) {
672 // if (self::isFileTypeValue($v)) {
673 // return self::anchorMarkup($v);
674 // // if (self::isImageTypeValue($v)) {
675 // // return "<img src='{$v}' alt='{$v}' width='250'/>";
676 // // } else {
677 // // return "<a href='{$v}' rel='noopener noreferrer' target='_blank' style='color:blue'>{$v}</a>";
678 // // }
679 // } else {
680 // return $v;
681 // }
682 // }, $subValue));
683 } else {
684 if (self::isFileTypeValue($subValue)) {
685 if ('signature' === $subFieldType) {
686 if ('signature-failed.png' === $subValue) {
687 $subValue = '';
688 } else {
689 $subValue = "<img src='{$subValue}' alt='{$subValue}' width='250'/>";
690 }
691 }
692 } else {
693 $subValue = $subValue;
694 }
695 }
696
697 $table .= "<td style='border: 1px solid #dddddd; padding: 8px;'>" . $subValue . '</td>';
698 }
699 $table .= '</tr>';
700 }
701 $table .= '</table>';
702 } elseif ('file-up' === $fieldType || 'advanced-file-up' === $fieldType) {
703 if (is_array($value)) {
704 $table .= self::unorderedAnchorListMarkup($value);
705 }
706 } elseif (self::isCompositeFieldType($fieldType)) {
707 $table .= self::joinCompositeFieldValue($value, $fieldType);
708 } elseif ('signature' === $fieldType) {
709 if ('signature-failed.png' === $subValue) {
710 $table .= '';
711 } else {
712 $table .= self::imgMarkup($value);
713 }
714 }
715 } else {
716 $table .= $value;
717 }
718
719 $table .= '</td></tr>';
720 }
721
722 $table .= '</table>';
723
724 return $table;
725 }
726
727 private static function unorderedAnchorListMarkup($list)
728 {
729 $ul = "<ul style='list-style-type: none; padding: 0; margin:0'>";
730 foreach ($list as $v) {
731 $ul .= '<li >' . self::anchorMarkup($v) . '</li>';
732 }
733 $ul .= '</ul>';
734 return $ul;
735 }
736
737 private static function imgMarkup($filename)
738 {
739 if (!is_scalar($filename)) {
740 return '';
741 }
742 $filename = (string) $filename;
743
744 return "<img src='" . self::escFileHref($filename) . "' alt='" . esc_attr($filename) . "' width='250'/>";
745 }
746
747 private static function anchorMarkup($filename)
748 {
749 if (!is_scalar($filename)) {
750 return '';
751 }
752 $filename = (string) $filename;
753
754 return "<a href='" . self::escFileHref($filename) . "' rel='noopener noreferrer' target='_blank' style='color:blue'>" . esc_html($filename) . '</a>';
755 }
756
757 /** Escape a file reference for an href/src. Not esc_url(): it rewrites a bare file name to `http://<name>`. */
758 private static function escFileHref($value)
759 {
760 return esc_attr(wp_kses_bad_protocol($value, wp_allowed_protocols()));
761 }
762
763 public static function replaceRepeaterFieldValue($stringToReplaceField, $fieldValues, $formID)
764 {
765 if (!is_string($stringToReplaceField) || empty($stringToReplaceField)) {
766 return $stringToReplaceField; // Return as-is if nothing to replace
767 }
768
769 $formManager = FormManager::getInstance($formID);
770 $formFields = $formManager->getFieldsBasedOnLayout(); // ordered form fields based on layout(lg) order
771
772 // Find all placeholders like ${field_key} example: ${b27-5}
773 preg_match_all('/\$\{(b\d+-\d+)\}/', $stringToReplaceField, $matches);
774
775 if (empty($matches[1])) {
776 return $stringToReplaceField;
777 }
778 // Clean field data
779 // $dataCleaning = self::removeEmptyValues($fieldValues);
780 $flatFieldData = self::restructureRepeaterData($fieldValues, $formManager);
781 // generate table for repeater fields
782 foreach ($matches[1] as $fk) {
783 $repeaterFieldKey = $fk;
784 $fieldType = isset($formFields[$repeaterFieldKey]['type']) && !empty($formFields[$repeaterFieldKey]['type']) ? $formFields[$repeaterFieldKey]['type'] : null;
785 if ('repeater' === $fieldType) {
786 $repeaterMarkup = self::repeaterFieldTable($fieldValues[$repeaterFieldKey] ?? [], $formFields, $repeaterFieldKey, $formID);
787 $stringToReplaceField = str_replace('${' . $fk . '}', $repeaterMarkup, $stringToReplaceField);
788 } else {
789 if ('signature' === $fieldType) {
790 $stringToReplaceField = self::replaceImgTagForRepeatedSignature($stringToReplaceField, $flatFieldData[$repeaterFieldKey], $repeaterFieldKey);
791 }
792 $repeaterFieldData = self::safeFlatString(
793 $flatFieldData[$repeaterFieldKey] ?? '',
794 $fieldType,
795 $repeaterFieldKey,
796 $flatFieldData,
797 $formFields
798 );
799
800 $stringToReplaceField = str_replace('${' . $fk . '}', $repeaterFieldData, $stringToReplaceField);
801 }
802 }
803 return $stringToReplaceField;
804 }
805
806 private static function replaceImgTagForRepeatedSignature($stringToReplaceField, $repeaterValue, $fldKey)
807 {
808 $data = self::decodeIfJson($repeaterValue);
809 if (!is_string($stringToReplaceField) || empty($stringToReplaceField) || empty($fldKey)) {
810 return $stringToReplaceField;
811 }
812
813 $pattern = '/<img\s+[^>]*src=[\'"]([^\'"]*' . preg_quote($fldKey, '/') . '[^\'"]*)[\'"][^>]*>/i';
814
815 if (!preg_match($pattern, $stringToReplaceField)) {
816 return $stringToReplaceField;
817 }
818
819 $values = [];
820 $appendValue = function ($value) use (&$values) {
821 if (is_array($value)) {
822 foreach ($value as $item) {
823 if (is_string($item) && '' !== trim($item) && 'signature-failed.png' !== $item) {
824 $values[] = $item;
825 }
826 }
827 return;
828 }
829
830 if (is_string($value) && '' !== trim($value) && 'signature-failed.png' !== $value) {
831 $values[] = $value;
832 }
833 };
834
835 $appendValue($data);
836
837 if (empty($values)) {
838 return preg_replace($pattern, '', $stringToReplaceField);
839 }
840
841 return preg_replace_callback($pattern, function ($matches) use ($values) {
842 $imgTags = array_map(function ($value) use ($matches) {
843 $src = htmlspecialchars($value, ENT_QUOTES);
844 $alt = htmlspecialchars($value, ENT_QUOTES);
845
846 $tag = $matches[0];
847 $tag = preg_replace('/\bsrc\s*=\s*([\'"])(.*?)\1/i', 'src="' . $src . '"', $tag);
848
849 if (preg_match('/\balt\s*=\s*([\'"])(.*?)\1/i', $tag)) {
850 $tag = preg_replace('/\balt\s*=\s*([\'"])(.*?)\1/i', 'alt="' . $alt . '"', $tag);
851 } else {
852 $tag = preg_replace('/<img\b/i', '<img alt="' . $alt . '"', $tag, 1);
853 }
854
855 return $tag;
856 }, $values);
857
858 return implode('', $imgTags);
859 }, $stringToReplaceField);
860 }
861
862 /**
863 * Restructures repeater field data to maintain original structure while
864 * aggregating nested repeater values into top-level indexed arrays.
865 *
866 * @param array $data Original field data structure
867 * @return array Restructured data with aggregated arrays
868 */
869 public static function restructureRepeaterData(array $data, $formManagerInstance): array
870 {
871 $result = $data;
872 // topkey === field Key topValue === field value
873 foreach ($data as $topKey => $topValue) {
874 if ($formManagerInstance->isRepeaterField($topKey)) {
875 $topValue = self::decodeIfJson($topValue);
876 //assigning the converted value to repeater field
877 $result[$topKey] = $topValue;
878 // topvalue here is repeater field value;
879 // entryIndex repeater field key , entry == repeater field value
880 foreach ($topValue as $entryIndex => $entry) {
881 if (!is_array($entry)) {
882 continue;
883 }
884 foreach ($entry as $subKey => $subValue) {
885 if (!isset($result[$subKey]) || !is_array($result[$subKey])) {
886 $result[$subKey] = [];
887 }
888 // Handle nested arrays within entries
889 $result[$subKey][$entryIndex] = $subValue;
890 }
891 }
892 }
893 }
894 return $result;
895 }
896
897 /**
898 * Return decoded data if incoming data is stringified and if it's a plain string (e.g "John Doe") it returns the plain string
899 *
900 * @param mixed $data
901 * @return mixed
902 */
903 private static function decodeIfJson($data)
904 {
905 if (!is_string($data)) {
906 return $data;
907 }
908
909 $decoded = json_decode($data, true);
910
911 return (JSON_ERROR_NONE === json_last_error()) ? $decoded : $data;
912 }
913
914 /**
915 * Safely converts any type of form value(Specially Repeater Field Value) to string.
916 *
917 * @param mixed $data
918 * @param string $fldType
919 * @param string|null $fieldKey
920 * @param array|null $allFieldData
921 * @param array|null $formFields
922 * @return string
923 */
924 public static function safeFlatString($data, $fldType, $fieldKey = null, $allFieldData = null, $formFields = null): string
925 {
926 $newData = self::decodeIfJson($data);
927
928 if (is_array($newData)) {
929 if (self::isCompositeFieldType($fldType)) {
930 return self::joinCompositeFieldValue($newData, $fldType);
931 }
932 return implode(', ', array_map(function ($item) use ($fldType, $fieldKey, $allFieldData, $formFields) {
933 return is_array($item)
934 ? '[' . implode(', ', array_map(function ($itm) use ($fldType) {
935 if (in_array($fldType, ['advanced-file-up', 'file-up']) || self::isFileTypeValue($itm)) {
936 return self::anchorMarkup($itm);
937 // if (self::isImageTypeValue($itm)) {
938 // return "<img src='{$itm}' alt='{$itm}' width='250'/>";
939 // } else {
940 // return "<a href='{$itm}' rel='noopener noreferrer' target='_blank' style='color:blue'>{$itm}</a>";
941 // }
942 } else {
943 return $itm;
944 }
945 }, $item)) . ']'
946 : self::safeFlatString($item, $fldType, $fieldKey, $allFieldData, $formFields);
947 }, $newData));
948 }
949
950 if (is_object($data)) {
951 return method_exists($data, '__toString') ? (string) $data : (json_encode($data) ?: '');
952 }
953
954 if (is_null($data)) {
955 return '';
956 }
957
958 if (self::isFileTypeValue($data)) {
959 if ('signature-failed.png' === $data) {
960 return '';
961 }
962 }
963
964 if (in_array($fldType, ['file-up', 'advanced-file-up'])) {
965 return self::anchorMarkup($newData);
966 }
967 if ('signature' === $fldType) {
968 return self::imgMarkup($newData);
969 }
970
971 return (string) $data;
972 }
973
974 private static function removeEmptyValues($fieldData)
975 {
976 if (!is_array($fieldData)) {
977 return $fieldData;
978 }
979 // Remove empty values from the array
980 return array_filter($fieldData, function ($value) {
981 // Check if the value is 0
982 if (0 === $value) {
983 return '0';
984 }
985 return !empty($value);
986 });
987 }
988
989 /**
990 * Gets the field type by field key
991 *
992 * @param string $fldKey
993 * @param mixed $formField
994 * @return string
995 */
996 private static function getFldType($fldKey, $formFields)
997 {
998 if (array_key_exists($fldKey, $formFields)) {
999 return $formFields[$fldKey]['type'];
1000 }
1001 }
1002
1003 private static function isCompositeFieldType($fieldType)
1004 {
1005 return in_array($fieldType, ['name', 'address'], true);
1006 }
1007
1008 /**
1009 * Removes internal/meta subfields (keys prefixed with "_", e.g. the address
1010 * field's _latitude / _longitude) so they never leak into human-readable
1011 * output (entry views, emails, SmartTags, PDF, exports).
1012 *
1013 * @param mixed $value
1014 * @return mixed
1015 */
1016 private static function stripMetaSubfields($value)
1017 {
1018 if (!is_array($value)) {
1019 return $value;
1020 }
1021 foreach (array_keys($value) as $key) {
1022 if (is_string($key) && '' !== $key && '_' === $key[0]) {
1023 unset($value[$key]);
1024 }
1025 }
1026 return $value;
1027 }
1028
1029 private static function joinCompositeFieldValue($value, $fieldType)
1030 {
1031 if (!is_array($value)) {
1032 return (string) $value;
1033 }
1034
1035 $value = self::stripMetaSubfields($value);
1036
1037 $parts = [];
1038 array_walk_recursive($value, function ($item) use (&$parts) {
1039 if (is_null($item)) {
1040 return;
1041 }
1042
1043 $item = (string) $item;
1044 if ('' === trim($item) && '0' !== $item) {
1045 return;
1046 }
1047
1048 $parts[] = $item;
1049 });
1050
1051 return implode('address' === $fieldType ? ', ' : ' ', $parts);
1052 }
1053
1054 /**
1055 * Return true is it's file type value by checking with extension
1056 *
1057 * @param string $filename
1058 * @return boolean
1059 */
1060 private static function isFileTypeValue($fileName)
1061 {
1062 if (!is_string($fileName)) {
1063 return false;
1064 }
1065 $ext = pathinfo($fileName, PATHINFO_EXTENSION);
1066
1067 if ('other' !== FileHandler::getFileTypeByExtension($ext)) {
1068 return true;
1069 }
1070 }
1071
1072 /**
1073 * Return true is it's image type value by checking with extension
1074 *
1075 * @param string $filename
1076 * @return boolean
1077 */
1078 private static function isImageTypeValue($fileName)
1079 {
1080 if (!is_string($fileName)) {
1081 return false;
1082 }
1083 $ext = pathinfo($fileName, PATHINFO_EXTENSION);
1084
1085 if ('image' === FileHandler::getFileTypeByExtension($ext)) {
1086 return true;
1087 }
1088 }
1089
1090 /**
1091 * Collect the column keys of a repeater table as the union of every row's keys,
1092 * not just the first row's. Conditional logic can hide a sub-field in one row and
1093 * show it in the next; keying off row 0 alone drops that column's header and
1094 * shifts every later row's cells. Ordering follows the repeater's own nested
1095 * layout when the form id is known, with any leftover keys appended.
1096 *
1097 * @param array $rows
1098 * @param string $repeaterFieldKey
1099 * @param int|string|null $formId
1100 * @return array
1101 */
1102 private static function repeaterColumnKeys($rows, $repeaterFieldKey, $formId = null)
1103 {
1104 $present = [];
1105 foreach ($rows as $row) {
1106 if (!is_array($row)) {
1107 continue;
1108 }
1109 foreach (array_keys($row) as $subKey) {
1110 $present[$subKey] = true;
1111 }
1112 }
1113
1114 if (empty($present)) {
1115 return [];
1116 }
1117
1118 $ordered = [];
1119 if ($formId) {
1120 $nestedLayout = FormManager::getInstance($formId)->getFormNestedLayout();
1121 $repeaterLayout = isset($nestedLayout->{$repeaterFieldKey}->lg) ? $nestedLayout->{$repeaterFieldKey}->lg : [];
1122 foreach ((array) $repeaterLayout as $fld) {
1123 $subKey = isset($fld->i) ? $fld->i : null;
1124 if ($subKey && isset($present[$subKey])) {
1125 $ordered[] = $subKey;
1126 unset($present[$subKey]);
1127 }
1128 }
1129 }
1130
1131 return array_merge($ordered, array_keys($present));
1132 }
1133
1134 private static function repeaterFieldTable($repeaterFieldData, $formFields, $repeaterFieldKey, $formId = null)
1135 {
1136 $repeaterFieldData = self::decodeIfJson($repeaterFieldData);
1137
1138 if (!is_array($repeaterFieldData) || !isset($repeaterFieldData[0]) || !is_array($repeaterFieldData[0])) {
1139 return ''; // Safely return empty if not a valid repeater structure
1140 }
1141 $table = "<table style='font-family: arial, sans-serif; border-collapse: collapse; width: 100%;'>";
1142 // $table .= '<tr>';
1143 // $table .= '<th style="border: 1px solid #dddddd; text-align: left; padding: 8px;">' . self::getLabel($formFields, $repeaterFieldKey) . '</th>';
1144 // $table .= '</tr>';
1145 // $table .= '<td style="border: 1px solid #dddddd; text-align: left; padding: 8px;">';
1146 // $table .= '<table style="width: 100%; border-collapse: collapse;">';
1147
1148 $headers = self::repeaterColumnKeys($repeaterFieldData, $repeaterFieldKey, $formId);
1149 $table .= '<tr>'; // open tr (for column header)
1150 foreach ($headers as $fk) {
1151 $table .= '<th style="border: 1px solid #dddddd; padding: 8px; ">' . self::getLabel($formFields, $fk) . '</th>';
1152 }
1153 $table .= '</tr>'; // close tr (for column header)
1154
1155 foreach ($repeaterFieldData as $row) {
1156 if (!is_array($row)) {
1157 continue;
1158 }
1159 $table .= '<tr>'; // open tr (for table data row)
1160 // Walk the column list, not the row's own keys, so a sub-field hidden by
1161 // conditional logic in this row renders an empty cell instead of shifting
1162 // every following cell one column to the left.
1163 foreach ($headers as $k) {
1164 $value = array_key_exists($k, $row) ? $row[$k] : '';
1165 $fldTyp = self::getFldType($k, $formFields);
1166 if (is_array($value)) {
1167 if (in_array($fldTyp, ['advanced-file-up', 'file-up'])) {
1168 $newValue = self::unorderedAnchorListMarkup($value);
1169 } elseif (self::isCompositeFieldType($fldTyp)) {
1170 $newValue = self::joinCompositeFieldValue($value, $fldTyp);
1171 } else {
1172 $newValue = implode(', ', $value);
1173 }
1174 } else {
1175 if (self::isFileTypeValue($value)) {
1176 $newValue = 'signature-failed.png' === $value
1177 ? ''
1178 : (self::isImageTypeValue($value)
1179 ? "<img src='{$value}' alt='{$value}' width='250'/>"
1180 : "<a href='{$value}' rel='noopener noreferrer' target='_blank' style='color:blue'>{$value}</a>");
1181 } else {
1182 $newValue = $value;
1183 }
1184 }
1185
1186 $table .= '<td style="border: 1px solid #dddddd; padding: 8px;">' . $newValue . '</td>';
1187 }
1188 $table .= '</tr>'; // close tr (for table data row)
1189 }
1190 // $table .= '</table>';
1191
1192 // $table .= '</td>';
1193 $table .= '</table>';
1194
1195 return $table;
1196 }
1197
1198 private static function getLabel($formFields, $key)
1199 {
1200 return $formFields[$key]['label'] ?? $key;
1201 }
1202
1203 /**
1204 * Derive a composite child field's key name from its bracketed HTML name.
1205 * e.g. childFieldName "name[first_name]" with parent "name" => "first_name".
1206 * Falls back to the bracket contents when the parent name is empty.
1207 */
1208 public static function deriveChildName($childFieldName, $parentFieldName)
1209 {
1210 $childFieldName = (string) $childFieldName;
1211 if (preg_match('/\[(.*?)\]/', $childFieldName, $matches)) {
1212 return $matches[1];
1213 }
1214
1215 return str_replace(['[', ']', (string) $parentFieldName], '', $childFieldName);
1216 }
1217
1218 /**
1219 * Pull a child value out of a parent composite field's nested submitted value.
1220 * Looks up by the derived child name first, then by the child field key.
1221 * Returns null when no match is found.
1222 */
1223 public static function extractChildValueFromParentValue($parentValue, $childName, $childKey)
1224 {
1225 if (is_object($parentValue)) {
1226 $parentValue = (array) $parentValue;
1227 }
1228
1229 if (!is_array($parentValue)) {
1230 return null;
1231 }
1232
1233 if (array_key_exists($childName, $parentValue)) {
1234 return $parentValue[$childName];
1235 }
1236
1237 if (array_key_exists($childKey, $parentValue)) {
1238 return $parentValue[$childKey];
1239 }
1240
1241 return null;
1242 }
1243 }
1244