| @@ -21,8 +21,12 @@ | ||
| 21 | 21 | 'key' => $fieldKey, |
| 22 | 22 | 'value' => $value, |
| 23 | 23 | 'type' => $fieldDetail->typ, |
| 24 | 24 | ]; |
| 25 | + if ('advanced-datetime' === $fieldData[$fieldKey]['type']) { | |
| 26 | + // Date conditions must parse the posted value in the field's own format. | |
| 27 | + $fieldData[$fieldKey]['dateFormat'] = DateConditionComparator::advancedValueFormat(isset($fieldDetail->config) ? $fieldDetail->config : null); | |
| 28 | + } | |
| 25 | 29 | if (isset($fieldDetail->mul)) { |
| 26 | 30 | $fieldData[$fieldKey] = |
| 27 | 31 | array_merge( |
| 28 | 32 | $fieldData[$fieldKey], |
| @@ -30,8 +34,13 @@ | ||
| 30 | 34 | 'mul' => $fieldDetail->mul, |
| 31 | 35 | ] |
| 32 | 36 | ); |
| 33 | 37 | } |
| 38 | + // A multiple-choice Image Select holds a list, like a checkbox; compared as one string | |
| 39 | + // only its last choice counted. | |
| 40 | + if ('image-select' === $fieldDetail->typ && isset($fieldDetail->inpType) && 'checkbox' === $fieldDetail->inpType) { | |
| 41 | + $fieldData[$fieldKey]['mul'] = true; | |
| 42 | + } | |
| 34 | 43 | } |
| 35 | 44 | return $fieldData; |
| 36 | 45 | } |
| 37 | 46 | |
| @@ -88,9 +97,16 @@ | ||
| 88 | 97 | || isset($conditionalActionIds['successMsg'][(string) $msg->id]) |
| 89 | 98 | ) { |
| 90 | 99 | continue; |
| 91 | 100 | } |
| 92 | - $returnableData = self::replaceFieldWithValue($msg->message_content, $fieldValue, true, null, true); | |
| 101 | + // Translate before smart-tag replacement (identity when unhooked). | |
| 102 | + $messageContent = (string) apply_filters( | |
| 103 | + 'bitform_translate_form_string', | |
| 104 | + (string) $msg->message_content, | |
| 105 | + 'msg-content-' . $msg->id, | |
| 106 | + $formId | |
| 107 | + ); | |
| 108 | + $returnableData = self::replaceFieldWithValue($messageContent, $fieldValue, true, null, true); | |
| 93 | 109 | $messageId = $msg->id; |
| 94 | 110 | if (isset($msgConfig->afterSubmit)) { |
| 95 | 111 | $afterSubmit = $msgConfig->afterSubmit; |
| 96 | 112 | } |
| @@ -109,8 +125,15 @@ | ||
| 109 | 125 | continue; |
| 110 | 126 | } |
| 111 | 127 | $url = Utilities::jsonObj($redirectPage->integration_details ?? '')->url ?? ''; |
| 112 | 128 | if (!empty($url)) { |
| 129 | + // Translated before smart-tag replacement: per-language redirect targets. | |
| 130 | + $url = (string) apply_filters( | |
| 131 | + 'bitform_translate_form_string', | |
| 132 | + (string) $url, | |
| 133 | + 'redirect-url-' . $redirectPage->id, | |
| 134 | + $formId | |
| 135 | + ); | |
| 113 | 136 | $url = self::replaceFieldWithValue($url, $fieldValue); |
| 114 | 137 | } |
| 115 | 138 | $returnableData = empty($url) ? '' : esc_url_raw($url); |
| 116 | 139 | break; |
| @@ -304,22 +327,15 @@ | ||
| 304 | 327 | } |
| 305 | 328 | return $ids; |
| 306 | 329 | } |
| 307 | 330 | |
| 331 | + /** | |
| 332 | + * @deprecated misspelled duplicate of calculate(); kept because it is public API. Same | |
| 333 | + * divide-by-zero guard so an external caller cannot fatal either. | |
| 334 | + */ | |
| 308 | 335 | public static function calculte($firstOperand, $secondOperand, $operator) |
| 309 | 336 | { |
| 310 | - switch ($operator) { | |
| 311 | - case '+': | |
| 312 | - return $firstOperand + $secondOperand; | |
| 313 | - case '-': | |
| 314 | - return $firstOperand - $secondOperand; | |
| 315 | - case '*': | |
| 316 | - return $firstOperand * $secondOperand; | |
| 317 | - case '/': | |
| 318 | - return $firstOperand / $secondOperand; | |
| 319 | - case '^': | |
| 320 | - return $firstOperand ** $secondOperand; | |
| 321 | - } | |
| 337 | + return self::calculate($firstOperand, $secondOperand, $operator); | |
| 322 | 338 | } |
| 323 | 339 | |
| 324 | 340 | public static function filterMailContentType() |
| 325 | 341 | { |
| @@ -328,11 +344,26 @@ | ||
| 328 | 344 | |
| 329 | 345 | public static function evalMathExpression($stringWithFieldValue) |
| 330 | 346 | { |
| 331 | 347 | $mathExpr = $stringWithFieldValue; |
| 332 | - if (empty($mathExpr)) { | |
| 348 | + if (empty($mathExpr) || !\is_scalar($mathExpr)) { | |
| 333 | 349 | return $stringWithFieldValue; |
| 334 | 350 | } |
| 351 | + $mathExpr = (string) $mathExpr; | |
| 352 | + | |
| 353 | + // The operand/operator checks below only look at \w+ runs and operator runs, so any other | |
| 354 | + // character was invisible to them. A quoted date ('2020-10-10') therefore passed as a | |
| 355 | + // subtraction chain and its quotes later surfaced as a bogus operator token. Require the whole | |
| 356 | + // string to be made of things a formula can contain. | |
| 357 | + if (1 !== preg_match('#^[0-9.+\-*/^()\[\]{}\s]+$#', $mathExpr)) { | |
| 358 | + return $stringWithFieldValue; | |
| 359 | + } | |
| 360 | + | |
| 361 | + // A bare date is not a subtraction: 2020-10-10 must stay a date, not become 2000. | |
| 362 | + if (1 === preg_match('/^\s*\d{4}-\d{1,2}-\d{1,2}\s*$/', $mathExpr)) { | |
| 363 | + return $stringWithFieldValue; | |
| 364 | + } | |
| 365 | + | |
| 335 | 366 | preg_match_all('/[\+\-\*\/\s]+/', $mathExpr, $isMathExpr); |
| 336 | 367 | if (empty($isMathExpr[0])) { |
| 337 | 368 | return $stringWithFieldValue; |
| 338 | 369 | } |
| @@ -354,9 +385,9 @@ | ||
| 354 | 385 | $mathExpr = str_replace(' ', '', $mathExpr); |
| 355 | 386 | $mathExpr = preg_replace('/\{|\[|\(/', '(', $mathExpr); |
| 356 | 387 | $mathExpr = preg_replace('/\}|\]/', ')', $mathExpr); |
| 357 | 388 | $calculated = self::infixToPostfixEvalute($mathExpr); |
| 358 | - if (!is_null($calculated)) { | |
| 389 | + if (!is_null($calculated) && isset($calculated[0])) { | |
| 359 | 390 | return (string) $calculated[0]; |
| 360 | 391 | } |
| 361 | 392 | |
| 362 | 393 | return (string) $stringWithFieldValue; |
| @@ -380,10 +411,18 @@ | ||
| 380 | 411 | } |
| 381 | 412 | if ('(' === $token) { |
| 382 | 413 | $operatorStack[] = $token; |
| 383 | 414 | } elseif (')' === $token) { |
| 415 | + // An unbalanced ')' used to read $operatorStack[-1] and warn on every iteration; treat | |
| 416 | + // the expression as non-arithmetic instead. | |
| 417 | + if (empty($operatorStack)) { | |
| 418 | + return null; | |
| 419 | + } | |
| 384 | 420 | while ('(' !== $operatorStack[count($operatorStack) - 1]) { |
| 385 | 421 | $outputQueue[] = array_pop($operatorStack); |
| 422 | + if (empty($operatorStack)) { | |
| 423 | + return null; | |
| 424 | + } | |
| 386 | 425 | if ('(' === $operatorStack[count($operatorStack) - 1]) { |
| 387 | 426 | array_pop($operatorStack); |
| 388 | 427 | break; |
| 389 | 428 | } |
| @@ -412,11 +451,22 @@ | ||
| 412 | 451 | if (is_numeric($value)) { |
| 413 | 452 | $resultStack[] = $value; |
| 414 | 453 | continue; |
| 415 | 454 | } |
| 455 | + // A token that is neither a number nor a real operator means the input was never an | |
| 456 | + // expression — e.g. a quoted date whose quotes accumulated into a token like "'2020". | |
| 457 | + // Bail out so evalMathExpression() returns the caller's string untouched; pushing the | |
| 458 | + // failure onto the stack would end up blanking that string. | |
| 459 | + if (!\in_array($value, ['+', '-', '*', '/', '^'], true) || count($resultStack) < 2) { | |
| 460 | + return null; | |
| 461 | + } | |
| 416 | 462 | $secondOperand = array_pop($resultStack); |
| 417 | 463 | $firstOperand = array_pop($resultStack); |
| 418 | - $resultStack[] = self::calculate($firstOperand, $secondOperand, $value); | |
| 464 | + $calculated = self::calculate($firstOperand, $secondOperand, $value); | |
| 465 | + if (\is_null($calculated)) { | |
| 466 | + return null; | |
| 467 | + } | |
| 468 | + $resultStack[] = $calculated; | |
| 419 | 469 | } |
| 420 | 470 | return $resultStack; |
| 421 | 471 | } |
| 422 | 472 | |
| @@ -432,19 +482,41 @@ | ||
| 432 | 482 | |
| 433 | 483 | return isset($precedence[$operator]) ? $precedence[$operator] : 0; |
| 434 | 484 | } |
| 435 | 485 | |
| 486 | + /** | |
| 487 | + * Apply one arithmetic operator. | |
| 488 | + * | |
| 489 | + * @param mixed $firstOperand | |
| 490 | + * @param mixed $secondOperand | |
| 491 | + * @param string $operator | |
| 492 | + * | |
| 493 | + * @return float|int|null null on unknown operator, non-numeric operand, or division by zero | |
| 494 | + */ | |
| 436 | 495 | public static function calculate($firstOperand, $secondOperand, $operator) |
| 437 | 496 | { |
| 438 | - $calculated = [ | |
| 439 | - '+' => $firstOperand + $secondOperand, | |
| 440 | - '-' => $firstOperand - $secondOperand, | |
| 441 | - '*' => $firstOperand * $secondOperand, | |
| 442 | - '/' => $firstOperand / $secondOperand, | |
| 443 | - '^' => $firstOperand ** $secondOperand, | |
| 444 | - ]; | |
| 497 | + if (!is_numeric($firstOperand) || !is_numeric($secondOperand)) { | |
| 498 | + return null; | |
| 499 | + } | |
| 500 | + $firstOperand = $firstOperand + 0; | |
| 501 | + $secondOperand = $secondOperand + 0; | |
| 445 | 502 | |
| 446 | - return isset($calculated[$operator]) ? $calculated[$operator] : ''; | |
| 503 | + switch ($operator) { | |
| 504 | + case '+': | |
| 505 | + return $firstOperand + $secondOperand; | |
| 506 | + case '-': | |
| 507 | + return $firstOperand - $secondOperand; | |
| 508 | + case '*': | |
| 509 | + return $firstOperand * $secondOperand; | |
| 510 | + case '/': | |
| 511 | + // Compare as float, not with `0 ==`: PHP 8 made `0 == ''` false, so a | |
| 512 | + // non-numeric operand slipped past the old guard into DivisionByZeroError. | |
| 513 | + return 0.0 === (float) $secondOperand ? null : $firstOperand / $secondOperand; | |
| 514 | + case '^': | |
| 515 | + return $firstOperand ** $secondOperand; | |
| 516 | + } | |
| 517 | + | |
| 518 | + return null; | |
| 447 | 519 | } |
| 448 | 520 | |
| 449 | 521 | /** |
| 450 | 522 | * Recursively sets a nested property in a given object. |