diff --git a/app/Api/V1/Controllers/Models/Rule/ExpressionController.php b/app/Api/V1/Controllers/Models/Rule/ExpressionController.php index dae57c3261..36588ae58f 100644 --- a/app/Api/V1/Controllers/Models/Rule/ExpressionController.php +++ b/app/Api/V1/Controllers/Models/Rule/ExpressionController.php @@ -74,15 +74,14 @@ class ExpressionController extends Controller $expressionLanguage = ExpressionLanguageFactory::get(); $evaluator = new ActionExpressionEvaluator($expressionLanguage, $expr); - try { - $evaluator->lint(); + if ($evaluator->isValid()) { return response()->json([ "valid" => true, ]); - } catch (SyntaxError $e) { + } else { return response()->json([ "valid" => false, - "error" => $e->getMessage() + "error" => $evaluator->getValidationError()->getMessage() ]); } } diff --git a/app/Api/V1/Requests/Models/Rule/StoreRequest.php b/app/Api/V1/Requests/Models/Rule/StoreRequest.php index dff5c76d8d..03532a2465 100644 --- a/app/Api/V1/Requests/Models/Rule/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Rule/StoreRequest.php @@ -123,7 +123,7 @@ class StoreRequest extends FormRequest 'triggers.*.stop_processing' => [new IsBoolean()], 'triggers.*.active' => [new IsBoolean()], 'actions.*.type' => 'required|in:'.implode(',', $validActions), - 'actions.*.value' => 'required_if:actions.*.type,'.$contextActions.'|ruleActionValue', + 'actions.*.value' => 'required_if:actions.*.type,'.$contextActions.'|ruleActionExpression|ruleActionValue', 'actions.*.stop_processing' => [new IsBoolean()], 'actions.*.active' => [new IsBoolean()], 'strict' => [new IsBoolean()], diff --git a/app/Api/V1/Requests/Models/Rule/UpdateRequest.php b/app/Api/V1/Requests/Models/Rule/UpdateRequest.php index 3e89716d7f..64fc66455b 100644 --- a/app/Api/V1/Requests/Models/Rule/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/Rule/UpdateRequest.php @@ -140,7 +140,7 @@ class UpdateRequest extends FormRequest 'triggers.*.stop_processing' => [new IsBoolean()], 'triggers.*.active' => [new IsBoolean()], 'actions.*.type' => 'required|in:'.implode(',', $validActions), - 'actions.*.value' => 'required_if:actions.*.type,'.$contextActions.'|ruleActionValue', + 'actions.*.value' => 'required_if:actions.*.type,'.$contextActions.'|ruleActionExpression|ruleActionValue', 'actions.*.stop_processing' => [new IsBoolean()], 'actions.*.active' => [new IsBoolean()], 'strict' => [new IsBoolean()], diff --git a/app/Http/Requests/RuleFormRequest.php b/app/Http/Requests/RuleFormRequest.php index 6d6f831815..2bc74cc52c 100644 --- a/app/Http/Requests/RuleFormRequest.php +++ b/app/Http/Requests/RuleFormRequest.php @@ -147,7 +147,7 @@ class RuleFormRequest extends FormRequest 'triggers.*.type' => 'required|in:'.implode(',', $validTriggers), 'triggers.*.value' => sprintf('required_if:triggers.*.type,%s|max:1024|min:1|ruleTriggerValue', $contextTriggers), 'actions.*.type' => 'required|in:'.implode(',', $validActions), - 'actions.*.value' => sprintf('required_if:actions.*.type,%s|min:0|max:1024|ruleActionValue', $contextActions), + 'actions.*.value' => sprintf('required_if:actions.*.type,%s|min:0|max:1024|ruleActionExpression|ruleActionValue', $contextActions), 'strict' => 'in:0,1', ]; diff --git a/app/TransactionRules/Expressions/ActionExpressionEvaluator.php b/app/TransactionRules/Expressions/ActionExpressionEvaluator.php index 1f2fe1b6cc..7e0b1a3175 100644 --- a/app/TransactionRules/Expressions/ActionExpressionEvaluator.php +++ b/app/TransactionRules/Expressions/ActionExpressionEvaluator.php @@ -31,9 +31,10 @@ class ActionExpressionEvaluator { private static array $NAMES = array("transaction"); + private ExpressionLanguage $expressionLanguage; private string $expr; private bool $isExpression; - private ExpressionLanguage $expressionLanguage; + private ?SyntaxError $validationError; public function __construct(ExpressionLanguage $expressionLanguage, string $expr) { @@ -41,6 +42,7 @@ class ActionExpressionEvaluator $this->expr = $expr; $this->isExpression = self::isExpression($expr); + $this->validationError = $this->validate(); } private static function isExpression(string $expr): bool @@ -48,17 +50,17 @@ class ActionExpressionEvaluator return str_starts_with($expr, "="); } - public function isValid(): bool + private function validate(): ?SyntaxError { if (!$this->isExpression) { - return true; + return null; } try { - $this->lint(array()); - return true; + $this->lint(); + return null; } catch (SyntaxError $e) { - return false; + return $e; } } @@ -67,7 +69,7 @@ class ActionExpressionEvaluator $this->expressionLanguage->lint($expr, self::$NAMES); } - public function lint(): void + private function lint(): void { if (!$this->isExpression) { return; @@ -76,6 +78,16 @@ class ActionExpressionEvaluator $this->lintExpression(substr($this->expr, 1)); } + public function isValid(): bool + { + return $this->validationError === null; + } + + public function getValidationError() + { + return $this->validationError; + } + private function evaluateExpression(string $expr, array $journal): string { $result = $this->expressionLanguage->evaluate($expr, [ diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index 891fcc6163..bcfb1c3d46 100644 --- a/app/Validation/FireflyValidator.php +++ b/app/Validation/FireflyValidator.php @@ -35,6 +35,8 @@ use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use FireflyIII\Services\Password\Verifier; use FireflyIII\Support\ParseDateString; +use FireflyIII\TransactionRules\Expressions\ActionExpressionEvaluator; +use FireflyIII\TransactionRules\Factory\ExpressionLanguageFactory; use FireflyIII\User; use Illuminate\Validation\Validator; use PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException; @@ -253,6 +255,31 @@ class FireflyValidator extends Validator return 1 === $count; } + public function validateRuleActionExpression(string $attribute, string $value = null): bool + { + $value ??= ''; + + $el = ExpressionLanguageFactory::get(); + $evaluator = new ActionExpressionEvaluator($el, $value); + + return $evaluator->isValid(); + } + + public function replaceRuleActionExpression(string $message, string $attribute): string + { + $value = $this->getValue($attribute); + + $el = ExpressionLanguageFactory::get(); + $evaluator = new ActionExpressionEvaluator($el, $value); + $err = $evaluator->getValidationError(); + + if ($err == null) { + return $message; + } + + return str_replace(":error", $err->getMessage(), $message); + } + public function validateRuleActionValue(string $attribute, string $value = null): bool { // first, get the index from this string: @@ -268,6 +295,11 @@ class FireflyValidator extends Validator return false; } + // if value is an expression, assume valid + if (str_starts_with($value, '=')) { + return true; + } + // if it's set_budget, verify the budget name: if ('set_budget' === $actionType) { /** @var BudgetRepositoryInterface $repository */ diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index cf6e6fde38..1d01e82c87 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -46,6 +46,7 @@ return [ 'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"', 'deleted_user' => 'Due to security constraints, you cannot register using this email address.', 'rule_trigger_value' => 'This value is invalid for the selected trigger.', + 'rule_action_expression' => 'Invalid expression. :error', 'rule_action_value' => 'This value is invalid for the selected action.', 'file_already_attached' => 'Uploaded file ":name" is already attached to this object.', 'file_attached' => 'Successfully uploaded file ":name".',