appwrite/src/Appwrite/Platform/Workers/Webhooks.php

266 lines
10 KiB
PHP
Raw Normal View History

2023-05-29 13:58:45 +00:00
<?php
namespace Appwrite\Platform\Workers;
use Appwrite\Event\Mail;
2025-01-30 04:53:53 +00:00
use Appwrite\Event\StatsUsage;
use Appwrite\Template\Template;
2023-05-29 13:58:45 +00:00
use Exception;
2023-11-14 08:50:26 +00:00
use Utopia\Database\Database;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Document;
2024-01-16 13:39:33 +00:00
use Utopia\Database\Query;
use Utopia\Logger\Log;
2023-05-29 13:58:45 +00:00
use Utopia\Platform\Action;
use Utopia\Queue\Message;
2024-04-01 11:02:47 +00:00
use Utopia\System\System;
2023-05-29 13:58:45 +00:00
class Webhooks extends Action
{
2023-10-01 17:39:26 +00:00
private array $errors = [];
2024-01-08 20:11:48 +00:00
private const MAX_FILE_SIZE = 5242880; // 5 MB
2023-05-29 13:58:45 +00:00
public static function getName(): string
{
return 'webhooks';
}
2023-06-04 16:25:56 +00:00
/**
* @throws Exception
*/
2023-05-29 13:58:45 +00:00
public function __construct()
{
$this
->desc('Webhooks worker')
->inject('message')
2025-01-16 06:05:22 +00:00
->inject('project')
->inject('dbForPlatform')
->inject('queueForMails')
2025-01-30 04:53:53 +00:00
->inject('queueForStatsUsage')
->inject('log')
2025-05-12 13:10:58 +00:00
->inject('plan')
2025-06-04 08:37:43 +00:00
->callback($this->action(...));
2023-05-29 13:58:45 +00:00
}
2023-06-02 03:54:34 +00:00
/**
2023-10-01 17:39:26 +00:00
* @param Message $message
2025-01-16 06:05:22 +00:00
* @param Document $project
* @param Database $dbForPlatform
* @param Mail $queueForMails
2025-05-12 13:10:58 +00:00
* @param StatsUsage $queueForStatsUsage
* @param Log $log
2025-05-12 13:10:58 +00:00
* @param array $plan
2023-10-01 17:39:26 +00:00
* @return void
2023-06-02 03:54:34 +00:00
* @throws Exception
*/
2025-05-12 13:10:58 +00:00
public function action(Message $message, Document $project, Database $dbForPlatform, Mail $queueForMails, StatsUsage $queueForStatsUsage, Log $log, array $plan): void
2023-05-29 13:58:45 +00:00
{
2024-01-18 09:13:11 +00:00
$this->errors = [];
2023-05-29 13:58:45 +00:00
$payload = $message->getPayload() ?? [];
2023-06-04 08:19:49 +00:00
2025-04-11 14:52:19 +00:00
2023-05-29 13:58:45 +00:00
if (empty($payload)) {
throw new Exception('Missing payload');
2023-06-04 16:25:56 +00:00
}
2025-03-02 20:27:13 +00:00
2023-06-04 16:25:56 +00:00
$events = $payload['events'];
$webhookPayload = json_encode($payload['payload']);
$user = new Document($payload['user'] ?? []);
2023-05-29 13:58:45 +00:00
$log->addTag('projectId', $project->getId());
2023-06-04 16:25:56 +00:00
foreach ($project->getAttribute('webhooks', []) as $webhook) {
2024-01-05 09:43:54 +00:00
if (array_intersect($webhook->getAttribute('events', []), $events)) {
2025-05-12 13:10:58 +00:00
$this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForPlatform, $queueForMails, $queueForStatsUsage, $plan);
2023-05-29 13:58:45 +00:00
}
2023-06-04 16:25:56 +00:00
}
2023-05-29 13:58:45 +00:00
2023-10-01 17:39:26 +00:00
if (!empty($this->errors)) {
throw new Exception(\implode(" / \n\n", $this->errors));
2023-06-04 16:25:56 +00:00
}
2023-05-29 13:58:45 +00:00
}
2023-10-01 17:39:26 +00:00
/**
* @param array $events
* @param string $payload
* @param Document $webhook
* @param Document $user
* @param Document $project
* @param Database $dbForPlatform
* @param Mail $queueForMails
2025-05-12 13:10:58 +00:00
* @param array $plan
2023-10-01 17:39:26 +00:00
* @return void
*/
2025-05-12 13:10:58 +00:00
private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForPlatform, Mail $queueForMails, StatsUsage $queueForStatsUsage, array $plan): void
2023-05-29 13:58:45 +00:00
{
2024-01-05 09:43:54 +00:00
if ($webhook->getAttribute('enabled') !== true) {
2023-11-14 08:50:26 +00:00
return;
}
2023-06-11 14:08:48 +00:00
2023-05-29 13:58:45 +00:00
$url = \rawurldecode($webhook->getAttribute('url'));
$signatureKey = $webhook->getAttribute('signatureKey');
$signature = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$httpUser = $webhook->getAttribute('httpUser');
$httpPass = $webhook->getAttribute('httpPass');
$ch = \curl_init($webhook->getAttribute('url'));
2023-10-01 17:39:26 +00:00
2023-05-29 13:58:45 +00:00
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
\curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
\curl_setopt($ch, CURLOPT_HEADER, 0);
2024-01-06 18:52:28 +00:00
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
2023-11-14 08:50:26 +00:00
\curl_setopt($ch, CURLOPT_TIMEOUT, 15);
2024-01-08 20:11:48 +00:00
\curl_setopt($ch, CURLOPT_MAXFILESIZE, self::MAX_FILE_SIZE);
2023-05-29 13:58:45 +00:00
\curl_setopt($ch, CURLOPT_USERAGENT, \sprintf(
APP_USERAGENT,
2024-06-04 01:31:05 +00:00
System::getEnv('_APP_VERSION', 'UNKNOWN'),
System::getEnv('_APP_EMAIL_SECURITY', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY))
2023-05-29 13:58:45 +00:00
));
\curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
[
2023-11-14 08:59:46 +00:00
'Content-Type: application/json',
'Content-Length: ' . \strlen($payload),
'X-' . APP_NAME . '-Webhook-Id: ' . $webhook->getId(),
'X-' . APP_NAME . '-Webhook-Events: ' . implode(',', $events),
'X-' . APP_NAME . '-Webhook-Name: ' . $webhook->getAttribute('name', ''),
'X-' . APP_NAME . '-Webhook-User-Id: ' . $user->getId(),
'X-' . APP_NAME . '-Webhook-Project-Id: ' . $project->getId(),
'X-' . APP_NAME . '-Webhook-Signature: ' . $signature,
2023-05-29 13:58:45 +00:00
]
);
2023-11-14 08:59:46 +00:00
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
2023-05-29 13:58:45 +00:00
if (!$webhook->getAttribute('security', true)) {
\curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
\curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
}
if (!empty($httpUser) && !empty($httpPass)) {
\curl_setopt($ch, CURLOPT_USERPWD, "$httpUser:$httpPass");
\curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
}
2024-01-06 19:25:41 +00:00
$responseBody = \curl_exec($ch);
$curlError = \curl_error($ch);
$statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
\curl_close($ch);
2024-01-06 18:52:28 +00:00
2024-01-06 19:25:41 +00:00
if (!empty($curlError) || $statusCode >= 400) {
$dbForPlatform->increaseDocumentAttribute('webhooks', $webhook->getId(), 'attempts', 1);
$webhook = $dbForPlatform->getDocument('webhooks', $webhook->getId());
2023-12-14 20:15:17 +00:00
$attempts = $webhook->getAttribute('attempts');
2024-01-06 18:52:28 +00:00
2024-01-06 19:25:41 +00:00
$logs = '';
$logs .= 'URL: ' . $webhook->getAttribute('url') . "\n";
$logs .= 'Method: ' . 'POST' . "\n";
2024-01-06 18:52:28 +00:00
2024-01-06 19:25:41 +00:00
if (!empty($curlError)) {
$logs .= 'CURL Error: ' . $curlError . "\n";
$logs .= 'Events: ' . implode(', ', $events) . "\n";
2024-01-06 18:52:28 +00:00
} else {
2024-01-06 19:25:41 +00:00
$logs .= 'Status code: ' . $statusCode . "\n";
$logs .= 'Body: ' . "\n" . \mb_strcut($responseBody, 0, 10000) . "\n"; // Limit to 10kb
2024-01-06 18:52:28 +00:00
}
2023-12-14 20:15:17 +00:00
$webhook->setAttribute('logs', $logs);
2023-11-14 08:50:26 +00:00
2024-04-01 11:02:47 +00:00
if ($attempts >= \intval(System::getEnv('_APP_WEBHOOK_MAX_FAILED_ATTEMPTS', '10'))) {
2024-01-15 11:37:12 +00:00
$webhook->setAttribute('enabled', false);
2025-05-12 13:10:58 +00:00
$this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForPlatform, $queueForMails, $plan);
2023-11-14 08:50:26 +00:00
}
$dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
2023-11-14 08:50:26 +00:00
2023-12-14 20:15:17 +00:00
$this->errors[] = $logs;
2025-01-30 04:53:53 +00:00
$queueForStatsUsage
2024-12-24 08:50:09 +00:00
->addMetric(METRIC_WEBHOOKS_FAILED, 1)
2025-05-26 05:42:11 +00:00
->addMetric(str_replace('{webhookInternalId}', $webhook->getSequence(), METRIC_WEBHOOK_ID_FAILED), 1)
2024-12-24 09:09:41 +00:00
;
2024-12-24 08:50:09 +00:00
2024-12-23 17:44:10 +00:00
} else {
2024-01-06 18:52:28 +00:00
$webhook->setAttribute('attempts', 0); // Reset attempts on success
$dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
2025-01-30 04:53:53 +00:00
$queueForStatsUsage
2024-12-24 08:50:09 +00:00
->addMetric(METRIC_WEBHOOKS_SENT, 1)
2025-05-26 05:42:11 +00:00
->addMetric(str_replace('{webhookInternalId}', $webhook->getSequence(), METRIC_WEBHOOK_ID_SENT), 1)
2024-12-24 08:50:09 +00:00
;
2023-05-29 13:58:45 +00:00
}
2024-12-23 17:44:10 +00:00
2025-01-30 04:53:53 +00:00
$queueForStatsUsage
2024-12-23 17:44:10 +00:00
->setProject($project)
->trigger();
2023-05-29 13:58:45 +00:00
}
2024-01-18 09:49:57 +00:00
/**
* @param int $attempts
* @param mixed $statusCode
* @param Document $webhook
* @param Document $project
* @param Database $dbForPlatform
2024-01-18 09:49:57 +00:00
* @param Mail $queueForMails
2025-05-12 13:10:58 +00:00
* @param array $plan
2024-01-18 09:49:57 +00:00
* @return void
*/
2025-05-12 13:10:58 +00:00
public function sendEmailAlert(int $attempts, mixed $statusCode, Document $webhook, Document $project, Database $dbForPlatform, Mail $queueForMails, array $plan): void
2024-01-18 09:49:57 +00:00
{
$memberships = $dbForPlatform->find('memberships', [
2024-01-18 09:49:57 +00:00
Query::equal('teamInternalId', [$project->getAttribute('teamInternalId')]),
Query::limit(APP_LIMIT_SUBQUERY)
]);
$userIds = array_column(\array_map(fn ($membership) => $membership->getArrayCopy(), $memberships), 'userId');
$users = $dbForPlatform->find('users', [
2024-01-18 09:49:57 +00:00
Query::equal('$id', $userIds),
]);
$projectId = $project->getId();
2025-06-12 06:13:11 +00:00
$region = $project->getAttribute('region', 'default');
2024-01-18 09:49:57 +00:00
$webhookId = $webhook->getId();
$template = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-webhook-failed.tpl');
$template->setParam('{{webhook}}', $webhook->getAttribute('name'));
$template->setParam('{{project}}', $project->getAttribute('name'));
$template->setParam('{{url}}', $webhook->getAttribute('url'));
$template->setParam('{{error}}', $curlError ?? 'The server returned ' . $statusCode . ' status code');
2025-06-12 06:13:11 +00:00
$template->setParam('{{path}}', "/console/project-$region-$projectId/settings/webhooks/$webhookId");
2024-01-18 09:49:57 +00:00
$template->setParam('{{attempts}}', $attempts);
2025-05-12 13:20:59 +00:00
2025-05-12 13:10:58 +00:00
$template->setParam('{{logoUrl}}', $plan['logoUrl'] ?? APP_EMAIL_LOGO_URL);
2025-05-15 07:44:14 +00:00
$template->setParam('{{accentColor}}', $plan['accentColor'] ?? APP_EMAIL_ACCENT_COLOR);
2025-05-12 13:20:59 +00:00
$template->setParam('{{twitterUrl}}', $plan['twitterUrl'] ?? APP_SOCIAL_TWITTER);
$template->setParam('{{discordUrl}}', $plan['discordUrl'] ?? APP_SOCIAL_DISCORD);
$template->setParam('{{githubUrl}}', $plan['githubUrl'] ?? APP_SOCIAL_GITHUB_APPWRITE);
$template->setParam('{{termsUrl}}', $plan['termsUrl'] ?? APP_EMAIL_TERMS_URL);
$template->setParam('{{privacyUrl}}', $plan['privacyUrl'] ?? APP_EMAIL_PRIVACY_URL);
2024-01-18 09:49:57 +00:00
2024-01-20 12:06:52 +00:00
// TODO: Use setbodyTemplate once #7307 is merged
2024-01-18 09:49:57 +00:00
$subject = 'Webhook deliveries have been paused';
2025-07-23 16:34:25 +00:00
$preview = 'Webhook deliveries to your endpoint have been paused.';
2024-01-18 09:49:57 +00:00
$body = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl');
$body
->setParam('{{subject}}', $subject)
->setParam('{{message}}', $template->render())
->setParam('{{year}}', date("Y"));
$queueForMails
->setSubject($subject)
2025-07-23 16:34:25 +00:00
->setPreview($preview)
2024-01-18 09:49:57 +00:00
->setBody($body->render());
foreach ($users as $user) {
$queueForMails
->setVariables(['user' => $user->getAttribute('name', '')])
->setName($user->getAttribute('name', ''))
->setRecipient($user->getAttribute('email'))
->trigger();
}
}
2023-05-29 13:58:45 +00:00
}