PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.2.2
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.2.2
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 3.2.2, at includes/Core/Util/FieldValueHandler.php

1,115 lines 37.9 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 public static function formatFieldValueForMail($fields, $fieldValues = [])
207 {
208 $formattedFldValues = $fieldValues;
209 $file_upload_types = Helpers::$file_upload_types;
210 $repeated_array_type_data_fields = Helpers::$repeated_array_type_data_fields;
211 foreach ($fields as $fldKey => $fldData) {
212 if (in_array($fldData->typ, $file_upload_types)) {
213 continue;
214 }
215 if (is_array($fieldValues) && array_key_exists($fldKey, $fieldValues)) {
216 $value = $fieldValues[$fldKey];
217 // if (is_array($value)) {
218 // $formattedFldValues[$fldKey] = htmlspecialchars(implode(', ', $value));
219 // } else {
220 // $formattedFldValues[$fldKey] = htmlspecialchars($value);
221 // }
222
223 // TODO: this code are temporary commented, need to change and remove the comment
224
225 // if (is_array($value)) {
226 // $arrValue = '';
227 // foreach ($value as $v) {
228 // if (is_array($v)) {
229 // foreach ($v as $k1 => $v1) {
230 // if (array_key_exists($k1, $repeaterFieldKey)) {
231 // $oldValue = $repeaterFieldKey[$k1];
232 // if (is_array($v1) && in_array($fields->{$k1}->typ, $repeated_array_type_data_fields)) {
233 // $newValues = '[' . implode(', ', $v1) . '] ';
234 // if (!preg_match('/\[.*\]/', $oldValue)) {
235 // $oldValue = '[' . $oldValue . '] ';
236 // }
237 // } else {
238 // $newValues = $v1;
239 // }
240 // $repeaterFieldKey[$k1] = $oldValue . ', ' . $newValues;
241 // } else {
242 // if (!empty($v1) && is_array($v1)) {
243 // $repeaterFieldKey[$k1] = htmlspecialchars(implode(', ', $v1));
244 // } else {
245 // $repeaterFieldKey[$k1] = htmlspecialchars($v1);
246 // }
247 // }
248 // }
249 // } else {
250 // $arrValue .= $v . ', ';
251 // }
252 // }
253 // $formattedFldValues[$fldKey] = htmlspecialchars(rtrim($arrValue, ', '));
254 // $arrValue = '';
255 // } else {
256 // $formattedFldValues[$fldKey] = htmlspecialchars($value);
257 // }
258 if ('textarea' === $fldData->typ) {
259 $formattedFldValues[$fldKey] = nl2br(htmlspecialchars($value));
260 }
261 if ('date' === $fldData->typ && !empty($value)) {
262 $formattedFldValues[$fldKey] = date_i18n(get_option('date_format'), strtotime(htmlspecialchars($value)));
263 }
264 }
265 }
266
267 $merge_values = array_merge($fieldValues, $formattedFldValues);
268 // $merge_values = array_merge($merge_values, $repeaterFieldKey);
269
270 return $merge_values;
271 }
272
273 public static function changeHrefPathInHTMLString($html_body, $path)
274 {
275 if (empty($html_body) || empty($path)) {
276 return $html_body;
277 }
278
279 return preg_replace_callback(
280 '/<a\s+[^>]*href=[\'"]([^\'"]+)[\'"][^>]*>/i',
281 function ($matches) use ($path) {
282 $href = $matches[1];
283
284 if (filter_var($href, FILTER_VALIDATE_URL)) {
285 return $matches[0];
286 }
287
288 if (preg_match('/^(mailto:|tel:|javascript:|#)/i', $href)) {
289 return $matches[0];
290 }
291 if (preg_match('/\$?\{back_to_view\}/', $matches[0])) {
292 return $matches[0];
293 }
294
295 $fullPath = rtrim($path, '/') . '/' . ltrim($href, '/');
296
297 return str_replace(
298 $href,
299 htmlspecialchars($fullPath, ENT_QUOTES),
300 $matches[0]
301 );
302 },
303 $html_body
304 );
305 }
306
307 public static function changeImagePathInHTMLString($html_body, $path)
308 {
309 if (empty($html_body) || empty($path)) {
310 return $html_body;
311 }
312
313 $allowedMimeTypes = [
314 'jpg' => ['image/jpeg', 'image/pjpeg'],
315 'jpeg' => ['image/jpeg', 'image/pjpeg'],
316 'png' => ['image/png'],
317 'svg' => ['image/svg+xml']
318 ];
319
320 return preg_replace_callback(
321 '/<img\s+[^>]*src=[\'"]([^\'"]*)[\'"][^>]*>/i',
322 function ($matches) use ($path, $allowedMimeTypes) {
323 $src = $matches[1];
324
325 // Already-embedded inline images (cid:) must be left untouched.
326 if (0 === stripos($src, 'cid:')) {
327 return $matches[0];
328 }
329
330 if (filter_var($src, FILTER_VALIDATE_URL)) {
331 return $matches[0];
332 }
333
334 if (!trim($src)) {
335 return '';
336 }
337
338 $fullPath = rtrim($path, '/') . '/' . ltrim($src, '/');
339
340 $extension = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));
341
342 if (!preg_match('/^(http|https):\/\//', $fullPath)) {
343 if (!file_exists($fullPath) || !isset($allowedMimeTypes[$extension])) {
344 Log::debug_log([
345 'status' => 'error',
346 'code' => 'file_not_found',
347 'message' => "File not found or unsupported file type: $fullPath",
348 ]);
349 return '';
350 } else {
351 Log::debug_log([
352 'status' => 'success',
353 'code' => 'file_found',
354 'message' => "File Found => location: {$fullPath}"
355 ]);
356 }
357 $mimeType = mime_content_type($fullPath);
358 if (!in_array($mimeType, $allowedMimeTypes[$extension], true)) {
359 Log::debug_log([
360 'status' => 'error',
361 'code' => 'unsupported_file_type',
362 'message' => "Unsupported file type: $mimeType for file: $fullPath",
363 ]);
364 return '';
365 }
366 }
367
368 return str_replace($src, htmlspecialchars($fullPath, ENT_QUOTES), $matches[0]);
369 },
370 $html_body
371 );
372 }
373
374 public static function sortValueBasedOnLayout($formId, $fieldValues)
375 {
376 $formManager = FormManager::getInstance($formId);
377 $layout = $formManager->getFormLayout();
378 $formLayout = $formManager->getFlatenFormLayout(); // returns all layouts (lg, md, sm)
379 $fieldKeyOrderbasedOnLayout = array_map(function ($fld) {
380 return $fld->i;
381 }, $formLayout->lg);
382 $ordered = [];
383
384 foreach ($fieldKeyOrderbasedOnLayout as $key) {
385 if (array_key_exists($key, $fieldValues)) {
386 $ordered[$key] = $fieldValues[$key];
387 }
388 }
389
390 foreach ($fieldValues as $k=>$v) {
391 if (!array_key_exists($k, $fieldKeyOrderbasedOnLayout)) {
392 $ordered[$k] = $fieldValues[$k];
393 }
394 }
395 return $ordered;
396 }
397
398 public static function replaceValueOfBf_all_data($stringToReplaceField, $fieldValues, $formId)
399 {
400 // $pattern = '/\$\{bf_all_data\}/'; // Corrected escaping
401 $pattern = '/\$\{bf_all_data(?:\.onlyValues)?\}/'; // Corrected escaping
402
403 preg_match_all($pattern, $stringToReplaceField, $matches);
404 $matchesArray = $matches[0] ?? [];
405 if (count($matchesArray) > 0) {
406 $formManager = FormManager::getInstance($formId);
407 $formFields = $formManager->getFields();
408 $orderedFormFields = $formManager->getFieldsBasedOnLayout(); // ordered form fields based on layout(lg) order
409 foreach ($matchesArray as $match) {
410 switch ($match) {
411 case '${bf_all_data}':
412 $fieldValues = self::bindFormData($orderedFormFields, $fieldValues, $formId);
413 $table = self::generateTable($fieldValues, $orderedFormFields);
414 $stringToReplaceField = str_replace('${bf_all_data}', $table, $stringToReplaceField);
415 break;
416
417 case '${bf_all_data.onlyValues}':
418 $fieldValues = self::bindFormData($orderedFormFields, $fieldValues, $formId, true);
419 $table = self::generateTable($fieldValues, $orderedFormFields);
420 $stringToReplaceField = str_replace('${bf_all_data.onlyValues}', $table, $stringToReplaceField);
421 break;
422 default:
423 Log::debug_log([
424 'status' => 'error',
425 'code' => 'unknown_placeholder',
426 'message' => "Unknown placeholder: $match",
427 ]);
428 break;
429 }
430 }
431 }
432 return $stringToReplaceField;
433 }
434
435 /**
436 * Ensures an <img> tag with a style attribute exists in the input.
437 *
438 * Behavior:
439 * - If the input is just an image filename (e.g., "1.png"), returns a complete <img> tag with the default style.
440 * - If the input is HTML with <img> tags:
441 * - If any <img> has a style, returns the HTML as-is.
442 * - If <img> exists without style, adds the default style to the first one found.
443 * - If no <img> tag or image file is found, returns the input unchanged.
444 *
445 * @param string $input Image filename or HTML string.
446 * @param string $defaultStyle Optional. The CSS style to apply if missing. Default: 'max-width: 100%; height: auto;'.
447 *
448 * @return string Modified HTML string with styled <img> tag if needed.
449 */
450 private static function ensureImgWithStyle($input, $defaultStyle = 'max-width: 100%; height: auto;')
451 {
452 $imgTagWithStylePattern = '/<img\b[^>]*\bstyle\s*=\s*["\'][^"\']*["\'][^>]*>/i';
453 $imgTagPattern = '/<img\b[^>]*>/i';
454 $filePattern = '/\.(jpg|jpeg|png|gif|webp)$/i';
455
456 if (preg_match($filePattern, trim($input)) && !preg_match('/<img\b/i', $input)) {
457 return '<img src="' . htmlspecialchars(trim($input)) . '" style="' . $defaultStyle . '" />';
458 }
459
460 if (preg_match($imgTagWithStylePattern, $input)) {
461 // <img> already has style, return as is
462 return $input;
463 } elseif (preg_match($imgTagPattern, $input, $match)) {
464 // <img> without style, add style
465 $updatedImg = preg_replace('/<img\b(.*?)(\/?)>/i', '<img$1 style="' . $defaultStyle . '" $2>', $match[0]);
466 return str_replace($match[0], $updatedImg, $input);
467 } else {
468 // No <img> tag found, return input
469 return $input;
470 }
471 }
472
473 private static function orderRepeaterData($repeaterFldKey, $repeaterData, $formId)
474 {
475 if (!$formId) {
476 return $repeaterData;
477 }
478
479 $formManager = FormManager::getInstance($formId);
480 $nestedLayout = $formManager->getFormNestedLayout();
481 $repeaterLayout = $nestedLayout->{$repeaterFldKey}->lg;
482 $orderedFldKey = array_map(function ($fld) {
483 return $fld->i;
484 }, $repeaterLayout);
485
486 $orderedRepeaterData = [];
487 foreach ($repeaterData as $rptr) {
488 $orderedFlds = [];
489 foreach ($orderedFldKey as $k) {
490 if (array_key_exists($k, $rptr)) {
491 $orderedFlds[$k] = $rptr[$k];
492 }
493 }
494
495 foreach ($rptr as $ky => $v) {
496 if (!array_key_exists($ky, $orderedFldKey)) {
497 $orderedFlds[$ky] = $v;
498 }
499 }
500
501 $orderedRepeaterData[] = $orderedFlds;
502 }
503 return $orderedRepeaterData;
504 }
505
506 private static function bindFormData($formFields, $formData, $formId, $isOnlyValues = false)
507 {
508 $entryID = isset($formData['entry_id']) ? $formData['entry_id'] : null;
509 return array_reduce(array_keys($formFields), function ($filteredData, $key) use ($formFields, $formData, $isOnlyValues, $formId) {
510 $field = $formFields[$key];
511
512 $fieldNewData = $filteredData;
513
514 $ignoreFields = ['button', 'recaptcha', 'html', 'divider', 'spacer', 'section', 'turnstile', 'hcaptcha', 'image'];
515
516 $arrayValueFldType = ['check', 'select', 'image-select'];
517
518 if (in_array($field['type'], $ignoreFields)) {
519 return $fieldNewData;
520 }
521
522 // Skip processing for hidden or empty fields only when $isOnlyValues is true
523 if ($isOnlyValues) {
524 // Check if the value is strictly an empty string or null, but allow 0
525 if (!isset($formData[$key]) || '' === $formData[$key] || null === $formData[$key]) {
526 return $fieldNewData;
527 }
528
529 if (isset($field['valid']['hide']) && $field['valid']['hide']) {
530 return $fieldNewData;
531 }
532 }
533
534 if (isset($formData[$key]) && !array_key_exists('parentFieldKey', $field)) {
535 if ('repeater' === $field['type']) {
536 $repeater_data = is_string($formData[$key]) ? json_decode($formData[$key], true) : $formData[$key];
537 // ordering repeater field according to nested repeater layout
538 $repeater_data = self::orderRepeaterData($key, $repeater_data, $formId);
539 if ($isOnlyValues) {
540 $repeater_data = array_filter($repeater_data, function ($sub) {
541 return array_filter($sub, fn ($value) => '' !== $value);
542 });
543 }
544 $fieldNewData[$key] = $repeater_data;
545 } elseif ('signature' === $field['type']) {
546 if ('signature-failed.png' !== $formData[$key]) {
547 $file_path = strpos($formData[$key], '/') ? $formData[$key] : $formData[$key];
548 $newPath = $file_path;
549 $fieldNewData[$key] = self::ensureImgWithStyle($newPath, 'max-width: 100%; height: auto;');
550 }
551 } elseif (in_array($field['type'], $arrayValueFldType)) {
552 $v = is_string($formData[$key]) ? json_decode($formData[$key], true) : $formData[$key];
553 $fieldNewData[$key] = $v && is_array($v) ? implode(', ', $v) : $formData[$key];
554 } else {
555 $fieldNewData[$key] = $formData[$key];
556 }
557 }
558
559 return $fieldNewData;
560 }, []);
561 }
562
563 private static function generateTable($fieldValues, $formFields)
564 {
565 if (empty($fieldValues)) {
566 Log::debug_log([
567 'status' => 'error',
568 'code' => 'no_fields_found',
569 'type' => 'bf_all_data',
570 'message' => 'No fields found for bf_all_data',
571 'fields' => $fieldValues,
572 'formFields' => $formFields,
573 ]);
574 return '<p>No data available.</p>';
575 }
576
577 $table = "<table style='font-family: arial, sans-serif; border-collapse: collapse; width: 100%;'>";
578
579 foreach ($fieldValues as $fk => $value) {
580 $value = self::decodeIfJson($value);
581 $fieldName = self::getLabel($formFields, $fk) ?? $fk;
582 $fieldType = $formFields[$fk]['type'];
583 $table .= "<tr>
584 <td style='border: 1px solid #dddddd; text-align: left; padding: 8px; font-weight: bold;'>{$fieldName}</td>
585 <td style='border: 1px solid #dddddd; text-align: left; padding: 8px;'>";
586
587 if (is_array($value)) {
588 if ('repeater' === $fieldType) {
589 $table .= "<table style='width: 100%; border-collapse: collapse;'>";
590
591 $table .= '<tr>';
592 foreach (array_keys($value[0]) as $subKey) {
593 $subLabel = self::getLabel($formFields, $subKey) ?? $subKey;
594 $table .= "<th style='border: 1px solid #dddddd; padding: 8px; background-color: #f2f2f2;'>" . $subLabel . '</th>';
595 }
596 $table .= '</tr>';
597
598 foreach ($value as $row) {
599 $table .= '<tr>';
600 foreach ($row as $subKey => $subValue) {
601 $subFieldType = self::getFldType($subKey, $formFields);
602 if (is_array($subValue)) {
603 if (self::isCompositeFieldType($subFieldType)) {
604 $subValue = self::joinCompositeFieldValue($subValue, $subFieldType);
605 } else {
606 $subValue = self::unorderedAnchorListMarkup($subValue);
607 }
608
609 // $subValue = implode(', ', array_map(function ($v) {
610 // if (self::isFileTypeValue($v)) {
611 // return self::anchorMarkup($v);
612 // // if (self::isImageTypeValue($v)) {
613 // // return "<img src='{$v}' alt='{$v}' width='250'/>";
614 // // } else {
615 // // return "<a href='{$v}' rel='noopener noreferrer' target='_blank' style='color:blue'>{$v}</a>";
616 // // }
617 // } else {
618 // return $v;
619 // }
620 // }, $subValue));
621 } else {
622 if (self::isFileTypeValue($subValue)) {
623 if ('signature' === $subFieldType) {
624 if ('signature-failed.png' === $subValue) {
625 $subValue = '';
626 } else {
627 $subValue = "<img src='{$subValue}' alt='{$subValue}' width='250'/>";
628 }
629 }
630 } else {
631 $subValue = $subValue;
632 }
633 }
634
635 $table .= "<td style='border: 1px solid #dddddd; padding: 8px;'>" . $subValue . '</td>';
636 }
637 $table .= '</tr>';
638 }
639 $table .= '</table>';
640 } elseif ('file-up' === $fieldType || 'advanced-file-up' === $fieldType) {
641 if (is_array($value)) {
642 $table .= self::unorderedAnchorListMarkup($value);
643 }
644 } elseif (self::isCompositeFieldType($fieldType)) {
645 $table .= self::joinCompositeFieldValue($value, $fieldType);
646 } elseif ('signature' === $fieldType) {
647 if ('signature-failed.png' === $subValue) {
648 $table .= '';
649 } else {
650 $table .= self::imgMarkup($value);
651 }
652 }
653 } else {
654 $table .= $value;
655 }
656
657 $table .= '</td></tr>';
658 }
659
660 $table .= '</table>';
661
662 return $table;
663 }
664
665 private static function unorderedAnchorListMarkup($list)
666 {
667 $ul = "<ul style='list-style-type: none; padding: 0; margin:0'>";
668 foreach ($list as $v) {
669 $ul .= '<li >' . self::anchorMarkup($v) . '</li>';
670 }
671 $ul .= '</ul>';
672 return $ul;
673 }
674
675 private static function imgMarkup($filename)
676 {
677 return "<img src='{$filename}' alt='{$filename}' width='250'/>";
678 }
679
680 private static function anchorMarkup($filename)
681 {
682 return "<a href='{$filename}' rel='noopener noreferrer' target='_blank' style='color:blue'>{$filename}</a>";
683 }
684
685 public static function replaceRepeaterFieldValue($stringToReplaceField, $fieldValues, $formID)
686 {
687 if (!is_string($stringToReplaceField) || empty($stringToReplaceField)) {
688 return $stringToReplaceField; // Return as-is if nothing to replace
689 }
690
691 $formManager = FormManager::getInstance($formID);
692 $formFields = $formManager->getFieldsBasedOnLayout(); // ordered form fields based on layout(lg) order
693
694 // Find all placeholders like ${field_key} example: ${b27-5}
695 preg_match_all('/\$\{(b\d+-\d+)\}/', $stringToReplaceField, $matches);
696
697 if (empty($matches[1])) {
698 return $stringToReplaceField;
699 }
700 // Clean field data
701 // $dataCleaning = self::removeEmptyValues($fieldValues);
702 $flatFieldData = self::restructureRepeaterData($fieldValues, $formManager);
703 // generate table for repeater fields
704 foreach ($matches[1] as $fk) {
705 $repeaterFieldKey = $fk;
706 $fieldType = isset($formFields[$repeaterFieldKey]['type']) && !empty($formFields[$repeaterFieldKey]['type']) ? $formFields[$repeaterFieldKey]['type'] : null;
707 if ('repeater' === $fieldType) {
708 $repeaterMarkup = self::repeaterFieldTable($fieldValues[$repeaterFieldKey] ?? [], $formFields, $repeaterFieldKey);
709 $stringToReplaceField = str_replace('${' . $fk . '}', $repeaterMarkup, $stringToReplaceField);
710 } else {
711 if ('signature' === $fieldType) {
712 $stringToReplaceField = self::replaceImgTagForRepeatedSignature($stringToReplaceField, $flatFieldData[$repeaterFieldKey], $repeaterFieldKey);
713 }
714 $repeaterFieldData = self::safeFlatString(
715 $flatFieldData[$repeaterFieldKey] ?? '',
716 $fieldType,
717 $repeaterFieldKey,
718 $flatFieldData,
719 $formFields
720 );
721
722 $stringToReplaceField = str_replace('${' . $fk . '}', $repeaterFieldData, $stringToReplaceField);
723 }
724 }
725 return $stringToReplaceField;
726 }
727
728 private static function replaceImgTagForRepeatedSignature($stringToReplaceField, $repeaterValue, $fldKey)
729 {
730 $data = self::decodeIfJson($repeaterValue);
731 if (!is_string($stringToReplaceField) || empty($stringToReplaceField) || empty($fldKey)) {
732 return $stringToReplaceField;
733 }
734
735 $pattern = '/<img\s+[^>]*src=[\'"]([^\'"]*' . preg_quote($fldKey, '/') . '[^\'"]*)[\'"][^>]*>/i';
736
737 if (!preg_match($pattern, $stringToReplaceField)) {
738 return $stringToReplaceField;
739 }
740
741 $values = [];
742 $appendValue = function ($value) use (&$values) {
743 if (is_array($value)) {
744 foreach ($value as $item) {
745 if (is_string($item) && '' !== trim($item) && 'signature-failed.png' !== $item) {
746 $values[] = $item;
747 }
748 }
749 return;
750 }
751
752 if (is_string($value) && '' !== trim($value) && 'signature-failed.png' !== $value) {
753 $values[] = $value;
754 }
755 };
756
757 $appendValue($data);
758
759 if (empty($values)) {
760 return preg_replace($pattern, '', $stringToReplaceField);
761 }
762
763 return preg_replace_callback($pattern, function ($matches) use ($values) {
764 $imgTags = array_map(function ($value) use ($matches) {
765 $src = htmlspecialchars($value, ENT_QUOTES);
766 $alt = htmlspecialchars($value, ENT_QUOTES);
767
768 $tag = $matches[0];
769 $tag = preg_replace('/\bsrc\s*=\s*([\'"])(.*?)\1/i', 'src="' . $src . '"', $tag);
770
771 if (preg_match('/\balt\s*=\s*([\'"])(.*?)\1/i', $tag)) {
772 $tag = preg_replace('/\balt\s*=\s*([\'"])(.*?)\1/i', 'alt="' . $alt . '"', $tag);
773 } else {
774 $tag = preg_replace('/<img\b/i', '<img alt="' . $alt . '"', $tag, 1);
775 }
776
777 return $tag;
778 }, $values);
779
780 return implode('', $imgTags);
781 }, $stringToReplaceField);
782 }
783
784 /**
785 * Restructures repeater field data to maintain original structure while
786 * aggregating nested repeater values into top-level indexed arrays.
787 *
788 * @param array $data Original field data structure
789 * @return array Restructured data with aggregated arrays
790 */
791 public static function restructureRepeaterData(array $data, $formManagerInstance): array
792 {
793 $result = $data;
794 // topkey === field Key topValue === field value
795 foreach ($data as $topKey => $topValue) {
796 if ($formManagerInstance->isRepeaterField($topKey)) {
797 $topValue = self::decodeIfJson($topValue);
798 //assigning the converted value to repeater field
799 $result[$topKey] = $topValue;
800 // topvalue here is repeater field value;
801 // entryIndex repeater field key , entry == repeater field value
802 foreach ($topValue as $entryIndex => $entry) {
803 if (!is_array($entry)) {
804 continue;
805 }
806 foreach ($entry as $subKey => $subValue) {
807 if (!isset($result[$subKey]) || !is_array($result[$subKey])) {
808 $result[$subKey] = [];
809 }
810 // Handle nested arrays within entries
811 $result[$subKey][$entryIndex] = $subValue;
812 }
813 }
814 }
815 }
816 return $result;
817 }
818
819 /**
820 * Return decoded data if incoming data is stringified and if it's a plain string (e.g "John Doe") it returns the plain string
821 *
822 * @param mixed $data
823 * @return mixed
824 */
825 private static function decodeIfJson($data)
826 {
827 if (!is_string($data)) {
828 return $data;
829 }
830
831 $decoded = json_decode($data, true);
832
833 return (JSON_ERROR_NONE === json_last_error()) ? $decoded : $data;
834 }
835
836 /**
837 * Safely converts any type of form value(Specially Repeater Field Value) to string.
838 *
839 * @param mixed $data
840 * @param string $fldType
841 * @param string|null $fieldKey
842 * @param array|null $allFieldData
843 * @param array|null $formFields
844 * @return string
845 */
846 public static function safeFlatString($data, $fldType, $fieldKey = null, $allFieldData = null, $formFields = null): string
847 {
848 $newData = self::decodeIfJson($data);
849
850 if (is_array($newData)) {
851 if (self::isCompositeFieldType($fldType)) {
852 return self::joinCompositeFieldValue($newData, $fldType);
853 }
854 return implode(', ', array_map(function ($item) use ($fldType, $fieldKey, $allFieldData, $formFields) {
855 return is_array($item)
856 ? '[' . implode(', ', array_map(function ($itm) use ($fldType) {
857 if (in_array($fldType, ['advanced-file-up', 'file-up']) || self::isFileTypeValue($itm)) {
858 return self::anchorMarkup($itm);
859 // if (self::isImageTypeValue($itm)) {
860 // return "<img src='{$itm}' alt='{$itm}' width='250'/>";
861 // } else {
862 // return "<a href='{$itm}' rel='noopener noreferrer' target='_blank' style='color:blue'>{$itm}</a>";
863 // }
864 } else {
865 return $itm;
866 }
867 }, $item)) . ']'
868 : self::safeFlatString($item, $fldType, $fieldKey, $allFieldData, $formFields);
869 }, $newData));
870 }
871
872 if (is_object($data)) {
873 return method_exists($data, '__toString') ? (string) $data : (json_encode($data) ?: '');
874 }
875
876 if (is_null($data)) {
877 return '';
878 }
879
880 if (self::isFileTypeValue($data)) {
881 if ('signature-failed.png' === $data) {
882 return '';
883 }
884 }
885
886 if (in_array($fldType, ['file-up', 'advanced-file-up'])) {
887 return self::anchorMarkup($newData);
888 }
889 if ('signature' === $fldType) {
890 return self::imgMarkup($newData);
891 }
892
893 return (string) $data;
894 }
895
896 private static function removeEmptyValues($fieldData)
897 {
898 if (!is_array($fieldData)) {
899 return $fieldData;
900 }
901 // Remove empty values from the array
902 return array_filter($fieldData, function ($value) {
903 // Check if the value is 0
904 if (0 === $value) {
905 return '0';
906 }
907 return !empty($value);
908 });
909 }
910
911 /**
912 * Gets the field type by field key
913 *
914 * @param string $fldKey
915 * @param mixed $formField
916 * @return string
917 */
918 private static function getFldType($fldKey, $formFields)
919 {
920 if (array_key_exists($fldKey, $formFields)) {
921 return $formFields[$fldKey]['type'];
922 }
923 }
924
925 private static function isCompositeFieldType($fieldType)
926 {
927 return in_array($fieldType, ['name', 'address'], true);
928 }
929
930 /**
931 * Removes internal/meta subfields (keys prefixed with "_", e.g. the address
932 * field's _latitude / _longitude) so they never leak into human-readable
933 * output (entry views, emails, SmartTags, PDF, exports).
934 *
935 * @param mixed $value
936 * @return mixed
937 */
938 private static function stripMetaSubfields($value)
939 {
940 if (!is_array($value)) {
941 return $value;
942 }
943 foreach (array_keys($value) as $key) {
944 if (is_string($key) && '' !== $key && '_' === $key[0]) {
945 unset($value[$key]);
946 }
947 }
948 return $value;
949 }
950
951 private static function joinCompositeFieldValue($value, $fieldType)
952 {
953 if (!is_array($value)) {
954 return (string) $value;
955 }
956
957 $value = self::stripMetaSubfields($value);
958
959 $parts = [];
960 array_walk_recursive($value, function ($item) use (&$parts) {
961 if (is_null($item)) {
962 return;
963 }
964
965 $item = (string) $item;
966 if ('' === trim($item) && '0' !== $item) {
967 return;
968 }
969
970 $parts[] = $item;
971 });
972
973 return implode('address' === $fieldType ? ', ' : ' ', $parts);
974 }
975
976 /**
977 * Return true is it's file type value by checking with extension
978 *
979 * @param string $filename
980 * @return boolean
981 */
982 private static function isFileTypeValue($fileName)
983 {
984 if (!is_string($fileName)) {
985 return false;
986 }
987 $ext = pathinfo($fileName, PATHINFO_EXTENSION);
988
989 if ('other' !== FileHandler::getFileTypeByExtension($ext)) {
990 return true;
991 }
992 }
993
994 /**
995 * Return true is it's image type value by checking with extension
996 *
997 * @param string $filename
998 * @return boolean
999 */
1000 private static function isImageTypeValue($fileName)
1001 {
1002 if (!is_string($fileName)) {
1003 return false;
1004 }
1005 $ext = pathinfo($fileName, PATHINFO_EXTENSION);
1006
1007 if ('image' === FileHandler::getFileTypeByExtension($ext)) {
1008 return true;
1009 }
1010 }
1011
1012 private static function repeaterFieldTable($repeaterFieldData, $formFields, $repeaterFieldKey)
1013 {
1014 $repeaterFieldData = self::decodeIfJson($repeaterFieldData);
1015
1016 if (!is_array($repeaterFieldData) || !isset($repeaterFieldData[0]) || !is_array($repeaterFieldData[0])) {
1017 return ''; // Safely return empty if not a valid repeater structure
1018 }
1019 $table = "<table style='font-family: arial, sans-serif; border-collapse: collapse; width: 100%;'>";
1020 // $table .= '<tr>';
1021 // $table .= '<th style="border: 1px solid #dddddd; text-align: left; padding: 8px;">' . self::getLabel($formFields, $repeaterFieldKey) . '</th>';
1022 // $table .= '</tr>';
1023 // $table .= '<td style="border: 1px solid #dddddd; text-align: left; padding: 8px;">';
1024 // $table .= '<table style="width: 100%; border-collapse: collapse;">';
1025
1026 $headers = array_keys($repeaterFieldData[0]);
1027 $table .= '<tr>'; // open tr (for column header)
1028 foreach ($headers as $fk) {
1029 $table .= '<th style="border: 1px solid #dddddd; padding: 8px; ">' . self::getLabel($formFields, $fk) . '</th>';
1030 }
1031 $table .= '</tr>'; // close tr (for column header)
1032
1033 foreach ($repeaterFieldData as $row) {
1034 $table .= '<tr>'; // open tr (for table data row)
1035 foreach ($row as $k=>$value) {
1036 $fldTyp = self::getFldType($k, $formFields);
1037 if (is_array($value)) {
1038 if (in_array($fldTyp, ['advanced-file-up', 'file-up'])) {
1039 $newValue = self::unorderedAnchorListMarkup($value);
1040 } elseif (self::isCompositeFieldType($fldTyp)) {
1041 $newValue = self::joinCompositeFieldValue($value, $fldTyp);
1042 } else {
1043 $newValue = implode(', ', $value);
1044 }
1045 } else {
1046 if (self::isFileTypeValue($value)) {
1047 $newValue = 'signature-failed.png' === $value
1048 ? ''
1049 : (self::isImageTypeValue($value)
1050 ? "<img src='{$value}' alt='{$value}' width='250'/>"
1051 : "<a href='{$value}' rel='noopener noreferrer' target='_blank' style='color:blue'>{$value}</a>");
1052 } else {
1053 $newValue = $value;
1054 }
1055 }
1056
1057 $table .= '<td style="border: 1px solid #dddddd; padding: 8px;">' . $newValue . '</td>';
1058 }
1059 $table .= '</tr>'; // close tr (for table data row)
1060 }
1061 // $table .= '</table>';
1062
1063 // $table .= '</td>';
1064 $table .= '</table>';
1065
1066 return $table;
1067 }
1068
1069 private static function getLabel($formFields, $key)
1070 {
1071 return $formFields[$key]['label'] ?? $key;
1072 }
1073
1074 /**
1075 * Derive a composite child field's key name from its bracketed HTML name.
1076 * e.g. childFieldName "name[first_name]" with parent "name" => "first_name".
1077 * Falls back to the bracket contents when the parent name is empty.
1078 */
1079 public static function deriveChildName($childFieldName, $parentFieldName)
1080 {
1081 $childFieldName = (string) $childFieldName;
1082 if (preg_match('/\[(.*?)\]/', $childFieldName, $matches)) {
1083 return $matches[1];
1084 }
1085
1086 return str_replace(['[', ']', (string) $parentFieldName], '', $childFieldName);
1087 }
1088
1089 /**
1090 * Pull a child value out of a parent composite field's nested submitted value.
1091 * Looks up by the derived child name first, then by the child field key.
1092 * Returns null when no match is found.
1093 */
1094 public static function extractChildValueFromParentValue($parentValue, $childName, $childKey)
1095 {
1096 if (is_object($parentValue)) {
1097 $parentValue = (array) $parentValue;
1098 }
1099
1100 if (!is_array($parentValue)) {
1101 return null;
1102 }
1103
1104 if (array_key_exists($childName, $parentValue)) {
1105 return $parentValue[$childName];
1106 }
1107
1108 if (array_key_exists($childKey, $parentValue)) {
1109 return $parentValue[$childKey];
1110 }
1111
1112 return null;
1113 }
1114 }
1115