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

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