PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 2.15.3
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v2.15.3
3.3.1 V-3.3.0 3.2.2 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 V3.0.3 V3.0.2 -3.0.1 V_3.0.0 1.1.1 1.1.8 1.2 1.3 1.4 1.4.18 1.5.2 1.9 2.0 2.10.0 2.10.1 All 138 releases
← All changes | includes/Core/Util/FieldValueHandler.php +43 -919 3.2.22.15.3 View file →
@@ -2,13 +2,12 @@
2 2
3 3 namespace BitCode\BitForm\Core\Util;
4 4
5 5 use BitCode\BitForm\Admin\Form\Helpers;
6 -use BitCode\BitForm\Core\Form\FormManager;
7 6
8 7 final class FieldValueHandler
9 8 {
10 - public static function replaceFieldWithValue($stringToReplaceField, $fieldValues, $formID = null, $stripShortcodesFromValues = false)
9 + public static function replaceFieldWithValue($stringToReplaceField, $fieldValues)
11 10 {
12 11 if (empty($stringToReplaceField)) {
13 12 return $stringToReplaceField;
14 13 }
@@ -14,19 +13,10 @@
14 13 }
15 14 if (!is_string($stringToReplaceField)) {
16 15 $stringToReplaceField = wp_json_encode($stringToReplaceField);
17 16 }
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 -
17 + $stringToReplaceField = static::replaceSmartTagWithValue($stringToReplaceField);
27 18 $fieldPattern = '/\${\w[^ ${}]*}/';
28 -
29 19 preg_match_all($fieldPattern, $stringToReplaceField, $matchedField);
30 20 if (empty($matchedField)) {
31 21 return $stringToReplaceField;
32 22 }
@@ -36,9 +26,9 @@
36 26 $fieldValue = null;
37 27 if (isset($fieldValues[$fieldName])) {
38 28 $targetFieldValue = isset($fieldValues[$fieldName]['value']) ? $fieldValues[$fieldName]['value'] : $fieldValues[$fieldName];
39 29 if ('array' === gettype($targetFieldValue) || 'object' === gettype($targetFieldValue)) {
40 - foreach (self::stripMetaSubfields((array) $targetFieldValue) as $singleTargetVal) {
30 + foreach ((array)$targetFieldValue as $singleTargetVal) {
41 31 if (isset($fieldValue)) {
42 32 if (is_numeric($fieldValue) && is_numeric($singleTargetVal)) {
43 33 $fieldValue = $fieldValue + $singleTargetVal;
44 34 } else {
@@ -51,12 +41,8 @@
51 41 // $fieldValue = wp_json_encode($targetFieldValue);
52 42 } else {
53 43 $fieldValue = strval($targetFieldValue);
54 44 }
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 45 $stringToReplaceField = str_replace($value, $fieldValue, $stringToReplaceField);
60 46 } else {
61 47 $stringToReplaceField = str_replace($value, '', $stringToReplaceField);
62 48 }
@@ -78,24 +64,8 @@
78 64 // }
79 65 return $stringToReplaceField;
80 66 }
81 67
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 68 /**
99 69 * Summary of getCountValue - get the count value from the function string "${_bf_count(item-1, item-2)}" => 2
100 70 *
101 71 * @param string $functionString
@@ -161,19 +131,18 @@
161 131 }
162 132 return $emailAddresses;
163 133 }
164 134
165 - public static function replaceSmartTagWithValue($contentWithSmartTag)
135 + public static function replaceSmartTagWithValue($fieldValues)
166 136 {
167 137 $fieldPattern = '/(\${_[^{]*?)(?=\})}/';
168 - $matchPattern = preg_match_all($fieldPattern, $contentWithSmartTag, $matchedField);
138 + $matchPattern = preg_match_all($fieldPattern, $fieldValues, $matchedField);
169 139 if (!$matchPattern) {
170 - return $contentWithSmartTag;
140 + return $fieldValues;
171 141 }
172 142
173 143 $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']))) {
144 + if (isset($_REQUEST['action']) && 'bitforms_trigger_workflow' === $_REQUEST['action']) {
176 145 $ajaxRequest = true;
177 146 }
178 147
179 148 foreach (array_unique($matchedField[0]) as $value) {
@@ -188,12 +157,11 @@
188 157 $fieldName = str_replace($matchCustomFormat[0], '', $fieldName);
189 158 }
190 159
191 160 $tagFieldValues = SmartTags::getSmartTagValue($fieldName, $ajaxRequest, $customValue);
192 -
193 - $contentWithSmartTag = str_replace($value, $tagFieldValues, $contentWithSmartTag);
161 + $fieldValues = str_replace($value, $tagFieldValues, $fieldValues);
194 162 }
195 - return $contentWithSmartTag;
163 + return $fieldValues;
196 164 }
197 165
198 166 public static function isEmpty($val)
199 167 {
@@ -202,11 +170,12 @@
202 170 }
203 171 return false;
204 172 }
205 173
206 - public static function formatFieldValueForMail($fields, $fieldValues = [])
174 + public static function formatFieldValueForMail($fields, $fieldValues)
207 175 {
208 176 $formattedFldValues = $fieldValues;
177 + $repeaterFieldKey = [];
209 178 $file_upload_types = Helpers::$file_upload_types;
210 179 $repeated_array_type_data_fields = Helpers::$repeated_array_type_data_fields;
211 180 foreach ($fields as $fldKey => $fldData) {
212 181 if (in_array($fldData->typ, $file_upload_types)) {
@@ -211,9 +180,9 @@
211 180 foreach ($fields as $fldKey => $fldData) {
212 181 if (in_array($fldData->typ, $file_upload_types)) {
213 182 continue;
214 183 }
215 - if (is_array($fieldValues) && array_key_exists($fldKey, $fieldValues)) {
184 + if (array_key_exists($fldKey, $fieldValues)) {
216 185 $value = $fieldValues[$fldKey];
217 186 // if (is_array($value)) {
218 187 // $formattedFldValues[$fldKey] = htmlspecialchars(implode(', ', $value));
219 188 // } else {
@@ -219,896 +188,51 @@
219 188 // } else {
220 189 // $formattedFldValues[$fldKey] = htmlspecialchars($value);
221 190 // }
222 191
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'/>";
192 + if (is_array($value)) {
193 + $arrValue = '';
194 + foreach ($value as $v) {
195 + if (is_array($v)) {
196 + foreach ($v as $k1 => $v1) {
197 + if (array_key_exists($k1, $repeaterFieldKey)) {
198 + $oldValue = $repeaterFieldKey[$k1];
199 + if (is_array($v1) && in_array($fields->{$k1}->typ, $repeated_array_type_data_fields)) {
200 + $newValues = '[' . implode(', ', $v1) . '] ';
201 + if (!preg_match('/\[.*\]/', $oldValue)) {
202 + $oldValue = '[' . $oldValue . '] ';
628 203 }
204 + } else {
205 + $newValues = $v1;
629 206 }
207 + $repeaterFieldKey[$k1] = $oldValue . ', ' . $newValues;
630 208 } else {
631 - $subValue = $subValue;
209 + if (!empty($v1) && is_array($v1)) {
210 + $repeaterFieldKey[$k1] = htmlspecialchars(implode(', ', $v1));
211 + } else {
212 + $repeaterFieldKey[$k1] = htmlspecialchars($v1);
213 + }
632 214 }
633 215 }
634 -
635 - $table .= "<td style='border: 1px solid #dddddd; padding: 8px;'>" . $subValue . '</td>';
216 + } else {
217 + $arrValue .= $v . ', ';
636 218 }
637 - $table .= '</tr>';
638 219 }
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);
220 + $formattedFldValues[$fldKey] = htmlspecialchars(rtrim($arrValue, ', '));
221 + $arrValue = '';
773 222 } else {
774 - $tag = preg_replace('/<img\b/i', '<img alt="' . $alt . '"', $tag, 1);
223 + $formattedFldValues[$fldKey] = htmlspecialchars($value);
775 224 }
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 - }
225 + if ('textarea' === $fldData->typ) {
226 + $formattedFldValues[$fldKey] = nl2br(htmlspecialchars($value));
813 227 }
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 - }
228 + if ('date' === $fldData->typ) {
229 + $formattedFldValues[$fldKey] = date_i18n(get_option('date_format'), strtotime(htmlspecialchars($value)));
1055 230 }
1056 -
1057 - $table .= '<td style="border: 1px solid #dddddd; padding: 8px;">' . $newValue . '</td>';
1058 231 }
1059 - $table .= '</tr>'; // close tr (for table data row)
1060 232 }
1061 - // $table .= '</table>';
1062 233
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;
234 + $merge_values = array_merge($fieldValues, $formattedFldValues);
235 + $merge_values = array_merge($merge_values, $repeaterFieldKey);
236 + return $merge_values;
1113 237 }
1114 238 }