From e4aff5ff4c94ff3a4f2493d46ad1d9d5583e55a9 Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 21 Aug 2025 20:07:12 +0200 Subject: [PATCH] Update webhook code. --- .../Requests/Models/Webhook/CreateRequest.php | 50 +-------- .../Requests/Models/Webhook/UpdateRequest.php | 52 +-------- app/Enums/WebhookTrigger.php | 1 + .../Webhook/StandardMessageGenerator.php | 105 +++++++++++------- app/Support/Request/ValidatesWebhooks.php | 69 ++++++++++++ config/webhooks.php | 7 +- resources/lang/en_US/firefly.php | 1 + resources/lang/en_US/validation.php | 1 + 8 files changed, 148 insertions(+), 138 deletions(-) create mode 100644 app/Support/Request/ValidatesWebhooks.php diff --git a/app/Api/V1/Requests/Models/Webhook/CreateRequest.php b/app/Api/V1/Requests/Models/Webhook/CreateRequest.php index 51bef14ad3..71093b3ec0 100644 --- a/app/Api/V1/Requests/Models/Webhook/CreateRequest.php +++ b/app/Api/V1/Requests/Models/Webhook/CreateRequest.php @@ -29,6 +29,7 @@ use FireflyIII\Models\Webhook; use FireflyIII\Rules\IsBoolean; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; +use FireflyIII\Support\Request\ValidatesWebhooks; use Illuminate\Contracts\Validation\Validator; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Log; @@ -40,6 +41,7 @@ class CreateRequest extends FormRequest { use ChecksLogin; use ConvertsDataTypes; + use ValidatesWebhooks; public function getData(): array { @@ -90,52 +92,4 @@ class CreateRequest extends FormRequest 'url' => ['required', sprintf('url:%s', $validProtocols)], ]; } - - public function withValidator(Validator $validator): void - { - $validator->after( - function (Validator $validator): void { - Log::debug('Validating webhook'); - if ($validator->failed()) { - return; - } - $data = $validator->getData(); - $triggers = $data['triggers'] ?? []; - $responses = $data['responses'] ?? []; - - if (0 === count($triggers) || 0 === count($responses)) { - Log::debug('No trigger or response, return.'); - - return; - } - $validTriggers = array_values(Webhook::getTriggers()); - $validResponses = array_values(Webhook::getResponses()); - foreach ($triggers as $trigger) { - if (!in_array($trigger, $validTriggers, true)) { - return; - } - } - foreach ($responses as $response) { - if (!in_array($response, $validResponses, true)) { - return; - } - } - // some combinations are illegal. - foreach ($triggers as $i => $trigger) { - $forbidden = config(sprintf('webhooks.forbidden_responses.%s', $trigger)); - if (null === $forbidden) { - $validator->errors()->add(sprintf('triggers.%d', $i), trans('validation.unknown_webhook_trigger', ['trigger' => $trigger,])); - continue; - } - foreach ($responses as $ii => $response) { - if (in_array($response, $forbidden, true)) { - Log::debug(sprintf('Trigger %s and response %s are forbidden.', $trigger, $response)); - $validator->errors()->add(sprintf('responses.%d', $ii), trans('validation.bad_webhook_combination', ['trigger' => $trigger, 'response' => $response,])); - return; - } - } - } - } - ); - } } diff --git a/app/Api/V1/Requests/Models/Webhook/UpdateRequest.php b/app/Api/V1/Requests/Models/Webhook/UpdateRequest.php index a3c2eed05a..7dcebc8552 100644 --- a/app/Api/V1/Requests/Models/Webhook/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/Webhook/UpdateRequest.php @@ -31,6 +31,7 @@ use FireflyIII\Models\Webhook; use FireflyIII\Rules\IsBoolean; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; +use FireflyIII\Support\Request\ValidatesWebhooks; use Illuminate\Contracts\Validation\Validator; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Log; @@ -42,6 +43,7 @@ class UpdateRequest extends FormRequest { use ChecksLogin; use ConvertsDataTypes; + use ValidatesWebhooks; public function getData(): array { @@ -97,54 +99,4 @@ class UpdateRequest extends FormRequest 'url' => [sprintf('url:%s', $validProtocols), sprintf('uniqueExistingWebhook:%d', $webhook->id)], ]; } - - public function withValidator(Validator $validator): void - { - $validator->after( - function (Validator $validator): void { - Log::debug('Validating webhook'); - - if ($validator->failed()) { - return; - } - $data = $validator->getData(); - $triggers = $data['triggers'] ?? []; - $responses = $data['responses'] ?? []; - - if (0 === count($triggers) || 0 === count($responses)) { - Log::debug('No trigger or response, return.'); - - return; - } - $validTriggers = array_values(Webhook::getTriggers()); - $validResponses = array_values(Webhook::getResponses()); - foreach ($triggers as $trigger) { - if (!in_array($trigger, $validTriggers, true)) { - return; - } - } - foreach ($responses as $response) { - if (!in_array($response, $validResponses, true)) { - return; - } - } - // some combinations are illegal. - foreach ($triggers as $i => $trigger) { - $forbidden = config(sprintf('webhooks.forbidden_responses.%s', $trigger)); - if (null === $forbidden) { - $validator->errors()->add(sprintf('triggers.%d', $i), trans('validation.unknown_webhook_trigger', ['trigger' => $trigger,])); - continue; - } - foreach ($responses as $ii => $response) { - if (in_array($response, $forbidden, true)) { - Log::debug(sprintf('Trigger %s and response %s are forbidden.', $trigger, $response)); - $validator->errors()->add(sprintf('responses.%d', $ii), trans('validation.bad_webhook_combination', ['trigger' => $trigger, 'response' => $response,])); - return; - } - } - } - - } - ); - } } diff --git a/app/Enums/WebhookTrigger.php b/app/Enums/WebhookTrigger.php index 9f2951ff1d..1f64cb6032 100644 --- a/app/Enums/WebhookTrigger.php +++ b/app/Enums/WebhookTrigger.php @@ -29,6 +29,7 @@ namespace FireflyIII\Enums; */ enum WebhookTrigger: int { + case ANY = 50; case STORE_TRANSACTION = 100; case UPDATE_TRANSACTION = 110; case DESTROY_TRANSACTION = 120; diff --git a/app/Generator/Webhook/StandardMessageGenerator.php b/app/Generator/Webhook/StandardMessageGenerator.php index 6877e7581c..f2748f098b 100644 --- a/app/Generator/Webhook/StandardMessageGenerator.php +++ b/app/Generator/Webhook/StandardMessageGenerator.php @@ -30,11 +30,12 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\Transaction; -use FireflyIII\Models\WebhookResponse as WebhookResponseModel; use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\Webhook; use FireflyIII\Models\WebhookMessage; +use FireflyIII\Models\WebhookResponse as WebhookResponseModel; +use FireflyIII\Models\WebhookTrigger as WebhookTriggerModel; use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment; use FireflyIII\Support\JsonApi\Enrichments\BudgetEnrichment; use FireflyIII\Support\JsonApi\Enrichments\BudgetLimitEnrichment; @@ -82,11 +83,11 @@ class StandardMessageGenerator implements MessageGeneratorInterface private function getWebhooks(): Collection { return $this->user->webhooks() - ->leftJoin('webhook_webhook_trigger','webhook_webhook_trigger.webhook_id','webhooks.id') - ->leftJoin('webhook_triggers','webhook_webhook_trigger.webhook_trigger_id','webhook_triggers.id') - ->where('active', true) - ->where('webhook_triggers.title', $this->trigger->name) - ->get(['webhooks.*']); + ->leftJoin('webhook_webhook_trigger', 'webhook_webhook_trigger.webhook_id', 'webhooks.id') + ->leftJoin('webhook_triggers', 'webhook_webhook_trigger.webhook_trigger_id', 'webhook_triggers.id') + ->where('active', true) + ->whereIn('webhook_triggers.title', [$this->trigger->name, WebhookTrigger::ANY->name]) + ->get(['webhooks.*']); } /** @@ -121,23 +122,24 @@ class StandardMessageGenerator implements MessageGeneratorInterface */ private function generateMessage(Webhook $webhook, Model $model): void { - $class = $model::class; + $class = $model::class; // Line is ignored because all of Firefly III's Models have an id property. Log::debug(sprintf('Now in generateMessage(#%d, %s#%d)', $webhook->id, $class, $model->id)); - $uuid = Uuid::uuid4(); + $uuid = Uuid::uuid4(); + /** @var WebhookResponseModel $response */ + $response = $webhook->webhookResponses()->first(); + $triggers = $this->getTriggerTitles($webhook->webhookTriggers()->get()); $basicMessage = [ 'uuid' => $uuid->toString(), 'user_id' => 0, 'user_group_id' => 0, 'trigger' => $this->trigger->name, - 'response' => $webhook->webhookResponses()->first()->title, // guess that the database is correct. + 'response' => $response->title, // guess that the database is correct. 'url' => $webhook->url, 'version' => sprintf('v%d', $this->getVersion()), 'content' => [], ]; - // depends on the model how user_id is set: - $relevantResponse = WebhookResponse::TRANSACTIONS->name; switch ($class) { default: // Line is ignored because all of Firefly III's Models have an id property. @@ -149,14 +151,14 @@ class StandardMessageGenerator implements MessageGeneratorInterface /** @var Budget $model */ $basicMessage['user_id'] = $model->user_id; $basicMessage['user_group_id'] = $model->user_group_id; - $relevantResponse = WebhookResponse::BUDGET->name; + $relevantResponse = WebhookResponse::BUDGET->name; break; case BudgetLimit::class: $basicMessage['user_id'] = $model->budget->user_id; $basicMessage['user_group_id'] = $model->budget->user_group_id; - $relevantResponse = WebhookResponse::BUDGET->name; + $relevantResponse = WebhookResponse::BUDGET->name; break; @@ -167,21 +169,9 @@ class StandardMessageGenerator implements MessageGeneratorInterface break; } + $responseTitle = $this->getRelevantResponse($triggers, $response, $class); - // then depends on the response what to put in the message: - /** @var WebhookResponseModel $webhookResponse */ - $webhookResponse = $webhook->webhookResponses()->first(); - $response = $webhookResponse->title; - Log::debug(sprintf('Expected response for this webhook is "%s".', $response)); - // if it's relevant, just switch to another. - if(WebhookResponse::RELEVANT->name === $response) { - // switch to whatever is actually relevant. - $response = $relevantResponse; - Log::debug(sprintf('Expected response for this webhook is now "%s".', $response)); - } - - - switch ($response) { + switch ($responseTitle) { default: Log::error(sprintf('The response code for webhook #%d is "%s" and the message generator cant handle it. Soft fail.', $webhook->id, $webhook->response)); @@ -190,23 +180,23 @@ class StandardMessageGenerator implements MessageGeneratorInterface case WebhookResponse::BUDGET->name: $basicMessage['content'] = []; if ($model instanceof Budget) { - $enrichment = new BudgetEnrichment(); + $enrichment = new BudgetEnrichment(); $enrichment->setUser($model->user); $model = $enrichment->enrichSingle($model); $transformer = new BudgetTransformer(); $basicMessage['content'] = $transformer->transform($model); } if ($model instanceof BudgetLimit) { - $user = $model->budget->user; - $enrichment = new BudgetLimitEnrichment(); + $user = $model->budget->user; + $enrichment = new BudgetLimitEnrichment(); $enrichment->setUser($user); - $parameters = new ParameterBag(); + $parameters = new ParameterBag(); $parameters->set('start', $model->start_date); $parameters->set('end', $model->end_date); - $model = $enrichment->enrichSingle($model); - $transformer = new BudgetLimitTransformer(); + $model = $enrichment->enrichSingle($model); + $transformer = new BudgetLimitTransformer(); $transformer->setParameters($parameters); $basicMessage['content'] = $transformer->transform($model); } @@ -220,7 +210,7 @@ class StandardMessageGenerator implements MessageGeneratorInterface case WebhookResponse::TRANSACTIONS->name: /** @var TransactionGroup $model */ - $transformer = new TransactionGroupTransformer(); + $transformer = new TransactionGroupTransformer(); try { $basicMessage['content'] = $transformer->transformObject($model); @@ -237,13 +227,13 @@ class StandardMessageGenerator implements MessageGeneratorInterface case WebhookResponse::ACCOUNTS->name: /** @var TransactionGroup $model */ - $accounts = $this->collectAccounts($model); - $enrichment = new AccountEnrichment(); + $accounts = $this->collectAccounts($model); + $enrichment = new AccountEnrichment(); $enrichment->setDate(null); $enrichment->setUser($model->user); - $accounts = $enrichment->enrich($accounts); + $accounts = $enrichment->enrich($accounts); foreach ($accounts as $account) { - $transformer = new AccountTransformer(); + $transformer = new AccountTransformer(); $transformer->setParameters(new ParameterBag()); $basicMessage['content'][] = $transformer->transform($account); } @@ -273,7 +263,7 @@ class StandardMessageGenerator implements MessageGeneratorInterface private function storeMessage(Webhook $webhook, array $message): void { - $webhookMessage = new WebhookMessage(); + $webhookMessage = new WebhookMessage(); $webhookMessage->webhook()->associate($webhook); $webhookMessage->sent = false; $webhookMessage->errored = false; @@ -302,4 +292,41 @@ class StandardMessageGenerator implements MessageGeneratorInterface { $this->webhooks = $webhooks; } + + private function getRelevantResponse(array $triggers, WebhookResponseModel $response, $class): string + { + // return none if none. + if (WebhookResponse::NONE->name === $response->title) { + Log::debug(sprintf('Return "%s" because requested nothing.', WebhookResponse::NONE->name)); + return WebhookResponse::NONE->name; + } + + if (WebhookResponse::RELEVANT->name === $response->title) { + Log::debug('Expected response is any relevant data.'); + // depends on the $class + switch ($class) { + case TransactionGroup::class: + Log::debug(sprintf('Return "%s" because class is %s', WebhookResponse::TRANSACTIONS->name, $class)); + return WebhookResponse::TRANSACTIONS->name; + case Budget::class: + case BudgetLimit::class: + Log::debug(sprintf('Return "%s" because class is %s', WebhookResponse::BUDGET->name, $class)); + return WebhookResponse::BUDGET->name; + default: + throw new FireflyException(sprintf('Cannot deal with "relevant" if the given object is a "%s"', $class)); + } + } + Log::debug(sprintf('Return response again: %s', $response->title)); + return $response->title; + } + + private function getTriggerTitles(Collection $collection): array + { + $return = []; + /** @var WebhookTriggerModel $item */ + foreach ($collection as $item) { + $return[] = $item->title; + } + return array_unique($return); + } } diff --git a/app/Support/Request/ValidatesWebhooks.php b/app/Support/Request/ValidatesWebhooks.php new file mode 100644 index 0000000000..0bc1649515 --- /dev/null +++ b/app/Support/Request/ValidatesWebhooks.php @@ -0,0 +1,69 @@ +after( + function (Validator $validator): void { + Log::debug('Validating webhook'); + if ($validator->failed()) { + return; + } + $data = $validator->getData(); + $triggers = $data['triggers'] ?? []; + $responses = $data['responses'] ?? []; + + if (0 === count($triggers) || 0 === count($responses)) { + Log::debug('No trigger or response, return.'); + + return; + } + $validTriggers = array_values(Webhook::getTriggers()); + $validResponses = array_values(Webhook::getResponses()); + $containsAny = false; + $count = 0; + foreach ($triggers as $trigger) { + if (!in_array($trigger, $validTriggers, true)) { + return; + } + $count++; + if($trigger === WebhookTrigger::ANY->name) { + $containsAny = true; + } + } + if($containsAny && $count > 1) { + $validator->errors()->add('triggers.0', trans('validation.only_any_trigger')); + return; + } + foreach ($responses as $response) { + if (!in_array($response, $validResponses, true)) { + return; + } + } + // some combinations are illegal. + foreach ($triggers as $i => $trigger) { + $forbidden = config(sprintf('webhooks.forbidden_responses.%s', $trigger)); + if (null === $forbidden) { + $validator->errors()->add(sprintf('triggers.%d', $i), trans('validation.unknown_webhook_trigger', ['trigger' => $trigger,])); + continue; + } + foreach ($responses as $ii => $response) { + if (in_array($response, $forbidden, true)) { + Log::debug(sprintf('Trigger %s and response %s are forbidden.', $trigger, $response)); + $validator->errors()->add(sprintf('responses.%d', $ii), trans('validation.bad_webhook_combination', ['trigger' => $trigger, 'response' => $response,])); + return; + } + } + } + } + ); + } +} diff --git a/config/webhooks.php b/config/webhooks.php index 49eaff18cd..323a710c8f 100644 --- a/config/webhooks.php +++ b/config/webhooks.php @@ -10,7 +10,7 @@ return [ WebhookTrigger::STORE_TRANSACTION->name => [ WebhookTrigger::STORE_BUDGET->name, WebhookTrigger::UPDATE_BUDGET->name, - WebhookTrigger::DESTROY_BUDGET->name, + WebhookTrigger::DESTROY_BUDGET->name, WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT->name, ], @@ -49,6 +49,11 @@ return [ ], ], 'forbidden_responses' => [ + WebhookTrigger::ANY->name => [ + WebhookResponse::BUDGET->name, + WebhookResponse::TRANSACTIONS->name, + WebhookResponse::ACCOUNTS->name, + ], WebhookTrigger::STORE_TRANSACTION->name => [ WebhookResponse::BUDGET->name, ], diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 08a16a6e3b..4a2ab3d092 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -241,6 +241,7 @@ return [ 'webhooks_breadcrumb' => 'Webhooks', 'webhooks_menu_disabled' => 'disabled', 'no_webhook_messages' => 'There are no webhook messages', + 'webhook_trigger_ANY' => 'After any event', 'webhook_trigger_STORE_TRANSACTION' => 'After transaction creation', 'webhook_trigger_UPDATE_TRANSACTION' => 'After transaction update', 'webhook_trigger_DESTROY_TRANSACTION' => 'After transaction delete', diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index 33ec3b34e8..a57b2f814c 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -37,6 +37,7 @@ return [ 'prohibited' => 'You must not submit anything in field.', 'bad_webhook_combination' => 'Webhook trigger ":trigger" cannot be combined with webhook response ":response".', 'unknown_webhook_trigger' => 'Unknown webhook trigger ":trigger".', + 'only_any_trigger' => 'If you select the "Any event"-trigger, you may not select any other triggers.', 'bad_type_source' => 'Firefly III can\'t determine the transaction type based on this source account.', 'bad_type_destination' => 'Firefly III can\'t determine the transaction type based on this destination account.', 'missing_where' => 'Array is missing "where"-clause',