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

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

871 lines 30.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)
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 ((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 $stringToReplaceField = str_replace($value, $fieldValue, $stringToReplaceField);
56 } else {
57 $stringToReplaceField = str_replace($value, '', $stringToReplaceField);
58 }
59 }
60
61 // check if the string is a function like : "${_bf_calc(${b27-5}*10)}"
62 // TO DO: Implement the function properly
63 // if (self::isFunction($stringToReplaceField)) {
64 // $functionName = self::getFunctionName($stringToReplaceField);
65
66 // switch ($functionName) {
67 // case '_bf_calc':
68 // return self::getFunctionParameter($stringToReplaceField);
69 // case '_bf_count':
70 // return self::getCountValue($stringToReplaceField);
71 // default:
72 // return 0;
73 // }
74 // }
75 return $stringToReplaceField;
76 }
77
78 /**
79 * Summary of getCountValue - get the count value from the function string "${_bf_count(item-1, item-2)}" => 2
80 *
81 * @param string $functionString
82 * @return int
83 */
84 private static function getCountValue(string $functionString): int
85 {
86 $options = self::getFunctionParameter($functionString);
87 $option = explode(',', $options);
88 return count($option);
89 }
90
91 /**
92 * Summary of getFunctionParameter - get the function parameter from the function string "${_bf_calc(2*10)}" => 2*10
93 *
94 * @param string $functionString
95 * @return string
96 */
97 private static function getFunctionParameter(string $functionString): string
98 {
99 $regexPattern = '/\(([^)]*)\)/';
100 preg_match($regexPattern, $functionString, $matches);
101 return $matches[1];
102 }
103
104 /**
105 * Summary of isFunction - check if the string is a function "${_bf_calc(${b27-5}*10)}" or not "${_bf_date}"
106 *
107 * @param string $functionString
108 * @return bool true if the string is a function else false
109 */
110 private static function isFunction(string $functionString): bool
111 {
112 $regexPattern = '/\([^)]*\)/';
113 return preg_match($regexPattern, $functionString);
114 }
115
116 /**
117 * Summary of getFunctionName - get the function name from the function string "${_bf_calc(${b27-5}*10)}" => _bf_calc
118 *
119 * @param string $functionString
120 * @return string
121 */
122 private static function getFunctionName(string $functionString): string
123 {
124 $regexPattern = '/\b([a-zA-Z_][a-zA-Z0-9_]*)\(/';
125 preg_match($regexPattern, $functionString, $matches);
126 return $matches[1];
127 }
128
129 public static function validateMailArry($emailAddresses, $fieldValues)
130 {
131 if (!is_array($emailAddresses)) {
132 return [FieldValueHandler::replaceFieldWithValue($emailAddresses, $fieldValues)];
133 }
134 foreach ($emailAddresses as $key => $email) {
135 if (!is_email($email)) {
136 $email = FieldValueHandler::replaceFieldWithValue($email, $fieldValues);
137 if (is_email($email)) {
138 $emailAddresses[$key] = $email;
139 }
140 }
141 }
142 return $emailAddresses;
143 }
144
145 public static function replaceSmartTagWithValue($contentWithSmartTag)
146 {
147 $fieldPattern = '/(\${_[^{]*?)(?=\})}/';
148 $matchPattern = preg_match_all($fieldPattern, $contentWithSmartTag, $matchedField);
149 if (!$matchPattern) {
150 return $contentWithSmartTag;
151 }
152
153 $ajaxRequest = false;
154 if (isset($_REQUEST['action']) && 'bitforms_trigger_workflow' === $_REQUEST['action']) {
155 $ajaxRequest = true;
156 }
157
158 foreach (array_unique($matchedField[0]) as $value) {
159 $fieldName = trim(substr($value, 2, strlen($value) - 3));
160
161 $matches = preg_match('/\("*([^\)]+"*)\)/', $value, $matchCustomFormat);
162
163 $customValue = '';
164 if ($matches) {
165 $removeQuote = ["'", '"'];
166 $customValue = str_replace($removeQuote, '', $matchCustomFormat[1]);
167 $fieldName = str_replace($matchCustomFormat[0], '', $fieldName);
168 }
169
170 $tagFieldValues = SmartTags::getSmartTagValue($fieldName, $ajaxRequest, $customValue);
171
172 $contentWithSmartTag = str_replace($value, $tagFieldValues, $contentWithSmartTag);
173 }
174 return $contentWithSmartTag;
175 }
176
177 public static function isEmpty($val)
178 {
179 if (empty($val) && !in_array($val, ['0', 0, 0.0], true)) {
180 return true;
181 }
182 return false;
183 }
184
185 public static function formatFieldValueForMail($fields, $fieldValues = [])
186 {
187 $formattedFldValues = $fieldValues;
188 $file_upload_types = Helpers::$file_upload_types;
189 $repeated_array_type_data_fields = Helpers::$repeated_array_type_data_fields;
190 foreach ($fields as $fldKey => $fldData) {
191 if (in_array($fldData->typ, $file_upload_types)) {
192 continue;
193 }
194 if (array_key_exists($fldKey, $fieldValues)) {
195 $value = $fieldValues[$fldKey];
196 // if (is_array($value)) {
197 // $formattedFldValues[$fldKey] = htmlspecialchars(implode(', ', $value));
198 // } else {
199 // $formattedFldValues[$fldKey] = htmlspecialchars($value);
200 // }
201
202 // TODO: this code are temporary commented, need to change and remove the comment
203
204 // if (is_array($value)) {
205 // $arrValue = '';
206 // foreach ($value as $v) {
207 // if (is_array($v)) {
208 // foreach ($v as $k1 => $v1) {
209 // if (array_key_exists($k1, $repeaterFieldKey)) {
210 // $oldValue = $repeaterFieldKey[$k1];
211 // if (is_array($v1) && in_array($fields->{$k1}->typ, $repeated_array_type_data_fields)) {
212 // $newValues = '[' . implode(', ', $v1) . '] ';
213 // if (!preg_match('/\[.*\]/', $oldValue)) {
214 // $oldValue = '[' . $oldValue . '] ';
215 // }
216 // } else {
217 // $newValues = $v1;
218 // }
219 // $repeaterFieldKey[$k1] = $oldValue . ', ' . $newValues;
220 // } else {
221 // if (!empty($v1) && is_array($v1)) {
222 // $repeaterFieldKey[$k1] = htmlspecialchars(implode(', ', $v1));
223 // } else {
224 // $repeaterFieldKey[$k1] = htmlspecialchars($v1);
225 // }
226 // }
227 // }
228 // } else {
229 // $arrValue .= $v . ', ';
230 // }
231 // }
232 // $formattedFldValues[$fldKey] = htmlspecialchars(rtrim($arrValue, ', '));
233 // $arrValue = '';
234 // } else {
235 // $formattedFldValues[$fldKey] = htmlspecialchars($value);
236 // }
237 if ('textarea' === $fldData->typ) {
238 $formattedFldValues[$fldKey] = nl2br(htmlspecialchars($value));
239 }
240 if ('date' === $fldData->typ && !empty($value)) {
241 $formattedFldValues[$fldKey] = date_i18n(get_option('date_format'), strtotime(htmlspecialchars($value)));
242 }
243 }
244 }
245
246 $merge_values = array_merge($fieldValues, $formattedFldValues);
247 // $merge_values = array_merge($merge_values, $repeaterFieldKey);
248
249 return $merge_values;
250 }
251
252 public static function changeHrefPathInHTMLString($html_body, $path)
253 {
254 if (empty($html_body) || empty($path)) {
255 return $html_body;
256 }
257
258 return preg_replace_callback(
259 '/<a\s+[^>]*href=[\'"]([^\'"]+)[\'"][^>]*>/i',
260 function ($matches) use ($path) {
261 $href = $matches[1];
262
263 if (filter_var($href, FILTER_VALIDATE_URL)) {
264 return $matches[0];
265 }
266
267 if (preg_match('/^(mailto:|tel:|javascript:|#)/i', $href)) {
268 return $matches[0];
269 }
270
271 $fullPath = rtrim($path, '/') . '/' . ltrim($href, '/');
272
273 return str_replace(
274 $href,
275 htmlspecialchars($fullPath, ENT_QUOTES),
276 $matches[0]
277 );
278 },
279 $html_body
280 );
281 }
282
283 public static function changeImagePathInHTMLString($html_body, $path)
284 {
285 $allowedExtensions = ['jpg', 'jpeg', 'png', 'svg'];
286 if (empty($html_body) || empty($path)) {
287 return $html_body;
288 }
289
290 $allowedMimeTypes = [
291 'jpg' => ['image/jpeg', 'image/pjpeg'],
292 'jpeg' => ['image/jpeg', 'image/pjpeg'],
293 'png' => ['image/png'],
294 'svg' => ['image/svg+xml']
295 ];
296
297 return preg_replace_callback(
298 '/<img\s+[^>]*src=[\'"]([^\'"]*)[\'"][^>]*>/i',
299 function ($matches) use ($path, $allowedExtensions, $allowedMimeTypes) {
300 $src = $matches[1];
301
302 if (filter_var($src, FILTER_VALIDATE_URL)) {
303 return $matches[0];
304 }
305
306 if (!trim($src)) {
307 return '';
308 }
309
310 $fullPath = rtrim($path, '/') . '/' . ltrim($src, '/');
311
312 $extension = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));
313
314 if (!preg_match('/^(http|https):\/\//', $fullPath)) {
315 if (!file_exists($fullPath) || !isset($allowedMimeTypes[$extension])) {
316 Log::debug_log([
317 'status' => 'error',
318 'code' => 'file_not_found',
319 'message' => "File not found or unsupported file type: $fullPath",
320 ]);
321 return '';
322 } else {
323 Log::debug_log([
324 'status' => 'success',
325 'code' => 'file_found',
326 'message' => "File Found => location: {$fullPath}"
327 ]);
328 }
329 $mimeType = mime_content_type($fullPath);
330 if (!in_array($mimeType, $allowedMimeTypes[$extension], true)) {
331 Log::debug_log([
332 'status' => 'error',
333 'code' => 'unsupported_file_type',
334 'message' => "Unsupported file type: $mimeType for file: $fullPath",
335 ]);
336 return '';
337 }
338 }
339
340 return str_replace($src, htmlspecialchars($fullPath, ENT_QUOTES), $matches[0]);
341 },
342 $html_body
343 );
344 }
345
346 public static function sortValueBasedOnLayout($formId, $fieldValues)
347 {
348 $formManager = FormManager::getInstance($formId);
349 $layout = $formManager->getFormLayout();
350 $formLayout = $formManager->getFlatenFormLayout(); // returns all layouts (lg, md, sm)
351 $fieldKeyOrderbasedOnLayout = array_map(function ($fld) {
352 return $fld->i;
353 }, $formLayout->lg);
354 $ordered = [];
355
356 foreach ($fieldKeyOrderbasedOnLayout as $key) {
357 if (array_key_exists($key, $fieldValues)) {
358 $ordered[$key] = $fieldValues[$key];
359 }
360 }
361
362 foreach ($fieldValues as $k=>$v) {
363 if (!array_key_exists($k, $fieldKeyOrderbasedOnLayout)) {
364 $ordered[$k] = $fieldValues[$k];
365 }
366 }
367 return $ordered;
368 }
369
370 public static function replaceValueOfBf_all_data($stringToReplaceField, $fieldValues, $formId)
371 {
372 // $pattern = '/\$\{bf_all_data\}/'; // Corrected escaping
373 $pattern = '/\$\{bf_all_data(?:\.onlyValues)?\}/'; // Corrected escaping
374
375 preg_match_all($pattern, $stringToReplaceField, $matches);
376 $matchesArray = $matches[0] ?? [];
377 if (count($matchesArray) > 0) {
378 $formManager = FormManager::getInstance($formId);
379 $formFields = $formManager->getFields();
380 $orderedFormFields = $formManager->getFieldsBasedOnLayout(); // ordered form fields based on layout(lg) order
381 foreach ($matchesArray as $match) {
382 switch ($match) {
383 case '${bf_all_data}':
384 $fieldValues = self::bindFormData($orderedFormFields, $fieldValues, $formId);
385 $table = self::generateTable($fieldValues, $orderedFormFields);
386 $stringToReplaceField = str_replace('${bf_all_data}', $table, $stringToReplaceField);
387 break;
388
389 case '${bf_all_data.onlyValues}':
390 $fieldValues = self::bindFormData($orderedFormFields, $fieldValues, $formId, true);
391 $table = self::generateTable($fieldValues, $orderedFormFields);
392 $stringToReplaceField = str_replace('${bf_all_data.onlyValues}', $table, $stringToReplaceField);
393 break;
394 default:
395 Log::debug_log([
396 'status' => 'error',
397 'code' => 'unknown_placeholder',
398 'message' => "Unknown placeholder: $match",
399 ]);
400 break;
401 }
402 }
403 }
404 return $stringToReplaceField;
405 }
406
407 /**
408 * Ensures an <img> tag with a style attribute exists in the input.
409 *
410 * Behavior:
411 * - If the input is just an image filename (e.g., "1.png"), returns a complete <img> tag with the default style.
412 * - If the input is HTML with <img> tags:
413 * - If any <img> has a style, returns the HTML as-is.
414 * - If <img> exists without style, adds the default style to the first one found.
415 * - If no <img> tag or image file is found, returns the input unchanged.
416 *
417 * @param string $input Image filename or HTML string.
418 * @param string $defaultStyle Optional. The CSS style to apply if missing. Default: 'max-width: 100%; height: auto;'.
419 *
420 * @return string Modified HTML string with styled <img> tag if needed.
421 */
422 private static function ensureImgWithStyle($input, $defaultStyle = 'max-width: 100%; height: auto;')
423 {
424 $imgTagWithStylePattern = '/<img\b[^>]*\bstyle\s*=\s*["\'][^"\']*["\'][^>]*>/i';
425 $imgTagPattern = '/<img\b[^>]*>/i';
426 $filePattern = '/\.(jpg|jpeg|png|gif|webp)$/i';
427
428 if (preg_match($filePattern, trim($input)) && !preg_match('/<img\b/i', $input)) {
429 return '<img src="' . htmlspecialchars(trim($input)) . '" style="' . $defaultStyle . '" />';
430 }
431
432 if (preg_match($imgTagWithStylePattern, $input)) {
433 // <img> already has style, return as is
434 return $input;
435 } elseif (preg_match($imgTagPattern, $input, $match)) {
436 // <img> without style, add style
437 $updatedImg = preg_replace('/<img\b(.*?)(\/?)>/i', '<img$1 style="' . $defaultStyle . '" $2>', $match[0]);
438 return str_replace($match[0], $updatedImg, $input);
439 } else {
440 // No <img> tag found, return input
441 return $input;
442 }
443 }
444
445 private static function orderRepeaterData($repeaterFldKey, $repeaterData, $formId)
446 {
447 if (!$formId) {
448 return $repeaterData;
449 }
450
451 $formManager = FormManager::getInstance($formId);
452 $nestedLayout = $formManager->getFormNestedLayout();
453 $repeaterLayout = $nestedLayout->{$repeaterFldKey}->lg;
454 $orderedFldKey = array_map(function ($fld) {
455 return $fld->i;
456 }, $repeaterLayout);
457
458 $orderedRepeaterData = [];
459 foreach ($repeaterData as $rptr) {
460 $orderedFlds = [];
461 foreach ($orderedFldKey as $k) {
462 if (array_key_exists($k, $rptr)) {
463 $orderedFlds[$k] = $rptr[$k];
464 }
465 }
466
467 foreach ($rptr as $ky => $v) {
468 if (!array_key_exists($ky, $orderedFldKey)) {
469 $orderedFlds[$ky] = $v;
470 }
471 }
472
473 $orderedRepeaterData[] = $orderedFlds;
474 }
475 return $orderedRepeaterData;
476 }
477
478 private static function bindFormData($formFields, $formData, $formId, $isOnlyValues = false)
479 {
480 $entryID = isset($formData['entry_id']) ? $formData['entry_id'] : null;
481 return array_reduce(array_keys($formFields), function ($filteredData, $key) use ($formFields, $formData, $isOnlyValues, $formId) {
482 $field = $formFields[$key];
483
484 $fieldNewData = $filteredData;
485
486 $ignoreFields = ['button', 'recaptcha', 'html', 'divider', 'spacer', 'section', 'file-up', 'turnstile', 'hcaptcha', 'advanced-file-up', 'image'];
487
488 $arrayValueFldType = ['check', 'select', 'image-select'];
489
490 if (in_array($field['type'], $ignoreFields)) {
491 return $fieldNewData;
492 }
493
494 // Skip processing for hidden or empty fields only when $isOnlyValues is true
495 if ($isOnlyValues) {
496 // Check if the value is strictly an empty string or null, but allow 0
497 if (!isset($formData[$key]) || '' === $formData[$key] || null === $formData[$key]) {
498 return $fieldNewData;
499 }
500
501 if (isset($field['valid']['hide']) && $field['valid']['hide']) {
502 return $fieldNewData;
503 }
504 }
505
506 if (isset($formData[$key])) {
507 if ('repeater' === $field['type']) {
508 $repeater_data = is_string($formData[$key]) ? json_decode($formData[$key], true) : $formData[$key];
509 // ordering repeater field according to nested repeater layout
510 $repeater_data = self::orderRepeaterData($key, $repeater_data, $formId);
511 if ($isOnlyValues) {
512 $repeater_data = array_filter($repeater_data, function ($sub) {
513 return array_filter($sub, fn ($value) => '' !== $value);
514 });
515 }
516 $fieldNewData[$key] = $repeater_data;
517 } elseif ('signature' === $field['type']) {
518 if ('signature-failed.png' !== $formData[$key]) {
519 $file_path = strpos($formData[$key], '/') ? $formData[$key] : $formData[$key];
520 $newPath = $file_path;
521 $fieldNewData[$key] = self::ensureImgWithStyle($newPath, 'max-width: 100%; height: auto;');
522 }
523 } elseif (in_array($field['type'], $arrayValueFldType)) {
524 $v = is_string($formData[$key]) ? json_decode($formData[$key], true) : $formData[$key];
525 $fieldNewData[$key] = $v && is_array($v) ? implode(', ', $v) : $formData[$key];
526 } else {
527 $fieldNewData[$key] = $formData[$key];
528 }
529 }
530
531 return $fieldNewData;
532 }, []);
533 }
534
535 private static function generateTable($fieldValues, $formFields)
536 {
537 if (empty($fieldValues)) {
538 Log::debug_log([
539 'status' => 'error',
540 'code' => 'no_fields_found',
541 'type' => 'bf_all_data',
542 'message' => 'No fields found for bf_all_data',
543 'fields' => $fieldValues,
544 'formFields' => $formFields,
545 ]);
546 return '<p>No data available.</p>';
547 }
548
549 $table = "<table style='font-family: arial, sans-serif; border-collapse: collapse; width: 100%;'>";
550
551 foreach ($fieldValues as $fk => $value) {
552 $fieldName = $formFields[$fk]['label'] ?? $fk;
553 $table .= "<tr>
554 <td style='border: 1px solid #dddddd; text-align: left; padding: 8px; font-weight: bold;'>{$fieldName}</td>
555 <td style='border: 1px solid #dddddd; text-align: left; padding: 8px;'>";
556
557 if (is_array($value)) {
558 $table .= "<table style='width: 100%; border-collapse: collapse;'>";
559
560 $table .= '<tr>';
561 foreach (array_keys($value[0]) as $subKey) {
562 $subLabel = $formFields[$subKey]['label'] ?? $subKey;
563 $table .= "<th style='border: 1px solid #dddddd; padding: 8px; background-color: #f2f2f2;'>" . $subLabel . '</th>';
564 }
565 $table .= '</tr>';
566
567 foreach ($value as $row) {
568 $table .= '<tr>';
569 foreach ($row as $subKey => $subValue) {
570 if (is_array($subValue)) {
571 $subValue = implode(', ', array_map(function ($v) {
572 if (self::isFileTypeValue($v)) {
573 if (self::isImageTypeValue($v)) {
574 return "<img src='{$v}' alt='{$v}' width='250'/>";
575 } else {
576 return "<a href='{$v}' rel='noopener noreferrer' target='_blank' style='color:blue'>{$v}</a>";
577 }
578 } else {
579 return $v;
580 }
581 }, $subValue));
582 } else {
583 if (self::isFileTypeValue($subValue)) {
584 if ('signature-failed.png' === $subValue) {
585 $subValue = '';
586 } else {
587 if (self::isImageTypeValue($subValue)) {
588 $subValue = "<img src='{$subValue}' alt='{$subValue}' width='250'/>";
589 } else {
590 $subValue = "<a href='{$subValue}' rel='noopener noreferrer' target='_blank' style='color:blue'>{$subValue}</a>";
591 }
592 }
593 } else {
594 $subValue = $subValue;
595 }
596 }
597
598 $table .= "<td style='border: 1px solid #dddddd; padding: 8px;'>" . $subValue . '</td>';
599 }
600 $table .= '</tr>';
601 }
602 $table .= '</table>';
603 } else {
604 $table .= $value;
605 }
606
607 $table .= '</td></tr>';
608 }
609
610 $table .= '</table>';
611
612 return $table;
613 }
614
615 public static function replaceRepeaterFieldValue($stringToReplaceField, $fieldValues, $formID)
616 {
617 if (!is_string($stringToReplaceField) || empty($stringToReplaceField)) {
618 return $stringToReplaceField; // Return as-is if nothing to replace
619 }
620
621 $formManager = FormManager::getInstance($formID);
622 $formFields = $formManager->getFieldsBasedOnLayout(); // ordered form fields based on layout(lg) order
623
624 // Find all placeholders like ${field_key} example: ${b27-5}
625 preg_match_all('/\$\{(b\d+-\d+)\}/', $stringToReplaceField, $matches);
626
627 if (empty($matches[1])) {
628 return $stringToReplaceField;
629 }
630 // Clean field data
631 // $dataCleaning = self::removeEmptyValues($fieldValues);
632 $flatFieldData = self::restructureRepeaterData($fieldValues, $formManager);
633 // generate table for repeater fields
634 foreach ($matches[1] as $fk) {
635 $repeaterFieldKey = $fk;
636 $fieldType = isset($formFields[$repeaterFieldKey]['type']) && !empty($formFields[$repeaterFieldKey]['type']) ? $formFields[$repeaterFieldKey]['type'] : null;
637 if ('repeater' === $fieldType) {
638 $repeaterMarkup = self::repeaterFieldTable($fieldValues[$repeaterFieldKey] ?? [], $formFields, $repeaterFieldKey);
639 $stringToReplaceField = str_replace('${' . $fk . '}', $repeaterMarkup, $stringToReplaceField);
640 } else {
641 $repeaterFieldData = self::safeFlatString($flatFieldData[$repeaterFieldKey] ?? '', $fieldType);
642 $stringToReplaceField = str_replace('${' . $fk . '}', $repeaterFieldData, $stringToReplaceField);
643 }
644 }
645 return $stringToReplaceField;
646 }
647
648 /**
649 * Restructures repeater field data to maintain original structure while
650 * aggregating nested repeater values into top-level indexed arrays.
651 *
652 * @param array $data Original field data structure
653 * @return array Restructured data with aggregated arrays
654 */
655 public static function restructureRepeaterData(array $data, $formManagerInstance): array
656 {
657 $result = $data;
658 // topkey === field Key topValue === field value
659 foreach ($data as $topKey => $topValue) {
660 if ($formManagerInstance->isRepeaterField($topKey)) {
661 $topValue = self::decodeIfJson($topValue);
662 //assigning the converted value to repeater field
663 $result[$topKey] = $topValue;
664 // topvalue here is repeater field value;
665 // entryIndex repeater field key , entry == repeater field value
666 foreach ($topValue as $entryIndex => $entry) {
667 if (!is_array($entry)) {
668 continue;
669 }
670
671 foreach ($entry as $subKey => $subValue) {
672 // Handle nested arrays within entries
673 if (is_array($subValue)) {
674 $result[$subKey][$entryIndex] = $subValue;
675 } else {
676 $result[$subKey][$entryIndex] = $subValue;
677 }
678 }
679 }
680 }
681 }
682 return $result;
683 }
684
685 /**
686 * Return decoded data if incoming data is stringified and if it's a plain string (e.g "John Doe") it returns the plain string
687 *
688 * @param mixed $data
689 * @return mixed
690 */
691 private static function decodeIfJson($data): mixed
692 {
693 if (!is_string($data)) {
694 return $data;
695 }
696
697 $decoded = json_decode($data, true);
698
699 return (JSON_ERROR_NONE === json_last_error()) ? $decoded : $data;
700 }
701
702 /**
703 * Safely converts any type of form value(Specially Repeater Field Value) to string.
704 *
705 * @param mixed $data
706 * @param string $fldType
707 * @return string
708 */
709 public static function safeFlatString($data, $fldType): string
710 {
711 $newData = self::decodeIfJson($data);
712
713 if (is_array($newData)) {
714 return implode(', ', array_map(function ($item) use ($fldType) {
715 return is_array($item)
716 ? '[' . implode(', ', array_map(function ($itm) use ($fldType) {
717 if (in_array($fldType, ['advanced-file-up', 'file-up']) || self::isFileTypeValue($itm)) {
718 if (self::isImageTypeValue($itm)) {
719 return "<img src='{$itm}' alt='{$itm}' width='250'/>";
720 } else {
721 return "<a href='{$itm}' rel='noopener noreferrer' target='_blank' style='color:blue'>{$itm}</a>";
722 }
723 } else {
724 return $itm;
725 }
726 }, $item)) . ']'
727 : self::safeFlatString($item, $fldType);
728 }, $newData));
729 }
730
731 if (is_object($data)) {
732 return method_exists($data, '__toString') ? (string) $data : (json_encode($data) ?: '');
733 }
734
735 if (is_null($data)) {
736 return '';
737 }
738
739 if (self::isFileTypeValue($data)) {
740 if ('signature-failed.png' === $data) {
741 return '';
742 }
743 }
744
745 return (string) $data;
746 }
747
748 private static function removeEmptyValues($fieldData)
749 {
750 if (!is_array($fieldData)) {
751 return $fieldData;
752 }
753 // Remove empty values from the array
754 return array_filter($fieldData, function ($value) {
755 // Check if the value is 0
756 if (0 === $value) {
757 return '0';
758 }
759 return !empty($value);
760 });
761 }
762
763 /**
764 * Gets the field type by field key
765 *
766 * @param string $fldKey
767 * @param mixed $formField
768 * @return string
769 */
770 private static function getFldType($fldKey, $formFields)
771 {
772 if (array_key_exists($fldKey, $formFields)) {
773 return $formFields[$fldKey]['type'];
774 }
775 }
776
777 /**
778 * Return true is it's file type value by checking with extension
779 *
780 * @param string $filename
781 * @return boolean
782 */
783 private static function isFileTypeValue($fileName)
784 {
785 if (!is_string($fileName)) {
786 return false;
787 }
788 $ext = pathinfo($fileName, PATHINFO_EXTENSION);
789
790 if ('other' !== FileHandler::getFileTypeByExtension($ext)) {
791 return true;
792 }
793 }
794
795 /**
796 * Return true is it's image type value by checking with extension
797 *
798 * @param string $filename
799 * @return boolean
800 */
801 private static function isImageTypeValue($fileName)
802 {
803 if (!is_string($fileName)) {
804 return false;
805 }
806 $ext = pathinfo($fileName, PATHINFO_EXTENSION);
807
808 if ('image' === FileHandler::getFileTypeByExtension($ext)) {
809 return true;
810 }
811 }
812
813 private static function repeaterFieldTable($repeaterFieldData, $formFields, $repeaterFieldKey)
814 {
815 $repeaterFieldData = self::decodeIfJson($repeaterFieldData);
816
817 if (!is_array($repeaterFieldData) || !isset($repeaterFieldData[0]) || !is_array($repeaterFieldData[0])) {
818 return ''; // Safely return empty if not a valid repeater structure
819 }
820 $table = "<table style='font-family: arial, sans-serif; border-collapse: collapse; width: 100%;'>";
821 // $table .= '<tr>';
822 // $table .= '<th style="border: 1px solid #dddddd; text-align: left; padding: 8px;">' . self::getLabel($formFields, $repeaterFieldKey) . '</th>';
823 // $table .= '</tr>';
824 // $table .= '<td style="border: 1px solid #dddddd; text-align: left; padding: 8px;">';
825 // $table .= '<table style="width: 100%; border-collapse: collapse;">';
826
827 $headers = array_keys($repeaterFieldData[0]);
828 $table .= '<tr>'; // open tr (for column header)
829 foreach ($headers as $fk) {
830 $table .= '<th style="border: 1px solid #dddddd; padding: 8px; ">' . self::getLabel($formFields, $fk) . '</th>';
831 }
832 $table .= '</tr>'; // close tr (for column header)
833
834 foreach ($repeaterFieldData as $row) {
835 $table .= '<tr>'; // open tr (for table data row)
836 foreach ($row as $k=>$value) {
837 $fldTyp = self::getFldType($k, $formFields);
838 if (is_array($value)) {
839 $newValue = implode(', ', array_map(function ($v) use ($fldTyp) {
840 if (in_array($fldTyp, ['advance-file-up', 'file-up', 'signature']) || self::isFileTypeValue($v)) {
841 if (self::isImageTypeValue($v)) {
842 return "<img src='{$v}' alt='{$v}' width='250'/>";
843 } else {
844 return "<a href='{$v}' rel='noopener noreferrer' target='_blank' style='color:blue'>{$v}</a>";
845 }
846 } else {
847 return $v;
848 }
849 }, $value));
850 } else {
851 $newValue = self::isFileTypeValue($value) ? ('signature-failed.png' !== $value ? (self::isImageTypeValue($value) ? "<img src='{$value}' alt='{$value}' width='250'/>" : "<a href='{$value}' rel='noopener noreferrer' target='_blank' style='color:blue'>{$value}</a>") : '') : $value;
852 }
853
854 $table .= '<td style="border: 1px solid #dddddd; padding: 8px;">' . $newValue . '</td>';
855 }
856 $table .= '</tr>'; // close tr (for table data row)
857 }
858 // $table .= '</table>';
859
860 // $table .= '</td>';
861 $table .= '</table>';
862
863 return $table;
864 }
865
866 private static function getLabel($formFields, $key)
867 {
868 return $formFields[$key]['label'] ?? $key;
869 }
870 }
871