| @@ -88,9 +88,16 @@ | ||
| 88 | 88 | || isset($conditionalActionIds['successMsg'][(string) $msg->id]) |
| 89 | 89 | ) { |
| 90 | 90 | continue; |
| 91 | 91 | } |
| 92 | - $returnableData = self::replaceFieldWithValue($msg->message_content, $fieldValue, true, null, true); | |
| 92 | + // Translate before smart-tag replacement (identity when unhooked). | |
| 93 | + $messageContent = (string) apply_filters( | |
| 94 | + 'bitform_translate_form_string', | |
| 95 | + (string) $msg->message_content, | |
| 96 | + 'msg-content-' . $msg->id, | |
| 97 | + $formId | |
| 98 | + ); | |
| 99 | + $returnableData = self::replaceFieldWithValue($messageContent, $fieldValue, true, null, true); | |
| 93 | 100 | $messageId = $msg->id; |
| 94 | 101 | if (isset($msgConfig->afterSubmit)) { |
| 95 | 102 | $afterSubmit = $msgConfig->afterSubmit; |
| 96 | 103 | } |
| @@ -109,8 +116,15 @@ | ||
| 109 | 116 | continue; |
| 110 | 117 | } |
| 111 | 118 | $url = Utilities::jsonObj($redirectPage->integration_details ?? '')->url ?? ''; |
| 112 | 119 | if (!empty($url)) { |
| 120 | + // Translated before smart-tag replacement: per-language redirect targets. | |
| 121 | + $url = (string) apply_filters( | |
| 122 | + 'bitform_translate_form_string', | |
| 123 | + (string) $url, | |
| 124 | + 'redirect-url-' . $redirectPage->id, | |
| 125 | + $formId | |
| 126 | + ); | |
| 113 | 127 | $url = self::replaceFieldWithValue($url, $fieldValue); |
| 114 | 128 | } |
| 115 | 129 | $returnableData = empty($url) ? '' : esc_url_raw($url); |
| 116 | 130 | break; |
| @@ -304,22 +318,15 @@ | ||
| 304 | 318 | } |
| 305 | 319 | return $ids; |
| 306 | 320 | } |
| 307 | 321 | |
| 322 | + /** | |
| 323 | + * @deprecated misspelled duplicate of calculate(); kept because it is public API. Same | |
| 324 | + * divide-by-zero guard so an external caller cannot fatal either. | |
| 325 | + */ | |
| 308 | 326 | public static function calculte($firstOperand, $secondOperand, $operator) |
| 309 | 327 | { |
| 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 | - } | |
| 328 | + return self::calculate($firstOperand, $secondOperand, $operator); | |
| 322 | 329 | } |
| 323 | 330 | |
| 324 | 331 | public static function filterMailContentType() |
| 325 | 332 | { |
| @@ -328,11 +335,26 @@ | ||
| 328 | 335 | |
| 329 | 336 | public static function evalMathExpression($stringWithFieldValue) |
| 330 | 337 | { |
| 331 | 338 | $mathExpr = $stringWithFieldValue; |
| 332 | - if (empty($mathExpr)) { | |
| 339 | + if (empty($mathExpr) || !\is_scalar($mathExpr)) { | |
| 333 | 340 | return $stringWithFieldValue; |
| 334 | 341 | } |
| 342 | + $mathExpr = (string) $mathExpr; | |
| 343 | + | |
| 344 | + // The operand/operator checks below only look at \w+ runs and operator runs, so any other | |
| 345 | + // character was invisible to them. A quoted date ('2020-10-10') therefore passed as a | |
| 346 | + // subtraction chain and its quotes later surfaced as a bogus operator token. Require the whole | |
| 347 | + // string to be made of things a formula can contain. | |
| 348 | + if (1 !== preg_match('#^[0-9.+\-*/^()\[\]{}\s]+$#', $mathExpr)) { | |
| 349 | + return $stringWithFieldValue; | |
| 350 | + } | |
| 351 | + | |
| 352 | + // A bare date is not a subtraction: 2020-10-10 must stay a date, not become 2000. | |
| 353 | + if (1 === preg_match('/^\s*\d{4}-\d{1,2}-\d{1,2}\s*$/', $mathExpr)) { | |
| 354 | + return $stringWithFieldValue; | |
| 355 | + } | |
| 356 | + | |
| 335 | 357 | preg_match_all('/[\+\-\*\/\s]+/', $mathExpr, $isMathExpr); |
| 336 | 358 | if (empty($isMathExpr[0])) { |
| 337 | 359 | return $stringWithFieldValue; |
| 338 | 360 | } |
| @@ -354,9 +376,9 @@ | ||
| 354 | 376 | $mathExpr = str_replace(' ', '', $mathExpr); |
| 355 | 377 | $mathExpr = preg_replace('/\{|\[|\(/', '(', $mathExpr); |
| 356 | 378 | $mathExpr = preg_replace('/\}|\]/', ')', $mathExpr); |
| 357 | 379 | $calculated = self::infixToPostfixEvalute($mathExpr); |
| 358 | - if (!is_null($calculated)) { | |
| 380 | + if (!is_null($calculated) && isset($calculated[0])) { | |
| 359 | 381 | return (string) $calculated[0]; |
| 360 | 382 | } |
| 361 | 383 | |
| 362 | 384 | return (string) $stringWithFieldValue; |
| @@ -380,10 +402,18 @@ | ||
| 380 | 402 | } |
| 381 | 403 | if ('(' === $token) { |
| 382 | 404 | $operatorStack[] = $token; |
| 383 | 405 | } elseif (')' === $token) { |
| 406 | + // An unbalanced ')' used to read $operatorStack[-1] and warn on every iteration; treat | |
| 407 | + // the expression as non-arithmetic instead. | |
| 408 | + if (empty($operatorStack)) { | |
| 409 | + return null; | |
| 410 | + } | |
| 384 | 411 | while ('(' !== $operatorStack[count($operatorStack) - 1]) { |
| 385 | 412 | $outputQueue[] = array_pop($operatorStack); |
| 413 | + if (empty($operatorStack)) { | |
| 414 | + return null; | |
| 415 | + } | |
| 386 | 416 | if ('(' === $operatorStack[count($operatorStack) - 1]) { |
| 387 | 417 | array_pop($operatorStack); |
| 388 | 418 | break; |
| 389 | 419 | } |
| @@ -412,11 +442,22 @@ | ||
| 412 | 442 | if (is_numeric($value)) { |
| 413 | 443 | $resultStack[] = $value; |
| 414 | 444 | continue; |
| 415 | 445 | } |
| 446 | + // A token that is neither a number nor a real operator means the input was never an | |
| 447 | + // expression — e.g. a quoted date whose quotes accumulated into a token like "'2020". | |
| 448 | + // Bail out so evalMathExpression() returns the caller's string untouched; pushing the | |
| 449 | + // failure onto the stack would end up blanking that string. | |
| 450 | + if (!\in_array($value, ['+', '-', '*', '/', '^'], true) || count($resultStack) < 2) { | |
| 451 | + return null; | |
| 452 | + } | |
| 416 | 453 | $secondOperand = array_pop($resultStack); |
| 417 | 454 | $firstOperand = array_pop($resultStack); |
| 418 | - $resultStack[] = self::calculate($firstOperand, $secondOperand, $value); | |
| 455 | + $calculated = self::calculate($firstOperand, $secondOperand, $value); | |
| 456 | + if (\is_null($calculated)) { | |
| 457 | + return null; | |
| 458 | + } | |
| 459 | + $resultStack[] = $calculated; | |
| 419 | 460 | } |
| 420 | 461 | return $resultStack; |
| 421 | 462 | } |
| 422 | 463 | |
| @@ -432,19 +473,41 @@ | ||
| 432 | 473 | |
| 433 | 474 | return isset($precedence[$operator]) ? $precedence[$operator] : 0; |
| 434 | 475 | } |
| 435 | 476 | |
| 477 | + /** | |
| 478 | + * Apply one arithmetic operator. | |
| 479 | + * | |
| 480 | + * @param mixed $firstOperand | |
| 481 | + * @param mixed $secondOperand | |
| 482 | + * @param string $operator | |
| 483 | + * | |
| 484 | + * @return float|int|null null on unknown operator, non-numeric operand, or division by zero | |
| 485 | + */ | |
| 436 | 486 | public static function calculate($firstOperand, $secondOperand, $operator) |
| 437 | 487 | { |
| 438 | - $calculated = [ | |
| 439 | - '+' => $firstOperand + $secondOperand, | |
| 440 | - '-' => $firstOperand - $secondOperand, | |
| 441 | - '*' => $firstOperand * $secondOperand, | |
| 442 | - '/' => $firstOperand / $secondOperand, | |
| 443 | - '^' => $firstOperand ** $secondOperand, | |
| 444 | - ]; | |
| 488 | + if (!is_numeric($firstOperand) || !is_numeric($secondOperand)) { | |
| 489 | + return null; | |
| 490 | + } | |
| 491 | + $firstOperand = $firstOperand + 0; | |
| 492 | + $secondOperand = $secondOperand + 0; | |
| 445 | 493 | |
| 446 | - return isset($calculated[$operator]) ? $calculated[$operator] : ''; | |
| 494 | + switch ($operator) { | |
| 495 | + case '+': | |
| 496 | + return $firstOperand + $secondOperand; | |
| 497 | + case '-': | |
| 498 | + return $firstOperand - $secondOperand; | |
| 499 | + case '*': | |
| 500 | + return $firstOperand * $secondOperand; | |
| 501 | + case '/': | |
| 502 | + // Compare as float, not with `0 ==`: PHP 8 made `0 == ''` false, so a | |
| 503 | + // non-numeric operand slipped past the old guard into DivisionByZeroError. | |
| 504 | + return 0.0 === (float) $secondOperand ? null : $firstOperand / $secondOperand; | |
| 505 | + case '^': | |
| 506 | + return $firstOperand ** $secondOperand; | |
| 507 | + } | |
| 508 | + | |
| 509 | + return null; | |
| 447 | 510 | } |
| 448 | 511 | |
| 449 | 512 | /** |
| 450 | 513 | * Recursively sets a nested property in a given object. |