PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / V3.3.2
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder vV3.3.2
V3.3.2 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 All 139 releases
← All changes | includes/Core/Util/FieldValueHandler.php +332 -11 V-3.3.0 → V3.3.2 View file →
@@ -6,8 +6,16 @@
6 6 use BitCode\BitForm\Core\Form\FormManager;
7 7
8 8 final class FieldValueHandler
9 9 {
10 + /**
11 + * @param mixed $stringToReplaceField
12 + * @param mixed $fieldValues
13 + * @param mixed $formID
14 + * @param bool $stripShortcodesFromValues
15 + *
16 + * @return string
17 + */
10 18 public static function replaceFieldWithValue($stringToReplaceField, $fieldValues, $formID = null, $stripShortcodesFromValues = false)
11 19 {
12 20 if (empty($stringToReplaceField)) {
13 21 return $stringToReplaceField;
@@ -16,8 +24,12 @@
16 24 $stringToReplaceField = wp_json_encode($stringToReplaceField);
17 25 }
18 26 $fieldValues = $formID ? self::sortValueBasedOnLayout($formID, $fieldValues) : $fieldValues;
19 27
28 + // Must run on the raw template: after substitution an empty field and an empty template
29 + // are the same empty string.
30 + $stringToReplaceField = self::resolveConditionalBlocks($stringToReplaceField, $fieldValues, $formID);
31 +
20 32 if ($formID) {
21 33 $stringToReplaceField = self::replaceValueOfBf_all_data($stringToReplaceField, $fieldValues, $formID);
22 34 $stringToReplaceField = self::replaceRepeaterFieldValue($stringToReplaceField, $fieldValues, $formID);
23 35 }
@@ -35,9 +47,13 @@
35 47 $fieldName = substr($value, 2, strlen($value) - 3);
36 48 $fieldValue = null;
37 49 if (isset($fieldValues[$fieldName])) {
38 50 $targetFieldValue = isset($fieldValues[$fieldName]['value']) ? $fieldValues[$fieldName]['value'] : $fieldValues[$fieldName];
39 - if ('array' === gettype($targetFieldValue) || 'object' === gettype($targetFieldValue)) {
51 + $targetFieldType = self::fieldTypeForSmartTag($fieldValues[$fieldName], $fieldName, $formID);
52 + if (self::isCompositeFieldType($targetFieldType) && ('array' === gettype($targetFieldValue) || 'object' === gettype($targetFieldValue))) {
53 + // Name/Address parts are words of one value: never summed, joined like everywhere else.
54 + $fieldValue = self::joinCompositeFieldValue((array) $targetFieldValue, $targetFieldType);
55 + } elseif ('array' === gettype($targetFieldValue) || 'object' === gettype($targetFieldValue)) {
40 56 foreach (self::stripMetaSubfields((array) $targetFieldValue) as $singleTargetVal) {
41 57 if (isset($fieldValue)) {
42 58 if (is_numeric($fieldValue) && is_numeric($singleTargetVal)) {
43 59 $fieldValue = $fieldValue + $singleTargetVal;
@@ -203,8 +219,267 @@
203 219 return false;
204 220 }
205 221
206 222 /**
223 + * Whether a resolved value renders as nothing.
224 + *
225 + * Stricter than isEmpty(): whitespace-only is blank (some smart-tag resolvers return a
226 + * single space), so is an all-blank array. `0` / `'0'` never are.
227 + *
228 + * @param mixed $val
229 + *
230 + * @return bool
231 + */
232 + public static function isBlank($val)
233 + {
234 + if (null === $val || false === $val) {
235 + return true;
236 + }
237 + if (is_object($val)) {
238 + $val = (array) $val;
239 + }
240 + if (is_array($val)) {
241 + foreach ($val as $key => $item) {
242 + // Composite meta sub-values (_latitude, …) never render on their own.
243 + if (is_string($key) && 0 === strpos($key, '_')) {
244 + continue;
245 + }
246 + if (!self::isBlank($item)) {
247 + return false;
248 + }
249 + }
250 + return true;
251 + }
252 + if (!is_scalar($val)) {
253 + return true;
254 + }
255 +
256 + return '' === trim(str_replace("\xc2\xa0", '', (string) $val));
257 + }
258 +
259 + /**
260 + * Resolve `${bf_if:…}` … `${bf_endif}` template blocks.
261 + *
262 + * Syntax, operators and traps: docs/template-conditional-blocks.md.
263 + *
264 + * @param string $content
265 + * @param array $fieldValues
266 + * @param mixed $formID needed to reach repeater rows; without it a child key reads blank
267 + *
268 + * @return string
269 + */
270 + private static function resolveConditionalBlocks($content, $fieldValues, $formID = null)
271 + {
272 + if (false === strpos($content, '${bf_if') && false === strpos($content, '${bf_unless')) {
273 + return self::stripConditionalBlockTags($content);
274 + }
275 +
276 + $conditionValues = $fieldValues;
277 + if ($formID) {
278 + $formManager = FormManager::getInstance($formID);
279 + // Repeater children have no top-level key; flatten the rows in, real values still win.
280 + $conditionValues = array_merge(self::restructureRepeaterData($fieldValues, $formManager), $fieldValues);
281 + }
282 +
283 + // Matches a block whose body holds no further opener, i.e. the innermost one.
284 + $innerMost = '/\$\{bf_(if|if_any|if_all|unless):([^{}$]*)\}((?:(?!\$\{bf_(?:if|if_any|if_all|unless):)[\s\S])*?)\$\{bf_endif\}/';
285 +
286 + // Bounded so a malformed template can never spin here.
287 + for ($pass = 0; $pass < 200; $pass++) {
288 + $resolved = preg_replace_callback($innerMost, function ($matches) use ($conditionValues) {
289 + $branches = preg_split('/\$\{bf_else\}/', $matches[3], 2);
290 + $truthy = isset($branches[0]) ? $branches[0] : '';
291 + $falsy = isset($branches[1]) ? $branches[1] : '';
292 +
293 + return self::evaluateBlockCondition($matches[1], $matches[2], $conditionValues) ? $truthy : $falsy;
294 + }, $content, -1, $replacedCount);
295 +
296 + if (null === $resolved) {
297 + break; // preg failure (e.g. backtrack limit): leave the content untouched
298 + }
299 + $content = $resolved;
300 + if (!$replacedCount) {
301 + break;
302 + }
303 + }
304 +
305 + return self::stripConditionalBlockTags($content);
306 + }
307 +
308 + /**
309 + * @param string $type if|if_any|if_all|unless
310 + * @param string $rawKeys comma separated conditions
311 + * @param array $fieldValues
312 + *
313 + * @return bool
314 + */
315 + private static function evaluateBlockCondition($type, $rawKeys, $fieldValues)
316 + {
317 + $conditions = array_filter(array_map('trim', explode(',', (string) $rawKeys)), function ($condition) {
318 + return '' !== $condition;
319 + });
320 + if (empty($conditions)) {
321 + return false;
322 + }
323 +
324 + $results = [];
325 + foreach ($conditions as $condition) {
326 + $results[] = self::conditionHolds($condition, $fieldValues);
327 + }
328 +
329 + if ('if_all' === $type) {
330 + return !in_array(false, $results, true);
331 + }
332 + if ('unless' === $type) {
333 + return !in_array(true, $results, true);
334 + }
335 +
336 + return in_array(true, $results, true);
337 + }
338 +
339 + /**
340 + * `key`, or `key operator value`.
341 + *
342 + * @param string $condition
343 + * @param array $fieldValues
344 + *
345 + * @return bool
346 + */
347 + private static function conditionHolds($condition, $fieldValues)
348 + {
349 + $operators = self::blockOperators();
350 + // Longest name first, or `not_equal` reads as `equal`. Field keys never contain a space.
351 + $pattern = '/^(\S+)\s+(' . implode('|', $operators) . ')(?:\s+([\s\S]*))?$/';
352 +
353 + if (!preg_match($pattern, trim($condition), $parts)) {
354 + return !self::isBlank(self::conditionValue(trim($condition), $fieldValues));
355 + }
356 +
357 + $value = self::conditionValue($parts[1], $fieldValues);
358 + $operator = $parts[2];
359 + $expected = isset($parts[3]) ? trim($parts[3]) : '';
360 +
361 + if ('null' === $operator) {
362 + return self::isBlank($value);
363 + }
364 + if ('not_null' === $operator) {
365 + return !self::isBlank($value);
366 + }
367 +
368 + // Multi-value fields and repeater children arrive as a list.
369 + $candidates = is_array($value) || is_object($value) ? self::stripMetaSubfields((array) $value) : [$value];
370 + $negated = in_array($operator, ['not_equal', 'not_contain'], true);
371 + foreach ($candidates as $candidate) {
372 + if (is_array($candidate) || is_object($candidate)) {
373 + continue;
374 + }
375 + // compareValue answers the positive form, so one match settles either case: it satisfies
376 + // `contain` and rules out `not_contain`.
377 + if (self::compareValue($operator, (string) $candidate, $expected)) {
378 + return !$negated;
379 + }
380 + }
381 +
382 + return $negated;
383 + }
384 +
385 + /**
386 + * @return string[] operator names, longest first
387 + */
388 + private static function blockOperators()
389 + {
390 + return [
391 + 'greater_or_equal',
392 + 'less_or_equal',
393 + 'not_contain',
394 + 'start_with',
395 + 'not_equal',
396 + 'not_null',
397 + 'end_with',
398 + 'contain',
399 + 'greater',
400 + 'equal',
401 + 'less',
402 + 'null',
403 + ];
404 + }
405 +
406 + /**
407 + * @param string $key field key, or a `_bf_*` smart tag
408 + * @param array $fieldValues
409 + *
410 + * @return mixed
411 + */
412 + private static function conditionValue($key, $fieldValues)
413 + {
414 + if (0 === strpos($key, '_')) {
415 + return SmartTags::getSmartTagValue($key, false, '');
416 + }
417 + $value = isset($fieldValues[$key]) ? $fieldValues[$key] : null;
418 + if (is_array($value) && isset($value['value'])) {
419 + $value = $value['value'];
420 + }
421 +
422 + return $value;
423 + }
424 +
425 + /**
426 + * @param string $operator
427 + * @param string $value the submitted value
428 + * @param string $expected the value written in the template
429 + *
430 + * @return bool
431 + */
432 + private static function compareValue($operator, $value, $expected)
433 + {
434 + switch ($operator) {
435 + case 'equal':
436 + case 'not_equal':
437 + return 0 === strcasecmp(trim($value), $expected);
438 + case 'contain':
439 + case 'not_contain':
440 + return '' !== $expected && false !== stripos($value, $expected);
441 + case 'start_with':
442 + return '' !== $expected && 0 === stripos($value, $expected);
443 + case 'end_with':
444 + return '' !== $expected && 0 === strcasecmp($expected, (string) substr($value, -strlen($expected)));
445 + case 'greater':
446 + return self::isNumericPair($value, $expected) && (float) $value > (float) $expected;
447 + case 'less':
448 + return self::isNumericPair($value, $expected) && (float) $value < (float) $expected;
449 + case 'greater_or_equal':
450 + return self::isNumericPair($value, $expected) && (float) $value >= (float) $expected;
451 + case 'less_or_equal':
452 + return self::isNumericPair($value, $expected) && (float) $value <= (float) $expected;
453 + default:
454 + return false;
455 + }
456 + }
457 +
458 + /**
459 + * @param string $value
460 + * @param string $expected
461 + *
462 + * @return bool both sides compare as numbers
463 + */
464 + private static function isNumericPair($value, $expected)
465 + {
466 + return is_numeric(trim($value)) && is_numeric($expected);
467 + }
468 +
469 + /**
470 + * Drop leftover block tags so an unbalanced template never leaks them into the output.
471 + *
472 + * @param string $content
473 + *
474 + * @return string
475 + */
476 + private static function stripConditionalBlockTags($content)
477 + {
478 + return preg_replace('/\$\{bf_(?:if|if_any|if_all|unless):[^{}$]*\}|\$\{bf_(?:else|endif)\}/', '', $content);
479 + }
480 +
481 + /**
207 482 * Values a field kept on an entry edit, posted as `<fieldKey>_old` instead of resubmitted.
208 483 *
209 484 * @param mixed $postData submitted data, keyed by field key
210 485 * @param string $fieldKey
@@ -460,18 +735,20 @@
460 735 $formManager = FormManager::getInstance($formId);
461 736 $formFields = $formManager->getFields();
462 737 $orderedFormFields = $formManager->getFieldsBasedOnLayout(); // ordered form fields based on layout(lg) order
463 738 foreach ($matchesArray as $match) {
739 + // Each tag binds from the untouched submitted values: reusing a filtered result would
740 + // let the first tag in a template starve the second.
464 741 switch ($match) {
465 742 case '${bf_all_data}':
466 - $fieldValues = self::bindFormData($orderedFormFields, $fieldValues, $formId);
467 - $table = self::generateTable($fieldValues, $orderedFormFields, $formId);
743 + $boundValues = self::bindFormData($orderedFormFields, $fieldValues, $formId);
744 + $table = self::generateTable($boundValues, $orderedFormFields, $formId);
468 745 $stringToReplaceField = str_replace('${bf_all_data}', $table, $stringToReplaceField);
469 746 break;
470 747
471 748 case '${bf_all_data.onlyValues}':
472 - $fieldValues = self::bindFormData($orderedFormFields, $fieldValues, $formId, true);
473 - $table = self::generateTable($fieldValues, $orderedFormFields, $formId);
749 + $boundValues = self::bindFormData($orderedFormFields, $fieldValues, $formId, true);
750 + $table = self::generateTable($boundValues, $orderedFormFields, $formId);
474 751 $stringToReplaceField = str_replace('${bf_all_data.onlyValues}', $table, $stringToReplaceField);
475 752 break;
476 753 default:
477 754 Log::debug_log([
@@ -574,10 +851,11 @@
574 851 }
575 852
576 853 // Skip processing for hidden or empty fields only when $isOnlyValues is true
577 854 if ($isOnlyValues) {
578 - // Check if the value is strictly an empty string or null, but allow 0
579 - if (!isset($formData[$key]) || '' === $formData[$key] || null === $formData[$key]) {
855 + // Blank means empty string, null, or an array with nothing in it (unchecked
856 + // checkbox group, file field with no upload). 0 is a real value.
857 + if (!isset($formData[$key]) || self::isBlank($formData[$key])) {
580 858 return $fieldNewData;
581 859 }
582 860
583 861 if (isset($field['valid']['hide']) && $field['valid']['hide']) {
@@ -705,12 +983,13 @@
705 983 }
706 984 } elseif (self::isCompositeFieldType($fieldType)) {
707 985 $table .= self::joinCompositeFieldValue($value, $fieldType);
708 986 } elseif ('signature' === $fieldType) {
709 - if ('signature-failed.png' === $subValue) {
710 - $table .= '';
711 - } else {
712 - $table .= self::imgMarkup($value);
987 + // A signature arrives here wrapped in a one-item list; the failed-capture
988 + // placeholder renders nothing.
989 + $signature = reset($value);
990 + if (false !== $signature && 'signature-failed.png' !== $signature) {
991 + $table .= self::imgMarkup($signature);
713 992 }
714 993 }
715 994 } else {
716 995 $table .= $value;
@@ -999,8 +1278,29 @@
999 1278 return $formFields[$fldKey]['type'];
1000 1279 }
1001 1280 }
1002 1281
1282 + /**
1283 + * The type of the field a Smart Tag points at: from the value itself when it carries
1284 + * one, otherwise from the form.
1285 + *
1286 + * @param mixed $fieldValue Entry of $fieldValues for this field
1287 + * @param string $fieldKey
1288 + * @param int|null $formID
1289 + * @return string|null
1290 + */
1291 + private static function fieldTypeForSmartTag($fieldValue, $fieldKey, $formID)
1292 + {
1293 + if (is_array($fieldValue) && isset($fieldValue['type']) && is_string($fieldValue['type'])) {
1294 + return $fieldValue['type'];
1295 + }
1296 + if (!$formID) {
1297 + return null;
1298 + }
1299 + $formFields = FormManager::getInstance($formID)->getFields();
1300 + return is_array($formFields) && isset($formFields[$fieldKey]['type']) ? $formFields[$fieldKey]['type'] : null;
1301 + }
1302 +
1003 1303 private static function isCompositeFieldType($fieldType)
1004 1304 {
1005 1305 return in_array($fieldType, ['name', 'address'], true);
1006 1306 }
@@ -1212,8 +1512,29 @@
1212 1512 return $matches[1];
1213 1513 }
1214 1514
1215 1515 return str_replace(['[', ']', (string) $parentFieldName], '', $childFieldName);
1516 + }
1517 +
1518 + /**
1519 + * The HTML name a composite child (Name/Address) renders with: the parent's current name plus
1520 + * the child's own suffix. The child stores a full "parent[suffix]" copy that goes stale when the
1521 + * parent is renamed, and a stale prefix posts the value away from the parent.
1522 + *
1523 + * @param string $childFieldName stored child name, e.g. "address-1[city]"
1524 + * @param string $parentFieldName parent's current name, e.g. "home_address"
1525 + *
1526 + * @return string e.g. "home_address[city]"; the stored name when it has no suffix or the parent has no name
1527 + */
1528 + public static function rebaseChildFieldName($childFieldName, $parentFieldName)
1529 + {
1530 + $childFieldName = (string) $childFieldName;
1531 + $parentFieldName = (string) $parentFieldName;
1532 + if ('' === $parentFieldName || !preg_match('/\[([^\]]+)\]$/', $childFieldName, $matches)) {
1533 + return $childFieldName;
1534 + }
1535 +
1536 + return $parentFieldName . '[' . $matches[1] . ']';
1216 1537 }
1217 1538
1218 1539 /**
1219 1540 * Pull a child value out of a parent composite field's nested submitted value.