PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.3.1
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.3.1
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 +285 -10 V-3.3.03.3.1 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 }
@@ -203,8 +215,267 @@
203 215 return false;
204 216 }
205 217
206 218 /**
219 + * Whether a resolved value renders as nothing.
220 + *
221 + * Stricter than isEmpty(): whitespace-only is blank (some smart-tag resolvers return a
222 + * single space), so is an all-blank array. `0` / `'0'` never are.
223 + *
224 + * @param mixed $val
225 + *
226 + * @return bool
227 + */
228 + public static function isBlank($val)
229 + {
230 + if (null === $val || false === $val) {
231 + return true;
232 + }
233 + if (is_object($val)) {
234 + $val = (array) $val;
235 + }
236 + if (is_array($val)) {
237 + foreach ($val as $key => $item) {
238 + // Composite meta sub-values (_latitude, …) never render on their own.
239 + if (is_string($key) && 0 === strpos($key, '_')) {
240 + continue;
241 + }
242 + if (!self::isBlank($item)) {
243 + return false;
244 + }
245 + }
246 + return true;
247 + }
248 + if (!is_scalar($val)) {
249 + return true;
250 + }
251 +
252 + return '' === trim(str_replace("\xc2\xa0", '', (string) $val));
253 + }
254 +
255 + /**
256 + * Resolve `${bf_if:…}` … `${bf_endif}` template blocks.
257 + *
258 + * Syntax, operators and traps: docs/template-conditional-blocks.md.
259 + *
260 + * @param string $content
261 + * @param array $fieldValues
262 + * @param mixed $formID needed to reach repeater rows; without it a child key reads blank
263 + *
264 + * @return string
265 + */
266 + private static function resolveConditionalBlocks($content, $fieldValues, $formID = null)
267 + {
268 + if (false === strpos($content, '${bf_if') && false === strpos($content, '${bf_unless')) {
269 + return self::stripConditionalBlockTags($content);
270 + }
271 +
272 + $conditionValues = $fieldValues;
273 + if ($formID) {
274 + $formManager = FormManager::getInstance($formID);
275 + // Repeater children have no top-level key; flatten the rows in, real values still win.
276 + $conditionValues = array_merge(self::restructureRepeaterData($fieldValues, $formManager), $fieldValues);
277 + }
278 +
279 + // Matches a block whose body holds no further opener, i.e. the innermost one.
280 + $innerMost = '/\$\{bf_(if|if_any|if_all|unless):([^{}$]*)\}((?:(?!\$\{bf_(?:if|if_any|if_all|unless):)[\s\S])*?)\$\{bf_endif\}/';
281 +
282 + // Bounded so a malformed template can never spin here.
283 + for ($pass = 0; $pass < 200; $pass++) {
284 + $resolved = preg_replace_callback($innerMost, function ($matches) use ($conditionValues) {
285 + $branches = preg_split('/\$\{bf_else\}/', $matches[3], 2);
286 + $truthy = isset($branches[0]) ? $branches[0] : '';
287 + $falsy = isset($branches[1]) ? $branches[1] : '';
288 +
289 + return self::evaluateBlockCondition($matches[1], $matches[2], $conditionValues) ? $truthy : $falsy;
290 + }, $content, -1, $replacedCount);
291 +
292 + if (null === $resolved) {
293 + break; // preg failure (e.g. backtrack limit): leave the content untouched
294 + }
295 + $content = $resolved;
296 + if (!$replacedCount) {
297 + break;
298 + }
299 + }
300 +
301 + return self::stripConditionalBlockTags($content);
302 + }
303 +
304 + /**
305 + * @param string $type if|if_any|if_all|unless
306 + * @param string $rawKeys comma separated conditions
307 + * @param array $fieldValues
308 + *
309 + * @return bool
310 + */
311 + private static function evaluateBlockCondition($type, $rawKeys, $fieldValues)
312 + {
313 + $conditions = array_filter(array_map('trim', explode(',', (string) $rawKeys)), function ($condition) {
314 + return '' !== $condition;
315 + });
316 + if (empty($conditions)) {
317 + return false;
318 + }
319 +
320 + $results = [];
321 + foreach ($conditions as $condition) {
322 + $results[] = self::conditionHolds($condition, $fieldValues);
323 + }
324 +
325 + if ('if_all' === $type) {
326 + return !in_array(false, $results, true);
327 + }
328 + if ('unless' === $type) {
329 + return !in_array(true, $results, true);
330 + }
331 +
332 + return in_array(true, $results, true);
333 + }
334 +
335 + /**
336 + * `key`, or `key operator value`.
337 + *
338 + * @param string $condition
339 + * @param array $fieldValues
340 + *
341 + * @return bool
342 + */
343 + private static function conditionHolds($condition, $fieldValues)
344 + {
345 + $operators = self::blockOperators();
346 + // Longest name first, or `not_equal` reads as `equal`. Field keys never contain a space.
347 + $pattern = '/^(\S+)\s+(' . implode('|', $operators) . ')(?:\s+([\s\S]*))?$/';
348 +
349 + if (!preg_match($pattern, trim($condition), $parts)) {
350 + return !self::isBlank(self::conditionValue(trim($condition), $fieldValues));
351 + }
352 +
353 + $value = self::conditionValue($parts[1], $fieldValues);
354 + $operator = $parts[2];
355 + $expected = isset($parts[3]) ? trim($parts[3]) : '';
356 +
357 + if ('null' === $operator) {
358 + return self::isBlank($value);
359 + }
360 + if ('not_null' === $operator) {
361 + return !self::isBlank($value);
362 + }
363 +
364 + // Multi-value fields and repeater children arrive as a list.
365 + $candidates = is_array($value) || is_object($value) ? self::stripMetaSubfields((array) $value) : [$value];
366 + $negated = in_array($operator, ['not_equal', 'not_contain'], true);
367 + foreach ($candidates as $candidate) {
368 + if (is_array($candidate) || is_object($candidate)) {
369 + continue;
370 + }
371 + // compareValue answers the positive form, so one match settles either case: it satisfies
372 + // `contain` and rules out `not_contain`.
373 + if (self::compareValue($operator, (string) $candidate, $expected)) {
374 + return !$negated;
375 + }
376 + }
377 +
378 + return $negated;
379 + }
380 +
381 + /**
382 + * @return string[] operator names, longest first
383 + */
384 + private static function blockOperators()
385 + {
386 + return [
387 + 'greater_or_equal',
388 + 'less_or_equal',
389 + 'not_contain',
390 + 'start_with',
391 + 'not_equal',
392 + 'not_null',
393 + 'end_with',
394 + 'contain',
395 + 'greater',
396 + 'equal',
397 + 'less',
398 + 'null',
399 + ];
400 + }
401 +
402 + /**
403 + * @param string $key field key, or a `_bf_*` smart tag
404 + * @param array $fieldValues
405 + *
406 + * @return mixed
407 + */
408 + private static function conditionValue($key, $fieldValues)
409 + {
410 + if (0 === strpos($key, '_')) {
411 + return SmartTags::getSmartTagValue($key, false, '');
412 + }
413 + $value = isset($fieldValues[$key]) ? $fieldValues[$key] : null;
414 + if (is_array($value) && isset($value['value'])) {
415 + $value = $value['value'];
416 + }
417 +
418 + return $value;
419 + }
420 +
421 + /**
422 + * @param string $operator
423 + * @param string $value the submitted value
424 + * @param string $expected the value written in the template
425 + *
426 + * @return bool
427 + */
428 + private static function compareValue($operator, $value, $expected)
429 + {
430 + switch ($operator) {
431 + case 'equal':
432 + case 'not_equal':
433 + return 0 === strcasecmp(trim($value), $expected);
434 + case 'contain':
435 + case 'not_contain':
436 + return '' !== $expected && false !== stripos($value, $expected);
437 + case 'start_with':
438 + return '' !== $expected && 0 === stripos($value, $expected);
439 + case 'end_with':
440 + return '' !== $expected && 0 === strcasecmp($expected, (string) substr($value, -strlen($expected)));
441 + case 'greater':
442 + return self::isNumericPair($value, $expected) && (float) $value > (float) $expected;
443 + case 'less':
444 + return self::isNumericPair($value, $expected) && (float) $value < (float) $expected;
445 + case 'greater_or_equal':
446 + return self::isNumericPair($value, $expected) && (float) $value >= (float) $expected;
447 + case 'less_or_equal':
448 + return self::isNumericPair($value, $expected) && (float) $value <= (float) $expected;
449 + default:
450 + return false;
451 + }
452 + }
453 +
454 + /**
455 + * @param string $value
456 + * @param string $expected
457 + *
458 + * @return bool both sides compare as numbers
459 + */
460 + private static function isNumericPair($value, $expected)
461 + {
462 + return is_numeric(trim($value)) && is_numeric($expected);
463 + }
464 +
465 + /**
466 + * Drop leftover block tags so an unbalanced template never leaks them into the output.
467 + *
468 + * @param string $content
469 + *
470 + * @return string
471 + */
472 + private static function stripConditionalBlockTags($content)
473 + {
474 + return preg_replace('/\$\{bf_(?:if|if_any|if_all|unless):[^{}$]*\}|\$\{bf_(?:else|endif)\}/', '', $content);
475 + }
476 +
477 + /**
207 478 * Values a field kept on an entry edit, posted as `<fieldKey>_old` instead of resubmitted.
208 479 *
209 480 * @param mixed $postData submitted data, keyed by field key
210 481 * @param string $fieldKey
@@ -460,18 +731,20 @@
460 731 $formManager = FormManager::getInstance($formId);
461 732 $formFields = $formManager->getFields();
462 733 $orderedFormFields = $formManager->getFieldsBasedOnLayout(); // ordered form fields based on layout(lg) order
463 734 foreach ($matchesArray as $match) {
735 + // Each tag binds from the untouched submitted values: reusing a filtered result would
736 + // let the first tag in a template starve the second.
464 737 switch ($match) {
465 738 case '${bf_all_data}':
466 - $fieldValues = self::bindFormData($orderedFormFields, $fieldValues, $formId);
467 - $table = self::generateTable($fieldValues, $orderedFormFields, $formId);
739 + $boundValues = self::bindFormData($orderedFormFields, $fieldValues, $formId);
740 + $table = self::generateTable($boundValues, $orderedFormFields, $formId);
468 741 $stringToReplaceField = str_replace('${bf_all_data}', $table, $stringToReplaceField);
469 742 break;
470 743
471 744 case '${bf_all_data.onlyValues}':
472 - $fieldValues = self::bindFormData($orderedFormFields, $fieldValues, $formId, true);
473 - $table = self::generateTable($fieldValues, $orderedFormFields, $formId);
745 + $boundValues = self::bindFormData($orderedFormFields, $fieldValues, $formId, true);
746 + $table = self::generateTable($boundValues, $orderedFormFields, $formId);
474 747 $stringToReplaceField = str_replace('${bf_all_data.onlyValues}', $table, $stringToReplaceField);
475 748 break;
476 749 default:
477 750 Log::debug_log([
@@ -574,10 +847,11 @@
574 847 }
575 848
576 849 // Skip processing for hidden or empty fields only when $isOnlyValues is true
577 850 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]) {
851 + // Blank means empty string, null, or an array with nothing in it (unchecked
852 + // checkbox group, file field with no upload). 0 is a real value.
853 + if (!isset($formData[$key]) || self::isBlank($formData[$key])) {
580 854 return $fieldNewData;
581 855 }
582 856
583 857 if (isset($field['valid']['hide']) && $field['valid']['hide']) {
@@ -705,12 +979,13 @@
705 979 }
706 980 } elseif (self::isCompositeFieldType($fieldType)) {
707 981 $table .= self::joinCompositeFieldValue($value, $fieldType);
708 982 } elseif ('signature' === $fieldType) {
709 - if ('signature-failed.png' === $subValue) {
710 - $table .= '';
711 - } else {
712 - $table .= self::imgMarkup($value);
983 + // A signature arrives here wrapped in a one-item list; the failed-capture
984 + // placeholder renders nothing.
985 + $signature = reset($value);
986 + if (false !== $signature && 'signature-failed.png' !== $signature) {
987 + $table .= self::imgMarkup($signature);
713 988 }
714 989 }
715 990 } else {
716 991 $table .= $value;