From e7dca6b2d5cbd61611a88160448c811faeea042a Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Tue, 5 Dec 2023 15:01:32 +0100 Subject: [PATCH 01/58] adds uniform logic for worker and extra params for email --- app/controllers/api/messaging.php | 132 ++++++++++++++++---- composer.json | 2 +- composer.lock | 70 ++++++----- src/Appwrite/Platform/Workers/Messaging.php | 104 +++++++++++---- 4 files changed, 230 insertions(+), 78 deletions(-) diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index f0cb4dd96..8e217b25c 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -54,21 +54,38 @@ App::post('/v1/messaging/providers/mailgun') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Email(), 'Sender email address.', true) + ->param('fromName', '', new Text(128), 'Sender Name.') + ->param('fromEmail', '', new Email(), 'Sender email address.') ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true) ->param('domain', '', new Text(0), 'Mailgun Domain.', true) ->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name. Reply to name must have reply to email as well.', true) + ->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email. Reply to email must have reply to name as well.', true) + ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) + ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $from, string $apiKey, string $domain, ?bool $isEuRegion, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $fromName, string $fromEmail, string $apiKey, string $domain, ?bool $isEuRegion, ?bool $enabled, string $replyToName, string $replyToEmail, array $cc, array $bcc, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; - $options = []; + $options = [ + 'fromName' => $fromName, + 'fromEmail' => $fromEmail, + ]; - if (!empty($from)) { - $options ['from'] = $from; + if (!empty($replyToName) && !empty($replyToEmail)) { + $options['replyToName'] = $replyToName; + $options['replyToEmail'] = $replyToEmail; + } + + if (!empty($cc)) { + $options['cc'] = $cc; + } + + if (!empty($bcc)) { + $options['bcc'] = $bcc; } $credentials = []; @@ -137,19 +154,36 @@ App::post('/v1/messaging/providers/sendgrid') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Email(), 'Sender email address.', true) + ->param('fromName', '', new Text(128), 'Sender Name.') + ->param('fromEmail', '', new Email(), 'Sender email address.') ->param('apiKey', '', new Text(0), 'Sendgrid API key.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true) + ->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true) + ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) + ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $from, string $apiKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $fromName, string $fromEmail, string $apiKey, ?bool $enabled, string $replyToName, string $replyToEmail, array $cc, array $bcc, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; - $options = []; + $options = [ + 'fromName' => $fromName, + 'fromEmail' => $fromEmail, + ]; - if (!empty($from)) { - $options ['from'] = $from; + if (!empty($replyToName) && !empty($replyToEmail)) { + $options['replyToName'] = $replyToName; + $options['replyToEmail'] = $replyToEmail; + } + + if (!empty($cc)) { + $options['cc'] = $cc; + } + + if (!empty($bcc)) { + $options['bcc'] = $bcc; } $credentials = []; @@ -890,13 +924,18 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true) - ->param('from', '', new Email(), 'Sender email address.', true) + ->param('fromName', '', new Text(128), 'Sender Name.', true) + ->param('fromEmail', '', new Email(), 'Sender email address.', true) + ->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true) + ->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true) + ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) + ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true) ->param('domain', '', new Text(0), 'Mailgun Domain.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $isEuRegion, string $from, string $apiKey, string $domain, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $isEuRegion, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, array $cc, array $bcc, string $apiKey, string $domain, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -912,12 +951,34 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') $provider->setAttribute('name', $name); } - if (!empty($from)) { - $provider->setAttribute('options', [ - 'from' => $from, - ]); + $options = $provider->getAttribute('options'); + + if (!empty($fromName)) { + $options['fromName'] = $fromName; } + if (!empty($fromEmail)) { + $options['fromEmail'] = $fromEmail; + } + + if (!empty($replyToName)) { + $options['replyToName'] = $replyToName; + } + + if (!empty($replyToEmail)) { + $options['replyToEmail'] = $replyToEmail; + } + + if (\count($cc) > 0) { + $options['cc'] = $cc; + } + + if (\count($bcc) > 0) { + $options['bcc'] = $bcc; + } + + $provider->setAttribute('options', $options); + $credentials = $provider->getAttribute('credentials'); if ($isEuRegion === true || $isEuRegion === false) { @@ -976,11 +1037,16 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->param('apiKey', '', new Text(0), 'Sendgrid API key.', true) - ->param('from', '', new Email(), 'Sender email address.', true) + ->param('fromName', '', new Text(128), 'Sender Name.', true) + ->param('fromEmail', '', new Email(), 'Sender email address.', true) + ->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true) + ->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true) + ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) + ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, array $cc, array $bcc, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -996,12 +1062,34 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId') $provider->setAttribute('name', $name); } - if (!empty($from)) { - $provider->setAttribute('options', [ - 'from' => $from, - ]); + $options = $provider->getAttribute('options'); + + if (!empty($fromName)) { + $options['fromName'] = $fromName; } + if (!empty($fromEmail)) { + $options['fromEmail'] = $fromEmail; + } + + if (!empty($replyToName)) { + $options['replyToName'] = $replyToName; + } + + if (!empty($replyToEmail)) { + $options['replyToEmail'] = $replyToEmail; + } + + if (\count($cc) > 0) { + $options['cc'] = $cc; + } + + if (\count($bcc) > 0) { + $options['bcc'] = $bcc; + } + + $provider->setAttribute('options', $options); + if (!empty($apiKey)) { $provider->setAttribute('credentials', [ 'apiKey' => $apiKey, diff --git a/composer.json b/composer.json index a24feca83..9dbb59a94 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "utopia-php/image": "0.5.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.3.*", - "utopia-php/messaging": "0.2.*", + "utopia-php/messaging": "0.6.*", "utopia-php/migration": "0.3.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.5.*", diff --git a/composer.lock b/composer.lock index 16f44a635..b865d57a8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "69bc2e21a65b78344393706b39d789b4", + "content-hash": "f5b11b64b696e0a17222fdaa81350c4c", "packages": [ { "name": "adhocore/jwt", @@ -402,16 +402,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9" + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9", - "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", "shasum": "" }, "require": { @@ -426,11 +426,11 @@ "psr/http-client-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", + "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.29 || ^9.5.23", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -508,7 +508,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.8.0" + "source": "https://github.com/guzzle/guzzle/tree/7.8.1" }, "funding": [ { @@ -524,28 +524,28 @@ "type": "tidelift" } ], - "time": "2023-08-27T10:20:53+00:00" + "time": "2023-12-03T20:35:24+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "111166291a0f8130081195ac4556a5587d7f1b5d" + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d", - "reference": "111166291a0f8130081195ac4556a5587d7f1b5d", + "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", - "phpunit/phpunit": "^8.5.29 || ^9.5.23" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" }, "type": "library", "extra": { @@ -591,7 +591,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.1" + "source": "https://github.com/guzzle/promises/tree/2.0.2" }, "funding": [ { @@ -607,20 +607,20 @@ "type": "tidelift" } ], - "time": "2023-08-03T15:11:55+00:00" + "time": "2023-12-03T20:19:20+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.6.1", + "version": "2.6.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727" + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/be45764272e8873c72dbe3d2edcfdfcc3bc9f727", - "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", "shasum": "" }, "require": { @@ -634,9 +634,9 @@ "psr/http-message-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", + "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.29 || ^9.5.23" + "phpunit/phpunit": "^8.5.36 || ^9.6.15" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -707,7 +707,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.6.1" + "source": "https://github.com/guzzle/psr7/tree/2.6.2" }, "funding": [ { @@ -723,7 +723,7 @@ "type": "tidelift" } ], - "time": "2023-08-27T10:13:57+00:00" + "time": "2023-12-03T20:05:35+00:00" }, { "name": "influxdb/influxdb-php", @@ -2270,16 +2270,16 @@ }, { "name": "utopia-php/messaging", - "version": "0.2.0", + "version": "0.6.0", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "2d0f474a106bb1da285f85e105c29b46085d3a43" + "reference": "a3ebf9d0714e760cfa36f9c5de75c7c0f31c4024" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/2d0f474a106bb1da285f85e105c29b46085d3a43", - "reference": "2d0f474a106bb1da285f85e105c29b46085d3a43", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/a3ebf9d0714e760cfa36f9c5de75c7c0f31c4024", + "reference": "a3ebf9d0714e760cfa36f9c5de75c7c0f31c4024", "shasum": "" }, "require": { @@ -2287,9 +2287,11 @@ "php": ">=8.0.0" }, "require-dev": { - "laravel/pint": "^1.2", + "ext-openssl": "*", + "laravel/pint": "1.13.*", "phpmailer/phpmailer": "6.8.*", - "phpunit/phpunit": "9.6.*" + "phpstan/phpstan": "1.10.*", + "phpunit/phpunit": "9.6.10" }, "type": "library", "autoload": { @@ -2312,9 +2314,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.2.0" + "source": "https://github.com/utopia-php/messaging/tree/0.6.0" }, - "time": "2023-09-14T20:48:42+00:00" + "time": "2023-12-05T11:08:43+00:00" }, { "name": "utopia-php/migration", @@ -5823,5 +5825,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 8f945e947..345da552a 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -13,22 +13,23 @@ use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Query; -use Utopia\Messaging\Adapters\SMS as SMSAdapter; -use Utopia\Messaging\Adapters\SMS\Mock; -use Utopia\Messaging\Adapters\SMS\Msg91; -use Utopia\Messaging\Adapters\SMS\Telesign; -use Utopia\Messaging\Adapters\SMS\Textmagic; -use Utopia\Messaging\Adapters\SMS\Twilio; -use Utopia\Messaging\Adapters\SMS\Vonage; -use Utopia\Messaging\Adapters\Push as PushAdapter; -use Utopia\Messaging\Adapters\Push\APNS; -use Utopia\Messaging\Adapters\Push\FCM; -use Utopia\Messaging\Adapters\Email as EmailAdapter; -use Utopia\Messaging\Adapters\Email\Mailgun; -use Utopia\Messaging\Adapters\Email\SendGrid; +use Utopia\Messaging\Adapter\Email as EmailAdapter; +use Utopia\Messaging\Adapter\Email\Mailgun; +use Utopia\Messaging\Adapter\Email\Sendgrid; +use Utopia\Messaging\Adapter\Push as PushAdapter; +use Utopia\Messaging\Adapter\Push\APNS; +use Utopia\Messaging\Adapter\Push\FCM; +use Utopia\Messaging\Adapter\SMS as SMSAdapter; +use Utopia\Messaging\Adapter\SMS\Mock; +use Utopia\Messaging\Adapter\SMS\Msg91; +use Utopia\Messaging\Adapter\SMS\Telesign; +use Utopia\Messaging\Adapter\SMS\Textmagic; +use Utopia\Messaging\Adapter\SMS\Twilio; +use Utopia\Messaging\Adapter\SMS\Vonage; use Utopia\Messaging\Messages\Email; use Utopia\Messaging\Messages\Push; use Utopia\Messaging\Messages\SMS; +use Utopia\Messaging\Response; use function Swoole\Coroutine\batch; @@ -169,8 +170,8 @@ class Messaging extends Action $batches = \array_chunk($identifiers, $maxBatchSize); $batchIndex = 0; - $results = batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex) { - return function () use ($batch, $message, $provider, $adapter, $batchIndex) { + $results = batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex, $dbForProject) { + return function () use ($batch, $message, $provider, $adapter, $batchIndex, $dbForProject) { $deliveredTotal = 0; $deliveryErrors = []; $messageData = clone $message; @@ -184,8 +185,24 @@ class Messaging extends Action }; try { - $adapter->send($data); - $deliveredTotal += \count($batch); + $response = new Response($provider->getAttribute('type')); + $response->fromArray(\json_decode($adapter->send($data))); + + $deliveredTotal += $response->getDeliveredTo(); + $details[] = $response->getDetails(); + foreach ($details as $detail) { + if ($detail['status'] === 'failure') { + $deliveryErrors[] = `Failed sending to target {$detail['recipient']} with error: {$detail['error']}`; + } + + // Deleting push targets when token has expired. + if ($detail['error'] === 'Expired token.') { + $target = $dbForProject->findOne('targets', [Query::equal('identifier', [$detail['recipient']])]); + if ($target instanceof Document && !$target->isEmpty()) { + $dbForProject->deleteDocument('targets', $target->getId()); + } + } + } } catch (\Exception $e) { $deliveryErrors[] = 'Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage(); } finally { @@ -313,7 +330,7 @@ class Messaging extends Action 'twilio' => new Twilio($credentials['accountSid'], $credentials['authToken']), 'textmagic' => new Textmagic($credentials['username'], $credentials['apiKey']), 'telesign' => new Telesign($credentials['username'], $credentials['password']), - 'msg91' => new Msg91($credentials['senderId'], $credentials['authKey']), + 'msg91' => new Msg91($credentials['senderId'], $credentials['authKey'], $credentials['templateId']), 'vonage' => new Vonage($credentials['apiKey'], $credentials['apiSecret']), default => null }; @@ -323,6 +340,7 @@ class Messaging extends Action { $credentials = $provider->getAttribute('credentials'); return match ($provider->getAttribute('provider')) { + 'mock' => new Mock('username', 'password'), 'apns' => new APNS( $credentials['authKey'], $credentials['authKeyId'], @@ -339,21 +357,65 @@ class Messaging extends Action { $credentials = $provider->getAttribute('credentials'); return match ($provider->getAttribute('provider')) { + 'mock' => new Mock('username', 'password'), 'mailgun' => new Mailgun($credentials['apiKey'], $credentials['domain'], $credentials['isEuRegion']), - 'sendgrid' => new SendGrid($credentials['apiKey']), + 'sendgrid' => new Sendgrid($credentials['apiKey']), default => null }; } private function buildEmailMessage(Document $message, Document $provider): Email { - $from = $provider['options']['from']; + $fromName = $provider['options']['fromName']; + $fromEmail = $provider['options']['fromEmail']; + $replyToEmail = null; + $replyToName = null; + $cc = null; + $bcc = null; + + if (isset($provider['options']['replyToName']) && isset($provider['options']['replyToEmail'])) { + $replyToName = $provider['options']['replyToName']; + $replyToEmail = $provider['options']['replyToEmail']; + } + + if (isset($provider['options']['cc'])) { + foreach ($provider['options']['cc'] as $ccEmail) { + if (is_array($cc)) { + $cc[] = [ + 'email' => $ccEmail, + ]; + } else { + $cc = [ + [ + 'email' => $ccEmail, + ] + ]; + } + } + } + + if (isset($provider['options']['bcc'])) { + foreach ($provider['options']['bcc'] as $bccEmail) { + if (is_array($bcc)) { + $bcc[] = [ + 'email' => $bccEmail, + ]; + } else { + $bcc = [ + [ + 'email' => $bccEmail, + ] + ]; + } + } + } + $to = $message['to']; $subject = $message['data']['subject']; $content = $message['data']['content']; $html = $message['data']['html']; - return new Email($to, $subject, $content, $from, null, $html); + return new Email($to, $subject, $content, $fromName, $fromEmail, $replyToName, $replyToEmail, $cc, $bcc, html: $html); } private function buildSMSMessage(Document $message, Document $provider): SMS From 194bbbb3508fb92a101f8b544986fe6d33fd9ab4 Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Tue, 5 Dec 2023 19:24:55 +0100 Subject: [PATCH 02/58] review changes --- app/controllers/api/messaging.php | 82 +++++++------------ src/Appwrite/Platform/Workers/Messaging.php | 8 +- tests/e2e/Services/GraphQL/Base.php | 16 ++-- tests/e2e/Services/GraphQL/UsersTest.php | 3 +- .../e2e/Services/Messaging/MessagingBase.php | 9 +- 5 files changed, 48 insertions(+), 70 deletions(-) diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 8e217b25c..3e784d29b 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -60,10 +60,8 @@ App::post('/v1/messaging/providers/mailgun') ->param('domain', '', new Text(0), 'Mailgun Domain.', true) ->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) - ->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name. Reply to name must have reply to email as well.', true) - ->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email. Reply to email must have reply to name as well.', true) - ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) - ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) + ->param('replyToName', '', new Text(128), 'Name set in the reply to field for the mail. Default value is sender name. Reply to name must have reply to email as well.', true) + ->param('replyToEmail', '', new Text(128), 'Email set in the reply to field for the mail. Default value is sender email. Reply to email must have reply to name as well.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') @@ -80,14 +78,6 @@ App::post('/v1/messaging/providers/mailgun') $options['replyToEmail'] = $replyToEmail; } - if (!empty($cc)) { - $options['cc'] = $cc; - } - - if (!empty($bcc)) { - $options['bcc'] = $bcc; - } - $credentials = []; if ($isEuRegion === true || $isEuRegion === false) { @@ -158,14 +148,12 @@ App::post('/v1/messaging/providers/sendgrid') ->param('fromEmail', '', new Email(), 'Sender email address.') ->param('apiKey', '', new Text(0), 'Sendgrid API key.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) - ->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true) - ->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true) - ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) - ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) + ->param('replyToName', '', new Text(128), 'Name set in the reply to field for the mail. Default value is sender name.', true) + ->param('replyToEmail', '', new Text(128), 'Email set in the reply to field for the mail. Default value is sender email.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $fromName, string $fromEmail, string $apiKey, ?bool $enabled, string $replyToName, string $replyToEmail, array $cc, array $bcc, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $fromName, string $fromEmail, string $apiKey, ?bool $enabled, string $replyToName, string $replyToEmail, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; $options = [ @@ -178,14 +166,6 @@ App::post('/v1/messaging/providers/sendgrid') $options['replyToEmail'] = $replyToEmail; } - if (!empty($cc)) { - $options['cc'] = $cc; - } - - if (!empty($bcc)) { - $options['bcc'] = $bcc; - } - $credentials = []; if (!empty($apiKey)) { @@ -926,16 +906,14 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') ->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true) ->param('fromName', '', new Text(128), 'Sender Name.', true) ->param('fromEmail', '', new Email(), 'Sender email address.', true) - ->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true) - ->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true) - ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) - ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) + ->param('replyToName', '', new Text(128), 'Name set in the reply to field for the mail. Default value is sender name.', true) + ->param('replyToEmail', '', new Text(128), 'Email set in the reply to field for the mail. Default value is sender email.', true) ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true) ->param('domain', '', new Text(0), 'Mailgun Domain.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $isEuRegion, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, array $cc, array $bcc, string $apiKey, string $domain, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $isEuRegion, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, string $apiKey, string $domain, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -969,14 +947,6 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') $options['replyToEmail'] = $replyToEmail; } - if (\count($cc) > 0) { - $options['cc'] = $cc; - } - - if (\count($bcc) > 0) { - $options['bcc'] = $bcc; - } - $provider->setAttribute('options', $options); $credentials = $provider->getAttribute('credentials'); @@ -1041,12 +1011,10 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId') ->param('fromEmail', '', new Email(), 'Sender email address.', true) ->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true) ->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true) - ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) - ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, array $cc, array $bcc, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -1080,14 +1048,6 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId') $options['replyToEmail'] = $replyToEmail; } - if (\count($cc) > 0) { - $options['cc'] = $cc; - } - - if (\count($bcc) > 0) { - $options['bcc'] = $bcc; - } - $provider->setAttribute('options', $options); if (!empty($apiKey)) { @@ -2315,9 +2275,11 @@ App::post('/v1/messaging/messages/email') ->param('messageId', '', new CustomId(), 'Message ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('subject', '', new Text(998), 'Email Subject.') ->param('content', '', new Text(64230), 'Email Content.') - ->param('topics', [], new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Topic IDs.', true) - ->param('users', [], new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of User IDs.', true) - ->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Targets IDs.', true) + ->param('topics', [], new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of topic IDs.', true) + ->param('users', [], new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of user IDs.', true) + ->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of target IDs.', true) + ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) + ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) ->param('description', '', new Text(256), 'Description for message.', true) ->param('status', 'processing', new WhiteList(['draft', 'canceled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) ->param('html', false, new Boolean(), 'Is content of type HTML', true) @@ -2327,7 +2289,7 @@ App::post('/v1/messaging/messages/email') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, string $description, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, string $description, array $cc, array $bcc, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) { @@ -2357,6 +2319,8 @@ App::post('/v1/messaging/messages/email') 'subject' => $subject, 'content' => $content, 'html' => $html, + 'cc' => $cc, + 'bcc' => $bcc, ], 'status' => $status, ])); @@ -2715,13 +2679,15 @@ App::patch('/v1/messaging/messages/email/:messageId') ->param('content', '', new Text(64230), 'Email Content.', true) ->param('status', '', new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) ->param('html', false, new Boolean(), 'Is content of type HTML', true) + ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) + ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $subject, string $description, string $content, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $subject, string $description, string $content, string $status, bool $html, array $cc, array $bcc, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { $message = $dbForProject->getDocument('messages', $messageId); if ($message->isEmpty()) { @@ -2774,6 +2740,14 @@ App::patch('/v1/messaging/messages/email/:messageId') $data['html'] = $html; } + if (count($cc) > 0) { + $data['cc'] = $cc; + } + + if (count($bcc) > 0) { + $data['bcc'] = $bcc; + } + $message->setAttribute('data', $data); if (!empty($description)) { diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 345da552a..0686088de 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -378,8 +378,8 @@ class Messaging extends Action $replyToEmail = $provider['options']['replyToEmail']; } - if (isset($provider['options']['cc'])) { - foreach ($provider['options']['cc'] as $ccEmail) { + if (\count($message['data']['cc']) > 0) { + foreach ($message['data']['cc'] as $ccEmail) { if (is_array($cc)) { $cc[] = [ 'email' => $ccEmail, @@ -394,8 +394,8 @@ class Messaging extends Action } } - if (isset($provider['options']['bcc'])) { - foreach ($provider['options']['bcc'] as $bccEmail) { + if (\count($message['data']['bcc'])) { + foreach ($message['data']['bcc'] as $bccEmail) { if (is_array($bcc)) { $bcc[] = [ 'email' => $bccEmail, diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 2854d0bf4..2593a58e1 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -1790,8 +1790,8 @@ trait Base } }'; case self::$CREATE_MAILGUN_PROVIDER: - return 'mutation createMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $from: String!, $isEuRegion: Boolean!) { - messagingCreateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, from: $from, isEuRegion: $isEuRegion) { + return 'mutation createMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $fromName: String!, $fromEmail: String!, $isEuRegion: Boolean!, $replyToName: String, $replyToEmail: String) { + messagingCreateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, fromName: $fromName, fromEmail: $fromEmail, isEuRegion: $isEuRegion, replyToName: $replyToName, replyToEmail: $replyToEmail) { _id name provider @@ -1800,8 +1800,8 @@ trait Base } }'; case self::$CREATE_SENDGRID_PROVIDER: - return 'mutation createSendgridProvider($providerId: String!, $name: String!, $from: String!, $apiKey: String!) { - messagingCreateSendgridProvider(providerId: $providerId, name: $name, from: $from, apiKey: $apiKey) { + return 'mutation createSendgridProvider($providerId: String!, $name: String!, $fromName: String!, $fromEmail: String!, $apiKey: String!, $replyToName: String, $replyToEmail: String) { + messagingCreateSendgridProvider(providerId: $providerId, name: $name, fromName: $fromName, fromEmail: $fromEmail, apiKey: $apiKey, replyToName: $replyToName, replyToEmail: $replyToEmail) { _id name provider @@ -2098,8 +2098,8 @@ trait Base } }'; case self::$CREATE_EMAIL: - return 'mutation createEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String!, $content: String!, $status: String, $description: String, $html: Boolean, $scheduledAt: String) { - messagingCreateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, scheduledAt: $scheduledAt) { + return 'mutation createEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String!, $content: String!, $status: String, $description: String, $html: Boolean, $cc: [String], $bcc: [String], $scheduledAt: String) { + messagingCreateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, cc: $cc, bcc: $bcc, scheduledAt: $scheduledAt) { _id topics users @@ -2178,8 +2178,8 @@ trait Base } }'; case self::$UPDATE_EMAIL: - return 'mutation updateEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String, $content: String, $status: String, $description: String, $html: Boolean, $scheduledAt: String) { - messagingUpdateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, scheduledAt: $scheduledAt) { + return 'mutation updateEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String, $content: String, $status: String, $description: String, $html: Boolean, $cc: [String], $bcc: [String], $scheduledAt: String) { + messagingUpdateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, cc: $cc, bcc: $bcc, scheduledAt: $scheduledAt) { _id topics users diff --git a/tests/e2e/Services/GraphQL/UsersTest.php b/tests/e2e/Services/GraphQL/UsersTest.php index 1dac9123a..d243a45a4 100644 --- a/tests/e2e/Services/GraphQL/UsersTest.php +++ b/tests/e2e/Services/GraphQL/UsersTest.php @@ -60,7 +60,8 @@ class UsersTest extends Scope 'name' => 'Mailgun1', 'apiKey' => 'api-key', 'domain' => 'domain', - 'from' => 'from@domain.com', + 'fromName' => 'sender name', + 'fromEmail' => 'from@domain.com', 'isEuRegion' => false, ], ]; diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php index 690e503e7..04fa9f4f2 100644 --- a/tests/e2e/Services/Messaging/MessagingBase.php +++ b/tests/e2e/Services/Messaging/MessagingBase.php @@ -23,7 +23,8 @@ trait MessagingBase 'name' => 'Mailgun1', 'apiKey' => 'my-apikey', 'domain' => 'my-domain', - 'from' => 'sender-email@my-domain.com', + 'fromName' => 'sender name', + 'fromEmail' => 'sender-email@my-domain.com', 'isEuRegion' => false, ], 'twilio' => [ @@ -552,7 +553,8 @@ trait MessagingBase $emailDSN = new DSN(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN')); $to = $emailDSN->getParam('to'); - $from = $emailDSN->getParam('from'); + $fromName = $emailDSN->getParam('fromName'); + $fromEmail = $emailDSN->getParam('fromEmail'); $isEuRegion = $emailDSN->getParam('isEuRegion'); $apiKey = $emailDSN->getPassword(); $domain = $emailDSN->getUser(); @@ -572,7 +574,8 @@ trait MessagingBase 'apiKey' => $apiKey, 'domain' => $domain, 'isEuRegion' => filter_var($isEuRegion, FILTER_VALIDATE_BOOLEAN), - 'from' => $from + 'fromName' => $fromName, + 'fromEmail' => $fromEmail ]); $this->assertEquals(201, $provider['headers']['status-code']); From 6d357d617dd6dd6a93fc22ec55ea240f733c4c46 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Mon, 11 Dec 2023 23:37:32 +0000 Subject: [PATCH 03/58] Add a flag to install and upgrade commands to not start Appwrite This can be useful for cases where the developer only wants the files to be generated and doesn't want to start Appwrite. --- src/Appwrite/Platform/Tasks/Install.php | 14 +++++++++----- src/Appwrite/Platform/Tasks/Upgrade.php | 8 +++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php index eb419ade1..8591f4bcc 100644 --- a/src/Appwrite/Platform/Tasks/Install.php +++ b/src/Appwrite/Platform/Tasks/Install.php @@ -8,6 +8,7 @@ use Appwrite\Docker\Env; use Appwrite\Utopia\View; use Utopia\CLI\Console; use Utopia\Config\Config; +use Utopia\Validator\Boolean; use Utopia\Validator\Text; use Utopia\Platform\Action; @@ -29,10 +30,11 @@ class Install extends Action ->param('organization', 'appwrite', new Text(0), 'Docker Registry organization', true) ->param('image', 'appwrite', new Text(0), 'Main appwrite docker image', true) ->param('interactive', 'Y', new Text(1), 'Run an interactive session', true) - ->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive)); + ->param('noStart', false, new Boolean(true), 'Run an interactive session', true) + ->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive, $noStart) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive, $noStart)); } - public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive): void + public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive, bool $noStart): void { $config = Config::getParam('variables'); $defaultHTTPPort = '80'; @@ -220,9 +222,11 @@ class Install extends Action } } - Console::log("Running \"docker compose up -d --remove-orphans --renew-anon-volumes\""); - - $exit = Console::execute("$env docker compose --project-directory $this->path up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr); + $exit = 0; + if (!$noStart) { + Console::log("Running \"docker compose up -d --remove-orphans --renew-anon-volumes\""); + $exit = Console::execute("$env docker compose --project-directory $this->path up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr); + } if ($exit !== 0) { $message = 'Failed to install Appwrite dockers'; diff --git a/src/Appwrite/Platform/Tasks/Upgrade.php b/src/Appwrite/Platform/Tasks/Upgrade.php index e3f045839..1f3e148a8 100644 --- a/src/Appwrite/Platform/Tasks/Upgrade.php +++ b/src/Appwrite/Platform/Tasks/Upgrade.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Tasks; use Utopia\CLI\Console; +use Utopia\Validator\Boolean; use Utopia\Validator\Text; class Upgrade extends Install @@ -21,10 +22,11 @@ class Upgrade extends Install ->param('organization', 'appwrite', new Text(0), 'Docker Registry organization', true) ->param('image', 'appwrite', new Text(0), 'Main appwrite docker image', true) ->param('interactive', 'Y', new Text(1), 'Run an interactive session', true) - ->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive)); + ->param('noStart', false, new Boolean(true), 'Run an interactive session', true) + ->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive, $noStart) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive, $noStart)); } - public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive): void + public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive, bool $noStart): void { // Check for previous installation $data = @file_get_contents($this->path . '/docker-compose.yml'); @@ -37,6 +39,6 @@ class Upgrade extends Install Console::log(' └── docker-compose.yml'); Console::exit(1); } - parent::action($httpPort, $httpsPort, $organization, $image, $interactive); + parent::action($httpPort, $httpsPort, $organization, $image, $interactive, $noStart); } } From e56b30e6a6c9d7d5416383e7329ccc4738a645eb Mon Sep 17 00:00:00 2001 From: Serhat Aksakal <49079271+fanksin@users.noreply.github.com> Date: Wed, 13 Dec 2023 12:10:44 +0300 Subject: [PATCH 04/58] Update tr.json --- app/config/locale/translations/tr.json | 56 ++++++++++++++------------ 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/app/config/locale/translations/tr.json b/app/config/locale/translations/tr.json index 6a94aeaca..e82317de0 100644 --- a/app/config/locale/translations/tr.json +++ b/app/config/locale/translations/tr.json @@ -3,30 +3,36 @@ "settings.locale": "tr", "settings.direction": "ltr", "emails.sender": "%s Takımı", - "emails.verification.subject": "", - "emails.verification.hello": "", - "emails.verification.body": "", - "emails.verification.footer": "", - "emails.verification.thanks": "", - "emails.verification.signature": "", - "emails.magicSession.subject": "", - "emails.magicSession.hello": "", - "emails.magicSession.body": "", - "emails.magicSession.footer": "", - "emails.magicSession.thanks": "", - "emails.magicSession.signature": "", - "emails.recovery.subject": "", - "emails.recovery.hello": "", - "emails.recovery.body": "", - "emails.recovery.footer": "", - "emails.recovery.thanks": "", - "emails.recovery.signature": "", - "emails.invitation.subject": "", - "emails.invitation.hello": "", - "emails.invitation.body": "", - "emails.invitation.footer": "", - "emails.invitation.thanks": "", - "emails.invitation.signature": "", + "emails.verification.subject": "Hesabını Doğrula", + "emails.verification.hello": "Merhaba {{user}}", + "emails.verification.body": "Eposta adresini doğrulamak için bu bağlantıyı kullanın.", + "emails.verification.footer": "Eğer bu eposta adresini doğrulamak isteyen siz değilseniz devam etmeyin.", + "emails.verification.thanks": "Teşekkürler", + "emails.verification.signature": "{{project}} takımı", + "emails.magicSession.subject": "Giriş", + "emails.magicSession.hello": "Merhaba,", + "emails.magicSession.body": "Giriş yapmak için tıklayın.", + "emails.magicSession.footer": "Eğer bu eposta adresini kullanarak giriş yapmak istemediyseniz devam etmeyin.", + "emails.magicSession.thanks": "Teşekkürler", + "emails.magicSession.signature": "{{project}} takımı", + "emails.recovery.subject": "Şifremi Sıfırla", + "emails.recovery.hello": "Merhaba {{user}}", + "emails.recovery.body": "{{project}} şifrenizi sıfırlamak için bu bağlantıyı kullanın.", + "emails.recovery.footer": "Eğer şifre sıfırlama talebinde bulunmadıysanız devam etmeyin.", + "emails.recovery.thanks": "Teşekkürler", + "emails.recovery.signature": "{{project}} takımı", + "emails.invitation.subject": "%s üzerinde %s Takımına Davet", + "emails.invitation.hello": "Merhaba", + "emails.invitation.body": "Bu epostayı aldınız, çünkü {{owner}} sizi {{project}} üzerinde {{team}} takımının üyesi olmaya davet etti.", + "emails.invitation.footer": "Eğer ilgilenmiyorsanız devam etmeyin.", + "emails.invitation.thanks": "Teşekkürler", + "emails.invitation.signature": "{{project}} takımı", + "emails.certificate.subject": "%s için sertifika hatası", + "emails.certificate.hello": "Merhaba", + "emails.certificate.body": "Alan adınız '{{domain}}' için sertifika oluşturulamadı. Deneme sayısı {{attempt}} ve hata sebebi: {{error}}", + "emails.certificate.footer": "Geçmiş sertifikanız ilk denemeden sonra 30 gün daha geçerli kalacaktır. Bu konuyu araştırmanızı öneriyoruz, aksi taktirde alan adınız SSL sertifikasız kalacaktır.", + "emails.certificate.thanks": "Teşekkürler", + "emails.certificate.signature": "{{project}} takımı", "locale.country.unknown": "Bilinmeyen", "countries.af": "Afganistan", "countries.ao": "Angola", @@ -229,4 +235,4 @@ "continents.na": "Kuzey Amerika", "continents.oc": "Okyanusya", "continents.sa": "Güney Amerika" -} \ No newline at end of file +} From 92a307c164b00f1b888b3ea9bd2f52b98bbbc7be Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 13 Dec 2023 18:45:05 +0000 Subject: [PATCH 05/58] Fix user identity attaching to wrong user Suppose a user has 2 accounts on Appwrite: 1. joe@example.com 2. joe@gmail.com Prior to this PR, if joe@example.com created a Google OAuth2 session using his joe@gmail.com email, a new joe@gmail.com identity would be created linked to joe@example.com. This is especially problematic because if the user tried to create a Google OAuth2 session using joe@gmail.com, Appwrite would lookup the user via email and find the joe@gmail.com user, but then find an identity from joe@example.com. This mismatching user ID would then cause an error. This PR prevents an identity from being created if the email from the OAuth2 provider matches another user's email. --- app/controllers/api/account.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index c210b19f4..54967fb50 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -550,11 +550,19 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') if (!$user->isEmpty()) { $userId = $user->getId(); - $identitiesWithMatchingEmail = $dbForProject->find('identities', [ + $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), Query::notEqual('userId', $userId), ]); - if (!empty($identitiesWithMatchingEmail)) { + if (!empty($identityWithMatchingEmail)) { + throw new Exception(Exception::USER_ALREADY_EXISTS); + } + + $userWithMatchingEmail = $dbForProject->find('users', [ + Query::equal('email', [$email]), + Query::notEqual('$id', $userId), + ]); + if (!empty($userWithMatchingEmail)) { throw new Exception(Exception::USER_ALREADY_EXISTS); } } From 7b99fab51215826c210c7079e4caea8a6369a857 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Dec 2023 03:19:24 +1300 Subject: [PATCH 06/58] Use targets for cc/bcc --- app/controllers/api/messaging.php | 72 ++++++++++------- composer.json | 2 +- composer.lock | 14 ++-- src/Appwrite/Platform/Workers/Messaging.php | 85 ++++++++------------- 4 files changed, 87 insertions(+), 86 deletions(-) diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 9519f25d9..354dae4c8 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -2278,8 +2278,8 @@ App::post('/v1/messaging/messages/email') ->param('topics', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs.', true) ->param('users', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of User IDs.', true) ->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Targets IDs.', true) - ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) - ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) + ->param('cc', [], new ArrayList(new UID()), 'Array of target IDs to be added as CC.', true) + ->param('bcc', [], new ArrayList(new UID()), 'Array of target IDs to be added as BCC.', true) ->param('description', '', new Text(256), 'Description for message.', true) ->param('status', 'processing', new WhiteList(['draft', 'canceled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) ->param('html', false, new Boolean(), 'Is content of type HTML', true) @@ -2290,22 +2290,30 @@ App::post('/v1/messaging/messages/email') ->inject('queueForMessaging') ->inject('response') ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, string $description, array $cc, array $bcc, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { - $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; + $messageId = $messageId == 'unique()' + ? ID::unique() + : $messageId; if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) { throw new Exception(Exception::MESSAGE_MISSING_TARGET); } - foreach ($targets as $target) { - $targetDocument = $dbForProject->getDocument('targets', $target); + $mergedTargets = \array_merge($targets, $cc, $bcc); - if ($targetDocument->isEmpty()) { + $foundTargets = $dbForProject->find('targets', [ + Query::equal('$id', $mergedTargets), + Query::equal('providerType', [MESSAGE_TYPE_EMAIL]), + Query::limit(\count($mergedTargets)), + ]); + + if (\count($foundTargets) !== \count($mergedTargets)) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL); + } + + foreach ($foundTargets as $target) { + if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); } - - if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_EMAIL) { - throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL . ' ' . $targetDocument->getId()); - } } $message = $dbForProject->createDocument('messages', new Document([ @@ -2368,22 +2376,28 @@ App::post('/v1/messaging/messages/sms') ->inject('queueForMessaging') ->inject('response') ->action(function (string $messageId, string $content, array $topics, array $users, array $targets, string $description, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { - $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; + $messageId = $messageId == 'unique()' + ? ID::unique() + : $messageId; if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) { throw new Exception(Exception::MESSAGE_MISSING_TARGET); } - foreach ($targets as $target) { - $targetDocument = $dbForProject->getDocument('targets', $target); + $foundTargets = $dbForProject->find('targets', [ + Query::equal('$id', $targets), + Query::equal('providerType', [MESSAGE_TYPE_SMS]), + Query::limit(\count($targets)), + ]); - if ($targetDocument->isEmpty()) { + if (\count($foundTargets) !== \count($targets)) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_SMS); + } + + foreach ($foundTargets as $target) { + if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); } - - if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_SMS) { - throw new Exception(Exception::MESSAGE_TARGET_NOT_SMS . ' ' . $targetDocument->getId()); - } } $message = $dbForProject->createDocument('messages', new Document([ @@ -2450,22 +2464,28 @@ App::post('/v1/messaging/messages/push') ->inject('queueForMessaging') ->inject('response') ->action(function (string $messageId, string $title, string $body, array $topics, array $users, array $targets, string $description, ?array $data, string $action, string $icon, string $sound, string $color, string $tag, string $badge, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { - $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; + $messageId = $messageId == 'unique()' + ? ID::unique() + : $messageId; if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) { throw new Exception(Exception::MESSAGE_MISSING_TARGET); } - foreach ($targets as $target) { - $targetDocument = $dbForProject->getDocument('targets', $target); + $foundTargets = $dbForProject->find('targets', [ + Query::equal('$id', $targets), + Query::equal('providerType', [MESSAGE_TYPE_PUSH]), + Query::limit(\count($targets)), + ]); - if ($targetDocument->isEmpty()) { + if (\count($foundTargets) !== \count($targets)) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_PUSH); + } + + foreach ($foundTargets as $target) { + if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); } - - if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) { - throw new Exception(Exception::MESSAGE_TARGET_NOT_PUSH . ' ' . $targetDocument->getId()); - } } $pushData = []; diff --git a/composer.json b/composer.json index 29f3bcdec..1ad0b055b 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "utopia-php/image": "0.5.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.3.*", - "utopia-php/messaging": "0.6.*", + "utopia-php/messaging": "0.7.*", "utopia-php/migration": "0.3.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.5.*", diff --git a/composer.lock b/composer.lock index f40521b41..fb10836c9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7041499af2e7b23795d8ef82c9d7a072", + "content-hash": "ec8d49765c3f6ccc5759c5eab267f326", "packages": [ { "name": "adhocore/jwt", @@ -2270,16 +2270,16 @@ }, { "name": "utopia-php/messaging", - "version": "0.6.0", + "version": "0.7.0", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "a3ebf9d0714e760cfa36f9c5de75c7c0f31c4024" + "reference": "b2d4b0334412390e839d250b848ff0f3466f7a55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/a3ebf9d0714e760cfa36f9c5de75c7c0f31c4024", - "reference": "a3ebf9d0714e760cfa36f9c5de75c7c0f31c4024", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/b2d4b0334412390e839d250b848ff0f3466f7a55", + "reference": "b2d4b0334412390e839d250b848ff0f3466f7a55", "shasum": "" }, "require": { @@ -2314,9 +2314,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.6.0" + "source": "https://github.com/utopia-php/messaging/tree/0.7.0" }, - "time": "2023-12-05T11:08:43+00:00" + "time": "2023-12-05T13:47:36+00:00" }, { "name": "utopia-php/migration", diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 0686088de..e04bace4c 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -63,7 +63,7 @@ class Messaging extends Action $payload = $message->getPayload() ?? []; if (empty($payload)) { - Console::error('Payload arg not found'); + Console::error('Payload not found.'); return; } @@ -85,14 +85,14 @@ class Messaging extends Action $usersId = $message->getAttribute('users', []); /** - * @var Document[] $recipients - */ + * @var Document[] $recipients + */ $recipients = []; if (\count($topicsId) > 0) { $topics = $dbForProject->find('topics', [Query::equal('$id', $topicsId)]); foreach ($topics as $topic) { - $targets = \array_filter($topic->getAttribute('targets'), fn (Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType')); + $targets = \array_filter($topic->getAttribute('targets'), fn(Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType')); $recipients = \array_merge($recipients, $targets); } } @@ -100,7 +100,7 @@ class Messaging extends Action if (\count($usersId) > 0) { $users = $dbForProject->find('users', [Query::equal('$id', $usersId)]); foreach ($users as $user) { - $targets = \array_filter($user->getAttribute('targets'), fn (Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType')); + $targets = \array_filter($user->getAttribute('targets'), fn(Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType')); $recipients = \array_merge($recipients, $targets); } } @@ -116,13 +116,13 @@ class Messaging extends Action ]); /** - * @var array> $identifiersByProviderId - */ + * @var array> $identifiersByProviderId + */ $identifiersByProviderId = []; /** - * @var Document[] $providers - */ + * @var Document[] $providers + */ $providers = []; foreach ($recipients as $recipient) { $providerId = $recipient->getAttribute('providerId'); @@ -140,12 +140,10 @@ class Messaging extends Action } /** - * @var array[] $results - */ + * @var array[] $results + */ $results = batch(\array_map(function ($providerId) use ($identifiersByProviderId, $providers, $primaryProvider, $message, $dbForProject) { return function () use ($providerId, $identifiersByProviderId, $providers, $primaryProvider, $message, $dbForProject) { - $provider = new Document(); - if ($primaryProvider->getId() === $providerId) { $provider = $primaryProvider; } else { @@ -156,13 +154,12 @@ class Messaging extends Action } } - $providers[] = $provider; $identifiers = $identifiersByProviderId[$providerId]; $adapter = match ($provider->getAttribute('type')) { MESSAGE_TYPE_SMS => $this->sms($provider), MESSAGE_TYPE_PUSH => $this->push($provider), - MESSAGE_TYPE_EMAIL => $this->email($provider), + MESSAGE_TYPE_EMAIL => $this->email($provider), default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) }; @@ -170,7 +167,7 @@ class Messaging extends Action $batches = \array_chunk($identifiers, $maxBatchSize); $batchIndex = 0; - $results = batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex, $dbForProject) { + return batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex, $dbForProject) { return function () use ($batch, $message, $provider, $adapter, $batchIndex, $dbForProject) { $deliveredTotal = 0; $deliveryErrors = []; @@ -180,7 +177,7 @@ class Messaging extends Action $data = match ($provider->getAttribute('type')) { MESSAGE_TYPE_SMS => $this->buildSMSMessage($messageData, $provider), MESSAGE_TYPE_PUSH => $this->buildPushMessage($messageData), - MESSAGE_TYPE_EMAIL => $this->buildEmailMessage($messageData, $provider), + MESSAGE_TYPE_EMAIL => $this->buildEmailMessage($dbForProject, $messageData, $provider), default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) }; @@ -214,8 +211,6 @@ class Messaging extends Action } }; }, $batches)); - - return $results; }; }, \array_keys($identifiersByProviderId))); @@ -364,58 +359,44 @@ class Messaging extends Action }; } - private function buildEmailMessage(Document $message, Document $provider): Email + private function buildEmailMessage(Database $dbForProject, Document $message, Document $provider): Email { $fromName = $provider['options']['fromName']; $fromEmail = $provider['options']['fromEmail']; $replyToEmail = null; $replyToName = null; - $cc = null; - $bcc = null; if (isset($provider['options']['replyToName']) && isset($provider['options']['replyToEmail'])) { $replyToName = $provider['options']['replyToName']; $replyToEmail = $provider['options']['replyToEmail']; } - if (\count($message['data']['cc']) > 0) { - foreach ($message['data']['cc'] as $ccEmail) { - if (is_array($cc)) { - $cc[] = [ - 'email' => $ccEmail, - ]; - } else { - $cc = [ - [ - 'email' => $ccEmail, - ] - ]; - } + $data = $message['data'] ?? []; + $ccTargets = $data['cc'] ?? []; + $bccTargets = $data['bcc'] ?? []; + $cc = []; + $bcc = []; + + if (\count($ccTargets) > 0) { + $ccTargets = $dbForProject->find('targets', [Query::equal('identifier', $ccTargets)]); + foreach ($ccTargets as $ccTarget) { + $cc[] = ['email' => $ccTarget['identifier']]; } } - if (\count($message['data']['bcc'])) { - foreach ($message['data']['bcc'] as $bccEmail) { - if (is_array($bcc)) { - $bcc[] = [ - 'email' => $bccEmail, - ]; - } else { - $bcc = [ - [ - 'email' => $bccEmail, - ] - ]; - } + if (\count($bccTargets) > 0) { + $bccTargets = $dbForProject->find('targets', [Query::equal('identifier', $bccTargets)]); + foreach ($bccTargets as $bccTarget) { + $bcc[] = ['email' => $bccTarget['identifier']]; } } $to = $message['to']; - $subject = $message['data']['subject']; - $content = $message['data']['content']; - $html = $message['data']['html']; + $subject = $data['subject']; + $content = $data['content']; + $html = $data['html']; - return new Email($to, $subject, $content, $fromName, $fromEmail, $replyToName, $replyToEmail, $cc, $bcc, html: $html); + return new Email($to, $subject, $content, $fromName, $fromEmail, $replyToName, $replyToEmail, $cc, $bcc, null, $html); } private function buildSMSMessage(Document $message, Document $provider): SMS From 4e26bdb5dfb840e17361246c7143ad76179f7b1d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Dec 2023 03:45:51 +1300 Subject: [PATCH 07/58] Update update route params --- app/controllers/api/messaging.php | 155 ++++++++++++++++-------------- 1 file changed, 85 insertions(+), 70 deletions(-) diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 354dae4c8..b01ab4b7a 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -31,6 +31,7 @@ use Utopia\Database\Validator\UID; use Utopia\Locale\Locale; use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; +use Utopia\Validator\Integer; use Utopia\Validator\JSON; use Utopia\Validator\Text; use MaxMind\Db\Reader; @@ -2694,20 +2695,20 @@ App::patch('/v1/messaging/messages/email/:messageId') ->param('topics', null, new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs.', true) ->param('users', null, new ArrayList(new Text(Database::LENGTH_KEY)), 'List of User IDs.', true) ->param('targets', null, new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Targets IDs.', true) - ->param('subject', '', new Text(998), 'Email Subject.', true) - ->param('description', '', new Text(256), 'Description for Message.', true) - ->param('content', '', new Text(64230), 'Email Content.', true) - ->param('status', '', new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) - ->param('html', false, new Boolean(), 'Is content of type HTML', true) - ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) - ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) + ->param('subject', null, new Text(998), 'Email Subject.', true) + ->param('description', null, new Text(256), 'Description for Message.', true) + ->param('content', null, new Text(64230), 'Email Content.', true) + ->param('status', null, new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) + ->param('html', null, new Boolean(), 'Is content of type HTML', true) + ->param('cc', null, new ArrayList(new UID()), 'Array of target IDs to be added as CC.', true) + ->param('bcc', null, new ArrayList(new UID()), 'Array of target IDs to be added as BCC.', true) ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $subject, string $description, string $content, string $status, bool $html, array $cc, array $bcc, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $description, ?string $content, ?string $status, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { $message = $dbForProject->getDocument('messages', $messageId); if ($message->isEmpty()) { @@ -2718,7 +2719,7 @@ App::patch('/v1/messaging/messages/email/:messageId') throw new Exception(Exception::MESSAGE_ALREADY_SENT); } - if (!is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) { + if (!\is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) { throw new Exception(Exception::MESSAGE_ALREADY_SCHEDULED); } @@ -2730,51 +2731,57 @@ App::patch('/v1/messaging/messages/email/:messageId') $message->setAttribute('users', $users); } - if (!\is_null($targets)) { - foreach ($targets as $target) { - $targetDocument = $dbForProject->getDocument('targets', $target); + if (!\is_null($targets) || !\is_null($cc) || !\is_null($bcc)) { + $mergedTargets = \array_merge(...\array_filter([$targets, $cc, $bcc])); - if ($targetDocument->isEmpty()) { + $foundTargets = $dbForProject->find('targets', [ + Query::equal('$id', $mergedTargets), + Query::equal('providerType', [MESSAGE_TYPE_EMAIL]), + Query::limit(\count($mergedTargets)), + ]); + if (\count($foundTargets) !== \count($mergedTargets)) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL); + } + foreach ($foundTargets as $target) { + if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); } - - if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_EMAIL) { - throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL . ' ' . $targetDocument->getId()); - } } - - $message->setAttribute('targets', $targets); } $data = $message->getAttribute('data'); - if (!empty($subject)) { + if (!\is_null($targets)) { + $message->setAttribute('targets', $targets); + } + + if (!\is_null($subject)) { $data['subject'] = $subject; } - if (!empty($content)) { + if (!\is_null($content)) { $data['content'] = $content; } - if (!empty($html)) { + if (!\is_null($html)) { $data['html'] = $html; } - if (count($cc) > 0) { + if (!\is_null($cc)) { $data['cc'] = $cc; } - if (count($bcc) > 0) { + if (!\is_null($bcc)) { $data['bcc'] = $bcc; } $message->setAttribute('data', $data); - if (!empty($description)) { + if (!\is_null($description)) { $message->setAttribute('description', $description); } - if (!empty($status)) { + if (!\is_null($status)) { $message->setAttribute('status', $status); } @@ -2816,16 +2823,16 @@ App::patch('/v1/messaging/messages/sms/:messageId') ->param('topics', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Topic IDs.', true) ->param('users', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of User IDs.', true) ->param('targets', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Targets IDs.', true) - ->param('description', '', new Text(256), 'Description for Message.', true) - ->param('content', '', new Text(64230), 'Email Content.', true) - ->param('status', '', new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) + ->param('description', null, new Text(256), 'Description for Message.', true) + ->param('content', null, new Text(64230), 'Email Content.', true) + ->param('status', null, new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $description, string $content, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $description, ?string $content, ?string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { $message = $dbForProject->getDocument('messages', $messageId); if ($message->isEmpty()) { @@ -2849,16 +2856,20 @@ App::patch('/v1/messaging/messages/sms/:messageId') } if (!\is_null($targets)) { - foreach ($targets as $target) { - $targetDocument = $dbForProject->getDocument('targets', $target); + $foundTargets = $dbForProject->find('targets', [ + Query::equal('$id', $targets), + Query::equal('providerType', [MESSAGE_TYPE_SMS]), + Query::limit(\count($targets)), + ]); - if ($targetDocument->isEmpty()) { + if (\count($foundTargets) !== \count($targets)) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_SMS); + } + + foreach ($foundTargets as $target) { + if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); } - - if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_SMS) { - throw new Exception(Exception::MESSAGE_TARGET_NOT_SMS . ' ' . $targetDocument->getId()); - } } $message->setAttribute('targets', $targets); @@ -2866,17 +2877,17 @@ App::patch('/v1/messaging/messages/sms/:messageId') $data = $message->getAttribute('data'); - if (!empty($content)) { + if (!\is_null($content)) { $data['content'] = $content; } $message->setAttribute('data', $data); - if (!empty($status)) { + if (!\is_null($status)) { $message->setAttribute('status', $status); } - if (!empty($description)) { + if (!\is_null($description)) { $message->setAttribute('description', $description); } @@ -2918,24 +2929,24 @@ App::patch('/v1/messaging/messages/push/:messageId') ->param('topics', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Topic IDs.', true) ->param('users', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of User IDs.', true) ->param('targets', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Targets IDs.', true) - ->param('description', '', new Text(256), 'Description for Message.', true) - ->param('title', '', new Text(256), 'Title for push notification.', true) - ->param('body', '', new Text(64230), 'Body for push notification.', true) + ->param('description', null, new Text(256), 'Description for Message.', true) + ->param('title', null, new Text(256), 'Title for push notification.', true) + ->param('body', null, new Text(64230), 'Body for push notification.', true) ->param('data', null, new JSON(), 'Additional Data for push notification.', true) - ->param('action', '', new Text(256), 'Action for push notification.', true) - ->param('icon', '', new Text(256), 'Icon for push notification. Available only for Android and Web Platform.', true) - ->param('sound', '', new Text(256), 'Sound for push notification. Available only for Android and IOS Platform.', true) - ->param('color', '', new Text(256), 'Color for push notification. Available only for Android Platform.', true) - ->param('tag', '', new Text(256), 'Tag for push notification. Available only for Android Platform.', true) - ->param('badge', '', new Text(256), 'Badge for push notification. Available only for IOS Platform.', true) - ->param('status', '', new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) + ->param('action', null, new Text(256), 'Action for push notification.', true) + ->param('icon', null, new Text(256), 'Icon for push notification. Available only for Android and Web platforms.', true) + ->param('sound', null, new Text(256), 'Sound for push notification. Available only for Android and iOS platforms.', true) + ->param('color', null, new Text(256), 'Color for push notification. Available only for Android platforms.', true) + ->param('tag', null, new Text(256), 'Tag for push notification. Available only for Android platforms.', true) + ->param('badge', null, new Integer(), 'Badge for push notification. Available only for iOS platforms.', true) + ->param('status', null, new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft, cancelled, or processing.', true) ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $description, string $title, string $body, ?array $data, string $action, string $icon, string $sound, string $color, string $tag, string $badge, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $description, ?string $title, ?string $body, ?array $data, ?string $action, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { $message = $dbForProject->getDocument('messages', $messageId); if ($message->isEmpty()) { @@ -2959,16 +2970,20 @@ App::patch('/v1/messaging/messages/push/:messageId') } if (!\is_null($targets)) { - foreach ($targets as $target) { - $targetDocument = $dbForProject->getDocument('targets', $target); + $foundTargets = $dbForProject->find('targets', [ + Query::equal('$id', $targets), + Query::equal('providerType', [MESSAGE_TYPE_PUSH]), + Query::limit(\count($targets)), + ]); - if ($targetDocument->isEmpty()) { + if (\count($foundTargets) !== \count($targets)) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_PUSH); + } + + foreach ($foundTargets as $target) { + if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); } - - if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) { - throw new Exception(Exception::MESSAGE_TARGET_NOT_PUSH . ' ' . $targetDocument->getId()); - } } $message->setAttribute('targets', $targets); @@ -2976,53 +2991,53 @@ App::patch('/v1/messaging/messages/push/:messageId') $pushData = $message->getAttribute('data'); - if ($title) { + if (!\is_null($title)) { $pushData['title'] = $title; } - if ($body) { + if (!\is_null($body)) { $pushData['body'] = $body; } - if (!is_null($data)) { + if (!\is_null($data)) { $pushData['data'] = $data; } - if ($action) { + if (!\is_null($action)) { $pushData['action'] = $action; } - if ($icon) { + if (!\is_null($icon)) { $pushData['icon'] = $icon; } - if ($sound) { + if (!\is_null($sound)) { $pushData['sound'] = $sound; } - if ($color) { + if (!\is_null($color)) { $pushData['color'] = $color; } - if ($tag) { + if (!\is_null($tag)) { $pushData['tag'] = $tag; } - if ($badge) { + if (!\is_null($badge)) { $pushData['badge'] = $badge; } $message->setAttribute('data', $pushData); - if (!empty($status)) { + if (!\is_null($status)) { $message->setAttribute('status', $status); } - if (!empty($description)) { + if (!\is_null($description)) { $message->setAttribute('description', $description); } - if (!is_null($scheduledAt)) { + if (!\is_null($scheduledAt)) { $message->setAttribute('scheduledAt', $scheduledAt); } From 8fc3f99a8e3ecaab57a7c04c3a172e7d0d4bf621 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Dec 2023 19:55:50 +1300 Subject: [PATCH 08/58] Update messaging lib --- composer.json | 2 +- composer.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 1ad0b055b..b92557d23 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "utopia-php/image": "0.5.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.3.*", - "utopia-php/messaging": "0.7.*", + "utopia-php/messaging": "0.8.*", "utopia-php/migration": "0.3.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.5.*", diff --git a/composer.lock b/composer.lock index fb10836c9..803678ff4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ec8d49765c3f6ccc5759c5eab267f326", + "content-hash": "359b1e3bd27ac7362c6f8d145e64ae36", "packages": [ { "name": "adhocore/jwt", @@ -2270,24 +2270,24 @@ }, { "name": "utopia-php/messaging", - "version": "0.7.0", + "version": "0.8.0", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "b2d4b0334412390e839d250b848ff0f3466f7a55" + "reference": "64eca3faf02a79831f219d4f3ae05cd278a88b4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/b2d4b0334412390e839d250b848ff0f3466f7a55", - "reference": "b2d4b0334412390e839d250b848ff0f3466f7a55", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/64eca3faf02a79831f219d4f3ae05cd278a88b4b", + "reference": "64eca3faf02a79831f219d4f3ae05cd278a88b4b", "shasum": "" }, "require": { "ext-curl": "*", + "ext-openssl": "*", "php": ">=8.0.0" }, "require-dev": { - "ext-openssl": "*", "laravel/pint": "1.13.*", "phpmailer/phpmailer": "6.8.*", "phpstan/phpstan": "1.10.*", @@ -2314,9 +2314,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.7.0" + "source": "https://github.com/utopia-php/messaging/tree/0.8.0" }, - "time": "2023-12-05T13:47:36+00:00" + "time": "2023-12-15T06:44:08+00:00" }, { "name": "utopia-php/migration", From 6b6946e39bfd4a653ed2bd9dec57b3de301995d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=B7=E5=8D=8E=20=E5=88=98?= Date: Fri, 15 Dec 2023 20:40:04 +0000 Subject: [PATCH 09/58] chore: update console --- .gitmodules | 2 +- app/console | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index cc08f93fc..0c2321bcf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "app/console"] path = app/console url = https://github.com/appwrite/console - branch = 3.2.15 + branch = 3.2.16 diff --git a/app/console b/app/console index 94e4c1a73..0a007a3b1 160000 --- a/app/console +++ b/app/console @@ -1 +1 @@ -Subproject commit 94e4c1a73024b0e974fbe6077674281f6e973c9d +Subproject commit 0a007a3b1b6eafc39dc19b7129f41643102f9676 From 6d17f8e87779b51e6eacd39297c2066c6af6bfee Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Fri, 15 Dec 2023 21:37:26 +0000 Subject: [PATCH 10/58] Fix user last activity not updating Add back a section of code that was mistakenly removed. --- app/controllers/shared/api.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index b37d76a81..1ea2fdc65 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -553,6 +553,22 @@ App::shutdown() ->setParam('project.{scope}.network.outbound', $response->getSize()) ->submit(); } + + /** + * Update user last activity + */ + if (!$user->isEmpty()) { + $accessedAt = $user->getAttribute('accessedAt', ''); + if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCCESS)) > $accessedAt) { + $user->setAttribute('accessedAt', DateTime::now()); + + if (APP_MODE_ADMIN !== $mode) { + $dbForProject->updateDocument('users', $user->getId(), $user); + } else { + $dbForConsole->updateDocument('users', $user->getId(), $user); + } + } + } }); App::init() From e41194ee3c6163c72ee7c3d24464e11088a720db Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Sat, 16 Dec 2023 17:42:29 +0000 Subject: [PATCH 11/58] Fix import to match class name The class is SMS rather than Sms. --- src/Appwrite/Platform/Workers/Messaging.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 76b86e4f0..876ca50d0 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -6,7 +6,7 @@ use Exception; use Utopia\App; use Utopia\CLI\Console; use Utopia\DSN\DSN; -use Utopia\Messaging\Messages\Sms; +use Utopia\Messaging\Messages\SMS; use Utopia\Messaging\Adapters\SMS\Mock; use Utopia\Messaging\Adapters\SMS\Msg91; use Utopia\Messaging\Adapters\SMS\Telesign; From 23b39fee02e17917fca1a2049fed17a973e1d60e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sun, 17 Dec 2023 12:41:20 +1300 Subject: [PATCH 12/58] FIx FCM tests --- app/controllers/api/messaging.php | 48 ++++++++--------- app/worker.php | 2 +- tests/e2e/Services/GraphQL/Base.php | 4 +- .../e2e/Services/Messaging/MessagingBase.php | 51 +++++++------------ 4 files changed, 46 insertions(+), 59 deletions(-) diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index b01ab4b7a..fdead6e6f 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -55,18 +55,18 @@ App::post('/v1/messaging/providers/mailgun') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('fromName', '', new Text(128), 'Sender Name.') - ->param('fromEmail', '', new Email(), 'Sender email address.') ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true) ->param('domain', '', new Text(0), 'Mailgun Domain.', true) ->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->param('fromName', '', new Text(128), 'Sender Name.', true) + ->param('fromEmail', '', new Email(), 'Sender email address.', true) ->param('replyToName', '', new Text(128), 'Name set in the reply to field for the mail. Default value is sender name. Reply to name must have reply to email as well.', true) ->param('replyToEmail', '', new Text(128), 'Email set in the reply to field for the mail. Default value is sender email. Reply to email must have reply to name as well.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $fromName, string $fromEmail, string $apiKey, string $domain, ?bool $isEuRegion, ?bool $enabled, string $replyToName, string $replyToEmail, array $cc, array $bcc, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $apiKey, string $domain, ?bool $isEuRegion, ?bool $enabled, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; $options = [ @@ -145,16 +145,16 @@ App::post('/v1/messaging/providers/sendgrid') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('fromName', '', new Text(128), 'Sender Name.') - ->param('fromEmail', '', new Email(), 'Sender email address.') ->param('apiKey', '', new Text(0), 'Sendgrid API key.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->param('fromName', '', new Text(128), 'Sender Name.', true) + ->param('fromEmail', '', new Email(), 'Sender email address.', true) ->param('replyToName', '', new Text(128), 'Name set in the reply to field for the mail. Default value is sender name.', true) ->param('replyToEmail', '', new Text(128), 'Email set in the reply to field for the mail. Default value is sender email.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $fromName, string $fromEmail, string $apiKey, ?bool $enabled, string $replyToName, string $replyToEmail, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $apiKey, ?bool $enabled, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; $options = [ @@ -608,7 +608,7 @@ App::post('/v1/messaging/providers/fcm') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('serverKey', '', new Text(0), 'FCM server key.', true) + ->param('serviceAccountJSON', '', new Text(0), 'FCM service account JSON.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->inject('queueForEvents') ->inject('dbForProject') @@ -903,18 +903,18 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) - ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true) + ->param('domain', '', new Text(0), 'Mailgun Domain.', true) ->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->param('fromName', '', new Text(128), 'Sender Name.', true) ->param('fromEmail', '', new Email(), 'Sender email address.', true) ->param('replyToName', '', new Text(128), 'Name set in the reply to field for the mail. Default value is sender name.', true) ->param('replyToEmail', '', new Text(128), 'Email set in the reply to field for the mail. Default value is sender email.', true) - ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true) - ->param('domain', '', new Text(0), 'Mailgun Domain.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $isEuRegion, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, string $apiKey, string $domain, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $apiKey, string $domain, ?bool $isEuRegion, ?bool $enabled, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -2290,7 +2290,7 @@ App::post('/v1/messaging/messages/email') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, string $description, array $cc, array $bcc, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, array $cc, array $bcc, string $description, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; @@ -2301,19 +2301,21 @@ App::post('/v1/messaging/messages/email') $mergedTargets = \array_merge($targets, $cc, $bcc); - $foundTargets = $dbForProject->find('targets', [ - Query::equal('$id', $mergedTargets), - Query::equal('providerType', [MESSAGE_TYPE_EMAIL]), - Query::limit(\count($mergedTargets)), - ]); + if (!empty($mergedTargets)) { + $foundTargets = $dbForProject->find('targets', [ + Query::equal('$id', $mergedTargets), + Query::equal('providerType', [MESSAGE_TYPE_EMAIL]), + Query::limit(\count($mergedTargets)), + ]); - if (\count($foundTargets) !== \count($mergedTargets)) { - throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL); - } + if (\count($foundTargets) !== \count($mergedTargets)) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL); + } - foreach ($foundTargets as $target) { - if ($target->isEmpty()) { - throw new Exception(Exception::USER_TARGET_NOT_FOUND); + foreach ($foundTargets as $target) { + if ($target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } } } diff --git a/app/worker.php b/app/worker.php index a8e560796..c9f1a0df1 100644 --- a/app/worker.php +++ b/app/worker.php @@ -277,7 +277,7 @@ $worker $worker->workerStart() ->action(function () use ($workerName) { - Console::info("Worker $workerName started"); + Console::info("Worker $workerName started"); }); $worker->start(); diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 2593a58e1..dc71b4df5 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -1860,8 +1860,8 @@ trait Base } }'; case self::$CREATE_FCM_PROVIDER: - return 'mutation createFcmProvider($providerId: String!, $name: String!, $serverKey: String!) { - messagingCreateFcmProvider(providerId: $providerId, name: $name, serverKey: $serverKey) { + return 'mutation createFcmProvider($providerId: String!, $name: String!, $serviceAccountJSON: String!) { + messagingCreateFcmProvider(providerId: $providerId, name: $name, serviceAccountJSON: $serviceAccountJSON) { _id name provider diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php index 04fa9f4f2..84b16597c 100644 --- a/tests/e2e/Services/Messaging/MessagingBase.php +++ b/tests/e2e/Services/Messaging/MessagingBase.php @@ -85,6 +85,7 @@ trait MessagingBase 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ]), $providersParams[$key]); + $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals($providersParams[$key]['name'], $response['body']['name']); \array_push($providers, $response['body']); @@ -555,16 +556,14 @@ trait MessagingBase $to = $emailDSN->getParam('to'); $fromName = $emailDSN->getParam('fromName'); $fromEmail = $emailDSN->getParam('fromEmail'); - $isEuRegion = $emailDSN->getParam('isEuRegion'); $apiKey = $emailDSN->getPassword(); - $domain = $emailDSN->getUser(); - if (empty($to) || empty($from) || empty($apiKey) || empty($domain) || empty($isEuRegion)) { + if (empty($to) || empty($apiKey)) { $this->markTestSkipped('Email provider not configured'); } // Create provider - $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/mailgun', \array_merge([ + $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid', \array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], @@ -572,8 +571,6 @@ trait MessagingBase 'providerId' => ID::unique(), 'name' => 'Mailgun-provider', 'apiKey' => $apiKey, - 'domain' => $domain, - 'isEuRegion' => filter_var($isEuRegion, FILTER_VALIDATE_BOOLEAN), 'fromName' => $fromName, 'fromEmail' => $fromEmail ]); @@ -607,27 +604,13 @@ trait MessagingBase $this->assertEquals(201, $user['headers']['status-code']); - // Create Target - $target = $this->client->call(Client::METHOD_POST, '/users/' . $user['body']['$id'] . '/targets', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'targetId' => ID::unique(), - 'providerType' => 'email', - 'providerId' => $provider['body']['$id'], - 'identifier' => $to, - ]); - - $this->assertEquals(201, $target['headers']['status-code']); - // Create Subscriber $subscriber = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topic['body']['$id'] . '/subscribers', \array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'subscriberId' => ID::unique(), - 'targetId' => $target['body']['$id'], + 'targetId' => $user['body']['targets'][0]['$id'], ]); $this->assertEquals(201, $subscriber['headers']['status-code']); @@ -640,13 +623,13 @@ trait MessagingBase ], [ 'messageId' => ID::unique(), 'topics' => [$topic['body']['$id']], - 'subject' => 'Khali beats Undertaker', - 'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + 'subject' => 'New blog post', + 'content' => 'Check out the new blog post at http://localhost', ]); $this->assertEquals(201, $email['headers']['status-code']); - \sleep(5); + \sleep(2); $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'], [ 'origin' => 'http://localhost', @@ -655,6 +638,8 @@ trait MessagingBase 'x-appwrite-key' => $this->getProject()['apiKey'], ]); + \var_dump($message); + $this->assertEquals(200, $message['headers']['status-code']); $this->assertEquals(1, $message['body']['deliveredTotal']); $this->assertEquals(0, \count($message['body']['deliveryErrors'])); @@ -665,7 +650,7 @@ trait MessagingBase /** * @depends testSendEmail */ - public function testUpdateEmail(array $email) + public function testUpdateEmail(array $email): void { $message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $email['body']['$id'], [ 'content-type' => 'application/json', @@ -724,8 +709,8 @@ trait MessagingBase $smsDSN = new DSN(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN')); $to = $smsDSN->getParam('to'); $from = $smsDSN->getParam('from'); - $authKey = $smsDSN->getPassword(); $senderId = $smsDSN->getUser(); + $authKey = $smsDSN->getPassword(); if (empty($to) || empty($from) || empty($senderId) || empty($authKey)) { $this->markTestSkipped('SMS provider not configured'); @@ -738,7 +723,7 @@ trait MessagingBase 'x-appwrite-key' => $this->getProject()['apiKey'], ]), [ 'providerId' => ID::unique(), - 'name' => 'Msg91-1', + 'name' => 'Msg91Sender', 'senderId' => $senderId, 'authKey' => $authKey, 'from' => $from @@ -850,7 +835,7 @@ trait MessagingBase 'messageId' => ID::unique(), 'status' => 'draft', 'topics' => [$sms['body']['topics'][0]], - 'content' => '047487', + 'content' => 'Your OTP code is 123456', ]); $this->assertEquals(201, $sms['headers']['status-code']); @@ -865,7 +850,7 @@ trait MessagingBase $this->assertEquals(200, $sms['headers']['status-code']); - \sleep(5); + \sleep(2); $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $sms['body']['$id'], [ 'origin' => 'http://localhost', @@ -885,9 +870,9 @@ trait MessagingBase $this->markTestSkipped('Push DSN empty'); } - $pushDSN = new DSN(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN')); - $to = $pushDSN->getParam('to'); - $serverKey = $pushDSN->getPassword(); + $dsn = new DSN(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN')); + $to = $dsn->getParam('to'); + $serviceAccountJSON = $dsn->getParam('saj'); if (empty($to) || empty($serverKey)) { $this->markTestSkipped('Push provider not configured'); @@ -901,7 +886,7 @@ trait MessagingBase ]), [ 'providerId' => ID::unique(), 'name' => 'FCM-1', - 'serverKey' => $serverKey, + 'serviceAccountJSON' => $serviceAccountJSON, ]); $this->assertEquals(201, $provider['headers']['status-code']); From 1957bc57c268cba249521e3982a7715dba47b859 Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Tue, 19 Dec 2023 18:28:25 +0530 Subject: [PATCH 13/58] makes messaging worker compatible with new messaging lib version --- composer.lock | 36 +++++++++---------- src/Appwrite/Platform/Workers/Messaging.php | 17 +++++---- .../e2e/Services/Messaging/MessagingBase.php | 1 - 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/composer.lock b/composer.lock index 803678ff4..3c195434b 100644 --- a/composer.lock +++ b/composer.lock @@ -2906,16 +2906,16 @@ }, { "name": "utopia-php/vcs", - "version": "0.6.2", + "version": "0.6.3", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "f135291b87cb45335fc6608722e7f89894bc33ee" + "reference": "86c3f42a2624bcccb7a67b74dcd7bd3a31fc2e4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/f135291b87cb45335fc6608722e7f89894bc33ee", - "reference": "f135291b87cb45335fc6608722e7f89894bc33ee", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/86c3f42a2624bcccb7a67b74dcd7bd3a31fc2e4b", + "reference": "86c3f42a2624bcccb7a67b74dcd7bd3a31fc2e4b", "shasum": "" }, "require": { @@ -2949,9 +2949,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/0.6.2" + "source": "https://github.com/utopia-php/vcs/tree/0.6.3" }, - "time": "2023-11-08T15:36:03+00:00" + "time": "2023-12-14T06:53:39+00:00" }, { "name": "utopia-php/websocket", @@ -3489,16 +3489,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.17.1", + "version": "v4.18.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", + "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", "shasum": "" }, "require": { @@ -3539,9 +3539,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0" }, - "time": "2023-08-13T19:53:39+00:00" + "time": "2023-12-10T21:03:43+00:00" }, { "name": "phar-io/manifest", @@ -3893,16 +3893,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.24.4", + "version": "1.24.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "6bd0c26f3786cd9b7c359675cb789e35a8e07496" + "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6bd0c26f3786cd9b7c359675cb789e35a8e07496", - "reference": "6bd0c26f3786cd9b7c359675cb789e35a8e07496", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fedf211ff14ec8381c9bf5714e33a7a552dd1acc", + "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc", "shasum": "" }, "require": { @@ -3934,9 +3934,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.4" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.5" }, - "time": "2023-11-26T18:29:22+00:00" + "time": "2023-12-16T09:33:33+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index e04bace4c..b075de762 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -123,7 +123,9 @@ class Messaging extends Action /** * @var Document[] $providers */ - $providers = []; + $providers = [ + $primaryProvider->getId() => $primaryProvider + ]; foreach ($recipients as $recipient) { $providerId = $recipient->getAttribute('providerId'); @@ -144,13 +146,16 @@ class Messaging extends Action */ $results = batch(\array_map(function ($providerId) use ($identifiersByProviderId, $providers, $primaryProvider, $message, $dbForProject) { return function () use ($providerId, $identifiersByProviderId, $providers, $primaryProvider, $message, $dbForProject) { - if ($primaryProvider->getId() === $providerId) { - $provider = $primaryProvider; - } else { + if (\array_key_exists($providerId, $providers)) { + $provider = $providers[$providerId]; + } + else { $provider = $dbForProject->getDocument('providers', $providerId, [Query::equal('enabled', [true])]); if ($provider->isEmpty()) { $provider = $primaryProvider; + } else { + $providers[$providerId] = $provider; } } @@ -183,7 +188,7 @@ class Messaging extends Action try { $response = new Response($provider->getAttribute('type')); - $response->fromArray(\json_decode($adapter->send($data))); + $response->fromArray($adapter->send($data)); $deliveredTotal += $response->getDeliveredTo(); $details[] = $response->getDetails(); @@ -193,7 +198,7 @@ class Messaging extends Action } // Deleting push targets when token has expired. - if ($detail['error'] === 'Expired token.') { + if ($detail['error'] === 'Expired device token.') { $target = $dbForProject->findOne('targets', [Query::equal('identifier', [$detail['recipient']])]); if ($target instanceof Document && !$target->isEmpty()) { $dbForProject->deleteDocument('targets', $target->getId()); diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php index 84b16597c..ca472591c 100644 --- a/tests/e2e/Services/Messaging/MessagingBase.php +++ b/tests/e2e/Services/Messaging/MessagingBase.php @@ -231,7 +231,6 @@ trait MessagingBase ], [ 'topicId' => ID::unique(), 'name' => 'my-app', - 'description' => 'web app' ]); $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals('my-app', $response['body']['name']); From 5d7eca7c99052cf305b4291294d6f5537f1f3793 Mon Sep 17 00:00:00 2001 From: prateek banga Date: Tue, 19 Dec 2023 23:45:20 +0530 Subject: [PATCH 14/58] lint fix and tests fix --- app/controllers/api/messaging.php | 20 ++++----- composer.lock | 2 +- src/Appwrite/Platform/Workers/Messaging.php | 5 +-- tests/e2e/Services/GraphQL/Base.php | 14 +++---- tests/e2e/Services/GraphQL/MessagingTest.php | 41 ++++++++++++++----- .../e2e/Services/Messaging/MessagingBase.php | 16 ++++++-- 6 files changed, 64 insertions(+), 34 deletions(-) diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index fdead6e6f..53135f219 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -608,21 +608,21 @@ App::post('/v1/messaging/providers/fcm') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('serviceAccountJSON', '', new Text(0), 'FCM service account JSON.', true) + ->param('serviceAccountJSON', null, new JSON(), 'FCM service account JSON.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $serverKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?array $serviceAccountJSON, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; $credentials = []; - if (!empty($serverKey)) { - $credentials['serverKey'] = $serverKey; + if (!\is_null($serviceAccountJSON)) { + $credentials['serviceAccountJSON'] = $serviceAccountJSON; } - if ($enabled === true && \array_key_exists('serverKey', $credentials)) { + if ($enabled === true && \array_key_exists('serviceAccountJSON', $credentials)) { $enabled = true; } else { $enabled = false; @@ -1500,11 +1500,11 @@ App::patch('/v1/messaging/providers/fcm/:providerId') ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) - ->param('serverKey', '', new Text(0), 'FCM Server Key.', true) + ->param('serviceAccountJSON', null, new JSON(), 'FCM service account JSON.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, string $serverKey, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, ?array $serviceAccountJSON, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -1520,12 +1520,12 @@ App::patch('/v1/messaging/providers/fcm/:providerId') $provider->setAttribute('name', $name); } - if (!empty($serverKey)) { - $provider->setAttribute('credentials', ['serverKey' => $serverKey]); + if (!\is_null($serviceAccountJSON)) { + $provider->setAttribute('credentials', ['serviceAccountJSON' => $serviceAccountJSON]); } if ($enabled === true || $enabled === false) { - if ($enabled === true && \array_key_exists('serverKey', $provider->getAttribute('credentials'))) { + if ($enabled === true && \array_key_exists('serviceAccountJSON', $provider->getAttribute('credentials'))) { $enabled = true; } else { $enabled = false; diff --git a/composer.lock b/composer.lock index 3c195434b..6b7fc0ac4 100644 --- a/composer.lock +++ b/composer.lock @@ -5849,5 +5849,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index b075de762..ed7273168 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -148,8 +148,7 @@ class Messaging extends Action return function () use ($providerId, $identifiersByProviderId, $providers, $primaryProvider, $message, $dbForProject) { if (\array_key_exists($providerId, $providers)) { $provider = $providers[$providerId]; - } - else { + } else { $provider = $dbForProject->getDocument('providers', $providerId, [Query::equal('enabled', [true])]); if ($provider->isEmpty()) { @@ -348,7 +347,7 @@ class Messaging extends Action $credentials['bundleId'], $credentials['endpoint'] ), - 'fcm' => new FCM($credentials['serverKey']), + 'fcm' => new FCM($credentials['serviceAccountJSON']), default => null }; } diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index dc71b4df5..2c630e3f7 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -1860,7 +1860,7 @@ trait Base } }'; case self::$CREATE_FCM_PROVIDER: - return 'mutation createFcmProvider($providerId: String!, $name: String!, $serviceAccountJSON: String!) { + return 'mutation createFcmProvider($providerId: String!, $name: String!, $serviceAccountJSON: Json) { messagingCreateFcmProvider(providerId: $providerId, name: $name, serviceAccountJSON: $serviceAccountJSON) { _id name @@ -1904,8 +1904,8 @@ trait Base } }'; case self::$UPDATE_MAILGUN_PROVIDER: - return 'mutation updateMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $isEuRegion: Boolean, $enabled: Boolean) { - messagingUpdateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, isEuRegion: $isEuRegion, enabled: $enabled) { + return 'mutation updateMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $isEuRegion: Boolean, $enabled: Boolean, $fromName: String, $fromEmail: String) { + messagingUpdateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, isEuRegion: $isEuRegion, enabled: $enabled, fromName: $fromName, fromEmail: $fromEmail) { _id name provider @@ -1914,8 +1914,8 @@ trait Base } }'; case self::$UPDATE_SENDGRID_PROVIDER: - return 'mutation messagingUpdateSendgridProvider($providerId: String!, $name: String!, $apiKey: String!) { - messagingUpdateSendgridProvider(providerId: $providerId, name: $name, apiKey: $apiKey) { + return 'mutation messagingUpdateSendgridProvider($providerId: String!, $name: String!, $apiKey: String!, $enabled: Boolean, $fromName: String, $fromEmail: String) { + messagingUpdateSendgridProvider(providerId: $providerId, name: $name, apiKey: $apiKey, enabled: $enabled, fromName: $fromName, fromEmail: $fromEmail) { _id name provider @@ -1974,8 +1974,8 @@ trait Base } }'; case self::$UPDATE_FCM_PROVIDER: - return 'mutation updateFcmProvider($providerId: String!, $name: String!, $serverKey: String!) { - messagingUpdateFcmProvider(providerId: $providerId, name: $name, serverKey: $serverKey) { + return 'mutation updateFcmProvider($providerId: String!, $name: String!, $serviceAccountJSON: Json) { + messagingUpdateFcmProvider(providerId: $providerId, name: $name, serviceAccountJSON: $serviceAccountJSON) { _id name provider diff --git a/tests/e2e/Services/GraphQL/MessagingTest.php b/tests/e2e/Services/GraphQL/MessagingTest.php index d1a084cfc..e778e4e30 100644 --- a/tests/e2e/Services/GraphQL/MessagingTest.php +++ b/tests/e2e/Services/GraphQL/MessagingTest.php @@ -23,14 +23,16 @@ class MessagingTest extends Scope 'providerId' => ID::unique(), 'name' => 'Sengrid1', 'apiKey' => 'my-apikey', - 'from' => 'sender-email@my-domain.com', + 'fromName' => 'Sender Name', + 'fromEmail' => 'sender-email@my-domain.com', ], 'Mailgun' => [ 'providerId' => ID::unique(), 'name' => 'Mailgun1', 'apiKey' => 'my-apikey', 'domain' => 'my-domain', - 'from' => 'sender-email@my-domain.com', + 'fromName' => 'Sender Name', + 'fromEmail' => 'sender-email@my-domain.com', 'isEuRegion' => false, ], 'Twilio' => [ @@ -71,7 +73,12 @@ class MessagingTest extends Scope 'Fcm' => [ 'providerId' => ID::unique(), 'name' => 'FCM1', - 'serverKey' => 'my-serverkey', + 'serviceAccountJSON' => [ + 'type' => 'service_account', + "project_id" => "omegle-copy", + "private_key_id" => "ewfwefwefwefwef", + "private_key" => "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkAgEAAoIBAQCeKDbvv4XvGuNAOxZBcxoNnvbINKlq0FtiqgsqLAgDOMt\nGPEANfni+D7lZRrMPhZhcL4YCjAUg+0ZI0D9d2LGofasj9GlBb57SZc/ud2L9FZZ\nk5liXrUk0SUirffBUmj5F/XPTJ+JXc89qPtt15+hqx30h2ID/wxN0AhmViLikR3o\n3YBHAYq0NbmAfQSfdsHX+lvNKvsAxU+LatRPE3tVcvSd3ZnP0zlHYVmp6UCeBWeW\nOTcSPecCcVXmBdMPtHGWdrNG2op1CmHc8JeYJMQ4xgz3obQcOX9+3USsysANjgta\nb07m6xS3AgMBAAECggEAIeSTVVCRZrq36zk8VgJ/r/NE4r95xEk2K/K/Lvb0fx75\no0BO5gsAkYqvgzem/LrVFCEFRkDGMbAhVQ5Fw1pN2U6CyA0hL4jUqgALtMImKJdX\nDa6I5Gibwd5+qt9NOZSgC/Kq14zAxhfQE3U2hyatohyx3Rsz/3lmJo90bX7Jp5md\nGBDOB3pFBqyfUvyHgeqCgvJvidJjxmwArLhUF8szuDRvmSs0lGsfqYprK0sb9phL\nP7Z3qMJk1J4IDL2abSGrTcMP+hk7ju1iqo7WfhIQCvM1TD5dRjYg2IYPIAIzszWz\nxSA67eJpQGSFfOuk82g3UMhfCD2DY2mCE/zkeid9jQKBgQDSB2xA+LpQDX2nuoDR\niZbPYBitxQtkbjieYTR8vwrIzyAvRtOwjnVKsXLyIbUYyHd6RFRDPeBcHb39KuRO\nz7VljQKTVB5RYUmqeGilor0TFaKMnneC7GFH6mWOJyf16DU7bkQw27Pg1e3xbF28\n5ig7QYPqEaDKLg6TMSLsBhdRDQKBgQDAxj9jS9UOTmF3N9T1JFzWfUB2r+AgwE4N\nSITmG/fSz9rlSg+XPh2ijpSrboUbuY/GYq5aCIy1twx09eta07Y/uD/GKLYrk873\no0TxQrnHSKl82fCyd2JPG/W8ocGDnj3u0Dp+tBrLxDiZN2pRurnlkt7P3QUg/gEG\nAovyd3ij0wKBgBbA7x1q1ORvUbmmHuaUfV4iDwpkWoOa3U9rQIBzQfvXVKlKhwyN\nom9hIg7RUAlLToZUeLyAK5pPLpIK34kaP5Cs4iaL6mzumUh6mvu20b0Ljvyk/lWU\nvkVIQ5BO9alSatHxdDnG04n8IzcQgmdAmAMzadMl7cF5k+KmZB4l2sjRAoGAP8JS\nPNlcAntSKUhCG0KHojmTFK5fBvYT2rjdm+4sLYGp+KRiO7fDvXxDF+BaDi11rDv/\nRrAFOiTs7dJYoZXcdX7POQ9GEWu1zJont1RGde9Gf5Dl12E9FsU8pcMqagnwmggt\nELMpGbQwtBxsAdQsoA3PvBhyFdNtKzu0ZeG1+RkCgYBOPhOCR88QPTmQANkwIMH+\n0vt+KhSjE3dhX7rzVkhoNmYF5AaSSpQ3F1JUlYntjblMQVjLesGvWa4gwCOF87xC\nJxHL6LkbNjAyUGZp7to6/F4vTmKoC6Xu/jTRRy2SVjdqiIa0Pm0eLLfRHmSI06pS\n+zLmdpZv/msPfGibbHcXUA==\n-----END PRIVATE KEY-----\n", + ] ], 'Apns' => [ 'providerId' => ID::unique(), @@ -97,6 +104,7 @@ class MessagingTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ]), $graphQLPayload); + var_dump($response['body']); \array_push($providers, $response['body']['data']['messagingCreate' . $key . 'Provider']); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals($providersParams[$key]['name'], $response['body']['data']['messagingCreate' . $key . 'Provider']['name']); @@ -155,7 +163,12 @@ class MessagingTest extends Scope 'Fcm' => [ 'providerId' => $providers[7]['_id'], 'name' => 'FCM2', - 'serverKey' => 'my-serverkey', + 'serviceAccountJSON' => [ + 'type' => 'service_account', + "project_id" => "omegle-copy", + "private_key_id" => "ewfwefwefwefwef", + "private_key" => "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkAgEAAoIBAQCeKDbvv4XvGuNAOxZBcxoNnvbINKlq0FtiqgsqLAgDOMt\nGPEANfni+D7lZRrMPhZhcL4YCjAUg+0ZI0D9d2LGofasj9GlBb57SZc/ud2L9FZZ\nk5liXrUk0SUirffBUmj5F/XPTJ+JXc89qPtt15+hqx30h2ID/wxN0AhmViLikR3o\n3YBHAYq0NbmAfQSfdsHX+lvNKvsAxU+LatRPE3tVcvSd3ZnP0zlHYVmp6UCeBWeW\nOTcSPecCcVXmBdMPtHGWdrNG2op1CmHc8JeYJMQ4xgz3obQcOX9+3USsysANjgta\nb07m6xS3AgMBAAECggEAIeSTVVCRZrq36zk8VgJ/r/NE4r95xEk2K/K/Lvb0fx75\no0BO5gsAkYqvgzem/LrVFCEFRkDGMbAhVQ5Fw1pN2U6CyA0hL4jUqgALtMImKJdX\nDa6I5Gibwd5+qt9NOZSgC/Kq14zAxhfQE3U2hyatohyx3Rsz/3lmJo90bX7Jp5md\nGBDOB3pFBqyfUvyHgeqCgvJvidJjxmwArLhUF8szuDRvmSs0lGsfqYprK0sb9phL\nP7Z3qMJk1J4IDL2abSGrTcMP+hk7ju1iqo7WfhIQCvM1TD5dRjYg2IYPIAIzszWz\nxSA67eJpQGSFfOuk82g3UMhfCD2DY2mCE/zkeid9jQKBgQDSB2xA+LpQDX2nuoDR\niZbPYBitxQtkbjieYTR8vwrIzyAvRtOwjnVKsXLyIbUYyHd6RFRDPeBcHb39KuRO\nz7VljQKTVB5RYUmqeGilor0TFaKMnneC7GFH6mWOJyf16DU7bkQw27Pg1e3xbF28\n5ig7QYPqEaDKLg6TMSLsBhdRDQKBgQDAxj9jS9UOTmF3N9T1JFzWfUB2r+AgwE4N\nSITmG/fSz9rlSg+XPh2ijpSrboUbuY/GYq5aCIy1twx09eta07Y/uD/GKLYrk873\no0TxQrnHSKl82fCyd2JPG/W8ocGDnj3u0Dp+tBrLxDiZN2pRurnlkt7P3QUg/gEG\nAovyd3ij0wKBgBbA7x1q1ORvUbmmHuaUfV4iDwpkWoOa3U9rQIBzQfvXVKlKhwyN\nom9hIg7RUAlLToZUeLyAK5pPLpIK34kaP5Cs4iaL6mzumUh6mvu20b0Ljvyk/lWU\nvkVIQ5BO9alSatHxdDnG04n8IzcQgmdAmAMzadMl7cF5k+KmZB4l2sjRAoGAP8JS\nPNlcAntSKUhCG0KHojmTFK5fBvYT2rjdm+4sLYGp+KRiO7fDvXxDF+BaDi11rDv/\nRrAFOiTs7dJYoZXcdX7POQ9GEWu1zJont1RGde9Gf5Dl12E9FsU8pcMqagnwmggt\nELMpGbQwtBxsAdQsoA3PvBhyFdNtKzu0ZeG1+RkCgYBOPhOCR88QPTmQANkwIMH+\n0vt+KhSjE3dhX7rzVkhoNmYF5AaSSpQ3F1JUlYntjblMQVjLesGvWa4gwCOF87xC\nJxHL6LkbNjAyUGZp7to6/F4vTmKoC6Xu/jTRRy2SVjdqiIa0Pm0eLLfRHmSI06pS\n+zLmdpZv/msPfGibbHcXUA==\n-----END PRIVATE KEY-----\n", + ] ], 'Apns' => [ 'providerId' => $providers[8]['_id'], @@ -374,7 +387,8 @@ class MessagingTest extends Scope 'providerId' => ID::unique(), 'name' => 'Sengrid1', 'apiKey' => 'my-apikey', - 'from' => 'sender-email@my-domain.com', + 'fromName' => 'Sender', + 'fromEmail' => 'sender-email@my-domain.com', ] ]; $query = $this->getQuery(self::$CREATE_SENDGRID_PROVIDER); @@ -543,7 +557,8 @@ class MessagingTest extends Scope $emailDSN = new DSN(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN')); $to = $emailDSN->getParam('to'); - $from = $emailDSN->getParam('from'); + $fromName = $emailDSN->getParam('fromName'); + $fromEmail = $emailDSN->getParam('fromEmail'); $isEuRegion = $emailDSN->getParam('isEuRegion'); $apiKey = $emailDSN->getPassword(); $domain = $emailDSN->getUser(); @@ -560,7 +575,8 @@ class MessagingTest extends Scope 'name' => 'Mailgun1', 'apiKey' => $apiKey, 'domain' => $domain, - 'from' => $from, + 'fromName' => $fromName, + 'fromEmail' => $fromEmail, 'isEuRegion' => filter_var($isEuRegion, FILTER_VALIDATE_BOOLEAN), ], ]; @@ -956,9 +972,9 @@ class MessagingTest extends Scope $pushDSN = new DSN(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN')); $to = $pushDSN->getParam('to'); - $serverKey = $pushDSN->getPassword(); + $serviceAccountJSON = $pushDSN->getParam('saj'); - if (empty($to) || empty($serverKey)) { + if (empty($to) || empty($serviceAccountJSON)) { $this->markTestSkipped('Push provider not configured'); } @@ -968,7 +984,12 @@ class MessagingTest extends Scope 'variables' => [ 'providerId' => ID::unique(), 'name' => 'FCM1', - 'serverKey' => $serverKey, + 'serviceAccountJSON' => [ + 'type' => 'service_account', + "project_id" => "token-test", + "private_key_id" => "bitcoin-is-the-future", + "private_key" => "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkAgEAAoIBAQCeKDbvv4XvGuNAOxZBcxoNnvbINKlq0FtiqgsqLAgDOMt\nGPEANfni+D7lZRrMPhZhcL4YCjAUg+0ZI0D9d2LGofasj9GlBb57SZc/ud2L9FZZ\nk5liXrUk0SUirffBUmj5F/XPTJ+JXc89qPtt15+hqx30h2ID/wxN0AhmViLikR3o\n3YBHAYq0NbmAfQSfdsHX+lvNKvsAxU+LatRPE3tVcvSd3ZnP0zlHYVmp6UCeBWeW\nOTcSPecCcVXmBdMPtHGWdrNG2op1CmHc8JeYJMQ4xgz3obQcOX9+3USsysANjgta\nb07m6xS3AgMBAAECggEAIeSTVVCRZrq36zk8VgJ/r/NE4r95xEk2K/K/Lvb0fx75\no0BO5gsAkYqvgzem/LrVFCEFRkDGMbAhVQ5Fw1pN2U6CyA0hL4jUqgALtMImKJdX\nDa6I5Gibwd5+qt9NOZSgC/Kq14zAxhfQE3U2hyatohyx3Rsz/3lmJo90bX7Jp5md\nGBDOB3pFBqyfUvyHgeqCgvJvidJjxmwArLhUF8szuDRvmSs0lGsfqYprK0sb9phL\nP7Z3qMJk1J4IDL2abSGrTcMP+hk7ju1iqo7WfhIQCvM1TD5dRjYg2IYPIAIzszWz\nxSA67eJpQGSFfOuk82g3UMhfCD2DY2mCE/zkeid9jQKBgQDSB2xA+LpQDX2nuoDR\niZbPYBitxQtkbjieYTR8vwrIzyAvRtOwjnVKsXLyIbUYyHd6RFRDPeBcHb39KuRO\nz7VljQKTVB5RYUmqeGilor0TFaKMnneC7GFH6mWOJyf16DU7bkQw27Pg1e3xbF28\n5ig7QYPqEaDKLg6TMSLsBhdRDQKBgQDAxj9jS9UOTmF3N9T1JFzWfUB2r+AgwE4N\nSITmG/fSz9rlSg+XPh2ijpSrboUbuY/GYq5aCIy1twx09eta07Y/uD/GKLYrk873\no0TxQrnHSKl82fCyd2JPG/W8ocGDnj3u0Dp+tBrLxDiZN2pRurnlkt7P3QUg/gEG\nAovyd3ij0wKBgBbA7x1q1ORvUbmmHuaUfV4iDwpkWoOa3U9rQIBzQfvXVKlKhwyN\nom9hIg7RUAlLToZUeLyAK5pPLpIK34kaP5Cs4iaL6mzumUh6mvu20b0Ljvyk/lWU\nvkVIQ5BO9alSatHxdDnG04n8IzcQgmdAmAMzadMl7cF5k+KmZB4l2sjRAoGAP8JS\nPNlcAntSKUhCG0KHojmTFK5fBvYT2rjdm+4sLYGp+KRiO7fDvXxDF+BaDi11rDv/\nRrAFOiTs7dJYoZXcdX7POQ9GEWu1zJont1RGde9Gf5Dl12E9FsU8pcMqagnwmggt\nELMpGbQwtBxsAdQsoA3PvBhyFdNtKzu0ZeG1+RkCgYBOPhOCR88QPTmQANkwIMH+\n0vt+KhSjE3dhX7rzVkhoNmYF5AaSSpQ3F1JUlYntjblMQVjLesGvWa4gwCOF87xC\nJxHL6LkbNjAyUGZp7to6/F4vTmKoC6Xu/jTRRy2SVjdqiIa0Pm0eLLfRHmSI06pS\n+zLmdpZv/msPfGibbHcXUA==\n-----END PRIVATE KEY-----\n", + ] ], ]; $provider = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php index ca472591c..9e1788928 100644 --- a/tests/e2e/Services/Messaging/MessagingBase.php +++ b/tests/e2e/Services/Messaging/MessagingBase.php @@ -65,7 +65,12 @@ trait MessagingBase 'fcm' => [ 'providerId' => ID::unique(), 'name' => 'FCM1', - 'serverKey' => 'my-serverkey', + 'serviceAccountJSON' => [ + 'type' => 'service_account', + "project_id" => "omegle-copy", + "private_key_id" => "ewfwefwefwefwef", + "private_key" => "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkAgEAAoIBAQCeKDbvv4XvGuNAOxZBcxoNnvbINKlq0FtiqgsqLAgDOMt\nGPEANfni+D7lZRrMPhZhcL4YCjAUg+0ZI0D9d2LGofasj9GlBb57SZc/ud2L9FZZ\nk5liXrUk0SUirffBUmj5F/XPTJ+JXc89qPtt15+hqx30h2ID/wxN0AhmViLikR3o\n3YBHAYq0NbmAfQSfdsHX+lvNKvsAxU+LatRPE3tVcvSd3ZnP0zlHYVmp6UCeBWeW\nOTcSPecCcVXmBdMPtHGWdrNG2op1CmHc8JeYJMQ4xgz3obQcOX9+3USsysANjgta\nb07m6xS3AgMBAAECggEAIeSTVVCRZrq36zk8VgJ/r/NE4r95xEk2K/K/Lvb0fx75\no0BO5gsAkYqvgzem/LrVFCEFRkDGMbAhVQ5Fw1pN2U6CyA0hL4jUqgALtMImKJdX\nDa6I5Gibwd5+qt9NOZSgC/Kq14zAxhfQE3U2hyatohyx3Rsz/3lmJo90bX7Jp5md\nGBDOB3pFBqyfUvyHgeqCgvJvidJjxmwArLhUF8szuDRvmSs0lGsfqYprK0sb9phL\nP7Z3qMJk1J4IDL2abSGrTcMP+hk7ju1iqo7WfhIQCvM1TD5dRjYg2IYPIAIzszWz\nxSA67eJpQGSFfOuk82g3UMhfCD2DY2mCE/zkeid9jQKBgQDSB2xA+LpQDX2nuoDR\niZbPYBitxQtkbjieYTR8vwrIzyAvRtOwjnVKsXLyIbUYyHd6RFRDPeBcHb39KuRO\nz7VljQKTVB5RYUmqeGilor0TFaKMnneC7GFH6mWOJyf16DU7bkQw27Pg1e3xbF28\n5ig7QYPqEaDKLg6TMSLsBhdRDQKBgQDAxj9jS9UOTmF3N9T1JFzWfUB2r+AgwE4N\nSITmG/fSz9rlSg+XPh2ijpSrboUbuY/GYq5aCIy1twx09eta07Y/uD/GKLYrk873\no0TxQrnHSKl82fCyd2JPG/W8ocGDnj3u0Dp+tBrLxDiZN2pRurnlkt7P3QUg/gEG\nAovyd3ij0wKBgBbA7x1q1ORvUbmmHuaUfV4iDwpkWoOa3U9rQIBzQfvXVKlKhwyN\nom9hIg7RUAlLToZUeLyAK5pPLpIK34kaP5Cs4iaL6mzumUh6mvu20b0Ljvyk/lWU\nvkVIQ5BO9alSatHxdDnG04n8IzcQgmdAmAMzadMl7cF5k+KmZB4l2sjRAoGAP8JS\nPNlcAntSKUhCG0KHojmTFK5fBvYT2rjdm+4sLYGp+KRiO7fDvXxDF+BaDi11rDv/\nRrAFOiTs7dJYoZXcdX7POQ9GEWu1zJont1RGde9Gf5Dl12E9FsU8pcMqagnwmggt\nELMpGbQwtBxsAdQsoA3PvBhyFdNtKzu0ZeG1+RkCgYBOPhOCR88QPTmQANkwIMH+\n0vt+KhSjE3dhX7rzVkhoNmYF5AaSSpQ3F1JUlYntjblMQVjLesGvWa4gwCOF87xC\nJxHL6LkbNjAyUGZp7to6/F4vTmKoC6Xu/jTRRy2SVjdqiIa0Pm0eLLfRHmSI06pS\n+zLmdpZv/msPfGibbHcXUA==\n-----END PRIVATE KEY-----\n", + ], ], 'apns' => [ 'providerId' => ID::unique(), @@ -136,7 +141,12 @@ trait MessagingBase ], 'fcm' => [ 'name' => 'FCM2', - 'serverKey' => 'my-serverkey', + 'serviceAccountJSON' => [ + 'type' => 'service_account', + "project_id" => "omegle-copy", + "private_key_id" => "ewfwefwefwefwef", + "private_key" => "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkAgEAAoIBAQCeKDbvv4XvGuNAOxZBcxoNnvbINKlq0FtiqgsqLAgDOMt\nGPEANfni+D7lZRrMPhZhcL4YCjAUg+0ZI0D9d2LGofasj9GlBb57SZc/ud2L9FZZ\nk5liXrUk0SUirffBUmj5F/XPTJ+JXc89qPtt15+hqx30h2ID/wxN0AhmViLikR3o\n3YBHAYq0NbmAfQSfdsHX+lvNKvsAxU+LatRPE3tVcvSd3ZnP0zlHYVmp6UCeBWeW\nOTcSPecCcVXmBdMPtHGWdrNG2op1CmHc8JeYJMQ4xgz3obQcOX9+3USsysANjgta\nb07m6xS3AgMBAAECggEAIeSTVVCRZrq36zk8VgJ/r/NE4r95xEk2K/K/Lvb0fx75\no0BO5gsAkYqvgzem/LrVFCEFRkDGMbAhVQ5Fw1pN2U6CyA0hL4jUqgALtMImKJdX\nDa6I5Gibwd5+qt9NOZSgC/Kq14zAxhfQE3U2hyatohyx3Rsz/3lmJo90bX7Jp5md\nGBDOB3pFBqyfUvyHgeqCgvJvidJjxmwArLhUF8szuDRvmSs0lGsfqYprK0sb9phL\nP7Z3qMJk1J4IDL2abSGrTcMP+hk7ju1iqo7WfhIQCvM1TD5dRjYg2IYPIAIzszWz\nxSA67eJpQGSFfOuk82g3UMhfCD2DY2mCE/zkeid9jQKBgQDSB2xA+LpQDX2nuoDR\niZbPYBitxQtkbjieYTR8vwrIzyAvRtOwjnVKsXLyIbUYyHd6RFRDPeBcHb39KuRO\nz7VljQKTVB5RYUmqeGilor0TFaKMnneC7GFH6mWOJyf16DU7bkQw27Pg1e3xbF28\n5ig7QYPqEaDKLg6TMSLsBhdRDQKBgQDAxj9jS9UOTmF3N9T1JFzWfUB2r+AgwE4N\nSITmG/fSz9rlSg+XPh2ijpSrboUbuY/GYq5aCIy1twx09eta07Y/uD/GKLYrk873\no0TxQrnHSKl82fCyd2JPG/W8ocGDnj3u0Dp+tBrLxDiZN2pRurnlkt7P3QUg/gEG\nAovyd3ij0wKBgBbA7x1q1ORvUbmmHuaUfV4iDwpkWoOa3U9rQIBzQfvXVKlKhwyN\nom9hIg7RUAlLToZUeLyAK5pPLpIK34kaP5Cs4iaL6mzumUh6mvu20b0Ljvyk/lWU\nvkVIQ5BO9alSatHxdDnG04n8IzcQgmdAmAMzadMl7cF5k+KmZB4l2sjRAoGAP8JS\nPNlcAntSKUhCG0KHojmTFK5fBvYT2rjdm+4sLYGp+KRiO7fDvXxDF+BaDi11rDv/\nRrAFOiTs7dJYoZXcdX7POQ9GEWu1zJont1RGde9Gf5Dl12E9FsU8pcMqagnwmggt\nELMpGbQwtBxsAdQsoA3PvBhyFdNtKzu0ZeG1+RkCgYBOPhOCR88QPTmQANkwIMH+\n0vt+KhSjE3dhX7rzVkhoNmYF5AaSSpQ3F1JUlYntjblMQVjLesGvWa4gwCOF87xC\nJxHL6LkbNjAyUGZp7to6/F4vTmKoC6Xu/jTRRy2SVjdqiIa0Pm0eLLfRHmSI06pS\n+zLmdpZv/msPfGibbHcXUA==\n-----END PRIVATE KEY-----\n", + ] ], 'apns' => [ 'name' => 'APNS2', @@ -873,7 +883,7 @@ trait MessagingBase $to = $dsn->getParam('to'); $serviceAccountJSON = $dsn->getParam('saj'); - if (empty($to) || empty($serverKey)) { + if (empty($to) || empty($serviceAccountJSON)) { $this->markTestSkipped('Push provider not configured'); } From d9319aa888efc966f48cc47703472afcf7fcf1b1 Mon Sep 17 00:00:00 2001 From: Utkarsh Ahuja Date: Sat, 23 Dec 2023 17:42:34 +0530 Subject: [PATCH 15/58] feat: added zoho oauth metadata --- app/config/providers.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/config/providers.php b/app/config/providers.php index 6ba54f28e..9d7efc2f1 100644 --- a/app/config/providers.php +++ b/app/config/providers.php @@ -362,6 +362,16 @@ return [ 'beta' => false, 'mock' => false, ], + 'zoho' => [ + 'name' => 'Zoho', + 'developers' => 'https://zoho.com/accounts/protocol/oauth.html', + 'icon' => 'icon-zoho', + 'enabled' => true, + 'sandbox' => false, + 'form' => false, + 'beta' => false, + 'mock' => false, + ], 'zoom' => [ 'name' => 'Zoom', 'developers' => 'https://marketplace.zoom.us/docs/guides/auth/oauth/', From 26ac88c32e3548c6b14add906e75ed512f4a8662 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 25 Dec 2023 05:54:59 +0000 Subject: [PATCH 16/58] validate create permission while updating chunk uploaded file --- app/controllers/api/storage.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 1fae48dae..d390f9ef1 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -621,8 +621,13 @@ App::post('/v1/storage/buckets/:bucketId/files') ->setAttribute('openSSLIV', $openSSLIV) ->setAttribute('metadata', $metadata) ->setAttribute('chunksUploaded', $chunksUploaded); - - $file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file); + + // Validate create permission + $validator = new Authorization(Database::PERMISSION_CREATE); + if (!$validator->isValid($bucket->getCreate())) { + throw new Exception(Exception::USER_UNAUTHORIZED); + } + $file = Authorization::skip(fn() => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); } } catch (AuthorizationException) { throw new Exception(Exception::USER_UNAUTHORIZED); @@ -659,7 +664,12 @@ App::post('/v1/storage/buckets/:bucketId/files') ->setAttribute('chunksUploaded', $chunksUploaded) ->setAttribute('metadata', $metadata); - $file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file); + // Validate create permission + $validator = new Authorization(Database::PERMISSION_CREATE); + if (!$validator->isValid($bucket->getCreate())) { + throw new Exception(Exception::USER_UNAUTHORIZED); + } + $file = Authorization::skip(fn() => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); } } catch (AuthorizationException) { throw new Exception(Exception::USER_UNAUTHORIZED); From b6b1b396b384c4d4b6cc520586c0468753a7d14e Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 25 Dec 2023 06:00:43 +0000 Subject: [PATCH 17/58] update chunk upload test --- tests/e2e/Services/Storage/StorageBase.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index 546374076..be01d9536 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -74,10 +74,7 @@ trait StorageBase 'name' => 'Test Bucket 2', 'fileSecurity' => true, 'permissions' => [ - Permission::read(Role::any()), Permission::create(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), ], ]); $this->assertEquals(201, $bucket2['headers']['status-code']); From a6b4ade39b72bb595bd5c7c03063bf6d475437d8 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 25 Dec 2023 06:06:18 +0000 Subject: [PATCH 18/58] update large file upload test to not include update permission --- tests/e2e/Services/Storage/StorageBase.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index be01d9536..eb6a52db6 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -107,9 +107,7 @@ trait StorageBase 'fileId' => $fileId, 'file' => $curlFile, 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), + Permission::read(Role::any()) ], ]); $counter++; From cbd3e85b389400740973a9e8948b457062adcb9e Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 25 Dec 2023 06:07:28 +0000 Subject: [PATCH 19/58] fix formatting --- app/controllers/api/storage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index d390f9ef1..687971c2d 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -621,7 +621,7 @@ App::post('/v1/storage/buckets/:bucketId/files') ->setAttribute('openSSLIV', $openSSLIV) ->setAttribute('metadata', $metadata) ->setAttribute('chunksUploaded', $chunksUploaded); - + // Validate create permission $validator = new Authorization(Database::PERMISSION_CREATE); if (!$validator->isValid($bucket->getCreate())) { From f1ba7b08ab40718aa08f0eb0b682676ca0a74ec7 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 27 Dec 2023 23:35:32 +0000 Subject: [PATCH 20/58] Update the delete identity endpoints to set the params and payload Because no payload was set, the event params (userId and identityId) wasn't picked up automatically. This updates the endpoints so that the payload is set, but also makes sure to set the userId and identityId params since the identityId param's key doesn't match the key in the payload. --- app/controllers/api/account.php | 10 ++++++++-- app/controllers/api/users.php | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index c210b19f4..9cbe6ce7e 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -842,7 +842,7 @@ App::get('/v1/account/identities') }); App::delete('/v1/account/identities/:identityId') - ->desc('Delete Identity') + ->desc('Delete identity') ->groups(['api', 'account']) ->label('scope', 'account') ->label('event', 'users.[userId].identities.[identityId].delete') @@ -859,7 +859,8 @@ App::delete('/v1/account/identities/:identityId') ->param('identityId', '', new UID(), 'Identity ID.') ->inject('response') ->inject('dbForProject') - ->action(function (string $identityId, Response $response, Database $dbForProject) { + ->inject('queueForEvents') + ->action(function (string $identityId, Response $response, Database $dbForProject, Event $queueForEvents) { $identity = $dbForProject->getDocument('identities', $identityId); @@ -869,6 +870,11 @@ App::delete('/v1/account/identities/:identityId') $dbForProject->deleteDocument('identities', $identityId); + $queueForEvents + ->setParam('userId', $identity->getAttribute('userId')) + ->setParam('identityId', $identity->getId()) + ->setPayload($response->output($identity, Response::MODEL_IDENTITY)); + return $response->noContent(); }); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 0869453cc..30f52c614 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -1211,7 +1211,7 @@ App::delete('/v1/users/:userId') }); App::delete('/v1/users/identities/:identityId') - ->desc('Delete Identity') + ->desc('Delete identity') ->groups(['api', 'users']) ->label('event', 'users.[userId].identities.[identityId].delete') ->label('scope', 'users.write') @@ -1227,7 +1227,8 @@ App::delete('/v1/users/identities/:identityId') ->param('identityId', '', new UID(), 'Identity ID.') ->inject('response') ->inject('dbForProject') - ->action(function (string $identityId, Response $response, Database $dbForProject) { + ->inject('queueForEvents') + ->action(function (string $identityId, Response $response, Database $dbForProject, Event $queueForEvents) { $identity = $dbForProject->getDocument('identities', $identityId); @@ -1237,6 +1238,11 @@ App::delete('/v1/users/identities/:identityId') $dbForProject->deleteDocument('identities', $identityId); + $queueForEvents + ->setParam('userId', $identity->getAttribute('userId')) + ->setParam('identityId', $identity->getId()) + ->setPayload($response->output($identity, Response::MODEL_IDENTITY)); + return $response->noContent(); }); From e8ff828039bd2e60c003d9a828b6ea5196a76052 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 28 Dec 2023 01:31:25 +0000 Subject: [PATCH 21/58] fix for file extension not supported - the error occured with jfif extension, which is essentially a jpeg file --- app/controllers/api/storage.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 1fae48dae..860075273 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -915,6 +915,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') } if (empty($output)) { + // when file extension is provided but it's not one of our + // supported outputs we fallback to `jpg` + if(!empty($type) && !array_key_exists($type, $outputs)) { + $type = 'jpg'; + } + // when file extension is not provided and the mime type is not one of our supported outputs // we fallback to `jpg` output format $output = empty($type) ? (array_search($mime, $outputs) ?? 'jpg') : $type; From 94a18ede78a89db8caf959d1aab723c6256e4b2f Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 28 Dec 2023 01:35:10 +0000 Subject: [PATCH 22/58] fix formatting --- app/controllers/api/storage.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 860075273..5ef810fec 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -915,12 +915,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') } if (empty($output)) { - // when file extension is provided but it's not one of our + // when file extension is provided but it's not one of our // supported outputs we fallback to `jpg` - if(!empty($type) && !array_key_exists($type, $outputs)) { + if (!empty($type) && !array_key_exists($type, $outputs)) { $type = 'jpg'; } - + // when file extension is not provided and the mime type is not one of our supported outputs // we fallback to `jpg` output format $output = empty($type) ? (array_search($mime, $outputs) ?? 'jpg') : $type; From 83a009dcba7955f9a2b317534b44b52ded571ddc Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 28 Dec 2023 02:22:05 +0000 Subject: [PATCH 23/58] update tests for preview --- tests/e2e/Services/Storage/StorageBase.php | 28 ++++++++++++++++++++- tests/resources/disk-a/preview-test.jfif | Bin 0 -> 131958 bytes 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/resources/disk-a/preview-test.jfif diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index 546374076..36d2ee236 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -27,7 +27,7 @@ trait StorageBase 'name' => 'Test Bucket', 'fileSecurity' => true, 'maximumFileSize' => 2000000, //2MB - 'allowedFileExtensions' => ["jpg", "png"], + 'allowedFileExtensions' => ["jpg", "png", 'jfif'], 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), @@ -462,6 +462,32 @@ trait StorageBase $this->assertEquals('image/png', $file2['headers']['content-type']); $this->assertNotEmpty($file2['body']); + // upload JXL file for preview + $fileJfif = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/disk-a/preview-test.jfif'), 'image/jxl', 'preview-test.jfif'), + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + $this->assertEquals(201, $fileJfif['headers']['status-code']); + $this->assertNotEmpty($fileJfif['body']['$id']); + + // TEST preview JXL + $preview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileJfif['body']['$id'] . '/preview', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $preview['headers']['status-code']); + $this->assertEquals('image/jpeg', $preview['headers']['content-type']); + $this->assertNotEmpty($preview['body']); + //new image preview features $file3 = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $data['fileId'] . '/preview', array_merge([ 'content-type' => 'application/json', diff --git a/tests/resources/disk-a/preview-test.jfif b/tests/resources/disk-a/preview-test.jfif new file mode 100644 index 0000000000000000000000000000000000000000..e50021f95d5254cdfd68dfaa91a8b851de46740b GIT binary patch literal 131958 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

Ru*<0cFRO#9N7PBF= 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 Date: Thu, 28 Dec 2023 11:26:45 +0000 Subject: [PATCH 24/58] improve order --- app/controllers/api/storage.php | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 5ef810fec..533e5c73a 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -872,14 +872,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') throw new Exception(Exception::USER_UNAUTHORIZED); } - if ((\strpos($request->getAccept(), 'image/webp') === false) && ('webp' === $output)) { // Fallback webp to jpeg when no browser support - $output = 'jpg'; - } - - $inputs = Config::getParam('storage-inputs'); - $outputs = Config::getParam('storage-outputs'); - $fileLogos = Config::getParam('storage-logos'); - if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { @@ -890,6 +882,14 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } + if ((\strpos($request->getAccept(), 'image/webp') === false) && ('webp' === $output)) { // Fallback webp to jpeg when no browser support + $output = 'jpg'; + } + + $inputs = Config::getParam('storage-inputs'); + $outputs = Config::getParam('storage-outputs'); + $fileLogos = Config::getParam('storage-logos'); + $path = $file->getAttribute('path'); $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); $algorithm = $file->getAttribute('algorithm', 'none'); @@ -926,7 +926,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $output = empty($type) ? (array_search($mime, $outputs) ?? 'jpg') : $type; } - $source = $deviceFiles->read($path); if (!empty($cipher)) { // Decrypt From a28be2bf48f91291af3b5099d239e81052075d50 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 28 Dec 2023 11:32:04 +0000 Subject: [PATCH 25/58] return when response is sent to prevent further execution --- app/controllers/api/storage.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 533e5c73a..99aa16226 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -1258,10 +1258,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') $response->send(substr($source, $start, ($end - $start + 1))); } $response->send($source); + return; } if (!empty($rangeHeader)) { $response->send($deviceFiles->read($path, $start, ($end - $start + 1))); + return; } $size = $deviceFiles->getFileSize($path); From 9cb5eb0180f94bba220c65b7b60f1e4815db7723 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 28 Dec 2023 11:42:40 +0000 Subject: [PATCH 26/58] use constants for compression type --- app/controllers/api/storage.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 99aa16226..5a03edaf4 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -892,7 +892,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $path = $file->getAttribute('path'); $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); - $algorithm = $file->getAttribute('algorithm', 'none'); + $algorithm = $file->getAttribute('algorithm', COMPRESSION_TYPE_NONE); $cipher = $file->getAttribute('openSSLCipher'); $mime = $file->getAttribute('mimeType'); if (!\in_array($mime, $inputs) || $file->getAttribute('sizeActual') > (int) App::getEnv('_APP_STORAGE_PREVIEW_LIMIT', 20000000)) { @@ -903,7 +903,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $path = $fileLogos['default_image']; } - $algorithm = 'none'; + $algorithm = COMPRESSION_TYPE_NONE; $cipher = null; $background = (empty($background)) ? 'eceff1' : $background; $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); @@ -940,11 +940,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') } switch ($algorithm) { - case 'zstd': + case COMPRESSION_TYPE_ZSTD: $compressor = new Zstd(); $source = $compressor->decompress($source); break; - case 'gzip': + case COMPRESSION_TYPE_GZIP: $compressor = new GZIP(); $source = $compressor->decompress($source); break; @@ -1085,15 +1085,15 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') ); } - switch ($file->getAttribute('algorithm', 'none')) { - case 'zstd': + switch ($file->getAttribute('algorithm', COMPRESSION_TYPE_NONE)) { + case COMPRESSION_TYPE_ZSTD: if (empty($source)) { $source = $deviceFiles->read($path); } $compressor = new Zstd(); $source = $compressor->decompress($source); break; - case 'gzip': + case COMPRESSION_TYPE_GZIP: if (empty($source)) { $source = $deviceFiles->read($path); } @@ -1236,15 +1236,15 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') ); } - switch ($file->getAttribute('algorithm', 'none')) { - case 'zstd': + switch ($file->getAttribute('algorithm', COMPRESSION_TYPE_NONE)) { + case COMPRESSION_TYPE_ZSTD: if (empty($source)) { $source = $deviceFiles->read($path); } $compressor = new Zstd(); $source = $compressor->decompress($source); break; - case 'gzip': + case COMPRESSION_TYPE_GZIP: if (empty($source)) { $source = $deviceFiles->read($path); } From 1c236959ba776e2877b42fb3ddfa8f3ac118a1f2 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Fri, 29 Dec 2023 01:49:56 +0000 Subject: [PATCH 27/58] fix algorithm attribute on file if size is above read buffer size --- app/controllers/api/storage.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 5a03edaf4..aaa1df9d3 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -551,6 +551,11 @@ App::post('/v1/storage/buckets/:bucketId/files') break; } $data = $compressor->compress($data); + } else { + // reset the algorithm to none as we do not compress the file + // if file size exceedes the APP_STORAGE_READ_BUFFER + // regardless the bucket compression algoorithm + $algorithm = COMPRESSION_TYPE_NONE; } if ($bucket->getAttribute('encryption', true) && $fileSize <= APP_STORAGE_READ_BUFFER) { From 09cec17285f5152af42d7a36a6188854c820e7c2 Mon Sep 17 00:00:00 2001 From: Utkarsh Ahuja Date: Fri, 29 Dec 2023 17:43:07 +0000 Subject: [PATCH 28/58] added Zoho OAuth class --- src/Appwrite/Auth/OAuth2/Zoho.php | 164 ++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 src/Appwrite/Auth/OAuth2/Zoho.php diff --git a/src/Appwrite/Auth/OAuth2/Zoho.php b/src/Appwrite/Auth/OAuth2/Zoho.php new file mode 100644 index 000000000..fefff9a9f --- /dev/null +++ b/src/Appwrite/Auth/OAuth2/Zoho.php @@ -0,0 +1,164 @@ +authEndpoint . '?' . + \http_build_query([ + 'response_type' => 'code', + 'client_id' => $this->appID, + 'state' => \json_encode($this->state), + 'redirect_uri' => $this->callback, + 'scope' => \implode(' ', $this->getScopes()) + ]); + + return $url; + } + + + /** + * @param string $code + * + * @return array + */ + protected function getTokens(string $code): array + { + if (empty($this->tokens)) { + $this->tokens = \json_decode($this->request( + 'POST', + $this->endpoint . '/oauth/v2/token', + ["Content-Type: application/x-www-form-urlencoded"], + \http_build_query([ + 'grant_type' => 'authorization_code', + "client_id" => $this->appID, + "client_secret" => $this->appSecret, + "redirect_uri" => $this->callback, + 'code' => $code, + 'scope' => \implode(' ', $this->getScopes()), + ]) + ), true); + $this->user = (isset($this->tokens['id_token'])) ? \explode('.', $this->tokens['id_token']) : [0 => '', 1 => '']; + $this->user = (isset($this->user[1])) ? \json_decode(\base64_decode($this->user[1]), true) : []; + } + + return $this->tokens; + } + + + /** + * @param string $refreshToken + * + * @return array + */ + public function refreshTokens(string $refreshToken): array + { + + $this->tokens = \json_decode($this->request( + 'POST', + $this->endpoint . '/oauth/v2/token', + ['Content-Type: application/x-www-form-urlencoded'], + \http_build_query([ + 'grant_type' => 'refresh_token', + 'refresh_token' => $refreshToken, + 'client_id' => $this->appID, + 'client_secret' => $this->appSecret, + ]) + ), true); + + if (empty($this->tokens['refresh_token'])) { + $this->tokens['refresh_token'] = $refreshToken; + } + + $this->user = (isset($this->tokens['id_token'])) ? \explode('.', $this->tokens['id_token']) : [0 => '', 1 => '']; + $this->user = (isset($this->user[1])) ? \json_decode(\base64_decode($this->user[1]), true) : []; + + return $this->tokens; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserID(string $accessToken): string + { + return $this->user['sub'] ?? ''; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserEmail(string $accessToken): string + { + return $this->user['email'] ?? ''; + } + + /** + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + return $this->user['email_verified'] ?? ''; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserName(string $accessToken): string + { + return $this->user['name'] ?? ''; + } +} \ No newline at end of file From 9b2fbc5c52476f453fd62ba977254dacb1b9dcb4 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 29 Dec 2023 21:52:50 +0100 Subject: [PATCH 29/58] feat: console hostname env variable --- .env | 3 ++- app/config/variables.php | 17 +++++++++-------- app/init.php | 12 ++++++++++++ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.env b/.env index ad551e705..81145b563 100644 --- a/.env +++ b/.env @@ -4,12 +4,13 @@ _APP_WORKER_PER_CORE=6 _APP_CONSOLE_WHITELIST_ROOT=disabled _APP_CONSOLE_WHITELIST_EMAILS= _APP_CONSOLE_WHITELIST_IPS= +_APP_CONSOLE_HOSTNAMES= _APP_SYSTEM_EMAIL_NAME=Appwrite _APP_SYSTEM_EMAIL_ADDRESS=team@appwrite.io _APP_SYSTEM_SECURITY_EMAIL_ADDRESS=security@appwrite.io _APP_SYSTEM_RESPONSE_FORMAT= _APP_OPTIONS_ABUSE=disabled -_APP_OPTIONS_ROUTER_PROTECTION=disbled +_APP_OPTIONS_ROUTER_PROTECTION=disabled _APP_OPTIONS_FORCE_HTTPS=disabled _APP_OPTIONS_FUNCTIONS_FORCE_HTTPS=disabled _APP_OPENSSL_KEY_V1=your-secret-key diff --git a/app/config/variables.php b/app/config/variables.php index 9d555bf01..a88145601 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -115,14 +115,6 @@ return [ 'question' => '', 'filter' => '' ], - // [ - // 'name' => '_APP_CONSOLE_WHITELIST_DOMAINS', - // 'description' => 'This option allows you to limit creation of users to Appwrite console for users sharing the same email domains. This option is very useful for team working with company emails domain.\n\nTo enable this option, pass a list of allowed email domains separated by a comma.', - // 'introduction' => '', - // 'default' => '', - // 'required' => false, - // 'question' => '', - // ], [ 'name' => '_APP_CONSOLE_WHITELIST_IPS', 'description' => "This last option allows you to limit creation of users in Appwrite console for users sharing the same set of IP addresses. This option is very useful for team working with a VPN service or a company IP.\n\nTo enable/activate this option, pass a list of allowed IP addresses separated by a comma.", @@ -132,6 +124,15 @@ return [ 'question' => '', 'filter' => '' ], + [ + 'name' => '_APP_CONSOLE_HOSTNAMES', + 'description' => 'This option allows you to add additional hostnames to your Appwrite console. This option is very useful for allowing access to the console project from additional domains. To enable it, pass a list of allowed hostnames separated by a comma.', + 'introduction' => '', + 'default' => '', + 'required' => false, + 'question' => '', + 'filter' => '' + ], [ 'name' => '_APP_SYSTEM_EMAIL_NAME', 'description' => 'This is the sender name value that will appear on email messages sent to developers from the Appwrite console. The default value is: \'Appwrite\'. You can use url encoded strings for spaces and special chars.', diff --git a/app/init.php b/app/init.php index 72bf75d41..ccd7b9657 100644 --- a/app/init.php +++ b/app/init.php @@ -946,6 +946,18 @@ App::setResource('clients', function ($request, $console, $project) { 'hostname' => $request->getHostname(), ], Document::SET_TYPE_APPEND); + $hostnames = explode(',', App::getEnv('_APP_CONSOLE_HOSTNAMES', '')); + if (is_array($hostnames)) { + foreach ($hostnames as $hostname) { + $console->setAttribute('platforms', [ + '$collection' => ID::custom('platforms'), + 'type' => Origin::CLIENT_TYPE_WEB, + 'name' => $hostname, + 'hostname' => $hostname, + ], Document::SET_TYPE_APPEND); + } + } + /** * Get All verified client URLs for both console and current projects * + Filter for duplicated entries From cc1272f80bb2e73c10d89b52e0142ba4c384b237 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 29 Dec 2023 22:09:59 +0100 Subject: [PATCH 30/58] feat: add new env variable to compsoe files --- app/views/install/compose.phtml | 5 +++-- docker-compose.yml | 17 +++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 898b46b3a..2321dbf29 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -79,6 +79,7 @@ services: - _APP_CONSOLE_WHITELIST_ROOT - _APP_CONSOLE_WHITELIST_EMAILS - _APP_CONSOLE_WHITELIST_IPS + - _APP_CONSOLE_HOSTNAMES - _APP_SYSTEM_EMAIL_NAME - _APP_SYSTEM_EMAIL_ADDRESS - _APP_SYSTEM_SECURITY_EMAIL_ADDRESS @@ -273,7 +274,7 @@ services: depends_on: - redis - mariadb - volumes: + volumes: - appwrite-uploads:/storage/uploads:rw - appwrite-cache:/storage/cache:rw - appwrite-functions:/storage/functions:rw @@ -420,7 +421,7 @@ services: depends_on: - redis - mariadb - volumes: + volumes: - appwrite-config:/storage/config:rw - appwrite-certificates:/storage/certificates:rw environment: diff --git a/docker-compose.yml b/docker-compose.yml index 97cf7e513..fcec1b773 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,7 +53,7 @@ services: DEBUG: false TESTING: true VERSION: dev - ports: + ports: - 9501:80 networks: - appwrite @@ -88,7 +88,7 @@ services: - mariadb - redis # - clamav - entrypoint: + entrypoint: - php - -e - app/http.php @@ -100,6 +100,7 @@ services: - _APP_CONSOLE_WHITELIST_ROOT - _APP_CONSOLE_WHITELIST_EMAILS - _APP_CONSOLE_WHITELIST_IPS + - _APP_CONSOLE_HOSTNAMES - _APP_SYSTEM_EMAIL_NAME - _APP_SYSTEM_EMAIL_ADDRESS - _APP_SYSTEM_SECURITY_EMAIL_ADDRESS @@ -195,7 +196,7 @@ services: container_name: appwrite-realtime image: appwrite-dev restart: unless-stopped - ports: + ports: - 9505:80 labels: - "traefik.enable=true" @@ -303,7 +304,7 @@ services: depends_on: - redis - mariadb - volumes: + volumes: - appwrite-uploads:/storage/uploads:rw - appwrite-cache:/storage/cache:rw - appwrite-functions:/storage/functions:rw @@ -357,7 +358,7 @@ services: image: appwrite-dev networks: - appwrite - volumes: + volumes: - ./app:/usr/src/code/app - ./src:/usr/src/code/src depends_on: @@ -456,7 +457,7 @@ services: depends_on: - redis - mariadb - volumes: + volumes: - appwrite-config:/storage/config:rw - appwrite-certificates:/storage/certificates:rw - ./app:/usr/src/code/app @@ -744,7 +745,7 @@ services: - _APP_REDIS_USER - _APP_REDIS_PASS - _APP_MIXPANEL_TOKEN - + appwrite-hamster-scheduler: entrypoint: hamster <<: *x-logging @@ -995,7 +996,7 @@ services: # - './debug:/tmp' # ports: # - '3001:80' - + graphql-explorer: container_name: appwrite-graphql-explorer image: appwrite/altair:0.3.0 From c99976560d13a56401b808650e38d6d817005cf7 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 31 Dec 2023 11:37:22 +0000 Subject: [PATCH 31/58] update utopia-storage --- composer.lock | 118 +++++++++++++++++++++++++------------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/composer.lock b/composer.lock index 351839666..17f0325de 100644 --- a/composer.lock +++ b/composer.lock @@ -1906,16 +1906,16 @@ }, { "name": "utopia-php/database", - "version": "0.45.2", + "version": "0.45.3", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "dc789f2c1fd8b5ee07ff883e11c9ad7970824788" + "reference": "33b4e9a4a6c29f6bb7e108e134b283d585955789" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/dc789f2c1fd8b5ee07ff883e11c9ad7970824788", - "reference": "dc789f2c1fd8b5ee07ff883e11c9ad7970824788", + "url": "https://api.github.com/repos/utopia-php/database/zipball/33b4e9a4a6c29f6bb7e108e134b283d585955789", + "reference": "33b4e9a4a6c29f6bb7e108e134b283d585955789", "shasum": "" }, "require": { @@ -1956,9 +1956,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.45.2" + "source": "https://github.com/utopia-php/database/tree/0.45.3" }, - "time": "2023-11-15T03:38:47+00:00" + "time": "2023-12-28T11:12:26+00:00" }, { "name": "utopia-php/domains", @@ -2476,16 +2476,16 @@ }, { "name": "utopia-php/platform", - "version": "0.5.0", + "version": "0.5.1", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "229a7b1fa1f39afd1532f7a515326a6afc222a26" + "reference": "3eceef0b6593fe0f7d2efd36d40402a395a4c285" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/229a7b1fa1f39afd1532f7a515326a6afc222a26", - "reference": "229a7b1fa1f39afd1532f7a515326a6afc222a26", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/3eceef0b6593fe0f7d2efd36d40402a395a4c285", + "reference": "3eceef0b6593fe0f7d2efd36d40402a395a4c285", "shasum": "" }, "require": { @@ -2493,7 +2493,7 @@ "ext-redis": "*", "php": ">=8.0", "utopia-php/cli": "0.15.*", - "utopia-php/framework": "0.31.*" + "utopia-php/framework": "0.*.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2519,9 +2519,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.5.0" + "source": "https://github.com/utopia-php/platform/tree/0.5.1" }, - "time": "2023-10-16T20:28:49+00:00" + "time": "2023-12-26T16:14:41+00:00" }, { "name": "utopia-php/pools", @@ -2742,16 +2742,16 @@ }, { "name": "utopia-php/storage", - "version": "0.18.1", + "version": "0.18.2", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "983e6dee137012f9f57f126d3c79aab54e4e8824" + "reference": "130e7c4f305c2e1d5aa86226aaee25c9d5a41073" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/983e6dee137012f9f57f126d3c79aab54e4e8824", - "reference": "983e6dee137012f9f57f126d3c79aab54e4e8824", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/130e7c4f305c2e1d5aa86226aaee25c9d5a41073", + "reference": "130e7c4f305c2e1d5aa86226aaee25c9d5a41073", "shasum": "" }, "require": { @@ -2791,9 +2791,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.18.1" + "source": "https://github.com/utopia-php/storage/tree/0.18.2" }, - "time": "2023-10-24T14:44:19+00:00" + "time": "2023-12-31T11:33:28+00:00" }, { "name": "utopia-php/swoole", @@ -2904,23 +2904,23 @@ }, { "name": "utopia-php/vcs", - "version": "0.6.2", + "version": "0.6.4", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "f135291b87cb45335fc6608722e7f89894bc33ee" + "reference": "b2595a50a4897a8c88319240810055b7a96efd6d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/f135291b87cb45335fc6608722e7f89894bc33ee", - "reference": "f135291b87cb45335fc6608722e7f89894bc33ee", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/b2595a50a4897a8c88319240810055b7a96efd6d", + "reference": "b2595a50a4897a8c88319240810055b7a96efd6d", "shasum": "" }, "require": { "adhocore/jwt": "^1.1", "php": ">=8.0", "utopia-php/cache": "^0.8.0", - "utopia-php/framework": "0.31.*" + "utopia-php/framework": "0.*.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2947,9 +2947,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/0.6.2" + "source": "https://github.com/utopia-php/vcs/tree/0.6.4" }, - "time": "2023-11-08T15:36:03+00:00" + "time": "2023-12-26T15:38:19+00:00" }, { "name": "utopia-php/websocket", @@ -3487,16 +3487,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.17.1", + "version": "v4.18.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", + "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", "shasum": "" }, "require": { @@ -3537,9 +3537,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0" }, - "time": "2023-08-13T19:53:39+00:00" + "time": "2023-12-10T21:03:43+00:00" }, { "name": "phar-io/manifest", @@ -3891,16 +3891,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.24.4", + "version": "1.24.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "6bd0c26f3786cd9b7c359675cb789e35a8e07496" + "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6bd0c26f3786cd9b7c359675cb789e35a8e07496", - "reference": "6bd0c26f3786cd9b7c359675cb789e35a8e07496", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fedf211ff14ec8381c9bf5714e33a7a552dd1acc", + "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc", "shasum": "" }, "require": { @@ -3932,29 +3932,29 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.4" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.5" }, - "time": "2023-11-26T18:29:22+00:00" + "time": "2023-12-16T09:33:33+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.29", + "version": "9.2.30", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -4004,7 +4004,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" }, "funding": [ { @@ -4012,7 +4012,7 @@ "type": "github" } ], - "time": "2023-09-19T04:57:46+00:00" + "time": "2023-12-22T06:47:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -4651,20 +4651,20 @@ }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -4696,7 +4696,7 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" }, "funding": [ { @@ -4704,7 +4704,7 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { "name": "sebastian/diff", @@ -4978,20 +4978,20 @@ }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -5023,7 +5023,7 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" }, "funding": [ { @@ -5031,7 +5031,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", From d070989f73c50b3515d1e5317673c22e69c7c570 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 31 Dec 2023 11:46:27 +0000 Subject: [PATCH 32/58] update utopia storage --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 17f0325de..acd65d61a 100644 --- a/composer.lock +++ b/composer.lock @@ -2742,16 +2742,16 @@ }, { "name": "utopia-php/storage", - "version": "0.18.2", + "version": "0.18.3", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "130e7c4f305c2e1d5aa86226aaee25c9d5a41073" + "reference": "faa0279519ac14f3501e8b138e0865ad9d12bff6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/130e7c4f305c2e1d5aa86226aaee25c9d5a41073", - "reference": "130e7c4f305c2e1d5aa86226aaee25c9d5a41073", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/faa0279519ac14f3501e8b138e0865ad9d12bff6", + "reference": "faa0279519ac14f3501e8b138e0865ad9d12bff6", "shasum": "" }, "require": { @@ -2791,9 +2791,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.18.2" + "source": "https://github.com/utopia-php/storage/tree/0.18.3" }, - "time": "2023-12-31T11:33:28+00:00" + "time": "2023-12-31T11:45:12+00:00" }, { "name": "utopia-php/swoole", From a2ad8700845b1b1b9c3e0fa4d0949798054a98a4 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 31 Dec 2023 11:46:45 +0000 Subject: [PATCH 33/58] use compression constant from utopia/storage --- app/controllers/api/storage.php | 35 ++++++++++--------- app/init.php | 4 --- src/Appwrite/Utopia/Response/Model/Bucket.php | 3 +- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index aaa1df9d3..91efb21f3 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -44,6 +44,7 @@ use Utopia\Validator\Text; use Utopia\Validator\WhiteList; use Utopia\DSN\DSN; use Utopia\Swoole\Request; +use Utopia\Storage\Compression\Compression; App::post('/v1/storage/buckets') ->desc('Create bucket') @@ -67,7 +68,7 @@ App::post('/v1/storage/buckets') ->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true) ->param('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0), new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true) ->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true) - ->param('compression', COMPRESSION_TYPE_NONE, new WhiteList([COMPRESSION_TYPE_NONE, COMPRESSION_TYPE_GZIP, COMPRESSION_TYPE_ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . COMPRESSION_TYPE_NONE . ', [' . COMPRESSION_TYPE_GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . COMPRESSION_TYPE_ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true) + ->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true) ->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true) ->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true) ->inject('response') @@ -241,7 +242,7 @@ App::put('/v1/storage/buckets/:bucketId') ->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true) ->param('maximumFileSize', null, new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human((int)App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true) ->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true) - ->param('compression', COMPRESSION_TYPE_NONE, new WhiteList([COMPRESSION_TYPE_NONE, COMPRESSION_TYPE_GZIP, COMPRESSION_TYPE_ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . COMPRESSION_TYPE_NONE . ', [' . COMPRESSION_TYPE_GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . COMPRESSION_TYPE_ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true) + ->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true) ->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true) ->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true) ->inject('response') @@ -538,14 +539,14 @@ App::post('/v1/storage/buckets/:bucketId/files') $fileHash = $deviceFiles->getFileHash($path); // Get file hash before compression and encryption $data = ''; // Compression - $algorithm = $bucket->getAttribute('compression', COMPRESSION_TYPE_NONE); - if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != COMPRESSION_TYPE_NONE) { + $algorithm = $bucket->getAttribute('compression', Compression::NONE); + if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != Compression::NONE) { $data = $deviceFiles->read($path); switch ($algorithm) { - case COMPRESSION_TYPE_ZSTD: + case Compression::ZSTD: $compressor = new Zstd(); break; - case COMPRESSION_TYPE_GZIP: + case Compression::GZIP: default: $compressor = new GZIP(); break; @@ -555,7 +556,7 @@ App::post('/v1/storage/buckets/:bucketId/files') // reset the algorithm to none as we do not compress the file // if file size exceedes the APP_STORAGE_READ_BUFFER // regardless the bucket compression algoorithm - $algorithm = COMPRESSION_TYPE_NONE; + $algorithm = Compression::NONE; } if ($bucket->getAttribute('encryption', true) && $fileSize <= APP_STORAGE_READ_BUFFER) { @@ -897,7 +898,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $path = $file->getAttribute('path'); $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); - $algorithm = $file->getAttribute('algorithm', COMPRESSION_TYPE_NONE); + $algorithm = $file->getAttribute('algorithm', Compression::NONE); $cipher = $file->getAttribute('openSSLCipher'); $mime = $file->getAttribute('mimeType'); if (!\in_array($mime, $inputs) || $file->getAttribute('sizeActual') > (int) App::getEnv('_APP_STORAGE_PREVIEW_LIMIT', 20000000)) { @@ -908,7 +909,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $path = $fileLogos['default_image']; } - $algorithm = COMPRESSION_TYPE_NONE; + $algorithm = Compression::NONE; $cipher = null; $background = (empty($background)) ? 'eceff1' : $background; $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); @@ -945,11 +946,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') } switch ($algorithm) { - case COMPRESSION_TYPE_ZSTD: + case Compression::ZSTD: $compressor = new Zstd(); $source = $compressor->decompress($source); break; - case COMPRESSION_TYPE_GZIP: + case Compression::GZIP: $compressor = new GZIP(); $source = $compressor->decompress($source); break; @@ -1090,15 +1091,15 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') ); } - switch ($file->getAttribute('algorithm', COMPRESSION_TYPE_NONE)) { - case COMPRESSION_TYPE_ZSTD: + switch ($file->getAttribute('algorithm', Compression::NONE)) { + case Compression::ZSTD: if (empty($source)) { $source = $deviceFiles->read($path); } $compressor = new Zstd(); $source = $compressor->decompress($source); break; - case COMPRESSION_TYPE_GZIP: + case Compression::GZIP: if (empty($source)) { $source = $deviceFiles->read($path); } @@ -1241,15 +1242,15 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') ); } - switch ($file->getAttribute('algorithm', COMPRESSION_TYPE_NONE)) { - case COMPRESSION_TYPE_ZSTD: + switch ($file->getAttribute('algorithm', Compression::NONE)) { + case Compression::ZSTD: if (empty($source)) { $source = $deviceFiles->read($path); } $compressor = new Zstd(); $source = $compressor->decompress($source); break; - case COMPRESSION_TYPE_GZIP: + case Compression::GZIP: if (empty($source)) { $source = $deviceFiles->read($path); } diff --git a/app/init.php b/app/init.php index 72bf75d41..924122ac2 100644 --- a/app/init.php +++ b/app/init.php @@ -173,10 +173,6 @@ const DELETE_TYPE_SESSIONS = 'sessions'; const DELETE_TYPE_CACHE_BY_TIMESTAMP = 'cacheByTimeStamp'; const DELETE_TYPE_CACHE_BY_RESOURCE = 'cacheByResource'; const DELETE_TYPE_SCHEDULES = 'schedules'; -// Compression type -const COMPRESSION_TYPE_NONE = 'none'; -const COMPRESSION_TYPE_GZIP = 'gzip'; -const COMPRESSION_TYPE_ZSTD = 'zstd'; // Mail Types const MAIL_TYPE_VERIFICATION = 'verification'; const MAIL_TYPE_MAGIC_SESSION = 'magicSession'; diff --git a/src/Appwrite/Utopia/Response/Model/Bucket.php b/src/Appwrite/Utopia/Response/Model/Bucket.php index 3c5715efc..f5261c026 100644 --- a/src/Appwrite/Utopia/Response/Model/Bucket.php +++ b/src/Appwrite/Utopia/Response/Model/Bucket.php @@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model; +use Utopia\Storage\Compression\Compression; class Bucket extends Model { @@ -68,7 +69,7 @@ class Bucket extends Model ]) ->addRule('compression', [ 'type' => self::TYPE_STRING, - 'description' => 'Compression algorithm choosen for compression. Will be one of ' . COMPRESSION_TYPE_NONE . ', [' . COMPRESSION_TYPE_GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . COMPRESSION_TYPE_ZSTD . '](https://en.wikipedia.org/wiki/Zstd).', + 'description' => 'Compression algorithm choosen for compression. Will be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd).', 'default' => '', 'example' => 'gzip', 'array' => false From acebe54e5e0ac576a10b6e983ffbe3011e889d17 Mon Sep 17 00:00:00 2001 From: Utkarsh Ahuja Date: Sun, 31 Dec 2023 19:27:12 +0530 Subject: [PATCH 34/58] fix: email verified false on null --- src/Appwrite/Auth/OAuth2/Zoho.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Auth/OAuth2/Zoho.php b/src/Appwrite/Auth/OAuth2/Zoho.php index fefff9a9f..245f85595 100644 --- a/src/Appwrite/Auth/OAuth2/Zoho.php +++ b/src/Appwrite/Auth/OAuth2/Zoho.php @@ -149,7 +149,11 @@ class Zoho extends OAuth2 */ public function isEmailVerified(string $accessToken): bool { - return $this->user['email_verified'] ?? ''; + if ($this->user['email_verified'] ?? false) { + return true; + } + + return false; } /** From 70916ad1aa96a7b5e8ed24bd2ece83df29d4b5b9 Mon Sep 17 00:00:00 2001 From: Utkarsh Ahuja Date: Tue, 2 Jan 2024 10:13:46 +0530 Subject: [PATCH 35/58] fix: removed authEndpoint --- src/Appwrite/Auth/OAuth2/Zoho.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Appwrite/Auth/OAuth2/Zoho.php b/src/Appwrite/Auth/OAuth2/Zoho.php index 245f85595..5413bd05d 100644 --- a/src/Appwrite/Auth/OAuth2/Zoho.php +++ b/src/Appwrite/Auth/OAuth2/Zoho.php @@ -14,11 +14,6 @@ class Zoho extends OAuth2 */ private string $endpoint = 'https://accounts.zoho.com'; - /** - * @var string - */ - private string $authEndpoint = 'https://accounts.zoho.com/oauth/v2/auth'; - /** * @var array */ @@ -50,7 +45,7 @@ class Zoho extends OAuth2 */ public function getLoginURL(): string { - $url = $this->authEndpoint . '?' . + $url = $this->endpoint . '/oauth/v2/auth?' . \http_build_query([ 'response_type' => 'code', 'client_id' => $this->appID, From 879320e23ec198cd9049856301fbfb5342b9af40 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 2 Jan 2024 11:53:48 +0545 Subject: [PATCH 36/58] update comment regarding validation --- app/controllers/api/storage.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index e668ec255..011f83f4b 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -628,7 +628,12 @@ App::post('/v1/storage/buckets/:bucketId/files') ->setAttribute('metadata', $metadata) ->setAttribute('chunksUploaded', $chunksUploaded); - // Validate create permission + /** + * Validate create permission and skip authorization in updateDocument + * Without this, the file creation will fail when user doesn't have update permission + * However as with chunk upload even if we are updating, we are essentially creating a file + * adding it's new chunk so we validate create permission instead of update + */ $validator = new Authorization(Database::PERMISSION_CREATE); if (!$validator->isValid($bucket->getCreate())) { throw new Exception(Exception::USER_UNAUTHORIZED); @@ -670,7 +675,12 @@ App::post('/v1/storage/buckets/:bucketId/files') ->setAttribute('chunksUploaded', $chunksUploaded) ->setAttribute('metadata', $metadata); - // Validate create permission + /** + * Validate create permission and skip authorization in updateDocument + * Without this, the file creation will fail when user doesn't have update permission + * However as with chunk upload even if we are updating, we are essentially creating a file + * adding it's new chunk so we validate create permission instead of update + */ $validator = new Authorization(Database::PERMISSION_CREATE); if (!$validator->isValid($bucket->getCreate())) { throw new Exception(Exception::USER_UNAUTHORIZED); From c4ab4ca16a70e793902c429f6367dfb13610f9af Mon Sep 17 00:00:00 2001 From: Utkarsh Ahuja <70762626+UtkarshAhuja2003@users.noreply.github.com> Date: Tue, 2 Jan 2024 13:15:07 +0530 Subject: [PATCH 37/58] fix: lint errors --- src/Appwrite/Auth/OAuth2/Zoho.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Auth/OAuth2/Zoho.php b/src/Appwrite/Auth/OAuth2/Zoho.php index 5413bd05d..c2accfbb6 100644 --- a/src/Appwrite/Auth/OAuth2/Zoho.php +++ b/src/Appwrite/Auth/OAuth2/Zoho.php @@ -160,4 +160,4 @@ class Zoho extends OAuth2 { return $this->user['name'] ?? ''; } -} \ No newline at end of file +} From 554e9f93b6ad3721d3e0721fb956d5a1742e28a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 2 Jan 2024 10:59:35 +0000 Subject: [PATCH 38/58] Implement max password length --- app/controllers/api/account.php | 6 +- composer.lock | 120 +++++++++--------- src/Appwrite/Auth/Validator/Password.php | 6 +- .../Auth/Validator/PasswordDictionary.php | 2 +- tests/e2e/Services/Account/AccountBase.php | 30 +++++ .../Auth/Validator/PasswordDictionaryTest.php | 11 ++ 6 files changed, 110 insertions(+), 65 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 68e3261a8..4057634f5 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -69,7 +69,7 @@ App::post('/v1/account') ->label('abuse-limit', 10) ->param('userId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('email', '', new Email(), 'User email.') - ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary']) + ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary']) ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) ->inject('request') ->inject('response') @@ -2724,8 +2724,8 @@ App::put('/v1/account/recovery') ->label('abuse-key', 'url:{url},userId:{param-userId}') ->param('userId', '', new UID(), 'User ID.') ->param('secret', '', new Text(256), 'Valid reset token.') - ->param('password', '', new Password(), 'New user password. Must be at least 8 chars.') - ->param('passwordAgain', '', new Password(), 'Repeat new user password. Must be at least 8 chars.') + ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary']) + ->param('passwordAgain', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'Repeat new user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary']) ->inject('response') ->inject('user') ->inject('dbForProject') diff --git a/composer.lock b/composer.lock index 9f8e84e02..acd65d61a 100644 --- a/composer.lock +++ b/composer.lock @@ -1906,16 +1906,16 @@ }, { "name": "utopia-php/database", - "version": "0.45.2", + "version": "0.45.3", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "dc789f2c1fd8b5ee07ff883e11c9ad7970824788" + "reference": "33b4e9a4a6c29f6bb7e108e134b283d585955789" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/dc789f2c1fd8b5ee07ff883e11c9ad7970824788", - "reference": "dc789f2c1fd8b5ee07ff883e11c9ad7970824788", + "url": "https://api.github.com/repos/utopia-php/database/zipball/33b4e9a4a6c29f6bb7e108e134b283d585955789", + "reference": "33b4e9a4a6c29f6bb7e108e134b283d585955789", "shasum": "" }, "require": { @@ -1956,9 +1956,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.45.2" + "source": "https://github.com/utopia-php/database/tree/0.45.3" }, - "time": "2023-11-15T03:38:47+00:00" + "time": "2023-12-28T11:12:26+00:00" }, { "name": "utopia-php/domains", @@ -2476,16 +2476,16 @@ }, { "name": "utopia-php/platform", - "version": "0.5.0", + "version": "0.5.1", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "229a7b1fa1f39afd1532f7a515326a6afc222a26" + "reference": "3eceef0b6593fe0f7d2efd36d40402a395a4c285" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/229a7b1fa1f39afd1532f7a515326a6afc222a26", - "reference": "229a7b1fa1f39afd1532f7a515326a6afc222a26", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/3eceef0b6593fe0f7d2efd36d40402a395a4c285", + "reference": "3eceef0b6593fe0f7d2efd36d40402a395a4c285", "shasum": "" }, "require": { @@ -2493,7 +2493,7 @@ "ext-redis": "*", "php": ">=8.0", "utopia-php/cli": "0.15.*", - "utopia-php/framework": "0.31.*" + "utopia-php/framework": "0.*.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2519,9 +2519,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.5.0" + "source": "https://github.com/utopia-php/platform/tree/0.5.1" }, - "time": "2023-10-16T20:28:49+00:00" + "time": "2023-12-26T16:14:41+00:00" }, { "name": "utopia-php/pools", @@ -2742,16 +2742,16 @@ }, { "name": "utopia-php/storage", - "version": "0.18.1", + "version": "0.18.3", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "983e6dee137012f9f57f126d3c79aab54e4e8824" + "reference": "faa0279519ac14f3501e8b138e0865ad9d12bff6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/983e6dee137012f9f57f126d3c79aab54e4e8824", - "reference": "983e6dee137012f9f57f126d3c79aab54e4e8824", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/faa0279519ac14f3501e8b138e0865ad9d12bff6", + "reference": "faa0279519ac14f3501e8b138e0865ad9d12bff6", "shasum": "" }, "require": { @@ -2791,9 +2791,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.18.1" + "source": "https://github.com/utopia-php/storage/tree/0.18.3" }, - "time": "2023-10-24T14:44:19+00:00" + "time": "2023-12-31T11:45:12+00:00" }, { "name": "utopia-php/swoole", @@ -2904,23 +2904,23 @@ }, { "name": "utopia-php/vcs", - "version": "0.6.2", + "version": "0.6.4", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "f135291b87cb45335fc6608722e7f89894bc33ee" + "reference": "b2595a50a4897a8c88319240810055b7a96efd6d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/f135291b87cb45335fc6608722e7f89894bc33ee", - "reference": "f135291b87cb45335fc6608722e7f89894bc33ee", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/b2595a50a4897a8c88319240810055b7a96efd6d", + "reference": "b2595a50a4897a8c88319240810055b7a96efd6d", "shasum": "" }, "require": { "adhocore/jwt": "^1.1", "php": ">=8.0", "utopia-php/cache": "^0.8.0", - "utopia-php/framework": "0.31.*" + "utopia-php/framework": "0.*.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2947,9 +2947,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/0.6.2" + "source": "https://github.com/utopia-php/vcs/tree/0.6.4" }, - "time": "2023-11-08T15:36:03+00:00" + "time": "2023-12-26T15:38:19+00:00" }, { "name": "utopia-php/websocket", @@ -3487,16 +3487,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.17.1", + "version": "v4.18.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", + "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", "shasum": "" }, "require": { @@ -3537,9 +3537,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0" }, - "time": "2023-08-13T19:53:39+00:00" + "time": "2023-12-10T21:03:43+00:00" }, { "name": "phar-io/manifest", @@ -3891,16 +3891,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.24.4", + "version": "1.24.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "6bd0c26f3786cd9b7c359675cb789e35a8e07496" + "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6bd0c26f3786cd9b7c359675cb789e35a8e07496", - "reference": "6bd0c26f3786cd9b7c359675cb789e35a8e07496", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fedf211ff14ec8381c9bf5714e33a7a552dd1acc", + "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc", "shasum": "" }, "require": { @@ -3932,29 +3932,29 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.4" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.5" }, - "time": "2023-11-26T18:29:22+00:00" + "time": "2023-12-16T09:33:33+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.29", + "version": "9.2.30", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -4004,7 +4004,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" }, "funding": [ { @@ -4012,7 +4012,7 @@ "type": "github" } ], - "time": "2023-09-19T04:57:46+00:00" + "time": "2023-12-22T06:47:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -4651,20 +4651,20 @@ }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -4696,7 +4696,7 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" }, "funding": [ { @@ -4704,7 +4704,7 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { "name": "sebastian/diff", @@ -4978,20 +4978,20 @@ }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -5023,7 +5023,7 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" }, "funding": [ { @@ -5031,7 +5031,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", @@ -5847,5 +5847,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Appwrite/Auth/Validator/Password.php b/src/Appwrite/Auth/Validator/Password.php index 93a9f7411..ffb72467e 100644 --- a/src/Appwrite/Auth/Validator/Password.php +++ b/src/Appwrite/Auth/Validator/Password.php @@ -20,7 +20,7 @@ class Password extends Validator */ public function getDescription(): string { - return 'Password must be at least 8 characters'; + return 'Password must be between 8 and 256 characters long.'; } /** @@ -40,6 +40,10 @@ class Password extends Validator return false; } + if (\strlen($value) > 256) { + return false; + } + return true; } diff --git a/src/Appwrite/Auth/Validator/PasswordDictionary.php b/src/Appwrite/Auth/Validator/PasswordDictionary.php index 003d68bc7..e128f497f 100644 --- a/src/Appwrite/Auth/Validator/PasswordDictionary.php +++ b/src/Appwrite/Auth/Validator/PasswordDictionary.php @@ -27,7 +27,7 @@ class PasswordDictionary extends Password */ public function getDescription(): string { - return 'Password must be at least 8 characters and should not be one of the commonly used password.'; + return 'Password must be between 8 and 265 characters long, and should not be one of the commonly used password.'; } /** diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index e6f5feaa8..fe9983d9b 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -94,6 +94,36 @@ trait AccountBase $this->assertEquals($response['headers']['status-code'], 400); + $shortPassword = 'short'; + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => ID::unique(), + 'email' => 'shortpass@appwrite.io', + 'password' => $shortPassword + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $longPassword = ''; + for ($i = 0; $i < 257; $i++) { // 256 is the limit + $longPassword .= 'p'; + } + + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => ID::unique(), + 'email' => 'longpass@appwrite.io', + 'password' => $longPassword, + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + return [ 'id' => $id, 'email' => $email, diff --git a/tests/unit/Auth/Validator/PasswordDictionaryTest.php b/tests/unit/Auth/Validator/PasswordDictionaryTest.php index fd7f51ff1..5c8d47923 100644 --- a/tests/unit/Auth/Validator/PasswordDictionaryTest.php +++ b/tests/unit/Auth/Validator/PasswordDictionaryTest.php @@ -24,5 +24,16 @@ class PasswordDictionaryTest extends TestCase $this->assertEquals($this->object->isValid('123456'), false); $this->assertEquals($this->object->isValid('password'), false); $this->assertEquals($this->object->isValid('myPasswordIsRight'), true); + + $pass = ''; // 256 chars + for ($i = 0; $i < 256; $i++) { + $pass .= 'p'; + } + + $this->assertEquals($this->object->isValid($pass), true); + + $pass .= 'p'; // 257 chars + + $this->assertEquals($this->object->isValid($pass), false); } } From d18d187a075fb94a3290f1e474d85e08bdefc581 Mon Sep 17 00:00:00 2001 From: Utkarsh Ahuja Date: Wed, 3 Jan 2024 11:20:59 +0530 Subject: [PATCH 39/58] fix: base changed --- app/config/{providers.php => oAuthProviders.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/config/{providers.php => oAuthProviders.php} (100%) diff --git a/app/config/providers.php b/app/config/oAuthProviders.php similarity index 100% rename from app/config/providers.php rename to app/config/oAuthProviders.php From 06e385b346e09d5fbe386070e0bbd60ab7ecf6b8 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 3 Jan 2024 19:12:21 +0000 Subject: [PATCH 40/58] Add changes from previous console platforms variable PR See https://github.com/appwrite/appwrite/pull/4581 --- .env | 2 +- app/config/variables.php | 2 +- tests/e2e/General/HTTPTest.php | 46 ++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/.env b/.env index 81145b563..9df5864bd 100644 --- a/.env +++ b/.env @@ -4,7 +4,7 @@ _APP_WORKER_PER_CORE=6 _APP_CONSOLE_WHITELIST_ROOT=disabled _APP_CONSOLE_WHITELIST_EMAILS= _APP_CONSOLE_WHITELIST_IPS= -_APP_CONSOLE_HOSTNAMES= +_APP_CONSOLE_HOSTNAMES=localhost,appwrite.io,*.appwrite.io _APP_SYSTEM_EMAIL_NAME=Appwrite _APP_SYSTEM_EMAIL_ADDRESS=team@appwrite.io _APP_SYSTEM_SECURITY_EMAIL_ADDRESS=security@appwrite.io diff --git a/app/config/variables.php b/app/config/variables.php index a88145601..69eca5753 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -127,7 +127,7 @@ return [ [ 'name' => '_APP_CONSOLE_HOSTNAMES', 'description' => 'This option allows you to add additional hostnames to your Appwrite console. This option is very useful for allowing access to the console project from additional domains. To enable it, pass a list of allowed hostnames separated by a comma.', - 'introduction' => '', + 'introduction' => '1.5.0', 'default' => '', 'required' => false, 'question' => '', diff --git a/tests/e2e/General/HTTPTest.php b/tests/e2e/General/HTTPTest.php index f83f28c26..bf8f6de27 100644 --- a/tests/e2e/General/HTTPTest.php +++ b/tests/e2e/General/HTTPTest.php @@ -171,4 +171,50 @@ class HTTPTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); } + + public function testCors() + { + /** + * Test for SUCCESS + */ + + $endpoint = '/v1/projects'; // Can be any non-404 route + + $response = $this->client->call(Client::METHOD_GET, $endpoint); + + $this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']); + + $response = $this->client->call(Client::METHOD_GET, $endpoint, [ + 'origin' => 'http://localhost', + ]); + + $this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']); + + $response = $this->client->call(Client::METHOD_GET, $endpoint, [ + 'origin' => 'http://appwrite.io', + ]); + + $this->assertEquals('http://appwrite.io', $response['headers']['access-control-allow-origin']); + + $response = $this->client->call(Client::METHOD_GET, $endpoint, [ + 'origin' => 'https://appwrite.io', + ]); + + $this->assertEquals('https://appwrite.io', $response['headers']['access-control-allow-origin']); + + $response = $this->client->call(Client::METHOD_GET, $endpoint, [ + 'origin' => 'http://cloud.appwrite.io', + ]); + + $this->assertEquals('http://cloud.appwrite.io', $response['headers']['access-control-allow-origin']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, $endpoint, [ + 'origin' => 'http://google.com', + ]); + + $this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']); + } } From faf39fcc81b824e81d6fd68029721117f878f2ce Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 3 Jan 2024 19:25:54 +0000 Subject: [PATCH 41/58] Fix _APP_CONSOLE_HOSTNAMES check Ensure invalid hostnames such as empty strings are not added as a hostname. --- app/init.php | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/init.php b/app/init.php index ccd7b9657..1bc362fdc 100644 --- a/app/init.php +++ b/app/init.php @@ -80,6 +80,7 @@ use Utopia\Queue\Connection; use Utopia\Storage\Storage; use Utopia\VCS\Adapter\Git\GitHub as VcsGitHub; use Utopia\Validator\Range; +use Utopia\Validator\Hostname; use Utopia\Validator\IP; use Utopia\Validator\URL; use Utopia\Validator\WhiteList; @@ -947,15 +948,18 @@ App::setResource('clients', function ($request, $console, $project) { ], Document::SET_TYPE_APPEND); $hostnames = explode(',', App::getEnv('_APP_CONSOLE_HOSTNAMES', '')); - if (is_array($hostnames)) { - foreach ($hostnames as $hostname) { - $console->setAttribute('platforms', [ - '$collection' => ID::custom('platforms'), - 'type' => Origin::CLIENT_TYPE_WEB, - 'name' => $hostname, - 'hostname' => $hostname, - ], Document::SET_TYPE_APPEND); + $validator = new Hostname(); + foreach ($hostnames as $hostname) { + $hostname = trim($hostname); + if (!$validator->isValid($hostname)) { + continue; } + $console->setAttribute('platforms', [ + '$collection' => ID::custom('platforms'), + 'type' => Origin::CLIENT_TYPE_WEB, + 'name' => $hostname, + 'hostname' => $hostname, + ], Document::SET_TYPE_APPEND); } /** From b6b5570b0b75f7e3c8a1fb6307bbadc589f1cc08 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 3 Jan 2024 19:48:31 +0000 Subject: [PATCH 42/58] Add General E2E tests to CI pipeline --- .github/workflows/tests.yml | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 842d61ff1..2fb6840f6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -75,8 +75,32 @@ jobs: - name: Run Unit Tests run: docker compose exec appwrite test /usr/src/code/tests/unit - e2e_test: - name: E2E Test + e2e_general_test: + name: E2E General Test + runs-on: ubuntu-latest + needs: setup + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: Load Cache + uses: actions/cache@v3 + with: + key: ${{ env.CACHE_KEY }} + path: /tmp/${{ env.IMAGE }}.tar + fail-on-cache-miss: true + + - name: Load and Start Appwrite + run: | + docker load --input /tmp/${{ env.IMAGE }}.tar + docker compose up -d + sleep 10 + + - name: Run General Tests + run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/General --debug + + e2e_service_test: + name: E2E Service Test runs-on: ubuntu-latest needs: setup strategy: @@ -119,4 +143,4 @@ jobs: sleep 10 - name: Run ${{matrix.service}} Tests - run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug \ No newline at end of file + run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug From 4cfd977bd7ddb9344840308e44891e996c51eb1d Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Thu, 4 Jan 2024 00:06:50 +0000 Subject: [PATCH 43/58] Add support for querying topic total --- .../Database/Validator/Queries/Topics.php | 1 + .../e2e/Services/Messaging/MessagingBase.php | 37 ++++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Topics.php b/src/Appwrite/Utopia/Database/Validator/Queries/Topics.php index 120b3edc3..27c818d31 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Topics.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Topics.php @@ -7,6 +7,7 @@ class Topics extends Base public const ALLOWED_ATTRIBUTES = [ 'name', 'description', + 'total' ]; /** diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php index 690e503e7..1feaf1538 100644 --- a/tests/e2e/Services/Messaging/MessagingBase.php +++ b/tests/e2e/Services/Messaging/MessagingBase.php @@ -5,6 +5,7 @@ namespace Tests\E2E\Services\Messaging; use Tests\E2E\Client; use Utopia\App; use Utopia\Database\Helpers\ID; +use Utopia\Database\Query; use Utopia\DSN\DSN; trait MessagingBase @@ -161,11 +162,11 @@ trait MessagingBase 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ], [ - 'name' => 'Mailgun2', - 'apiKey' => 'my-apikey', - 'domain' => 'my-domain', - 'isEuRegion' => true, - 'enabled' => false, + 'name' => 'Mailgun2', + 'apiKey' => 'my-apikey', + 'domain' => 'my-domain', + 'isEuRegion' => true, + 'enabled' => false, ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('Mailgun2', $response['body']['name']); @@ -272,6 +273,32 @@ trait MessagingBase $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(1, \count($response['body']['topics'])); + $response = $this->client->call(Client::METHOD_GET, '/messaging/topics', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'queries' => [ + 'equal("total", [0])' + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, \count($response['body']['topics'])); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/topics', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'queries' => [ + 'greaterThan("total", 0)' + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(0, \count($response['body']['topics'])); + return $topicId; } From fbd277f6aa0ff161838a60e79b950e5a3f58e3f6 Mon Sep 17 00:00:00 2001 From: Ben Humphries Date: Thu, 4 Jan 2024 05:46:08 -0500 Subject: [PATCH 44/58] executor: pass build timeout to runtimes (#7350) open-runtimes executor (v1/runtimes) supports passing a timeout parameter that defaults to 600 seconds. ->param('timeout', 600, new Integer(), 'Commands execution time in seconds.', true) https://github.com/open-runtimes/executor/blob/main/app/http.php#L383 This change passes the _APP_FUNCTIONS_BUILD_TIMEOUT env var. --- src/Executor/Executor.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index c7388069a..8e7374704 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -72,6 +72,7 @@ class Executor ) { $runtimeId = "$projectId-$deploymentId-build"; $route = "/runtimes"; + $timeout = (int) App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900); $params = [ 'runtimeId' => $runtimeId, 'source' => $source, @@ -84,10 +85,9 @@ class Executor 'cpus' => $this->cpus, 'memory' => $this->memory, 'version' => $version, + 'timeout' => $timeout, ]; - $timeout = (int) App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900); - $response = $this->call(self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout); $status = $response['headers']['status-code']; @@ -111,7 +111,7 @@ class Executor string $projectId, callable $callback ) { - $timeout = (int) App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900); + $timeout = (int) App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900); $runtimeId = "$projectId-$deploymentId-build"; $route = "/runtimes/{$runtimeId}/logs"; From 10dfadbbc70857c0188ad55b39fbbd452e84b53f Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Tue, 2 Jan 2024 22:42:17 +0000 Subject: [PATCH 45/58] Create an enum for Message status --- src/Appwrite/Enum/MessageStatus.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/Appwrite/Enum/MessageStatus.php diff --git a/src/Appwrite/Enum/MessageStatus.php b/src/Appwrite/Enum/MessageStatus.php new file mode 100644 index 000000000..77ac1a257 --- /dev/null +++ b/src/Appwrite/Enum/MessageStatus.php @@ -0,0 +1,27 @@ + Date: Thu, 4 Jan 2024 23:58:48 +0000 Subject: [PATCH 46/58] Replace backticks with double quotes --- src/Appwrite/Platform/Workers/Messaging.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index ed7273168..ebf657ecc 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -193,7 +193,7 @@ class Messaging extends Action $details[] = $response->getDetails(); foreach ($details as $detail) { if ($detail['status'] === 'failure') { - $deliveryErrors[] = `Failed sending to target {$detail['recipient']} with error: {$detail['error']}`; + $deliveryErrors[] = "Failed sending to target {$detail['recipient']} with error: {$detail['error']}"; } // Deleting push targets when token has expired. From 7e9525f0f9d025ce184e8e4f912e54b8d3e2fc6a Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Fri, 5 Jan 2024 00:10:57 +0000 Subject: [PATCH 47/58] Update validation of topics, users, and targets They should be an array of UID and not just array of Text. --- app/controllers/api/messaging.php | 46 +++++++++++++++---------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 53135f219..9a9fdb8b1 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -236,7 +236,7 @@ App::post('/v1/messaging/providers/msg91') $options = []; if (!empty($from)) { - $options ['from'] = $from; + $options['from'] = $from; } $credentials = []; @@ -313,7 +313,7 @@ App::post('/v1/messaging/providers/telesign') $options = []; if (!empty($from)) { - $options ['from'] = $from; + $options['from'] = $from; } $credentials = []; @@ -390,7 +390,7 @@ App::post('/v1/messaging/providers/textmagic') $options = []; if (!empty($from)) { - $options ['from'] = $from; + $options['from'] = $from; } $credentials = []; @@ -467,7 +467,7 @@ App::post('/v1/messaging/providers/twilio') $options = []; if (!empty($from)) { - $options ['from'] = $from; + $options['from'] = $from; } $credentials = []; @@ -544,7 +544,7 @@ App::post('/v1/messaging/providers/vonage') $options = []; if (!empty($from)) { - $options ['from'] = $from; + $options['from'] = $from; } $credentials = []; @@ -2276,9 +2276,9 @@ App::post('/v1/messaging/messages/email') ->param('messageId', '', new CustomId(), 'Message ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('subject', '', new Text(998), 'Email Subject.') ->param('content', '', new Text(64230), 'Email Content.') - ->param('topics', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs.', true) - ->param('users', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of User IDs.', true) - ->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Targets IDs.', true) + ->param('topics', [], new ArrayList(new UID()), 'List of Topic IDs.', true) + ->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true) + ->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true) ->param('cc', [], new ArrayList(new UID()), 'Array of target IDs to be added as CC.', true) ->param('bcc', [], new ArrayList(new UID()), 'Array of target IDs to be added as BCC.', true) ->param('description', '', new Text(256), 'Description for message.', true) @@ -2367,9 +2367,9 @@ App::post('/v1/messaging/messages/sms') ->label('sdk.response.model', Response::MODEL_MESSAGE) ->param('messageId', '', new CustomId(), 'Message ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('content', '', new Text(64230), 'SMS Content.') - ->param('topics', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs.', true) - ->param('users', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of User IDs.', true) - ->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Targets IDs.', true) + ->param('topics', [], new ArrayList(new UID()), 'List of Topic IDs.', true) + ->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true) + ->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true) ->param('description', '', new Text(256), 'Description for Message.', true) ->param('status', 'processing', new WhiteList(['draft', 'canceled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) @@ -2448,9 +2448,9 @@ App::post('/v1/messaging/messages/push') ->param('messageId', '', new CustomId(), 'Message ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('title', '', new Text(256), 'Title for push notification.') ->param('body', '', new Text(64230), 'Body for push notification.') - ->param('topics', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs.', true) - ->param('users', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of User IDs.', true) - ->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Targets IDs.', true) + ->param('topics', [], new ArrayList(new UID()), 'List of Topic IDs.', true) + ->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true) + ->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true) ->param('description', '', new Text(256), 'Description for Message.', true) ->param('data', null, new JSON(), 'Additional Data for push notification.', true) ->param('action', '', new Text(256), 'Action for push notification.', true) @@ -2694,9 +2694,9 @@ App::patch('/v1/messaging/messages/email/:messageId') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_MESSAGE) ->param('messageId', '', new UID(), 'Message ID.') - ->param('topics', null, new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs.', true) - ->param('users', null, new ArrayList(new Text(Database::LENGTH_KEY)), 'List of User IDs.', true) - ->param('targets', null, new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Targets IDs.', true) + ->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true) + ->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true) + ->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true) ->param('subject', null, new Text(998), 'Email Subject.', true) ->param('description', null, new Text(256), 'Description for Message.', true) ->param('content', null, new Text(64230), 'Email Content.', true) @@ -2822,9 +2822,9 @@ App::patch('/v1/messaging/messages/sms/:messageId') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_MESSAGE) ->param('messageId', '', new UID(), 'Message ID.') - ->param('topics', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Topic IDs.', true) - ->param('users', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of User IDs.', true) - ->param('targets', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Targets IDs.', true) + ->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true) + ->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true) + ->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true) ->param('description', null, new Text(256), 'Description for Message.', true) ->param('content', null, new Text(64230), 'Email Content.', true) ->param('status', null, new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) @@ -2928,9 +2928,9 @@ App::patch('/v1/messaging/messages/push/:messageId') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_MESSAGE) ->param('messageId', '', new UID(), 'Message ID.') - ->param('topics', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Topic IDs.', true) - ->param('users', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of User IDs.', true) - ->param('targets', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Targets IDs.', true) + ->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true) + ->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true) + ->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true) ->param('description', null, new Text(256), 'Description for Message.', true) ->param('title', null, new Text(256), 'Title for push notification.', true) ->param('body', null, new Text(64230), 'Body for push notification.', true) From 2de8c7a68a0a2d4d1092e918d149d3867014f118 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Fri, 5 Jan 2024 00:31:00 +0000 Subject: [PATCH 48/58] Update composer.lock to fix merge conflict --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 6b7fc0ac4..182da9802 100644 --- a/composer.lock +++ b/composer.lock @@ -2906,16 +2906,16 @@ }, { "name": "utopia-php/vcs", - "version": "0.6.3", + "version": "0.6.4", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "86c3f42a2624bcccb7a67b74dcd7bd3a31fc2e4b" + "reference": "b2595a50a4897a8c88319240810055b7a96efd6d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/86c3f42a2624bcccb7a67b74dcd7bd3a31fc2e4b", - "reference": "86c3f42a2624bcccb7a67b74dcd7bd3a31fc2e4b", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/b2595a50a4897a8c88319240810055b7a96efd6d", + "reference": "b2595a50a4897a8c88319240810055b7a96efd6d", "shasum": "" }, "require": { @@ -2949,9 +2949,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/0.6.3" + "source": "https://github.com/utopia-php/vcs/tree/0.6.4" }, - "time": "2023-12-14T06:53:39+00:00" + "time": "2023-12-26T15:38:19+00:00" }, { "name": "utopia-php/websocket", From 864703fb4ee5711af4a05d6927111a9a70b38210 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Mon, 8 Jan 2024 20:56:51 +0000 Subject: [PATCH 49/58] Update the install/upgrade arguments to be kebab instead of camel case Kebab case like --http-port is much more common in CLIs than --httpPort. --- src/Appwrite/Platform/Tasks/Install.php | 6 +++--- src/Appwrite/Platform/Tasks/Upgrade.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php index 8591f4bcc..c6107c6ba 100644 --- a/src/Appwrite/Platform/Tasks/Install.php +++ b/src/Appwrite/Platform/Tasks/Install.php @@ -25,12 +25,12 @@ class Install extends Action { $this ->desc('Install Appwrite') - ->param('httpPort', '', new Text(4), 'Server HTTP port', true) - ->param('httpsPort', '', new Text(4), 'Server HTTPS port', true) + ->param('http-port', '', new Text(4), 'Server HTTP port', true) + ->param('https-port', '', new Text(4), 'Server HTTPS port', true) ->param('organization', 'appwrite', new Text(0), 'Docker Registry organization', true) ->param('image', 'appwrite', new Text(0), 'Main appwrite docker image', true) ->param('interactive', 'Y', new Text(1), 'Run an interactive session', true) - ->param('noStart', false, new Boolean(true), 'Run an interactive session', true) + ->param('no-start', false, new Boolean(true), 'Run an interactive session', true) ->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive, $noStart) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive, $noStart)); } diff --git a/src/Appwrite/Platform/Tasks/Upgrade.php b/src/Appwrite/Platform/Tasks/Upgrade.php index 1f3e148a8..341ce42fc 100644 --- a/src/Appwrite/Platform/Tasks/Upgrade.php +++ b/src/Appwrite/Platform/Tasks/Upgrade.php @@ -17,12 +17,12 @@ class Upgrade extends Install { $this ->desc('Upgrade Appwrite') - ->param('httpPort', '', new Text(4), 'Server HTTP port', true) - ->param('httpsPort', '', new Text(4), 'Server HTTPS port', true) + ->param('http-port', '', new Text(4), 'Server HTTP port', true) + ->param('https-port', '', new Text(4), 'Server HTTPS port', true) ->param('organization', 'appwrite', new Text(0), 'Docker Registry organization', true) ->param('image', 'appwrite', new Text(0), 'Main appwrite docker image', true) ->param('interactive', 'Y', new Text(1), 'Run an interactive session', true) - ->param('noStart', false, new Boolean(true), 'Run an interactive session', true) + ->param('no-start', false, new Boolean(true), 'Run an interactive session', true) ->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive, $noStart) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive, $noStart)); } From fc81c032b4ccb43bbf13f618ef660f74008c27ed Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 9 Jan 2024 13:41:55 +1300 Subject: [PATCH 50/58] Clean up test values --- tests/e2e/Services/GraphQL/MessagingTest.php | 22 +++++++++---------- .../e2e/Services/Messaging/MessagingBase.php | 16 ++++++-------- .../Projects/ProjectsConsoleClientTest.php | 1 - 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/tests/e2e/Services/GraphQL/MessagingTest.php b/tests/e2e/Services/GraphQL/MessagingTest.php index e778e4e30..182841148 100644 --- a/tests/e2e/Services/GraphQL/MessagingTest.php +++ b/tests/e2e/Services/GraphQL/MessagingTest.php @@ -75,9 +75,9 @@ class MessagingTest extends Scope 'name' => 'FCM1', 'serviceAccountJSON' => [ 'type' => 'service_account', - "project_id" => "omegle-copy", - "private_key_id" => "ewfwefwefwefwef", - "private_key" => "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkAgEAAoIBAQCeKDbvv4XvGuNAOxZBcxoNnvbINKlq0FtiqgsqLAgDOMt\nGPEANfni+D7lZRrMPhZhcL4YCjAUg+0ZI0D9d2LGofasj9GlBb57SZc/ud2L9FZZ\nk5liXrUk0SUirffBUmj5F/XPTJ+JXc89qPtt15+hqx30h2ID/wxN0AhmViLikR3o\n3YBHAYq0NbmAfQSfdsHX+lvNKvsAxU+LatRPE3tVcvSd3ZnP0zlHYVmp6UCeBWeW\nOTcSPecCcVXmBdMPtHGWdrNG2op1CmHc8JeYJMQ4xgz3obQcOX9+3USsysANjgta\nb07m6xS3AgMBAAECggEAIeSTVVCRZrq36zk8VgJ/r/NE4r95xEk2K/K/Lvb0fx75\no0BO5gsAkYqvgzem/LrVFCEFRkDGMbAhVQ5Fw1pN2U6CyA0hL4jUqgALtMImKJdX\nDa6I5Gibwd5+qt9NOZSgC/Kq14zAxhfQE3U2hyatohyx3Rsz/3lmJo90bX7Jp5md\nGBDOB3pFBqyfUvyHgeqCgvJvidJjxmwArLhUF8szuDRvmSs0lGsfqYprK0sb9phL\nP7Z3qMJk1J4IDL2abSGrTcMP+hk7ju1iqo7WfhIQCvM1TD5dRjYg2IYPIAIzszWz\nxSA67eJpQGSFfOuk82g3UMhfCD2DY2mCE/zkeid9jQKBgQDSB2xA+LpQDX2nuoDR\niZbPYBitxQtkbjieYTR8vwrIzyAvRtOwjnVKsXLyIbUYyHd6RFRDPeBcHb39KuRO\nz7VljQKTVB5RYUmqeGilor0TFaKMnneC7GFH6mWOJyf16DU7bkQw27Pg1e3xbF28\n5ig7QYPqEaDKLg6TMSLsBhdRDQKBgQDAxj9jS9UOTmF3N9T1JFzWfUB2r+AgwE4N\nSITmG/fSz9rlSg+XPh2ijpSrboUbuY/GYq5aCIy1twx09eta07Y/uD/GKLYrk873\no0TxQrnHSKl82fCyd2JPG/W8ocGDnj3u0Dp+tBrLxDiZN2pRurnlkt7P3QUg/gEG\nAovyd3ij0wKBgBbA7x1q1ORvUbmmHuaUfV4iDwpkWoOa3U9rQIBzQfvXVKlKhwyN\nom9hIg7RUAlLToZUeLyAK5pPLpIK34kaP5Cs4iaL6mzumUh6mvu20b0Ljvyk/lWU\nvkVIQ5BO9alSatHxdDnG04n8IzcQgmdAmAMzadMl7cF5k+KmZB4l2sjRAoGAP8JS\nPNlcAntSKUhCG0KHojmTFK5fBvYT2rjdm+4sLYGp+KRiO7fDvXxDF+BaDi11rDv/\nRrAFOiTs7dJYoZXcdX7POQ9GEWu1zJont1RGde9Gf5Dl12E9FsU8pcMqagnwmggt\nELMpGbQwtBxsAdQsoA3PvBhyFdNtKzu0ZeG1+RkCgYBOPhOCR88QPTmQANkwIMH+\n0vt+KhSjE3dhX7rzVkhoNmYF5AaSSpQ3F1JUlYntjblMQVjLesGvWa4gwCOF87xC\nJxHL6LkbNjAyUGZp7to6/F4vTmKoC6Xu/jTRRy2SVjdqiIa0Pm0eLLfRHmSI06pS\n+zLmdpZv/msPfGibbHcXUA==\n-----END PRIVATE KEY-----\n", + "project_id" => "test-project", + "private_key_id" => "test-private-key-id", + "private_key" => "test-private-key", ] ], 'Apns' => [ @@ -104,7 +104,7 @@ class MessagingTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ]), $graphQLPayload); - var_dump($response['body']); + \array_push($providers, $response['body']['data']['messagingCreate' . $key . 'Provider']); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals($providersParams[$key]['name'], $response['body']['data']['messagingCreate' . $key . 'Provider']['name']); @@ -165,9 +165,9 @@ class MessagingTest extends Scope 'name' => 'FCM2', 'serviceAccountJSON' => [ 'type' => 'service_account', - "project_id" => "omegle-copy", - "private_key_id" => "ewfwefwefwefwef", - "private_key" => "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkAgEAAoIBAQCeKDbvv4XvGuNAOxZBcxoNnvbINKlq0FtiqgsqLAgDOMt\nGPEANfni+D7lZRrMPhZhcL4YCjAUg+0ZI0D9d2LGofasj9GlBb57SZc/ud2L9FZZ\nk5liXrUk0SUirffBUmj5F/XPTJ+JXc89qPtt15+hqx30h2ID/wxN0AhmViLikR3o\n3YBHAYq0NbmAfQSfdsHX+lvNKvsAxU+LatRPE3tVcvSd3ZnP0zlHYVmp6UCeBWeW\nOTcSPecCcVXmBdMPtHGWdrNG2op1CmHc8JeYJMQ4xgz3obQcOX9+3USsysANjgta\nb07m6xS3AgMBAAECggEAIeSTVVCRZrq36zk8VgJ/r/NE4r95xEk2K/K/Lvb0fx75\no0BO5gsAkYqvgzem/LrVFCEFRkDGMbAhVQ5Fw1pN2U6CyA0hL4jUqgALtMImKJdX\nDa6I5Gibwd5+qt9NOZSgC/Kq14zAxhfQE3U2hyatohyx3Rsz/3lmJo90bX7Jp5md\nGBDOB3pFBqyfUvyHgeqCgvJvidJjxmwArLhUF8szuDRvmSs0lGsfqYprK0sb9phL\nP7Z3qMJk1J4IDL2abSGrTcMP+hk7ju1iqo7WfhIQCvM1TD5dRjYg2IYPIAIzszWz\nxSA67eJpQGSFfOuk82g3UMhfCD2DY2mCE/zkeid9jQKBgQDSB2xA+LpQDX2nuoDR\niZbPYBitxQtkbjieYTR8vwrIzyAvRtOwjnVKsXLyIbUYyHd6RFRDPeBcHb39KuRO\nz7VljQKTVB5RYUmqeGilor0TFaKMnneC7GFH6mWOJyf16DU7bkQw27Pg1e3xbF28\n5ig7QYPqEaDKLg6TMSLsBhdRDQKBgQDAxj9jS9UOTmF3N9T1JFzWfUB2r+AgwE4N\nSITmG/fSz9rlSg+XPh2ijpSrboUbuY/GYq5aCIy1twx09eta07Y/uD/GKLYrk873\no0TxQrnHSKl82fCyd2JPG/W8ocGDnj3u0Dp+tBrLxDiZN2pRurnlkt7P3QUg/gEG\nAovyd3ij0wKBgBbA7x1q1ORvUbmmHuaUfV4iDwpkWoOa3U9rQIBzQfvXVKlKhwyN\nom9hIg7RUAlLToZUeLyAK5pPLpIK34kaP5Cs4iaL6mzumUh6mvu20b0Ljvyk/lWU\nvkVIQ5BO9alSatHxdDnG04n8IzcQgmdAmAMzadMl7cF5k+KmZB4l2sjRAoGAP8JS\nPNlcAntSKUhCG0KHojmTFK5fBvYT2rjdm+4sLYGp+KRiO7fDvXxDF+BaDi11rDv/\nRrAFOiTs7dJYoZXcdX7POQ9GEWu1zJont1RGde9Gf5Dl12E9FsU8pcMqagnwmggt\nELMpGbQwtBxsAdQsoA3PvBhyFdNtKzu0ZeG1+RkCgYBOPhOCR88QPTmQANkwIMH+\n0vt+KhSjE3dhX7rzVkhoNmYF5AaSSpQ3F1JUlYntjblMQVjLesGvWa4gwCOF87xC\nJxHL6LkbNjAyUGZp7to6/F4vTmKoC6Xu/jTRRy2SVjdqiIa0Pm0eLLfRHmSI06pS\n+zLmdpZv/msPfGibbHcXUA==\n-----END PRIVATE KEY-----\n", + 'project_id' => 'test-project', + 'private_key_id' => 'test-project-id', + 'private_key' => "test-private-key", ] ], 'Apns' => [ @@ -972,7 +972,7 @@ class MessagingTest extends Scope $pushDSN = new DSN(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN')); $to = $pushDSN->getParam('to'); - $serviceAccountJSON = $pushDSN->getParam('saj'); + $serviceAccountJSON = $pushDSN->getParam('serviceAccountJSON'); if (empty($to) || empty($serviceAccountJSON)) { $this->markTestSkipped('Push provider not configured'); @@ -986,9 +986,9 @@ class MessagingTest extends Scope 'name' => 'FCM1', 'serviceAccountJSON' => [ 'type' => 'service_account', - "project_id" => "token-test", - "private_key_id" => "bitcoin-is-the-future", - "private_key" => "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkAgEAAoIBAQCeKDbvv4XvGuNAOxZBcxoNnvbINKlq0FtiqgsqLAgDOMt\nGPEANfni+D7lZRrMPhZhcL4YCjAUg+0ZI0D9d2LGofasj9GlBb57SZc/ud2L9FZZ\nk5liXrUk0SUirffBUmj5F/XPTJ+JXc89qPtt15+hqx30h2ID/wxN0AhmViLikR3o\n3YBHAYq0NbmAfQSfdsHX+lvNKvsAxU+LatRPE3tVcvSd3ZnP0zlHYVmp6UCeBWeW\nOTcSPecCcVXmBdMPtHGWdrNG2op1CmHc8JeYJMQ4xgz3obQcOX9+3USsysANjgta\nb07m6xS3AgMBAAECggEAIeSTVVCRZrq36zk8VgJ/r/NE4r95xEk2K/K/Lvb0fx75\no0BO5gsAkYqvgzem/LrVFCEFRkDGMbAhVQ5Fw1pN2U6CyA0hL4jUqgALtMImKJdX\nDa6I5Gibwd5+qt9NOZSgC/Kq14zAxhfQE3U2hyatohyx3Rsz/3lmJo90bX7Jp5md\nGBDOB3pFBqyfUvyHgeqCgvJvidJjxmwArLhUF8szuDRvmSs0lGsfqYprK0sb9phL\nP7Z3qMJk1J4IDL2abSGrTcMP+hk7ju1iqo7WfhIQCvM1TD5dRjYg2IYPIAIzszWz\nxSA67eJpQGSFfOuk82g3UMhfCD2DY2mCE/zkeid9jQKBgQDSB2xA+LpQDX2nuoDR\niZbPYBitxQtkbjieYTR8vwrIzyAvRtOwjnVKsXLyIbUYyHd6RFRDPeBcHb39KuRO\nz7VljQKTVB5RYUmqeGilor0TFaKMnneC7GFH6mWOJyf16DU7bkQw27Pg1e3xbF28\n5ig7QYPqEaDKLg6TMSLsBhdRDQKBgQDAxj9jS9UOTmF3N9T1JFzWfUB2r+AgwE4N\nSITmG/fSz9rlSg+XPh2ijpSrboUbuY/GYq5aCIy1twx09eta07Y/uD/GKLYrk873\no0TxQrnHSKl82fCyd2JPG/W8ocGDnj3u0Dp+tBrLxDiZN2pRurnlkt7P3QUg/gEG\nAovyd3ij0wKBgBbA7x1q1ORvUbmmHuaUfV4iDwpkWoOa3U9rQIBzQfvXVKlKhwyN\nom9hIg7RUAlLToZUeLyAK5pPLpIK34kaP5Cs4iaL6mzumUh6mvu20b0Ljvyk/lWU\nvkVIQ5BO9alSatHxdDnG04n8IzcQgmdAmAMzadMl7cF5k+KmZB4l2sjRAoGAP8JS\nPNlcAntSKUhCG0KHojmTFK5fBvYT2rjdm+4sLYGp+KRiO7fDvXxDF+BaDi11rDv/\nRrAFOiTs7dJYoZXcdX7POQ9GEWu1zJont1RGde9Gf5Dl12E9FsU8pcMqagnwmggt\nELMpGbQwtBxsAdQsoA3PvBhyFdNtKzu0ZeG1+RkCgYBOPhOCR88QPTmQANkwIMH+\n0vt+KhSjE3dhX7rzVkhoNmYF5AaSSpQ3F1JUlYntjblMQVjLesGvWa4gwCOF87xC\nJxHL6LkbNjAyUGZp7to6/F4vTmKoC6Xu/jTRRy2SVjdqiIa0Pm0eLLfRHmSI06pS\n+zLmdpZv/msPfGibbHcXUA==\n-----END PRIVATE KEY-----\n", + "project_id" => "test-project", + "private_key_id" => "test-private-key-id", + "private_key" => "test-private-key", ] ], ]; diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php index 9e1788928..5b76d3756 100644 --- a/tests/e2e/Services/Messaging/MessagingBase.php +++ b/tests/e2e/Services/Messaging/MessagingBase.php @@ -67,9 +67,9 @@ trait MessagingBase 'name' => 'FCM1', 'serviceAccountJSON' => [ 'type' => 'service_account', - "project_id" => "omegle-copy", - "private_key_id" => "ewfwefwefwefwef", - "private_key" => "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkAgEAAoIBAQCeKDbvv4XvGuNAOxZBcxoNnvbINKlq0FtiqgsqLAgDOMt\nGPEANfni+D7lZRrMPhZhcL4YCjAUg+0ZI0D9d2LGofasj9GlBb57SZc/ud2L9FZZ\nk5liXrUk0SUirffBUmj5F/XPTJ+JXc89qPtt15+hqx30h2ID/wxN0AhmViLikR3o\n3YBHAYq0NbmAfQSfdsHX+lvNKvsAxU+LatRPE3tVcvSd3ZnP0zlHYVmp6UCeBWeW\nOTcSPecCcVXmBdMPtHGWdrNG2op1CmHc8JeYJMQ4xgz3obQcOX9+3USsysANjgta\nb07m6xS3AgMBAAECggEAIeSTVVCRZrq36zk8VgJ/r/NE4r95xEk2K/K/Lvb0fx75\no0BO5gsAkYqvgzem/LrVFCEFRkDGMbAhVQ5Fw1pN2U6CyA0hL4jUqgALtMImKJdX\nDa6I5Gibwd5+qt9NOZSgC/Kq14zAxhfQE3U2hyatohyx3Rsz/3lmJo90bX7Jp5md\nGBDOB3pFBqyfUvyHgeqCgvJvidJjxmwArLhUF8szuDRvmSs0lGsfqYprK0sb9phL\nP7Z3qMJk1J4IDL2abSGrTcMP+hk7ju1iqo7WfhIQCvM1TD5dRjYg2IYPIAIzszWz\nxSA67eJpQGSFfOuk82g3UMhfCD2DY2mCE/zkeid9jQKBgQDSB2xA+LpQDX2nuoDR\niZbPYBitxQtkbjieYTR8vwrIzyAvRtOwjnVKsXLyIbUYyHd6RFRDPeBcHb39KuRO\nz7VljQKTVB5RYUmqeGilor0TFaKMnneC7GFH6mWOJyf16DU7bkQw27Pg1e3xbF28\n5ig7QYPqEaDKLg6TMSLsBhdRDQKBgQDAxj9jS9UOTmF3N9T1JFzWfUB2r+AgwE4N\nSITmG/fSz9rlSg+XPh2ijpSrboUbuY/GYq5aCIy1twx09eta07Y/uD/GKLYrk873\no0TxQrnHSKl82fCyd2JPG/W8ocGDnj3u0Dp+tBrLxDiZN2pRurnlkt7P3QUg/gEG\nAovyd3ij0wKBgBbA7x1q1ORvUbmmHuaUfV4iDwpkWoOa3U9rQIBzQfvXVKlKhwyN\nom9hIg7RUAlLToZUeLyAK5pPLpIK34kaP5Cs4iaL6mzumUh6mvu20b0Ljvyk/lWU\nvkVIQ5BO9alSatHxdDnG04n8IzcQgmdAmAMzadMl7cF5k+KmZB4l2sjRAoGAP8JS\nPNlcAntSKUhCG0KHojmTFK5fBvYT2rjdm+4sLYGp+KRiO7fDvXxDF+BaDi11rDv/\nRrAFOiTs7dJYoZXcdX7POQ9GEWu1zJont1RGde9Gf5Dl12E9FsU8pcMqagnwmggt\nELMpGbQwtBxsAdQsoA3PvBhyFdNtKzu0ZeG1+RkCgYBOPhOCR88QPTmQANkwIMH+\n0vt+KhSjE3dhX7rzVkhoNmYF5AaSSpQ3F1JUlYntjblMQVjLesGvWa4gwCOF87xC\nJxHL6LkbNjAyUGZp7to6/F4vTmKoC6Xu/jTRRy2SVjdqiIa0Pm0eLLfRHmSI06pS\n+zLmdpZv/msPfGibbHcXUA==\n-----END PRIVATE KEY-----\n", + "project_id" => "test-project", + "private_key_id" => "test-private-key-id", + "private_key" => "test-private-key", ], ], 'apns' => [ @@ -143,9 +143,9 @@ trait MessagingBase 'name' => 'FCM2', 'serviceAccountJSON' => [ 'type' => 'service_account', - "project_id" => "omegle-copy", - "private_key_id" => "ewfwefwefwefwef", - "private_key" => "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkAgEAAoIBAQCeKDbvv4XvGuNAOxZBcxoNnvbINKlq0FtiqgsqLAgDOMt\nGPEANfni+D7lZRrMPhZhcL4YCjAUg+0ZI0D9d2LGofasj9GlBb57SZc/ud2L9FZZ\nk5liXrUk0SUirffBUmj5F/XPTJ+JXc89qPtt15+hqx30h2ID/wxN0AhmViLikR3o\n3YBHAYq0NbmAfQSfdsHX+lvNKvsAxU+LatRPE3tVcvSd3ZnP0zlHYVmp6UCeBWeW\nOTcSPecCcVXmBdMPtHGWdrNG2op1CmHc8JeYJMQ4xgz3obQcOX9+3USsysANjgta\nb07m6xS3AgMBAAECggEAIeSTVVCRZrq36zk8VgJ/r/NE4r95xEk2K/K/Lvb0fx75\no0BO5gsAkYqvgzem/LrVFCEFRkDGMbAhVQ5Fw1pN2U6CyA0hL4jUqgALtMImKJdX\nDa6I5Gibwd5+qt9NOZSgC/Kq14zAxhfQE3U2hyatohyx3Rsz/3lmJo90bX7Jp5md\nGBDOB3pFBqyfUvyHgeqCgvJvidJjxmwArLhUF8szuDRvmSs0lGsfqYprK0sb9phL\nP7Z3qMJk1J4IDL2abSGrTcMP+hk7ju1iqo7WfhIQCvM1TD5dRjYg2IYPIAIzszWz\nxSA67eJpQGSFfOuk82g3UMhfCD2DY2mCE/zkeid9jQKBgQDSB2xA+LpQDX2nuoDR\niZbPYBitxQtkbjieYTR8vwrIzyAvRtOwjnVKsXLyIbUYyHd6RFRDPeBcHb39KuRO\nz7VljQKTVB5RYUmqeGilor0TFaKMnneC7GFH6mWOJyf16DU7bkQw27Pg1e3xbF28\n5ig7QYPqEaDKLg6TMSLsBhdRDQKBgQDAxj9jS9UOTmF3N9T1JFzWfUB2r+AgwE4N\nSITmG/fSz9rlSg+XPh2ijpSrboUbuY/GYq5aCIy1twx09eta07Y/uD/GKLYrk873\no0TxQrnHSKl82fCyd2JPG/W8ocGDnj3u0Dp+tBrLxDiZN2pRurnlkt7P3QUg/gEG\nAovyd3ij0wKBgBbA7x1q1ORvUbmmHuaUfV4iDwpkWoOa3U9rQIBzQfvXVKlKhwyN\nom9hIg7RUAlLToZUeLyAK5pPLpIK34kaP5Cs4iaL6mzumUh6mvu20b0Ljvyk/lWU\nvkVIQ5BO9alSatHxdDnG04n8IzcQgmdAmAMzadMl7cF5k+KmZB4l2sjRAoGAP8JS\nPNlcAntSKUhCG0KHojmTFK5fBvYT2rjdm+4sLYGp+KRiO7fDvXxDF+BaDi11rDv/\nRrAFOiTs7dJYoZXcdX7POQ9GEWu1zJont1RGde9Gf5Dl12E9FsU8pcMqagnwmggt\nELMpGbQwtBxsAdQsoA3PvBhyFdNtKzu0ZeG1+RkCgYBOPhOCR88QPTmQANkwIMH+\n0vt+KhSjE3dhX7rzVkhoNmYF5AaSSpQ3F1JUlYntjblMQVjLesGvWa4gwCOF87xC\nJxHL6LkbNjAyUGZp7to6/F4vTmKoC6Xu/jTRRy2SVjdqiIa0Pm0eLLfRHmSI06pS\n+zLmdpZv/msPfGibbHcXUA==\n-----END PRIVATE KEY-----\n", + "project_id" => "test-project", + "private_key_id" => "test-private-key-id", + "private_key" => "test-private-key", ] ], 'apns' => [ @@ -647,8 +647,6 @@ trait MessagingBase 'x-appwrite-key' => $this->getProject()['apiKey'], ]); - \var_dump($message); - $this->assertEquals(200, $message['headers']['status-code']); $this->assertEquals(1, $message['body']['deliveredTotal']); $this->assertEquals(0, \count($message['body']['deliveryErrors'])); @@ -881,7 +879,7 @@ trait MessagingBase $dsn = new DSN(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN')); $to = $dsn->getParam('to'); - $serviceAccountJSON = $dsn->getParam('saj'); + $serviceAccountJSON = $dsn->getParam('serviceAccountJSON'); if (empty($to) || empty($serviceAccountJSON)) { $this->markTestSkipped('Push provider not configured'); diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 3e63bd874..4d844c755 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -1661,7 +1661,6 @@ class ProjectsConsoleClientTest extends Scope foreach ($response['body'] as $key => $value) { if (\preg_match($pattern, $key)) { - \var_dump('Matched key: ' . $key); $matches[$key] = $value; } } From 86158838a14d77952dc4fb8ddfdf44181432b543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 9 Jan 2024 12:23:13 +0000 Subject: [PATCH 51/58] Magic URL improvements --- app/config/locale/templates/email-base.tpl | 6 +++--- .../locale/templates/email-magic-url.tpl | 20 +++++++++++++++++++ app/config/locale/translations/en.json | 12 ++++++----- app/controllers/api/account.php | 19 ++++++++++++++---- src/Appwrite/Auth/Auth.php | 2 +- 5 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 app/config/locale/templates/email-magic-url.tpl diff --git a/app/config/locale/templates/email-base.tpl b/app/config/locale/templates/email-base.tpl index f41a9530e..346f2f158 100644 --- a/app/config/locale/templates/email-base.tpl +++ b/app/config/locale/templates/email-base.tpl @@ -8,13 +8,13 @@ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Poppins:wght@500;600&display=swap" rel="stylesheet">