From f68d49c4d63fce8a663b5f0c7d6b3852dae3feef Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 29 Aug 2025 18:19:29 +0530 Subject: [PATCH 01/28] updated the migration error size attribute --- app/config/collections/projects.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 7fc82b7441..4fd44c7725 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -2345,7 +2345,7 @@ return [ '$id' => ID::custom('errors'), 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 65535, + 'size' => 131070, 'signed' => true, 'required' => true, 'default' => null, From ac604b11e3b241e504d14f14b2e09750e855c705 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 1 Sep 2025 13:13:52 +0530 Subject: [PATCH 02/28] added migration script --- src/Appwrite/Migration/Version/V23.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Appwrite/Migration/Version/V23.php b/src/Appwrite/Migration/Version/V23.php index d5caf2ab3c..c26c45d732 100644 --- a/src/Appwrite/Migration/Version/V23.php +++ b/src/Appwrite/Migration/Version/V23.php @@ -6,6 +6,7 @@ use Appwrite\Migration\Migration; use Exception; use Throwable; use Utopia\CLI\Console; +use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; @@ -29,6 +30,9 @@ class V23 extends Migration Console::info('Migrating databases'); $this->migrateDatabases(); + + Console::info('Migrating migration collection'); + $this->updateMigrateErrorSize(); } /** @@ -49,4 +53,26 @@ class V23 extends Migration $this->dbForProject->updateDocuments('databases', new Document(['type' => 'legacy'])); } + /** + * Update migration collection error attribute + * + * @return void + * @throws Exception|Throwable + */ + + private function updateMigrateErrorSize(): void + { + if ($this->project->getId() === 'console') { + return; + } + + $collection = Config::getParam('collections', [])['projects'] ?? []; + $migrationAttributes = $collection['migrations']['attributes']; + $attributeKey = \array_search('errors', \array_column($migrationAttributes, '$id')); + $migrationAttributes[$attributeKey]['size'] = 131070; + $migration = $this->dbForProject->getCollection('migrations'); + $migration->setAttribute('attributes', $migrationAttributes); + $this->dbForProject->updateDocument($migration->getCollection(), $migration->getId(), $migration); + $this->dbForProject->purgeCachedCollection('migrations'); + } } From c13a70e226e2b9347c0d1e756a6e96e609088974 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 23 Oct 2025 12:38:18 +0300 Subject: [PATCH 03/28] Add users attributes --- app/config/collections/common.php | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/app/config/collections/common.php b/app/config/collections/common.php index 6de7eb224b..2b3d10e457 100644 --- a/app/config/collections/common.php +++ b/app/config/collections/common.php @@ -364,6 +364,50 @@ return [ 'array' => false, 'filters' => ['datetime'], ], + [ + '$id' => ID::custom('emailIsFree'), + 'type' => Database::VAR_BOOLEAN, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('emailIsDisposable'), + 'type' => Database::VAR_BOOLEAN, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('emailIsCorporate'), + 'type' => Database::VAR_BOOLEAN, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('emailIsCanonical'), + 'type' => Database::VAR_BOOLEAN, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], ], 'indexes' => [ [ From 037d60633bbfe803e2c099cfcfda8d27c8c3a0d8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 27 Oct 2025 15:59:20 +0200 Subject: [PATCH 04/28] Add emailCanonical --- app/config/collections/common.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/config/collections/common.php b/app/config/collections/common.php index 2b3d10e457..d8e1c1699a 100644 --- a/app/config/collections/common.php +++ b/app/config/collections/common.php @@ -364,6 +364,17 @@ return [ 'array' => false, 'filters' => ['datetime'], ], + [ + '$id' => ID::custom('emailCanonical'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 320, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => ID::custom('emailIsFree'), 'type' => Database::VAR_BOOLEAN, From e40e88c32233d42ae5ded50a43eb34e98e71389e Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 30 Oct 2025 18:09:36 +0200 Subject: [PATCH 05/28] Update emails attributes --- app/controllers/api/account.php | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 5563fc6a59..68cf189395 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -57,6 +57,7 @@ use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; +use Utopia\Emails\Email as EmailCanonical; use Utopia\Locale\Locale; use Utopia\Storage\Validator\FileName; use Utopia\System\System; @@ -1674,7 +1675,15 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') } if (empty($user->getAttribute('email'))) { - $user->setAttribute('email', $oauth2->getUserEmail($accessToken)); + $emailCanonical = new EmailCanonical($oauth2->getUserEmail($accessToken)); + + $user + ->setAttribute('email', $oauth2->getUserEmail($accessToken)) + ->setAttribute('emailCanonical', $emailCanonical->getCanonical()) + ->setAttribute('emailIsCanonical', $emailCanonical->isCanonicalSupported()) + ->setAttribute('emailIsCorporate', $emailCanonical->isCorporate()) + ->setAttribute('emailIsDisposable', $emailCanonical->isDisposable()) + ->setAttribute('emailIsFree', $emailCanonical->isFree()); } if (empty($user->getAttribute('name'))) { @@ -2219,6 +2228,8 @@ App::post('/v1/account/tokens/email') $userId = $userId === 'unique()' ? ID::unique() : $userId; + $emailCanonical = new EmailCanonical($email); + $user->setAttributes([ '$id' => $userId, '$permissions' => [ @@ -2241,6 +2252,11 @@ App::post('/v1/account/tokens/email') 'memberships' => null, 'search' => implode(' ', [$userId, $email]), 'accessedAt' => DateTime::now(), + 'emailCanonical' => $emailCanonical->getCanonical(), + 'emailIsCanonical' => $emailCanonical->isCanonicalSupported(), + 'emailIsCorporate' => $emailCanonical->isCorporate(), + 'emailIsDisposable' => $emailCanonical->isDisposable(), + 'emailIsFree' => $emailCanonical->isFree(), ]); $user->removeAttribute('$sequence'); @@ -3050,9 +3066,17 @@ App::patch('/v1/account/email') throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } + $emailCanonical = new EmailCanonical($email); + $user ->setAttribute('email', $email) ->setAttribute('emailVerification', false) // After this user needs to confirm mail again + ->setAttribute('emailVerification', false) + ->setAttribute('emailCanonical', $emailCanonical->getCanonical()) + ->setAttribute('emailIsCanonical', $emailCanonical->isCanonicalSupported()) + ->setAttribute('emailIsCorporate', $emailCanonical->isCorporate()) + ->setAttribute('emailIsDisposable', $emailCanonical->isDisposable()) + ->setAttribute('emailIsFree', $emailCanonical->isFree()) ; if (empty($passwordUpdate)) { From fea16309092535dd3ac5017136cffa206518312c Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 30 Oct 2025 18:18:09 +0200 Subject: [PATCH 06/28] Remove redundant emailVerification --- app/controllers/api/account.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 68cf189395..cdd7280fb7 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -3071,7 +3071,6 @@ App::patch('/v1/account/email') $user ->setAttribute('email', $email) ->setAttribute('emailVerification', false) // After this user needs to confirm mail again - ->setAttribute('emailVerification', false) ->setAttribute('emailCanonical', $emailCanonical->getCanonical()) ->setAttribute('emailIsCanonical', $emailCanonical->isCanonicalSupported()) ->setAttribute('emailIsCorporate', $emailCanonical->isCorporate()) From ca8cb6f2601d3e520ef9023387d442364bf21f46 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 2 Nov 2025 09:13:55 +0200 Subject: [PATCH 07/28] Add utopia-php/emails --- app/controllers/api/account.php | 6 +- composer.json | 1 + composer.lock | 162 ++++++++++++++++++++++---------- 3 files changed, 116 insertions(+), 53 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index cdd7280fb7..6c31f28754 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1675,10 +1675,12 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') } if (empty($user->getAttribute('email'))) { - $emailCanonical = new EmailCanonical($oauth2->getUserEmail($accessToken)); + $oauth2Email = $oauth2->getUserEmail($accessToken); + + $emailCanonical = new EmailCanonical($oauth2Email); $user - ->setAttribute('email', $oauth2->getUserEmail($accessToken)) + ->setAttribute('email', $oauth2Email) ->setAttribute('emailCanonical', $emailCanonical->getCanonical()) ->setAttribute('emailIsCanonical', $emailCanonical->isCanonicalSupported()) ->setAttribute('emailIsCorporate', $emailCanonical->isCorporate()) diff --git a/composer.json b/composer.json index 3e00015220..f008c8e089 100644 --- a/composer.json +++ b/composer.json @@ -60,6 +60,7 @@ "utopia-php/framework": "0.33.*", "utopia-php/fetch": "0.4.*", "utopia-php/image": "0.8.*", + "utopia-php/emails": "0.6.*", "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", diff --git a/composer.lock b/composer.lock index 36c2c94265..394e2a9e3e 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": "f826d3b283b10af98dfd565c4187a83a", + "content-hash": "c53027e4009de3901769fe29b0d1a68f", "packages": [ { "name": "adhocore/jwt", @@ -3840,16 +3840,16 @@ }, { "name": "utopia-php/database", - "version": "3.0.2", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "b92554e2e7b3b00f0f0acb2b53c6a11e1349b81e" + "reference": "b6541a9cd9b21786a5020327f582838afdb159aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/b92554e2e7b3b00f0f0acb2b53c6a11e1349b81e", - "reference": "b92554e2e7b3b00f0f0acb2b53c6a11e1349b81e", + "url": "https://api.github.com/repos/utopia-php/database/zipball/b6541a9cd9b21786a5020327f582838afdb159aa", + "reference": "b6541a9cd9b21786a5020327f582838afdb159aa", "shasum": "" }, "require": { @@ -3892,22 +3892,22 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/3.0.2" + "source": "https://github.com/utopia-php/database/tree/3.1.2" }, - "time": "2025-10-20T23:58:56+00:00" + "time": "2025-10-30T13:10:13+00:00" }, { "name": "utopia-php/detector", - "version": "0.2.0", + "version": "0.2.2", "source": { "type": "git", "url": "https://github.com/utopia-php/detector.git", - "reference": "795ed56169af833fd6a4ea58a6c747e05ccc7ba6" + "reference": "9a41be5f21efe2d865de79b08dff94fff85ce5e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/detector/zipball/795ed56169af833fd6a4ea58a6c747e05ccc7ba6", - "reference": "795ed56169af833fd6a4ea58a6c747e05ccc7ba6", + "url": "https://api.github.com/repos/utopia-php/detector/zipball/9a41be5f21efe2d865de79b08dff94fff85ce5e9", + "reference": "9a41be5f21efe2d865de79b08dff94fff85ce5e9", "shasum": "" }, "require": { @@ -3937,9 +3937,9 @@ ], "support": { "issues": "https://github.com/utopia-php/detector/issues", - "source": "https://github.com/utopia-php/detector/tree/0.2.0" + "source": "https://github.com/utopia-php/detector/tree/0.2.2" }, - "time": "2025-10-21T13:57:30+00:00" + "time": "2025-10-31T12:43:31+00:00" }, { "name": "utopia-php/dns", @@ -4106,6 +4106,66 @@ }, "time": "2024-05-07T02:01:25+00:00" }, + { + "name": "utopia-php/emails", + "version": "0.6.2", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/emails.git", + "reference": "9c4c40cf7c03c2e9e21364566f9b192d03ea93c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/emails/zipball/9c4c40cf7c03c2e9e21364566f9b192d03ea93c9", + "reference": "9c4c40cf7c03c2e9e21364566f9b192d03ea93c9", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "utopia-php/cli": "^0.15", + "utopia-php/domains": "^0.9", + "utopia-php/fetch": "^0.4", + "utopia-php/validators": "^0.0.2" + }, + "require-dev": { + "laravel/pint": "1.25.*", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Emails\\": "src/Emails" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eldad Fux", + "email": "eldad@appwrite.io" + } + ], + "description": "Utopia Emails library is simple and lite library for parsing and validating email addresses. This library is aiming to be as simple and easy to learn and use.", + "keywords": [ + "RFC5322", + "email", + "emails", + "framework", + "parsing", + "php", + "upf", + "utopia", + "validation" + ], + "support": { + "issues": "https://github.com/utopia-php/emails/issues", + "source": "https://github.com/utopia-php/emails/tree/0.6.2" + }, + "time": "2025-10-28T16:08:17+00:00" + }, { "name": "utopia-php/fetch", "version": "0.4.2", @@ -4395,16 +4455,16 @@ }, { "name": "utopia-php/migration", - "version": "1.3.2", + "version": "1.3.3", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "f5c1d2cae764290766a4c2d1546c1d51de95b67f" + "reference": "731b3a963c58c30e0b2368695d57a7e8fcb7455c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/f5c1d2cae764290766a4c2d1546c1d51de95b67f", - "reference": "f5c1d2cae764290766a4c2d1546c1d51de95b67f", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/731b3a963c58c30e0b2368695d57a7e8fcb7455c", + "reference": "731b3a963c58c30e0b2368695d57a7e8fcb7455c", "shasum": "" }, "require": { @@ -4444,9 +4504,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.3.2" + "source": "https://github.com/utopia-php/migration/tree/1.3.3" }, - "time": "2025-10-22T12:30:47+00:00" + "time": "2025-10-28T04:02:08+00:00" }, { "name": "utopia-php/mongo", @@ -5193,16 +5253,16 @@ }, { "name": "webmozart/assert", - "version": "1.12.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "541057574806f942c94662b817a50f63f7345360" + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/541057574806f942c94662b817a50f63f7345360", - "reference": "541057574806f942c94662b817a50f63f7345360", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", "shasum": "" }, "require": { @@ -5245,9 +5305,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.12.0" + "source": "https://github.com/webmozarts/assert/tree/1.12.1" }, - "time": "2025-10-20T12:43:39+00:00" + "time": "2025-10-29T15:56:20+00:00" }, { "name": "webonyx/graphql-php", @@ -5318,16 +5378,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.4.11", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "5970defc3c6e64817fe9847c0b33c87af71709c5" + "reference": "42df22195d6457e52e4c819678168470b114a816" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/5970defc3c6e64817fe9847c0b33c87af71709c5", - "reference": "5970defc3c6e64817fe9847c0b33c87af71709c5", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/42df22195d6457e52e4c819678168470b114a816", + "reference": "42df22195d6457e52e4c819678168470b114a816", "shasum": "" }, "require": { @@ -5363,9 +5423,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.4.11" + "source": "https://github.com/appwrite/sdk-generator/tree/1.5.0" }, - "time": "2025-10-24T10:03:09+00:00" + "time": "2025-10-31T10:10:25+00:00" }, { "name": "doctrine/annotations", @@ -6069,16 +6129,16 @@ }, { "name": "phpbench/phpbench", - "version": "1.4.1", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/phpbench/phpbench.git", - "reference": "78cd98a9aa34e0f8f80ca01972a8b88d2c30194b" + "reference": "bb61ae6c54b3d58642be154eb09f4e73c3511018" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpbench/phpbench/zipball/78cd98a9aa34e0f8f80ca01972a8b88d2c30194b", - "reference": "78cd98a9aa34e0f8f80ca01972a8b88d2c30194b", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/bb61ae6c54b3d58642be154eb09f4e73c3511018", + "reference": "bb61ae6c54b3d58642be154eb09f4e73c3511018", "shasum": "" }, "require": { @@ -6105,7 +6165,7 @@ "ergebnis/composer-normalize": "^2.39", "friendsofphp/php-cs-fixer": "^3.0", "jangregor/phpstan-prophecy": "^1.0", - "phpspec/prophecy": "dev-master", + "phpspec/prophecy": "^1.22", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", "phpstan/phpstan-phpunit": "^1.0", @@ -6155,7 +6215,7 @@ ], "support": { "issues": "https://github.com/phpbench/phpbench/issues", - "source": "https://github.com/phpbench/phpbench/tree/1.4.1" + "source": "https://github.com/phpbench/phpbench/tree/1.4.2" }, "funding": [ { @@ -6163,7 +6223,7 @@ "type": "github" } ], - "time": "2025-03-12T08:01:40+00:00" + "time": "2025-10-26T14:21:59+00:00" }, { "name": "phpstan/phpstan", @@ -7812,16 +7872,16 @@ }, { "name": "symfony/console", - "version": "v7.3.4", + "version": "v7.3.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db" + "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", - "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", + "url": "https://api.github.com/repos/symfony/console/zipball/cdb80fa5869653c83cfe1a9084a673b6daf57ea7", + "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7", "shasum": "" }, "require": { @@ -7886,7 +7946,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.4" + "source": "https://github.com/symfony/console/tree/v7.3.5" }, "funding": [ { @@ -7906,7 +7966,7 @@ "type": "tidelift" } ], - "time": "2025-09-22T15:31:00+00:00" + "time": "2025-10-14T15:46:26+00:00" }, { "name": "symfony/filesystem", @@ -7980,16 +8040,16 @@ }, { "name": "symfony/finder", - "version": "v7.3.2", + "version": "v7.3.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" + "reference": "9f696d2f1e340484b4683f7853b273abff94421f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", + "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", + "reference": "9f696d2f1e340484b4683f7853b273abff94421f", "shasum": "" }, "require": { @@ -8024,7 +8084,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.2" + "source": "https://github.com/symfony/finder/tree/v7.3.5" }, "funding": [ { @@ -8044,7 +8104,7 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2025-10-15T18:45:57+00:00" }, { "name": "symfony/options-resolver", @@ -8856,5 +8916,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } From 371cab0658fd3136c21e49256402faa215090e23 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 2 Nov 2025 10:59:28 +0200 Subject: [PATCH 08/28] try catch EmailCanonical --- app/controllers/api/account.php | 48 ++++++++++++++++++++++----------- app/controllers/api/teams.php | 12 +++++++++ app/controllers/api/users.php | 12 +++++++++ 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 6c31f28754..e54dd3ef42 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -374,6 +374,13 @@ App::post('/v1/account') $passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0; $password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS); + + try { + $emailCanonical = new EmailCanonical($email); + } catch (Throwable) { + $emailCanonical = null; + } + try { $userId = $userId == 'unique()' ? ID::unique() : $userId; $user->setAttributes([ @@ -402,7 +409,13 @@ App::post('/v1/account') 'authenticators' => null, 'search' => implode(' ', [$userId, $email, $name]), 'accessedAt' => DateTime::now(), + 'emailCanonical' => $emailCanonical?->getCanonical(), + 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), + 'emailIsCorporate' => $emailCanonical?->isCorporate(), + 'emailIsDisposable' => $emailCanonical?->isDisposable(), // todo: fix throw + 'emailIsFree' => $emailCanonical?->isFree(), // todo: fix throw ]); + $user->removeAttribute('$sequence'); $user = Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); try { @@ -1675,17 +1688,18 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') } if (empty($user->getAttribute('email'))) { - $oauth2Email = $oauth2->getUserEmail($accessToken); + $user->setAttribute('email', $oauth2->getUserEmail($accessToken)); - $emailCanonical = new EmailCanonical($oauth2Email); + try { + $emailCanonical = new EmailCanonical($user->getAttribute('email')); - $user - ->setAttribute('email', $oauth2Email) - ->setAttribute('emailCanonical', $emailCanonical->getCanonical()) - ->setAttribute('emailIsCanonical', $emailCanonical->isCanonicalSupported()) - ->setAttribute('emailIsCorporate', $emailCanonical->isCorporate()) - ->setAttribute('emailIsDisposable', $emailCanonical->isDisposable()) - ->setAttribute('emailIsFree', $emailCanonical->isFree()); + $user->setAttribute('emailCanonical', $emailCanonical->getCanonical()); + $user->setAttribute('emailIsCanonical', $emailCanonical->isCanonicalSupported()); + $user->setAttribute('emailIsCorporate', $emailCanonical->isCorporate()); + $user->setAttribute('emailIsDisposable', $emailCanonical->isDisposable()); + $user->setAttribute('emailIsFree', $emailCanonical->isFree()); + } catch (Throwable) { + } } if (empty($user->getAttribute('name'))) { @@ -2230,7 +2244,11 @@ App::post('/v1/account/tokens/email') $userId = $userId === 'unique()' ? ID::unique() : $userId; - $emailCanonical = new EmailCanonical($email); + try { + $emailCanonical = new EmailCanonical($email); + } catch (Throwable) { + $emailCanonical = null; + } $user->setAttributes([ '$id' => $userId, @@ -2254,11 +2272,11 @@ App::post('/v1/account/tokens/email') 'memberships' => null, 'search' => implode(' ', [$userId, $email]), 'accessedAt' => DateTime::now(), - 'emailCanonical' => $emailCanonical->getCanonical(), - 'emailIsCanonical' => $emailCanonical->isCanonicalSupported(), - 'emailIsCorporate' => $emailCanonical->isCorporate(), - 'emailIsDisposable' => $emailCanonical->isDisposable(), - 'emailIsFree' => $emailCanonical->isFree(), + 'emailCanonical' => $emailCanonical?->getCanonical(), + 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), + 'emailIsCorporate' => $emailCanonical?->isCorporate(), + 'emailIsDisposable' => $emailCanonical?->isDisposable(), // todo: fix throw + 'emailIsFree' => $emailCanonical?->isFree(), // todo: fix throw ]); $user->removeAttribute('$sequence'); diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 7398e451b5..cb956390b7 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -48,6 +48,7 @@ use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; +use Utopia\Emails\Email as EmailCanonical; use Utopia\Locale\Locale; use Utopia\System\System; use Utopia\Validator\ArrayList; @@ -564,6 +565,12 @@ App::post('/v1/teams/:teamId/memberships') throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } + try { + $emailCanonical = new EmailCanonical($email); + } catch (Throwable) { + $emailCanonical = null; + } + try { $userId = ID::unique(); $invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([ @@ -596,6 +603,11 @@ App::post('/v1/teams/:teamId/memberships') 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email, $name]), + 'emailCanonical' => $emailCanonical?->getCanonical(), + 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), + 'emailIsCorporate' => $emailCanonical?->isCorporate(), + 'emailIsDisposable' => $emailCanonical?->isDisposable(), // todo: fix throw + 'emailIsFree' => $emailCanonical?->isFree(), // todo: fix throw ]))); } catch (Duplicate $th) { throw new Exception(Exception::USER_ALREADY_EXISTS); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 5498a33bf5..dccfec1101 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -49,6 +49,7 @@ use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; +use Utopia\Emails\Email as EmailCanonical; use Utopia\Locale\Locale; use Utopia\System\System; use Utopia\Validator\ArrayList; @@ -97,6 +98,12 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e } } + try { + $emailCanonical = new EmailCanonical($email ?? ''); + } catch (Throwable) { + $emailCanonical = null; + } + $password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null; $user = new Document([ '$id' => $userId, @@ -124,6 +131,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email, $phone, $name]), + 'emailCanonical' => $emailCanonical?->getCanonical(), + 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), + 'emailIsCorporate' => $emailCanonical?->isCorporate(), + 'emailIsDisposable' => $emailCanonical?->isDisposable(), // todo: fix throw + 'emailIsFree' => $emailCanonical?->isFree(), // todo: fix throw ]); if ($hash === 'plaintext') { From 503145734b8ccf18f5e45b7ca1ac4e3430b8908f Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 2 Nov 2025 11:06:56 +0200 Subject: [PATCH 09/28] Move user Document out of try block --- app/controllers/api/teams.php | 77 ++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index cb956390b7..7fbf4e2d38 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -571,44 +571,47 @@ App::post('/v1/teams/:teamId/memberships') $emailCanonical = null; } + $userId = ID::unique(); + + $userDocument = new Document([ + '$id' => $userId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::read(Role::user($userId)), + Permission::update(Role::user($userId)), + Permission::delete(Role::user($userId)), + ], + 'email' => empty($email) ? null : $email, + 'phone' => empty($phone) ? null : $phone, + 'emailVerification' => false, + 'status' => true, + // TODO: Set password empty? + 'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS), + 'hash' => Auth::DEFAULT_ALGO, + 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS, + /** + * Set the password update time to 0 for users created using + * team invite and OAuth to allow password updates without an + * old password + */ + 'passwordUpdate' => null, + 'registration' => DateTime::now(), + 'reset' => false, + 'name' => $name, + 'prefs' => new \stdClass(), + 'sessions' => null, + 'tokens' => null, + 'memberships' => null, + 'search' => implode(' ', [$userId, $email, $name]), + 'emailCanonical' => $emailCanonical?->getCanonical(), + 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), + 'emailIsCorporate' => $emailCanonical?->isCorporate(), + 'emailIsDisposable' => $emailCanonical?->isDisposable(), // todo: fix throw + 'emailIsFree' => $emailCanonical?->isFree(), // todo: fix throw + ]); + try { - $userId = ID::unique(); - $invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([ - '$id' => $userId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::read(Role::user($userId)), - Permission::update(Role::user($userId)), - Permission::delete(Role::user($userId)), - ], - 'email' => empty($email) ? null : $email, - 'phone' => empty($phone) ? null : $phone, - 'emailVerification' => false, - 'status' => true, - // TODO: Set password empty? - 'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS), - 'hash' => Auth::DEFAULT_ALGO, - 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS, - /** - * Set the password update time to 0 for users created using - * team invite and OAuth to allow password updates without an - * old password - */ - 'passwordUpdate' => null, - 'registration' => DateTime::now(), - 'reset' => false, - 'name' => $name, - 'prefs' => new \stdClass(), - 'sessions' => null, - 'tokens' => null, - 'memberships' => null, - 'search' => implode(' ', [$userId, $email, $name]), - 'emailCanonical' => $emailCanonical?->getCanonical(), - 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), - 'emailIsCorporate' => $emailCanonical?->isCorporate(), - 'emailIsDisposable' => $emailCanonical?->isDisposable(), // todo: fix throw - 'emailIsFree' => $emailCanonical?->isFree(), // todo: fix throw - ]))); + $invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', $userDocument)); } catch (Duplicate $th) { throw new Exception(Exception::USER_ALREADY_EXISTS); } From 6a41f8b2b9d587eb6ae381c88f90faed6be0d41f Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 2 Nov 2025 11:16:12 +0200 Subject: [PATCH 10/28] Remove todos --- app/controllers/api/account.php | 8 ++++---- app/controllers/api/teams.php | 4 ++-- app/controllers/api/users.php | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index e54dd3ef42..00b4c619d7 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -412,8 +412,8 @@ App::post('/v1/account') 'emailCanonical' => $emailCanonical?->getCanonical(), 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), 'emailIsCorporate' => $emailCanonical?->isCorporate(), - 'emailIsDisposable' => $emailCanonical?->isDisposable(), // todo: fix throw - 'emailIsFree' => $emailCanonical?->isFree(), // todo: fix throw + 'emailIsDisposable' => $emailCanonical?->isDisposable(), + 'emailIsFree' => $emailCanonical?->isFree(), ]); $user->removeAttribute('$sequence'); @@ -2275,8 +2275,8 @@ App::post('/v1/account/tokens/email') 'emailCanonical' => $emailCanonical?->getCanonical(), 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), 'emailIsCorporate' => $emailCanonical?->isCorporate(), - 'emailIsDisposable' => $emailCanonical?->isDisposable(), // todo: fix throw - 'emailIsFree' => $emailCanonical?->isFree(), // todo: fix throw + 'emailIsDisposable' => $emailCanonical?->isDisposable(), + 'emailIsFree' => $emailCanonical?->isFree(), ]); $user->removeAttribute('$sequence'); diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 7fbf4e2d38..3a9908ffe3 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -606,8 +606,8 @@ App::post('/v1/teams/:teamId/memberships') 'emailCanonical' => $emailCanonical?->getCanonical(), 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), 'emailIsCorporate' => $emailCanonical?->isCorporate(), - 'emailIsDisposable' => $emailCanonical?->isDisposable(), // todo: fix throw - 'emailIsFree' => $emailCanonical?->isFree(), // todo: fix throw + 'emailIsDisposable' => $emailCanonical?->isDisposable(), + 'emailIsFree' => $emailCanonical?->isFree(), ]); try { diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index dccfec1101..d88148b5a1 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -134,8 +134,8 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e 'emailCanonical' => $emailCanonical?->getCanonical(), 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), 'emailIsCorporate' => $emailCanonical?->isCorporate(), - 'emailIsDisposable' => $emailCanonical?->isDisposable(), // todo: fix throw - 'emailIsFree' => $emailCanonical?->isFree(), // todo: fix throw + 'emailIsDisposable' => $emailCanonical?->isDisposable(), + 'emailIsFree' => $emailCanonical?->isFree(), ]); if ($hash === 'plaintext') { From fe9d49c3465b803769773a20cf4cad97cf0e748d Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 4 Nov 2025 17:13:53 +0200 Subject: [PATCH 11/28] Response Model --- src/Appwrite/Utopia/Response/Model/User.php | 30 +++++++++++++++++++++ tests/e2e/Services/Account/AccountBase.php | 4 ++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Utopia/Response/Model/User.php b/src/Appwrite/Utopia/Response/Model/User.php index 672b8885a0..c9ec5e36bc 100644 --- a/src/Appwrite/Utopia/Response/Model/User.php +++ b/src/Appwrite/Utopia/Response/Model/User.php @@ -139,6 +139,36 @@ class User extends Model 'default' => '', 'example' => self::TYPE_DATETIME_EXAMPLE, ]) + ->addRule('emailCanonical', [ + 'type' => self::TYPE_STRING, + 'description' => 'User email address.', + 'default' => null, + 'example' => 'john@appwrite.io', + ]) + ->addRule('emailIsCanonical', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'User email is canonical.', + 'default' => null, + 'example' => true, + ]) + ->addRule('emailIsCorporate', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'User email is corporate.', + 'default' => null, + 'example' => true, + ]) + ->addRule('emailIsDisposable', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'User email is disposable.', + 'default' => null, + 'example' => true, + ]) + ->addRule('emailIsFree', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'User email is free.', + 'default' => null, + 'example' => true, + ]) ; } diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index b2f85637a8..d51632c6c9 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -29,7 +29,7 @@ trait AccountBase ]); $id = $response['body']['$id']; - + var_dump($response['body']); $this->assertEquals(201, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); $this->assertNotEmpty($response['body']['$id']); @@ -41,6 +41,8 @@ trait AccountBase $this->assertNotEmpty($response['body']['accessedAt']); $this->assertArrayHasKey('targets', $response['body']); $this->assertEquals($email, $response['body']['targets'][0]['identifier']); + $this->assertEquals('shmuel', 'fogel'); + /** * Test for FAILURE From 1f0e3e0f51bd8f166e50ae2aa8386077ba67099a Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 5 Nov 2025 13:21:52 +0200 Subject: [PATCH 12/28] lock file --- composer.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/composer.lock b/composer.lock index 394e2a9e3e..c950ec5492 100644 --- a/composer.lock +++ b/composer.lock @@ -3840,16 +3840,16 @@ }, { "name": "utopia-php/database", - "version": "3.1.2", + "version": "3.1.3", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "b6541a9cd9b21786a5020327f582838afdb159aa" + "reference": "b38cc9887a8fefedcb9a962168dd6f28b7082fc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/b6541a9cd9b21786a5020327f582838afdb159aa", - "reference": "b6541a9cd9b21786a5020327f582838afdb159aa", + "url": "https://api.github.com/repos/utopia-php/database/zipball/b38cc9887a8fefedcb9a962168dd6f28b7082fc1", + "reference": "b38cc9887a8fefedcb9a962168dd6f28b7082fc1", "shasum": "" }, "require": { @@ -3892,9 +3892,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/3.1.2" + "source": "https://github.com/utopia-php/database/tree/3.1.3" }, - "time": "2025-10-30T13:10:13+00:00" + "time": "2025-11-04T11:41:54+00:00" }, { "name": "utopia-php/detector", @@ -5378,16 +5378,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.5.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "42df22195d6457e52e4c819678168470b114a816" + "reference": "cd712674e34136f706e9170641ed6f4ce160e772" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/42df22195d6457e52e4c819678168470b114a816", - "reference": "42df22195d6457e52e4c819678168470b114a816", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/cd712674e34136f706e9170641ed6f4ce160e772", + "reference": "cd712674e34136f706e9170641ed6f4ce160e772", "shasum": "" }, "require": { @@ -5423,9 +5423,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.5.0" + "source": "https://github.com/appwrite/sdk-generator/tree/1.5.1" }, - "time": "2025-10-31T10:10:25+00:00" + "time": "2025-11-04T09:55:47+00:00" }, { "name": "doctrine/annotations", From 7c408d675ea258f202eac6d9090447f912afde64 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 7 Nov 2025 14:30:22 +0530 Subject: [PATCH 13/28] pr followups --- app/config/collections/projects.php | 2 +- src/Appwrite/Migration/Version/V23.php | 49 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index c7205c667d..dae0337dc9 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -2345,7 +2345,7 @@ return [ '$id' => ID::custom('errors'), 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 131070, + 'size' => 1_000_000, 'signed' => true, 'required' => true, 'default' => null, diff --git a/src/Appwrite/Migration/Version/V23.php b/src/Appwrite/Migration/Version/V23.php index f9fd63346f..a4027b506e 100644 --- a/src/Appwrite/Migration/Version/V23.php +++ b/src/Appwrite/Migration/Version/V23.php @@ -133,6 +133,13 @@ class V23 extends Migration } $this->dbForProject->purgeCachedCollection($id); break; + case 'migrations': + try { + $this->updateMigrateErrorSize(); + } catch (\Throwable $th) { + Console::warning("Failed to migration error attribute size in collection {$id}: {$th->getMessage()}"); + } + default: break; } @@ -202,4 +209,46 @@ class V23 extends Migration } return $document; } + + /** + * Update migration attribute size + * @return void + */ + private function updateMigrateErrorSize(): void + { + + if ($this->project->getId() === 'console') { + return; + } + + // Read-modify-write from the live schema to avoid overwriting unrelated changes. + $migration = $this->dbForProject->getCollection('migrations'); + $attributes = $migration->getAttribute('attributes', []); + $attrsArray = \array_map(fn (Document $doc) => $doc->getArrayCopy(), $attributes); + $errorsIdx = \array_search('errors', \array_column($attrsArray, '$id')); + + if ($errorsIdx === false) { + Console::warning("Skipping: 'errors' attribute not found in migrations collection for project {$this->project->getId()}"); + return; + } + + $desiredSize = 1_000_000; + $migrationAttributes = Config::getParam('collections', [])['projects']['migrations']['attributes'] ?? []; + $migrationIndex = \array_search('errors', \array_column($migrationAttributes, '$id')); + + if ($migrationIndex !== false && isset($migrationAttributes[$migrationIndex]['size'])) { + $desiredSize = (int) $migrationAttributes[$migrationIndex]['size']; + } + + $currentSize = (int) ($attributes[$errorsIdx]['size'] ?? 0); + + if ($currentSize === $desiredSize) { + Console::warning("Skipping: 'errors' attribute already of desired size {$desiredSize} in migrations collection for project {$this->project->getId()}"); + return; + } + $attributes[$errorsIdx]['size'] = $desiredSize; + $migration->setAttribute('attributes', $attributes); + $this->dbForProject->updateDocument($migration->getCollection(), $migration->getId(), $migration); + $this->dbForProject->purgeCachedCollection('migrations'); + } } From b06460b5626a45ef7f2fd4ae31a521e3e0cbf14b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 10 Nov 2025 14:19:26 +0530 Subject: [PATCH 14/28] chore: release cli and php --- app/config/platforms.php | 4 +- composer.lock | 136 +++++++++--------- .../examples/avatars/get-screenshot.md | 34 +++++ docs/sdks/cli/CHANGELOG.md | 4 + docs/sdks/php/CHANGELOG.md | 6 + 5 files changed, 117 insertions(+), 67 deletions(-) create mode 100644 docs/examples/1.8.x/server-php/examples/avatars/get-screenshot.md diff --git a/app/config/platforms.php b/app/config/platforms.php index 361ec6b935..c60274294b 100644 --- a/app/config/platforms.php +++ b/app/config/platforms.php @@ -226,7 +226,7 @@ return [ [ 'key' => 'cli', 'name' => 'Command Line', - 'version' => '11.1.0', + 'version' => '11.1.1', 'url' => 'https://github.com/appwrite/sdk-for-cli', 'package' => 'https://www.npmjs.com/package/appwrite-cli', 'enabled' => true, @@ -281,7 +281,7 @@ return [ [ 'key' => 'php', 'name' => 'PHP', - 'version' => '17.5.0', + 'version' => '17.6.0', 'url' => 'https://github.com/appwrite/sdk-for-php', 'package' => 'https://packagist.org/packages/appwrite/appwrite', 'enabled' => true, diff --git a/composer.lock b/composer.lock index 87eaf28a3e..bff2111d85 100644 --- a/composer.lock +++ b/composer.lock @@ -2673,16 +2673,16 @@ }, { "name": "symfony/http-client", - "version": "v7.3.4", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62" + "reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/4b62871a01c49457cf2a8e560af7ee8a94b87a62", - "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62", + "url": "https://api.github.com/repos/symfony/http-client/zipball/3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de", + "reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de", "shasum": "" }, "require": { @@ -2749,7 +2749,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.4" + "source": "https://github.com/symfony/http-client/tree/v7.3.6" }, "funding": [ { @@ -2769,7 +2769,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-11-05T17:41:46+00:00" }, { "name": "symfony/http-client-contracts", @@ -3176,16 +3176,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -3239,7 +3239,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -3250,12 +3250,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "tbachert/spi", @@ -3840,16 +3844,16 @@ }, { "name": "utopia-php/database", - "version": "3.1.5", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "76568b81f25d89fc1e0c53f0370f139130eeb939" + "reference": "f2d01b6b38057891184f62107bf70a55bc2ea068" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/76568b81f25d89fc1e0c53f0370f139130eeb939", - "reference": "76568b81f25d89fc1e0c53f0370f139130eeb939", + "url": "https://api.github.com/repos/utopia-php/database/zipball/f2d01b6b38057891184f62107bf70a55bc2ea068", + "reference": "f2d01b6b38057891184f62107bf70a55bc2ea068", "shasum": "" }, "require": { @@ -3892,9 +3896,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/3.1.5" + "source": "https://github.com/utopia-php/database/tree/3.2.0" }, - "time": "2025-11-05T10:17:55+00:00" + "time": "2025-11-06T05:41:54+00:00" }, { "name": "utopia-php/detector", @@ -3943,22 +3947,24 @@ }, { "name": "utopia-php/dns", - "version": "1.1.0", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/utopia-php/dns.git", - "reference": "d6eca184883262bdcb4261e57491c91b16079b9a" + "reference": "1e6b4bac735329c9e5ec69a6a5d899ec2d050707" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/dns/zipball/d6eca184883262bdcb4261e57491c91b16079b9a", - "reference": "d6eca184883262bdcb4261e57491c91b16079b9a", + "url": "https://api.github.com/repos/utopia-php/dns/zipball/1e6b4bac735329c9e5ec69a6a5d899ec2d050707", + "reference": "1e6b4bac735329c9e5ec69a6a5d899ec2d050707", "shasum": "" }, "require": { "php": ">=8.3", "utopia-php/console": "0.0.*", - "utopia-php/telemetry": "0.1.*" + "utopia-php/domains": "0.9.*", + "utopia-php/telemetry": "0.1.*", + "utopia-php/validators": "^0.0.2" }, "require-dev": { "laravel/pint": "1.25.*", @@ -3992,9 +3998,9 @@ ], "support": { "issues": "https://github.com/utopia-php/dns/issues", - "source": "https://github.com/utopia-php/dns/tree/1.1.0" + "source": "https://github.com/utopia-php/dns/tree/1.1.3" }, - "time": "2025-11-03T22:49:02+00:00" + "time": "2025-11-06T19:08:29+00:00" }, { "name": "utopia-php/domains", @@ -5377,16 +5383,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.5.1", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "cd712674e34136f706e9170641ed6f4ce160e772" + "reference": "7a34a2c31266408838400cf1b263671dc6b4637a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/cd712674e34136f706e9170641ed6f4ce160e772", - "reference": "cd712674e34136f706e9170641ed6f4ce160e772", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/7a34a2c31266408838400cf1b263671dc6b4637a", + "reference": "7a34a2c31266408838400cf1b263671dc6b4637a", "shasum": "" }, "require": { @@ -5422,9 +5428,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.5.1" + "source": "https://github.com/appwrite/sdk-generator/tree/1.5.2" }, - "time": "2025-11-04T09:55:47+00:00" + "time": "2025-11-10T07:56:53+00:00" }, { "name": "doctrine/annotations", @@ -6077,24 +6083,24 @@ }, { "name": "phpbench/container", - "version": "2.2.2", + "version": "2.2.3", "source": { "type": "git", "url": "https://github.com/phpbench/container.git", - "reference": "a59b929e00b87b532ca6d0edd8eca0967655af33" + "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpbench/container/zipball/a59b929e00b87b532ca6d0edd8eca0967655af33", - "reference": "a59b929e00b87b532ca6d0edd8eca0967655af33", + "url": "https://api.github.com/repos/phpbench/container/zipball/0c7b2d36c1ea53fe27302fb8873ded7172047196", + "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196", "shasum": "" }, "require": { "psr/container": "^1.0|^2.0", - "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0 || ^7.0" + "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.16", + "php-cs-fixer/shim": "^3.89", "phpstan/phpstan": "^0.12.52", "phpunit/phpunit": "^8" }, @@ -6122,22 +6128,22 @@ "description": "Simple, configurable, service container.", "support": { "issues": "https://github.com/phpbench/container/issues", - "source": "https://github.com/phpbench/container/tree/2.2.2" + "source": "https://github.com/phpbench/container/tree/2.2.3" }, - "time": "2023-10-30T13:38:26+00:00" + "time": "2025-11-06T09:05:13+00:00" }, { "name": "phpbench/phpbench", - "version": "1.4.2", + "version": "1.4.3", "source": { "type": "git", "url": "https://github.com/phpbench/phpbench.git", - "reference": "bb61ae6c54b3d58642be154eb09f4e73c3511018" + "reference": "b641dde59d969ea42eed70a39f9b51950bc96878" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpbench/phpbench/zipball/bb61ae6c54b3d58642be154eb09f4e73c3511018", - "reference": "bb61ae6c54b3d58642be154eb09f4e73c3511018", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/b641dde59d969ea42eed70a39f9b51950bc96878", + "reference": "b641dde59d969ea42eed70a39f9b51950bc96878", "shasum": "" }, "require": { @@ -6152,26 +6158,26 @@ "phpbench/container": "^2.2", "psr/log": "^1.1 || ^2.0 || ^3.0", "seld/jsonlint": "^1.1", - "symfony/console": "^6.1 || ^7.0", - "symfony/filesystem": "^6.1 || ^7.0", - "symfony/finder": "^6.1 || ^7.0", - "symfony/options-resolver": "^6.1 || ^7.0", - "symfony/process": "^6.1 || ^7.0", + "symfony/console": "^6.1 || ^7.0 || ^8.0", + "symfony/filesystem": "^6.1 || ^7.0 || ^8.0", + "symfony/finder": "^6.1 || ^7.0 || ^8.0", + "symfony/options-resolver": "^6.1 || ^7.0 || ^8.0", + "symfony/process": "^6.1 || ^7.0 || ^8.0", "webmozart/glob": "^4.6" }, "require-dev": { "dantleech/invoke": "^2.0", "ergebnis/composer-normalize": "^2.39", - "friendsofphp/php-cs-fixer": "^3.0", "jangregor/phpstan-prophecy": "^1.0", + "php-cs-fixer/shim": "^3.9", "phpspec/prophecy": "^1.22", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^10.4 || ^11.0", "rector/rector": "^1.2", - "symfony/error-handler": "^6.1 || ^7.0", - "symfony/var-dumper": "^6.1 || ^7.0" + "symfony/error-handler": "^6.1 || ^7.0 || ^8.0", + "symfony/var-dumper": "^6.1 || ^7.0 || ^8.0" }, "suggest": { "ext-xdebug": "For Xdebug profiling extension." @@ -6214,7 +6220,7 @@ ], "support": { "issues": "https://github.com/phpbench/phpbench/issues", - "source": "https://github.com/phpbench/phpbench/tree/1.4.2" + "source": "https://github.com/phpbench/phpbench/tree/1.4.3" }, "funding": [ { @@ -6222,7 +6228,7 @@ "type": "github" } ], - "time": "2025-10-26T14:21:59+00:00" + "time": "2025-11-06T19:07:31+00:00" }, { "name": "phpstan/phpstan", @@ -7871,16 +7877,16 @@ }, { "name": "symfony/console", - "version": "v7.3.5", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7" + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cdb80fa5869653c83cfe1a9084a673b6daf57ea7", - "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7", + "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", "shasum": "" }, "require": { @@ -7945,7 +7951,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.5" + "source": "https://github.com/symfony/console/tree/v7.3.6" }, "funding": [ { @@ -7965,20 +7971,20 @@ "type": "tidelift" } ], - "time": "2025-10-14T15:46:26+00:00" + "time": "2025-11-04T01:21:42+00:00" }, { "name": "symfony/filesystem", - "version": "v7.3.2", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" + "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", - "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a", + "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a", "shasum": "" }, "require": { @@ -8015,7 +8021,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.2" + "source": "https://github.com/symfony/filesystem/tree/v7.3.6" }, "funding": [ { @@ -8035,7 +8041,7 @@ "type": "tidelift" } ], - "time": "2025-07-07T08:17:47+00:00" + "time": "2025-11-05T09:52:27+00:00" }, { "name": "symfony/finder", diff --git a/docs/examples/1.8.x/server-php/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-php/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..6d416ebad3 --- /dev/null +++ b/docs/examples/1.8.x/server-php/examples/avatars/get-screenshot.md @@ -0,0 +1,34 @@ +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setSession(''); // The user session to authenticate with + +$avatars = new Avatars($client); + +$result = $avatars->getScreenshot( + url: 'https://example.com', + headers: [], // optional + viewportWidth: 1, // optional + viewportHeight: 1, // optional + scale: 0.1, // optional + theme: ::LIGHT(), // optional + userAgent: '', // optional + fullpage: false, // optional + locale: '', // optional + timezone: ::AFRICAABIDJAN(), // optional + latitude: -90, // optional + longitude: -180, // optional + accuracy: 0, // optional + touch: false, // optional + permissions: [], // optional + sleep: 0, // optional + width: 0, // optional + height: 0, // optional + quality: -1, // optional + output: ::JPG() // optional +); \ No newline at end of file diff --git a/docs/sdks/cli/CHANGELOG.md b/docs/sdks/cli/CHANGELOG.md index ac1624401c..8e50441769 100644 --- a/docs/sdks/cli/CHANGELOG.md +++ b/docs/sdks/cli/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 11.1.1 + +* Fix duplicate `enums` during type generation by prefixing them with table name. For example, `enum MyEnum` will now be generated as `enum MyTableMyEnum` to avoid conflicts. + ## 11.1.0 * Add `total` parameter to list queries allowing skipping counting rows in a table for improved performance diff --git a/docs/sdks/php/CHANGELOG.md b/docs/sdks/php/CHANGELOG.md index 6e8d4d7545..1b74b0ee5a 100644 --- a/docs/sdks/php/CHANGELOG.md +++ b/docs/sdks/php/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 17.6.0 + +* Fix duplicate methods issue (e.g., `updateMFA` and `updateMfa`) causing build and runtime errors +* Add support for `getScreenshot` method to `Avatars` service +* Add `Output`, `Theme` and `Timezone` enums + ## 17.5.0 * Add `total` parameter to list queries allowing skipping counting rows in a table for improved performance From 2ff0ae61c847384f3190af4350dafc6d9bbef700 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 10 Nov 2025 15:00:09 +0530 Subject: [PATCH 15/28] update php --- app/config/platforms.php | 2 +- docs/sdks/php/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/platforms.php b/app/config/platforms.php index c60274294b..e183ab1bcb 100644 --- a/app/config/platforms.php +++ b/app/config/platforms.php @@ -281,7 +281,7 @@ return [ [ 'key' => 'php', 'name' => 'PHP', - 'version' => '17.6.0', + 'version' => '18.0.0', 'url' => 'https://github.com/appwrite/sdk-for-php', 'package' => 'https://packagist.org/packages/appwrite/appwrite', 'enabled' => true, diff --git a/docs/sdks/php/CHANGELOG.md b/docs/sdks/php/CHANGELOG.md index 1b74b0ee5a..8727bf6b97 100644 --- a/docs/sdks/php/CHANGELOG.md +++ b/docs/sdks/php/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## 17.6.0 +## 18.0.0 * Fix duplicate methods issue (e.g., `updateMFA` and `updateMfa`) causing build and runtime errors * Add support for `getScreenshot` method to `Avatars` service From 4a148e6ef9313a014b04c1741b374c5f52e40a90 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 10 Nov 2025 15:06:22 +0530 Subject: [PATCH 16/28] remove broken execute --- src/Appwrite/Platform/Tasks/SDKs.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index cf4f107e8e..f587e0f946 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -259,8 +259,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } if ($createRelease) { - Console::execute('git config --global user.email "$GIT_EMAIL"', stdin: '', stdout: '', stderr: ''); - $releaseVersion = $language['version']; $repoName = $language['gitUserName'] . '/' . $language['gitRepoName']; From 4dc700927ef20e460fca262caddf03c73603c8ae Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 10 Nov 2025 15:39:54 +0530 Subject: [PATCH 17/28] fix: examples --- composer.lock | 12 ++++++------ .../server-php/examples/avatars/get-screenshot.md | 9 ++++++--- .../databases/create-relationship-attribute.md | 1 + .../databases/update-relationship-attribute.md | 1 + .../examples/functions/create-execution.md | 1 + .../1.8.x/server-php/examples/functions/create.md | 4 ++-- .../examples/functions/get-deployment-download.md | 1 + .../1.8.x/server-php/examples/functions/update.md | 3 ++- .../server-php/examples/health/get-failed-jobs.md | 4 ++-- .../server-php/examples/messaging/create-push.md | 1 + .../examples/messaging/create-smtp-provider.md | 1 + .../server-php/examples/messaging/update-push.md | 1 + .../examples/messaging/update-smtp-provider.md | 1 + .../1.8.x/server-php/examples/sites/create.md | 11 ++++++----- .../examples/sites/get-deployment-download.md | 1 + .../1.8.x/server-php/examples/sites/update.md | 10 ++++++---- .../server-php/examples/storage/create-bucket.md | 3 ++- .../server-php/examples/storage/get-file-preview.md | 2 ++ .../server-php/examples/storage/update-bucket.md | 3 ++- .../examples/tablesdb/create-relationship-column.md | 1 + .../examples/tablesdb/update-relationship-column.md | 1 + .../server-php/examples/users/create-sha-user.md | 1 + 22 files changed, 48 insertions(+), 25 deletions(-) diff --git a/composer.lock b/composer.lock index bff2111d85..fe245d7837 100644 --- a/composer.lock +++ b/composer.lock @@ -5383,16 +5383,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.5.2", + "version": "1.5.3", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "7a34a2c31266408838400cf1b263671dc6b4637a" + "reference": "1a7a3b89147aa8c1bde5247f8eeb7e4832c6016d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/7a34a2c31266408838400cf1b263671dc6b4637a", - "reference": "7a34a2c31266408838400cf1b263671dc6b4637a", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/1a7a3b89147aa8c1bde5247f8eeb7e4832c6016d", + "reference": "1a7a3b89147aa8c1bde5247f8eeb7e4832c6016d", "shasum": "" }, "require": { @@ -5428,9 +5428,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.5.2" + "source": "https://github.com/appwrite/sdk-generator/tree/1.5.3" }, - "time": "2025-11-10T07:56:53+00:00" + "time": "2025-11-10T09:50:41+00:00" }, { "name": "doctrine/annotations", diff --git a/docs/examples/1.8.x/server-php/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-php/examples/avatars/get-screenshot.md index 6d416ebad3..b9dfd23862 100644 --- a/docs/examples/1.8.x/server-php/examples/avatars/get-screenshot.md +++ b/docs/examples/1.8.x/server-php/examples/avatars/get-screenshot.md @@ -2,6 +2,9 @@ use Appwrite\Client; use Appwrite\Services\Avatars; +use Appwrite\Enums\Theme; +use Appwrite\Enums\Timezone; +use Appwrite\Enums\Output; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint @@ -16,11 +19,11 @@ $result = $avatars->getScreenshot( viewportWidth: 1, // optional viewportHeight: 1, // optional scale: 0.1, // optional - theme: ::LIGHT(), // optional + theme: Theme::LIGHT(), // optional userAgent: '', // optional fullpage: false, // optional locale: '', // optional - timezone: ::AFRICAABIDJAN(), // optional + timezone: Timezone::AFRICAABIDJAN(), // optional latitude: -90, // optional longitude: -180, // optional accuracy: 0, // optional @@ -30,5 +33,5 @@ $result = $avatars->getScreenshot( width: 0, // optional height: 0, // optional quality: -1, // optional - output: ::JPG() // optional + output: Output::JPG() // optional ); \ No newline at end of file diff --git a/docs/examples/1.8.x/server-php/examples/databases/create-relationship-attribute.md b/docs/examples/1.8.x/server-php/examples/databases/create-relationship-attribute.md index caccd36031..551fe17a9d 100644 --- a/docs/examples/1.8.x/server-php/examples/databases/create-relationship-attribute.md +++ b/docs/examples/1.8.x/server-php/examples/databases/create-relationship-attribute.md @@ -3,6 +3,7 @@ use Appwrite\Client; use Appwrite\Services\Databases; use Appwrite\Enums\RelationshipType; +use Appwrite\Enums\RelationMutate; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint diff --git a/docs/examples/1.8.x/server-php/examples/databases/update-relationship-attribute.md b/docs/examples/1.8.x/server-php/examples/databases/update-relationship-attribute.md index 01783cf3bf..a4d6888711 100644 --- a/docs/examples/1.8.x/server-php/examples/databases/update-relationship-attribute.md +++ b/docs/examples/1.8.x/server-php/examples/databases/update-relationship-attribute.md @@ -2,6 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Databases; +use Appwrite\Enums\RelationMutate; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint diff --git a/docs/examples/1.8.x/server-php/examples/functions/create-execution.md b/docs/examples/1.8.x/server-php/examples/functions/create-execution.md index cd11b5ea6e..9c12e87374 100644 --- a/docs/examples/1.8.x/server-php/examples/functions/create-execution.md +++ b/docs/examples/1.8.x/server-php/examples/functions/create-execution.md @@ -2,6 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Functions; +use Appwrite\Enums\ExecutionMethod; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint diff --git a/docs/examples/1.8.x/server-php/examples/functions/create.md b/docs/examples/1.8.x/server-php/examples/functions/create.md index 3d37b8068e..f7176871bd 100644 --- a/docs/examples/1.8.x/server-php/examples/functions/create.md +++ b/docs/examples/1.8.x/server-php/examples/functions/create.md @@ -2,7 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Functions; -use Appwrite\Enums\; +use Appwrite\Enums\Runtime; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint @@ -14,7 +14,7 @@ $functions = new Functions($client); $result = $functions->create( functionId: '', name: '', - runtime: ::NODE145(), + runtime: Runtime::NODE145(), execute: ["any"], // optional events: [], // optional schedule: '', // optional diff --git a/docs/examples/1.8.x/server-php/examples/functions/get-deployment-download.md b/docs/examples/1.8.x/server-php/examples/functions/get-deployment-download.md index 7b3e18751e..a06f97b662 100644 --- a/docs/examples/1.8.x/server-php/examples/functions/get-deployment-download.md +++ b/docs/examples/1.8.x/server-php/examples/functions/get-deployment-download.md @@ -2,6 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Functions; +use Appwrite\Enums\DeploymentDownloadType; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint diff --git a/docs/examples/1.8.x/server-php/examples/functions/update.md b/docs/examples/1.8.x/server-php/examples/functions/update.md index ea8d863ae5..da5ee88931 100644 --- a/docs/examples/1.8.x/server-php/examples/functions/update.md +++ b/docs/examples/1.8.x/server-php/examples/functions/update.md @@ -2,6 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Functions; +use Appwrite\Enums\Runtime; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint @@ -13,7 +14,7 @@ $functions = new Functions($client); $result = $functions->update( functionId: '', name: '', - runtime: ::NODE145(), // optional + runtime: Runtime::NODE145(), // optional execute: ["any"], // optional events: [], // optional schedule: '', // optional diff --git a/docs/examples/1.8.x/server-php/examples/health/get-failed-jobs.md b/docs/examples/1.8.x/server-php/examples/health/get-failed-jobs.md index 02959db3b5..63bc1c83f2 100644 --- a/docs/examples/1.8.x/server-php/examples/health/get-failed-jobs.md +++ b/docs/examples/1.8.x/server-php/examples/health/get-failed-jobs.md @@ -2,7 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Health; -use Appwrite\Enums\; +use Appwrite\Enums\Name; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint @@ -12,6 +12,6 @@ $client = (new Client()) $health = new Health($client); $result = $health->getFailedJobs( - name: ::V1DATABASE(), + name: Name::V1DATABASE(), threshold: null // optional ); \ No newline at end of file diff --git a/docs/examples/1.8.x/server-php/examples/messaging/create-push.md b/docs/examples/1.8.x/server-php/examples/messaging/create-push.md index 51fc0d0a92..614c758c80 100644 --- a/docs/examples/1.8.x/server-php/examples/messaging/create-push.md +++ b/docs/examples/1.8.x/server-php/examples/messaging/create-push.md @@ -2,6 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Messaging; +use Appwrite\Enums\MessagePriority; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint diff --git a/docs/examples/1.8.x/server-php/examples/messaging/create-smtp-provider.md b/docs/examples/1.8.x/server-php/examples/messaging/create-smtp-provider.md index 017f20cc15..953bbcf44f 100644 --- a/docs/examples/1.8.x/server-php/examples/messaging/create-smtp-provider.md +++ b/docs/examples/1.8.x/server-php/examples/messaging/create-smtp-provider.md @@ -2,6 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Messaging; +use Appwrite\Enums\SmtpEncryption; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint diff --git a/docs/examples/1.8.x/server-php/examples/messaging/update-push.md b/docs/examples/1.8.x/server-php/examples/messaging/update-push.md index 05a51783c9..0fea9a135f 100644 --- a/docs/examples/1.8.x/server-php/examples/messaging/update-push.md +++ b/docs/examples/1.8.x/server-php/examples/messaging/update-push.md @@ -2,6 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Messaging; +use Appwrite\Enums\MessagePriority; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint diff --git a/docs/examples/1.8.x/server-php/examples/messaging/update-smtp-provider.md b/docs/examples/1.8.x/server-php/examples/messaging/update-smtp-provider.md index 3bc80d2789..495f332131 100644 --- a/docs/examples/1.8.x/server-php/examples/messaging/update-smtp-provider.md +++ b/docs/examples/1.8.x/server-php/examples/messaging/update-smtp-provider.md @@ -2,6 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Messaging; +use Appwrite\Enums\SmtpEncryption; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint diff --git a/docs/examples/1.8.x/server-php/examples/sites/create.md b/docs/examples/1.8.x/server-php/examples/sites/create.md index 4a1c3a4fcb..6f1fc5ac27 100644 --- a/docs/examples/1.8.x/server-php/examples/sites/create.md +++ b/docs/examples/1.8.x/server-php/examples/sites/create.md @@ -2,8 +2,9 @@ use Appwrite\Client; use Appwrite\Services\Sites; -use Appwrite\Enums\; -use Appwrite\Enums\; +use Appwrite\Enums\Framework; +use Appwrite\Enums\BuildRuntime; +use Appwrite\Enums\Adapter; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint @@ -15,15 +16,15 @@ $sites = new Sites($client); $result = $sites->create( siteId: '', name: '', - framework: ::ANALOG(), - buildRuntime: ::NODE145(), + framework: Framework::ANALOG(), + buildRuntime: BuildRuntime::NODE145(), enabled: false, // optional logging: false, // optional timeout: 1, // optional installCommand: '', // optional buildCommand: '', // optional outputDirectory: '', // optional - adapter: ::STATIC(), // optional + adapter: Adapter::STATIC(), // optional installationId: '', // optional fallbackFile: '', // optional providerRepositoryId: '', // optional diff --git a/docs/examples/1.8.x/server-php/examples/sites/get-deployment-download.md b/docs/examples/1.8.x/server-php/examples/sites/get-deployment-download.md index 91c6b6e52a..61fad0bd74 100644 --- a/docs/examples/1.8.x/server-php/examples/sites/get-deployment-download.md +++ b/docs/examples/1.8.x/server-php/examples/sites/get-deployment-download.md @@ -2,6 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Sites; +use Appwrite\Enums\DeploymentDownloadType; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint diff --git a/docs/examples/1.8.x/server-php/examples/sites/update.md b/docs/examples/1.8.x/server-php/examples/sites/update.md index f2ca54a987..d2a6c9d256 100644 --- a/docs/examples/1.8.x/server-php/examples/sites/update.md +++ b/docs/examples/1.8.x/server-php/examples/sites/update.md @@ -2,7 +2,9 @@ use Appwrite\Client; use Appwrite\Services\Sites; -use Appwrite\Enums\; +use Appwrite\Enums\Framework; +use Appwrite\Enums\BuildRuntime; +use Appwrite\Enums\Adapter; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint @@ -14,15 +16,15 @@ $sites = new Sites($client); $result = $sites->update( siteId: '', name: '', - framework: ::ANALOG(), + framework: Framework::ANALOG(), enabled: false, // optional logging: false, // optional timeout: 1, // optional installCommand: '', // optional buildCommand: '', // optional outputDirectory: '', // optional - buildRuntime: ::NODE145(), // optional - adapter: ::STATIC(), // optional + buildRuntime: BuildRuntime::NODE145(), // optional + adapter: Adapter::STATIC(), // optional fallbackFile: '', // optional installationId: '', // optional providerRepositoryId: '', // optional diff --git a/docs/examples/1.8.x/server-php/examples/storage/create-bucket.md b/docs/examples/1.8.x/server-php/examples/storage/create-bucket.md index 2e7cc1d15c..3d4f717e4d 100644 --- a/docs/examples/1.8.x/server-php/examples/storage/create-bucket.md +++ b/docs/examples/1.8.x/server-php/examples/storage/create-bucket.md @@ -2,6 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Storage; +use Appwrite\Enums\Compression; use Appwrite\Permission; use Appwrite\Role; @@ -20,7 +21,7 @@ $result = $storage->createBucket( enabled: false, // optional maximumFileSize: 1, // optional allowedFileExtensions: [], // optional - compression: ::NONE(), // optional + compression: Compression::NONE(), // optional encryption: false, // optional antivirus: false // optional ); \ No newline at end of file diff --git a/docs/examples/1.8.x/server-php/examples/storage/get-file-preview.md b/docs/examples/1.8.x/server-php/examples/storage/get-file-preview.md index 0b65fc326a..aaa15a22fb 100644 --- a/docs/examples/1.8.x/server-php/examples/storage/get-file-preview.md +++ b/docs/examples/1.8.x/server-php/examples/storage/get-file-preview.md @@ -2,6 +2,8 @@ use Appwrite\Client; use Appwrite\Services\Storage; +use Appwrite\Enums\ImageGravity; +use Appwrite\Enums\ImageFormat; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint diff --git a/docs/examples/1.8.x/server-php/examples/storage/update-bucket.md b/docs/examples/1.8.x/server-php/examples/storage/update-bucket.md index 819798cb95..77f4262c2d 100644 --- a/docs/examples/1.8.x/server-php/examples/storage/update-bucket.md +++ b/docs/examples/1.8.x/server-php/examples/storage/update-bucket.md @@ -2,6 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Storage; +use Appwrite\Enums\Compression; use Appwrite\Permission; use Appwrite\Role; @@ -20,7 +21,7 @@ $result = $storage->updateBucket( enabled: false, // optional maximumFileSize: 1, // optional allowedFileExtensions: [], // optional - compression: ::NONE(), // optional + compression: Compression::NONE(), // optional encryption: false, // optional antivirus: false // optional ); \ No newline at end of file diff --git a/docs/examples/1.8.x/server-php/examples/tablesdb/create-relationship-column.md b/docs/examples/1.8.x/server-php/examples/tablesdb/create-relationship-column.md index 031d1fd1aa..7f9a06cc03 100644 --- a/docs/examples/1.8.x/server-php/examples/tablesdb/create-relationship-column.md +++ b/docs/examples/1.8.x/server-php/examples/tablesdb/create-relationship-column.md @@ -3,6 +3,7 @@ use Appwrite\Client; use Appwrite\Services\TablesDB; use Appwrite\Enums\RelationshipType; +use Appwrite\Enums\RelationMutate; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint diff --git a/docs/examples/1.8.x/server-php/examples/tablesdb/update-relationship-column.md b/docs/examples/1.8.x/server-php/examples/tablesdb/update-relationship-column.md index 834dc18cee..cc2e2ccaef 100644 --- a/docs/examples/1.8.x/server-php/examples/tablesdb/update-relationship-column.md +++ b/docs/examples/1.8.x/server-php/examples/tablesdb/update-relationship-column.md @@ -2,6 +2,7 @@ use Appwrite\Client; use Appwrite\Services\TablesDB; +use Appwrite\Enums\RelationMutate; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint diff --git a/docs/examples/1.8.x/server-php/examples/users/create-sha-user.md b/docs/examples/1.8.x/server-php/examples/users/create-sha-user.md index 0b9a27ed8e..812bcd5eb5 100644 --- a/docs/examples/1.8.x/server-php/examples/users/create-sha-user.md +++ b/docs/examples/1.8.x/server-php/examples/users/create-sha-user.md @@ -2,6 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Users; +use Appwrite\Enums\PasswordHash; $client = (new Client()) ->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint From 057397197bbc5ebed5663e25862f50f8394389bd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 10 Nov 2025 18:20:31 +0530 Subject: [PATCH 18/28] update to 18.0.1 --- app/config/platforms.php | 2 +- docs/sdks/php/CHANGELOG.md | 4 ++++ src/Appwrite/Platform/Tasks/SDKs.php | 17 +++++++++++------ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/config/platforms.php b/app/config/platforms.php index e183ab1bcb..5d72a8914c 100644 --- a/app/config/platforms.php +++ b/app/config/platforms.php @@ -281,7 +281,7 @@ return [ [ 'key' => 'php', 'name' => 'PHP', - 'version' => '18.0.0', + 'version' => '18.0.1', 'url' => 'https://github.com/appwrite/sdk-for-php', 'package' => 'https://packagist.org/packages/appwrite/appwrite', 'enabled' => true, diff --git a/docs/sdks/php/CHANGELOG.md b/docs/sdks/php/CHANGELOG.md index 8727bf6b97..14a26e441d 100644 --- a/docs/sdks/php/CHANGELOG.md +++ b/docs/sdks/php/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 18.0.1 + +* Fix `TablesDB` service to use correct file name + ## 18.0.0 * Fix duplicate methods issue (e.g., `updateMFA` and `updateMfa`) causing build and runtime errors diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index f587e0f946..d3c605655f 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -427,16 +427,21 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND mkdir -p ' . $target . ' && \ cd ' . $target . ' && \ git init && \ + git config core.ignorecase false && \ + git config pull.rebase false && \ git remote add origin ' . $gitUrl . ' && \ git fetch origin && \ - git checkout ' . $repoBranch . ' || git checkout -b ' . $repoBranch . ' && \ + (git checkout -f ' . $repoBranch . ' 2>/dev/null || git checkout -b ' . $repoBranch . ') && \ git pull origin ' . $repoBranch . ' && \ - git checkout ' . $gitBranch . ' || git checkout -b ' . $gitBranch . ' && \ - git fetch origin ' . $gitBranch . ' || git push -u origin ' . $gitBranch . ' && \ - git pull origin ' . $gitBranch . ' && \ - find . -mindepth 1 ! -path "./.git*" -delete && \ + (git checkout -f ' . $gitBranch . ' 2>/dev/null || git checkout -b ' . $gitBranch . ') && \ + (git fetch origin ' . $gitBranch . ' 2>/dev/null || git push -u origin ' . $gitBranch . ') && \ + git reset --hard origin/' . $gitBranch . ' 2>/dev/null || true && \ + (test -d .github && cp -r .github /tmp/.github-backup-$$ || true) && \ + git rm -rf --cached . && \ + git clean -fdx -e .git -e .github && \ cp -r ' . $result . '/. ' . $target . '/ && \ - git add . && \ + (test -d /tmp/.github-backup-$$ && cp -r /tmp/.github-backup-$$/.github . && rm -rf /tmp/.github-backup-$$ || true) && \ + git add -A && \ git commit -m "' . $message . '" && \ git push -u origin ' . $gitBranch . ' '); From 5afae248aead81a96410d1b06ba0a73e2108ffd9 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 10 Nov 2025 15:00:18 +0200 Subject: [PATCH 19/28] Test Update --- composer.json | 1 - tests/e2e/Services/Account/AccountBase.php | 9 ++++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 9694c2dc51..62abce9ca2 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,6 @@ "utopia-php/framework": "0.33.*", "utopia-php/fetch": "0.4.*", "utopia-php/image": "0.8.*", - "utopia-php/emails": "0.6.*", "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index d51632c6c9..9f35932700 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -29,7 +29,7 @@ trait AccountBase ]); $id = $response['body']['$id']; - var_dump($response['body']); + $this->assertEquals(201, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); $this->assertNotEmpty($response['body']['$id']); @@ -41,8 +41,11 @@ trait AccountBase $this->assertNotEmpty($response['body']['accessedAt']); $this->assertArrayHasKey('targets', $response['body']); $this->assertEquals($email, $response['body']['targets'][0]['identifier']); - $this->assertEquals('shmuel', 'fogel'); - + $this->assertArrayNotHasKey('emailCanonical', $response['body']); + $this->assertArrayNotHasKey('emailIsFree', $response['body']); + $this->assertArrayNotHasKey('emailIsDisposable', $response['body']); + $this->assertArrayNotHasKey('emailIsCorporate', $response['body']); + $this->assertArrayNotHasKey('emailIsCanonical', $response['body']); /** * Test for FAILURE From 6096c42ff9fa9fd80b4a4dee355fe925ec1b1e82 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 10 Nov 2025 17:30:40 +0200 Subject: [PATCH 20/28] More updates --- app/controllers/api/account.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 737f5c7016..41d746e7dc 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1612,6 +1612,12 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $failureRedirect(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } + try { + $emailCanonical = new EmailCanonical($email); + } catch (Throwable) { + $emailCanonical = null; + } + try { $userId = ID::unique(); $user->setAttributes([ @@ -1639,7 +1645,13 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'authenticators' => null, 'search' => implode(' ', [$userId, $email, $name]), 'accessedAt' => DateTime::now(), + 'emailCanonical' => $emailCanonical?->getCanonical(), + 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), + 'emailIsCorporate' => $emailCanonical?->isCorporate(), + 'emailIsDisposable' => $emailCanonical?->isDisposable(), + 'emailIsFree' => $emailCanonical?->isFree(), ]); + $user->removeAttribute('$sequence'); $userDoc = Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); $dbForProject->createDocument('targets', new Document([ @@ -2015,6 +2027,12 @@ App::post('/v1/account/tokens/magic-url') $userId = $userId === 'unique()' ? ID::unique() : $userId; + try { + $emailCanonical = new EmailCanonical($email); + } catch (Throwable) { + $emailCanonical = null; + } + $user->setAttributes([ '$id' => $userId, '$permissions' => [ @@ -2039,6 +2057,11 @@ App::post('/v1/account/tokens/magic-url') 'authenticators' => null, 'search' => implode(' ', [$userId, $email]), 'accessedAt' => DateTime::now(), + 'emailCanonical' => $emailCanonical?->getCanonical(), + 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), + 'emailIsCorporate' => $emailCanonical?->isCorporate(), + 'emailIsDisposable' => $emailCanonical?->isDisposable(), + 'emailIsFree' => $emailCanonical?->isFree(), ]); $user->removeAttribute('$sequence'); @@ -2645,6 +2668,11 @@ App::post('/v1/account/tokens/phone') 'memberships' => null, 'search' => implode(' ', [$userId, $phone]), 'accessedAt' => DateTime::now(), + 'emailCanonical' => null, + 'emailIsCanonical' => null, + 'emailIsCorporate' => null, + 'emailIsDisposable' => null, + 'emailIsFree' => null, ]); $user->removeAttribute('$sequence'); From ce7f40dd34b4641e493d76c88f451e787952247c Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 11 Nov 2025 08:16:09 +0200 Subject: [PATCH 21/28] Revert response model --- src/Appwrite/Utopia/Response/Model/User.php | 30 --------------------- 1 file changed, 30 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/User.php b/src/Appwrite/Utopia/Response/Model/User.php index c9ec5e36bc..672b8885a0 100644 --- a/src/Appwrite/Utopia/Response/Model/User.php +++ b/src/Appwrite/Utopia/Response/Model/User.php @@ -139,36 +139,6 @@ class User extends Model 'default' => '', 'example' => self::TYPE_DATETIME_EXAMPLE, ]) - ->addRule('emailCanonical', [ - 'type' => self::TYPE_STRING, - 'description' => 'User email address.', - 'default' => null, - 'example' => 'john@appwrite.io', - ]) - ->addRule('emailIsCanonical', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'User email is canonical.', - 'default' => null, - 'example' => true, - ]) - ->addRule('emailIsCorporate', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'User email is corporate.', - 'default' => null, - 'example' => true, - ]) - ->addRule('emailIsDisposable', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'User email is disposable.', - 'default' => null, - 'example' => true, - ]) - ->addRule('emailIsFree', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'User email is free.', - 'default' => null, - 'example' => true, - ]) ; } From b28e53696766487a3c0b079801a3d3ef433671d8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 11 Nov 2025 10:35:39 +0200 Subject: [PATCH 22/28] Add try catch --- app/controllers/api/account.php | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 41d746e7dc..11824ec066 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1725,14 +1725,15 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') try { $emailCanonical = new EmailCanonical($user->getAttribute('email')); - - $user->setAttribute('emailCanonical', $emailCanonical->getCanonical()); - $user->setAttribute('emailIsCanonical', $emailCanonical->isCanonicalSupported()); - $user->setAttribute('emailIsCorporate', $emailCanonical->isCorporate()); - $user->setAttribute('emailIsDisposable', $emailCanonical->isDisposable()); - $user->setAttribute('emailIsFree', $emailCanonical->isFree()); } catch (Throwable) { + $emailCanonical = null; } + + $user->setAttribute('emailCanonical', $emailCanonical?->getCanonical()); + $user->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported()); + $user->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate()); + $user->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable()); + $user->setAttribute('emailIsFree', $emailCanonical?->isFree()); } if (empty($user->getAttribute('name'))) { @@ -3136,16 +3137,20 @@ App::patch('/v1/account/email') throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } - $emailCanonical = new EmailCanonical($email); + try { + $emailCanonical = new EmailCanonical($email); + } catch (Throwable) { + $emailCanonical = null; + } $user ->setAttribute('email', $email) ->setAttribute('emailVerification', false) // After this user needs to confirm mail again - ->setAttribute('emailCanonical', $emailCanonical->getCanonical()) - ->setAttribute('emailIsCanonical', $emailCanonical->isCanonicalSupported()) - ->setAttribute('emailIsCorporate', $emailCanonical->isCorporate()) - ->setAttribute('emailIsDisposable', $emailCanonical->isDisposable()) - ->setAttribute('emailIsFree', $emailCanonical->isFree()) + ->setAttribute('emailCanonical', $emailCanonical?->getCanonical()) + ->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported()) + ->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate()) + ->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable()) + ->setAttribute('emailIsFree', $emailCanonical?->isFree()) ; if (empty($passwordUpdate)) { From cc97d78b5a0eeb12d625f399f08116f2e755e016 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 11 Nov 2025 11:35:18 +0200 Subject: [PATCH 23/28] Add try catch --- app/controllers/api/users.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index f32d394d48..0efb355d46 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -99,7 +99,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e } try { - $emailCanonical = new EmailCanonical($email ?? ''); + $emailCanonical = new EmailCanonical($email); } catch (Throwable) { $emailCanonical = null; } From 27d0c511acea05096a0702dce9373a2b4af2ac7a Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 11 Nov 2025 12:09:40 +0200 Subject: [PATCH 24/28] patch users email --- app/controllers/api/users.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 0efb355d46..db08709054 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -1449,9 +1449,20 @@ App::patch('/v1/users/:userId/email') $oldEmail = $user->getAttribute('email'); + try { + $emailCanonical = new EmailCanonical($email); + } catch (Throwable) { + $emailCanonical = null; + } + $user ->setAttribute('email', $email) ->setAttribute('emailVerification', false) + ->setAttribute('emailCanonical', $emailCanonical?->getCanonical()) + ->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported()) + ->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate()) + ->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable()) + ->setAttribute('emailIsFree', $emailCanonical?->isFree()) ; try { From da0a6b167fc376a0aa8bbd475d3d5be0e9872bbe Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 11 Nov 2025 15:00:09 +0200 Subject: [PATCH 25/28] EmailValidator --- app/controllers/api/account.php | 86 ++++++++++++++++----------------- app/controllers/api/teams.php | 16 +++--- app/controllers/api/users.php | 30 ++++++------ 3 files changed, 66 insertions(+), 66 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 11824ec066..258479b10c 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -57,7 +57,7 @@ use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; -use Utopia\Emails\Email as EmailCanonical; +use Utopia\Emails\Email as EmailValidator; use Utopia\Locale\Locale; use Utopia\Storage\Validator\FileName; use Utopia\System\System; @@ -397,9 +397,9 @@ App::post('/v1/account') $password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS); try { - $emailCanonical = new EmailCanonical($email); + $emailValidator = new EmailValidator($email); } catch (Throwable) { - $emailCanonical = null; + $emailValidator = null; } try { @@ -430,11 +430,11 @@ App::post('/v1/account') 'authenticators' => null, 'search' => implode(' ', [$userId, $email, $name]), 'accessedAt' => DateTime::now(), - 'emailCanonical' => $emailCanonical?->getCanonical(), - 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), - 'emailIsCorporate' => $emailCanonical?->isCorporate(), - 'emailIsDisposable' => $emailCanonical?->isDisposable(), - 'emailIsFree' => $emailCanonical?->isFree(), + 'emailCanonical' => $emailValidator?->getCanonical(), + 'emailIsCanonical' => $emailValidator?->isCanonicalSupported(), + 'emailIsCorporate' => $emailValidator?->isCorporate(), + 'emailIsDisposable' => $emailValidator?->isDisposable(), + 'emailIsFree' => $emailValidator?->isFree(), ]); $user->removeAttribute('$sequence'); @@ -1613,9 +1613,9 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') } try { - $emailCanonical = new EmailCanonical($email); + $emailValidator = new EmailValidator($email); } catch (Throwable) { - $emailCanonical = null; + $emailValidator = null; } try { @@ -1645,11 +1645,11 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'authenticators' => null, 'search' => implode(' ', [$userId, $email, $name]), 'accessedAt' => DateTime::now(), - 'emailCanonical' => $emailCanonical?->getCanonical(), - 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), - 'emailIsCorporate' => $emailCanonical?->isCorporate(), - 'emailIsDisposable' => $emailCanonical?->isDisposable(), - 'emailIsFree' => $emailCanonical?->isFree(), + 'emailCanonical' => $emailValidator?->getCanonical(), + 'emailIsCanonical' => $emailValidator?->isCanonicalSupported(), + 'emailIsCorporate' => $emailValidator?->isCorporate(), + 'emailIsDisposable' => $emailValidator?->isDisposable(), + 'emailIsFree' => $emailValidator?->isFree(), ]); $user->removeAttribute('$sequence'); @@ -1724,16 +1724,16 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $user->setAttribute('email', $oauth2->getUserEmail($accessToken)); try { - $emailCanonical = new EmailCanonical($user->getAttribute('email')); + $emailValidator = new EmailValidator($user->getAttribute('email')); } catch (Throwable) { - $emailCanonical = null; + $emailValidator = null; } - $user->setAttribute('emailCanonical', $emailCanonical?->getCanonical()); - $user->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported()); - $user->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate()); - $user->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable()); - $user->setAttribute('emailIsFree', $emailCanonical?->isFree()); + $user->setAttribute('emailCanonical', $emailValidator?->getCanonical()); + $user->setAttribute('emailIsCanonical', $emailValidator?->isCanonicalSupported()); + $user->setAttribute('emailIsCorporate', $emailValidator?->isCorporate()); + $user->setAttribute('emailIsDisposable', $emailValidator?->isDisposable()); + $user->setAttribute('emailIsFree', $emailValidator?->isFree()); } if (empty($user->getAttribute('name'))) { @@ -2029,9 +2029,9 @@ App::post('/v1/account/tokens/magic-url') $userId = $userId === 'unique()' ? ID::unique() : $userId; try { - $emailCanonical = new EmailCanonical($email); + $emailValidator = new EmailValidator($email); } catch (Throwable) { - $emailCanonical = null; + $emailValidator = null; } $user->setAttributes([ @@ -2058,11 +2058,11 @@ App::post('/v1/account/tokens/magic-url') 'authenticators' => null, 'search' => implode(' ', [$userId, $email]), 'accessedAt' => DateTime::now(), - 'emailCanonical' => $emailCanonical?->getCanonical(), - 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), - 'emailIsCorporate' => $emailCanonical?->isCorporate(), - 'emailIsDisposable' => $emailCanonical?->isDisposable(), - 'emailIsFree' => $emailCanonical?->isFree(), + 'emailCanonical' => $emailValidator?->getCanonical(), + 'emailIsCanonical' => $emailValidator?->isCanonicalSupported(), + 'emailIsCorporate' => $emailValidator?->isCorporate(), + 'emailIsDisposable' => $emailValidator?->isDisposable(), + 'emailIsFree' => $emailValidator?->isFree(), ]); $user->removeAttribute('$sequence'); @@ -2290,9 +2290,9 @@ App::post('/v1/account/tokens/email') $userId = $userId === 'unique()' ? ID::unique() : $userId; try { - $emailCanonical = new EmailCanonical($email); + $emailValidator = new EmailValidator($email); } catch (Throwable) { - $emailCanonical = null; + $emailValidator = null; } $user->setAttributes([ @@ -2317,11 +2317,11 @@ App::post('/v1/account/tokens/email') 'memberships' => null, 'search' => implode(' ', [$userId, $email]), 'accessedAt' => DateTime::now(), - 'emailCanonical' => $emailCanonical?->getCanonical(), - 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), - 'emailIsCorporate' => $emailCanonical?->isCorporate(), - 'emailIsDisposable' => $emailCanonical?->isDisposable(), - 'emailIsFree' => $emailCanonical?->isFree(), + 'emailCanonical' => $emailValidator?->getCanonical(), + 'emailIsCanonical' => $emailValidator?->isCanonicalSupported(), + 'emailIsCorporate' => $emailValidator?->isCorporate(), + 'emailIsDisposable' => $emailValidator?->isDisposable(), + 'emailIsFree' => $emailValidator?->isFree(), ]); $user->removeAttribute('$sequence'); @@ -3138,19 +3138,19 @@ App::patch('/v1/account/email') } try { - $emailCanonical = new EmailCanonical($email); + $emailValidator = new EmailValidator($email); } catch (Throwable) { - $emailCanonical = null; + $emailValidator = null; } $user ->setAttribute('email', $email) ->setAttribute('emailVerification', false) // After this user needs to confirm mail again - ->setAttribute('emailCanonical', $emailCanonical?->getCanonical()) - ->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported()) - ->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate()) - ->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable()) - ->setAttribute('emailIsFree', $emailCanonical?->isFree()) + ->setAttribute('emailCanonical', $emailValidator?->getCanonical()) + ->setAttribute('emailIsCanonical', $emailValidator?->isCanonicalSupported()) + ->setAttribute('emailIsCorporate', $emailValidator?->isCorporate()) + ->setAttribute('emailIsDisposable', $emailValidator?->isDisposable()) + ->setAttribute('emailIsFree', $emailValidator?->isFree()) ; if (empty($passwordUpdate)) { diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index cbf1be4052..85120abe29 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -48,7 +48,7 @@ use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; -use Utopia\Emails\Email as EmailCanonical; +use Utopia\Emails\Email as EmailValidator; use Utopia\Locale\Locale; use Utopia\System\System; use Utopia\Validator\ArrayList; @@ -568,9 +568,9 @@ App::post('/v1/teams/:teamId/memberships') } try { - $emailCanonical = new EmailCanonical($email); + $emailValidator = new EmailValidator($email); } catch (Throwable) { - $emailCanonical = null; + $emailValidator = null; } $userId = ID::unique(); @@ -605,11 +605,11 @@ App::post('/v1/teams/:teamId/memberships') 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email, $name]), - 'emailCanonical' => $emailCanonical?->getCanonical(), - 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), - 'emailIsCorporate' => $emailCanonical?->isCorporate(), - 'emailIsDisposable' => $emailCanonical?->isDisposable(), - 'emailIsFree' => $emailCanonical?->isFree(), + 'emailCanonical' => $emailValidator?->getCanonical(), + 'emailIsCanonical' => $emailValidator?->isCanonicalSupported(), + 'emailIsCorporate' => $emailValidator?->isCorporate(), + 'emailIsDisposable' => $emailValidator?->isDisposable(), + 'emailIsFree' => $emailValidator?->isFree(), ]); try { diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index db08709054..75504e581b 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -49,7 +49,7 @@ use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; -use Utopia\Emails\Email as EmailCanonical; +use Utopia\Emails\Email as EmailValidator; use Utopia\Locale\Locale; use Utopia\System\System; use Utopia\Validator\ArrayList; @@ -99,9 +99,9 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e } try { - $emailCanonical = new EmailCanonical($email); + $emailValidator = new EmailValidator($email); } catch (Throwable) { - $emailCanonical = null; + $emailValidator = null; } $password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null; @@ -131,11 +131,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email, $phone, $name]), - 'emailCanonical' => $emailCanonical?->getCanonical(), - 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), - 'emailIsCorporate' => $emailCanonical?->isCorporate(), - 'emailIsDisposable' => $emailCanonical?->isDisposable(), - 'emailIsFree' => $emailCanonical?->isFree(), + 'emailCanonical' => $emailValidator?->getCanonical(), + 'emailIsCanonical' => $emailValidator?->isCanonicalSupported(), + 'emailIsCorporate' => $emailValidator?->isCorporate(), + 'emailIsDisposable' => $emailValidator?->isDisposable(), + 'emailIsFree' => $emailValidator?->isFree(), ]); if ($hash === 'plaintext') { @@ -1450,19 +1450,19 @@ App::patch('/v1/users/:userId/email') $oldEmail = $user->getAttribute('email'); try { - $emailCanonical = new EmailCanonical($email); + $emailValidator = new EmailValidator($email); } catch (Throwable) { - $emailCanonical = null; + $emailValidator = null; } $user ->setAttribute('email', $email) ->setAttribute('emailVerification', false) - ->setAttribute('emailCanonical', $emailCanonical?->getCanonical()) - ->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported()) - ->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate()) - ->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable()) - ->setAttribute('emailIsFree', $emailCanonical?->isFree()) + ->setAttribute('emailCanonical', $emailValidator?->getCanonical()) + ->setAttribute('emailIsCanonical', $emailValidator?->isCanonicalSupported()) + ->setAttribute('emailIsCorporate', $emailValidator?->isCorporate()) + ->setAttribute('emailIsDisposable', $emailValidator?->isDisposable()) + ->setAttribute('emailIsFree', $emailValidator?->isFree()) ; try { From 8b4a56cd9b829c9bb7c0e07fbe6736570f569126 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 11 Nov 2025 15:25:10 +0200 Subject: [PATCH 26/28] $emailCanonical --- app/controllers/api/account.php | 100 ++++++++++++++++---------------- app/controllers/api/teams.php | 20 +++---- app/controllers/api/users.php | 54 ++++++++--------- 3 files changed, 87 insertions(+), 87 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 258479b10c..b7959bb6a9 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -20,7 +20,7 @@ use Appwrite\Event\Messaging; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; -use Appwrite\Network\Validator\Email; +use Appwrite\Network\Validator\Email as EmailValidator; use Appwrite\Network\Validator\Redirect; use Appwrite\OpenSSL\OpenSSL; use Appwrite\SDK\AuthType; @@ -57,7 +57,7 @@ use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; -use Utopia\Emails\Email as EmailValidator; +use Utopia\Emails\Email; use Utopia\Locale\Locale; use Utopia\Storage\Validator\FileName; use Utopia\System\System; @@ -338,7 +338,7 @@ App::post('/v1/account') )) ->label('abuse-limit', 10) ->param('userId', '', new CustomId(), 'User 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('email', '', new EmailValidator(), 'User email.') ->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') @@ -397,9 +397,9 @@ App::post('/v1/account') $password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS); try { - $emailValidator = new EmailValidator($email); + $emailCanonical = new Email($email); } catch (Throwable) { - $emailValidator = null; + $emailCanonical = null; } try { @@ -430,11 +430,11 @@ App::post('/v1/account') 'authenticators' => null, 'search' => implode(' ', [$userId, $email, $name]), 'accessedAt' => DateTime::now(), - 'emailCanonical' => $emailValidator?->getCanonical(), - 'emailIsCanonical' => $emailValidator?->isCanonicalSupported(), - 'emailIsCorporate' => $emailValidator?->isCorporate(), - 'emailIsDisposable' => $emailValidator?->isDisposable(), - 'emailIsFree' => $emailValidator?->isFree(), + 'emailCanonical' => $emailCanonical?->getCanonical(), + 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), + 'emailIsCorporate' => $emailCanonical?->isCorporate(), + 'emailIsDisposable' => $emailCanonical?->isDisposable(), + 'emailIsFree' => $emailCanonical?->isFree(), ]); $user->removeAttribute('$sequence'); @@ -917,7 +917,7 @@ App::post('/v1/account/sessions/email') )) ->label('abuse-limit', 10) ->label('abuse-key', 'url:{url},email:{param-email}') - ->param('email', '', new Email(), 'User email.') + ->param('email', '', new EmailValidator(), 'User email.') ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') ->inject('request') ->inject('response') @@ -1613,9 +1613,9 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') } try { - $emailValidator = new EmailValidator($email); + $emailCanonical = new Email($email); } catch (Throwable) { - $emailValidator = null; + $emailCanonical = null; } try { @@ -1645,11 +1645,11 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'authenticators' => null, 'search' => implode(' ', [$userId, $email, $name]), 'accessedAt' => DateTime::now(), - 'emailCanonical' => $emailValidator?->getCanonical(), - 'emailIsCanonical' => $emailValidator?->isCanonicalSupported(), - 'emailIsCorporate' => $emailValidator?->isCorporate(), - 'emailIsDisposable' => $emailValidator?->isDisposable(), - 'emailIsFree' => $emailValidator?->isFree(), + 'emailCanonical' => $emailCanonical?->getCanonical(), + 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), + 'emailIsCorporate' => $emailCanonical?->isCorporate(), + 'emailIsDisposable' => $emailCanonical?->isDisposable(), + 'emailIsFree' => $emailCanonical?->isFree(), ]); $user->removeAttribute('$sequence'); @@ -1724,16 +1724,16 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $user->setAttribute('email', $oauth2->getUserEmail($accessToken)); try { - $emailValidator = new EmailValidator($user->getAttribute('email')); + $emailCanonical = new Email($user->getAttribute('email')); } catch (Throwable) { - $emailValidator = null; + $emailCanonical = null; } - $user->setAttribute('emailCanonical', $emailValidator?->getCanonical()); - $user->setAttribute('emailIsCanonical', $emailValidator?->isCanonicalSupported()); - $user->setAttribute('emailIsCorporate', $emailValidator?->isCorporate()); - $user->setAttribute('emailIsDisposable', $emailValidator?->isDisposable()); - $user->setAttribute('emailIsFree', $emailValidator?->isFree()); + $user->setAttribute('emailCanonical', $emailCanonical?->getCanonical()); + $user->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported()); + $user->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate()); + $user->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable()); + $user->setAttribute('emailIsFree', $emailCanonical?->isFree()); } if (empty($user->getAttribute('name'))) { @@ -1982,7 +1982,7 @@ App::post('/v1/account/tokens/magic-url') ->label('abuse-limit', 60) ->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}']) ->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. If the email address has never been used, a new account is created using the provided userId. Otherwise, if the email address is already attached to an account, the user ID is ignored.') - ->param('email', '', new Email(), 'User email.') + ->param('email', '', new EmailValidator(), 'User email.') ->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey']) ->param('phrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.', true) ->inject('request') @@ -2029,9 +2029,9 @@ App::post('/v1/account/tokens/magic-url') $userId = $userId === 'unique()' ? ID::unique() : $userId; try { - $emailValidator = new EmailValidator($email); + $emailCanonical = new Email($email); } catch (Throwable) { - $emailValidator = null; + $emailCanonical = null; } $user->setAttributes([ @@ -2058,11 +2058,11 @@ App::post('/v1/account/tokens/magic-url') 'authenticators' => null, 'search' => implode(' ', [$userId, $email]), 'accessedAt' => DateTime::now(), - 'emailCanonical' => $emailValidator?->getCanonical(), - 'emailIsCanonical' => $emailValidator?->isCanonicalSupported(), - 'emailIsCorporate' => $emailValidator?->isCorporate(), - 'emailIsDisposable' => $emailValidator?->isDisposable(), - 'emailIsFree' => $emailValidator?->isFree(), + 'emailCanonical' => $emailCanonical?->getCanonical(), + 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), + 'emailIsCorporate' => $emailCanonical?->isCorporate(), + 'emailIsDisposable' => $emailCanonical?->isDisposable(), + 'emailIsFree' => $emailCanonical?->isFree(), ]); $user->removeAttribute('$sequence'); @@ -2246,7 +2246,7 @@ App::post('/v1/account/tokens/email') ->label('abuse-limit', 10) ->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}']) ->param('userId', '', new CustomId(), 'User 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. If the email address has never been used, a new account is created using the provided userId. Otherwise, if the email address is already attached to an account, the user ID is ignored.') - ->param('email', '', new Email(), 'User email.') + ->param('email', '', new EmailValidator(), 'User email.') ->param('phrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.', true) ->inject('request') ->inject('response') @@ -2290,9 +2290,9 @@ App::post('/v1/account/tokens/email') $userId = $userId === 'unique()' ? ID::unique() : $userId; try { - $emailValidator = new EmailValidator($email); + $emailCanonical = new Email($email); } catch (Throwable) { - $emailValidator = null; + $emailCanonical = null; } $user->setAttributes([ @@ -2317,11 +2317,11 @@ App::post('/v1/account/tokens/email') 'memberships' => null, 'search' => implode(' ', [$userId, $email]), 'accessedAt' => DateTime::now(), - 'emailCanonical' => $emailValidator?->getCanonical(), - 'emailIsCanonical' => $emailValidator?->isCanonicalSupported(), - 'emailIsCorporate' => $emailValidator?->isCorporate(), - 'emailIsDisposable' => $emailValidator?->isDisposable(), - 'emailIsFree' => $emailValidator?->isFree(), + 'emailCanonical' => $emailCanonical?->getCanonical(), + 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), + 'emailIsCorporate' => $emailCanonical?->isCorporate(), + 'emailIsDisposable' => $emailCanonical?->isDisposable(), + 'emailIsFree' => $emailCanonical?->isFree(), ]); $user->removeAttribute('$sequence'); @@ -3102,7 +3102,7 @@ App::patch('/v1/account/email') ], contentType: ContentType::JSON )) - ->param('email', '', new Email(), 'User email.') + ->param('email', '', new EmailValidator(), 'User email.') ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') ->inject('requestTimestamp') ->inject('response') @@ -3138,19 +3138,19 @@ App::patch('/v1/account/email') } try { - $emailValidator = new EmailValidator($email); + $emailCanonical = new Email($email); } catch (Throwable) { - $emailValidator = null; + $emailCanonical = null; } $user ->setAttribute('email', $email) ->setAttribute('emailVerification', false) // After this user needs to confirm mail again - ->setAttribute('emailCanonical', $emailValidator?->getCanonical()) - ->setAttribute('emailIsCanonical', $emailValidator?->isCanonicalSupported()) - ->setAttribute('emailIsCorporate', $emailValidator?->isCorporate()) - ->setAttribute('emailIsDisposable', $emailValidator?->isDisposable()) - ->setAttribute('emailIsFree', $emailValidator?->isFree()) + ->setAttribute('emailCanonical', $emailCanonical?->getCanonical()) + ->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported()) + ->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate()) + ->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable()) + ->setAttribute('emailIsFree', $emailCanonical?->isFree()) ; if (empty($passwordUpdate)) { @@ -3387,7 +3387,7 @@ App::post('/v1/account/recovery') )) ->label('abuse-limit', 10) ->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}']) - ->param('email', '', new Email(), 'User email.') + ->param('email', '', new EmailValidator(), 'User email.') ->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['platforms', 'devKey']) ->inject('request') ->inject('response') diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 85120abe29..554ef6f4fe 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -10,7 +10,7 @@ use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; -use Appwrite\Network\Validator\Email; +use Appwrite\Network\Validator\Email as EmailValidator; use Appwrite\Network\Validator\Redirect; use Appwrite\Platform\Workers\Deletes; use Appwrite\SDK\AuthType; @@ -48,7 +48,7 @@ use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; -use Utopia\Emails\Email as EmailValidator; +use Utopia\Emails\Email; use Utopia\Locale\Locale; use Utopia\System\System; use Utopia\Validator\ArrayList; @@ -469,7 +469,7 @@ App::post('/v1/teams/:teamId/memberships') )) ->label('abuse-limit', 10) ->param('teamId', '', new UID(), 'Team ID.') - ->param('email', '', new Email(), 'Email of the new team member.', true) + ->param('email', '', new EmailValidator(), 'Email of the new team member.', true) ->param('userId', '', new UID(), 'ID of the user to be added to a team.', true) ->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) ->param('roles', [], function (Document $project) { @@ -568,9 +568,9 @@ App::post('/v1/teams/:teamId/memberships') } try { - $emailValidator = new EmailValidator($email); + $emailCanonical = new Email($email); } catch (Throwable) { - $emailValidator = null; + $emailCanonical = null; } $userId = ID::unique(); @@ -605,11 +605,11 @@ App::post('/v1/teams/:teamId/memberships') 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email, $name]), - 'emailCanonical' => $emailValidator?->getCanonical(), - 'emailIsCanonical' => $emailValidator?->isCanonicalSupported(), - 'emailIsCorporate' => $emailValidator?->isCorporate(), - 'emailIsDisposable' => $emailValidator?->isDisposable(), - 'emailIsFree' => $emailValidator?->isFree(), + 'emailCanonical' => $emailCanonical?->getCanonical(), + 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), + 'emailIsCorporate' => $emailCanonical?->isCorporate(), + 'emailIsDisposable' => $emailCanonical?->isDisposable(), + 'emailIsFree' => $emailCanonical?->isFree(), ]); try { diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 75504e581b..956c9da042 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -16,7 +16,7 @@ use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; -use Appwrite\Network\Validator\Email; +use Appwrite\Network\Validator\Email as EmailValidator; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; @@ -49,7 +49,7 @@ use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; -use Utopia\Emails\Email as EmailValidator; +use Utopia\Emails\Email; use Utopia\Locale\Locale; use Utopia\System\System; use Utopia\Validator\ArrayList; @@ -99,9 +99,9 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e } try { - $emailValidator = new EmailValidator($email); + $emailCanonical = new Email($email); } catch (Throwable) { - $emailValidator = null; + $emailCanonical = null; } $password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null; @@ -131,11 +131,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email, $phone, $name]), - 'emailCanonical' => $emailValidator?->getCanonical(), - 'emailIsCanonical' => $emailValidator?->isCanonicalSupported(), - 'emailIsCorporate' => $emailValidator?->isCorporate(), - 'emailIsDisposable' => $emailValidator?->isDisposable(), - 'emailIsFree' => $emailValidator?->isFree(), + 'emailCanonical' => $emailCanonical?->getCanonical(), + 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(), + 'emailIsCorporate' => $emailCanonical?->isCorporate(), + 'emailIsDisposable' => $emailCanonical?->isDisposable(), + 'emailIsFree' => $emailCanonical?->isFree(), ]); if ($hash === 'plaintext') { @@ -220,7 +220,7 @@ App::post('/v1/users') ] )) ->param('userId', '', new CustomId(), 'User 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', null, new Email(), 'User email.', true) + ->param('email', null, new EmailValidator(), 'User email.', true) ->param('phone', null, new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'Plain text user password. Must be at least 8 chars.', true, ['project', 'passwordsDictionary']) ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) @@ -255,7 +255,7 @@ App::post('/v1/users/bcrypt') ] )) ->param('userId', '', new CustomId(), 'User 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('email', '', new EmailValidator(), 'User email.') ->param('password', '', new Password(), 'User password hashed using Bcrypt.') ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) ->inject('response') @@ -290,7 +290,7 @@ App::post('/v1/users/md5') ] )) ->param('userId', '', new CustomId(), 'User 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('email', '', new EmailValidator(), 'User email.') ->param('password', '', new Password(), 'User password hashed using MD5.') ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) ->inject('response') @@ -325,7 +325,7 @@ App::post('/v1/users/argon2') ] )) ->param('userId', '', new CustomId(), 'User 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('email', '', new EmailValidator(), 'User email.') ->param('password', '', new Password(), 'User password hashed using Argon2.') ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) ->inject('response') @@ -360,7 +360,7 @@ App::post('/v1/users/sha') ] )) ->param('userId', '', new CustomId(), 'User 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('email', '', new EmailValidator(), 'User email.') ->param('password', '', new Password(), 'User password hashed using SHA.') ->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true) ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) @@ -402,7 +402,7 @@ App::post('/v1/users/phpass') ] )) ->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or pass the string `ID.unique()`to auto generate it. 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('email', '', new EmailValidator(), 'User email.') ->param('password', '', new Password(), 'User password hashed using PHPass.') ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) ->inject('response') @@ -437,7 +437,7 @@ App::post('/v1/users/scrypt') ] )) ->param('userId', '', new CustomId(), 'User 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('email', '', new EmailValidator(), 'User email.') ->param('password', '', new Password(), 'User password hashed using Scrypt.') ->param('passwordSalt', '', new Text(128), 'Optional salt used to hash password.') ->param('passwordCpu', 8, new Integer(), 'Optional CPU cost used to hash password.') @@ -485,7 +485,7 @@ App::post('/v1/users/scrypt-modified') ] )) ->param('userId', '', new CustomId(), 'User 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('email', '', new EmailValidator(), 'User email.') ->param('password', '', new Password(), 'User password hashed using Scrypt Modified.') ->param('passwordSalt', '', new Text(128), 'Salt used to hash password.') ->param('passwordSaltSeparator', '', new Text(128), 'Salt separator used to hash password.') @@ -539,7 +539,7 @@ App::post('/v1/users/:userId/targets') switch ($providerType) { case 'email': - $validator = new Email(); + $validator = new EmailValidator(); if (!$validator->isValid($identifier)) { throw new Exception(Exception::GENERAL_INVALID_EMAIL); } @@ -1414,7 +1414,7 @@ App::patch('/v1/users/:userId/email') ] )) ->param('userId', '', new UID(), 'User ID.') - ->param('email', '', new Email(allowEmpty: true), 'User email.') + ->param('email', '', new EmailValidator(allowEmpty: true), 'User email.') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') @@ -1450,19 +1450,19 @@ App::patch('/v1/users/:userId/email') $oldEmail = $user->getAttribute('email'); try { - $emailValidator = new EmailValidator($email); + $emailCanonical = new EmailValidator($email); } catch (Throwable) { - $emailValidator = null; + $emailCanonical = null; } $user ->setAttribute('email', $email) ->setAttribute('emailVerification', false) - ->setAttribute('emailCanonical', $emailValidator?->getCanonical()) - ->setAttribute('emailIsCanonical', $emailValidator?->isCanonicalSupported()) - ->setAttribute('emailIsCorporate', $emailValidator?->isCorporate()) - ->setAttribute('emailIsDisposable', $emailValidator?->isDisposable()) - ->setAttribute('emailIsFree', $emailValidator?->isFree()) + ->setAttribute('emailCanonical', $emailCanonical?->getCanonical()) + ->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported()) + ->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate()) + ->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable()) + ->setAttribute('emailIsFree', $emailCanonical?->isFree()) ; try { @@ -1723,7 +1723,7 @@ App::patch('/v1/users/:userId/targets/:targetId') switch ($providerType) { case 'email': - $validator = new Email(); + $validator = new EmailValidator(); if (!$validator->isValid($identifier)) { throw new Exception(Exception::GENERAL_INVALID_EMAIL); } From 97454c076887bb183a40277c89a067a44e4c089b Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 11 Nov 2025 15:33:16 +0200 Subject: [PATCH 27/28] EmailValidator chnage to email --- app/controllers/api/users.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 956c9da042..a3ef4f852b 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -1450,7 +1450,7 @@ App::patch('/v1/users/:userId/email') $oldEmail = $user->getAttribute('email'); try { - $emailCanonical = new EmailValidator($email); + $emailCanonical = new Email($email); } catch (Throwable) { $emailCanonical = null; } From e01ac65ce81072b4ae42183ed0da063af4eb5970 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 12 Nov 2025 00:26:27 +0000 Subject: [PATCH 28/28] Feat: upgrade runtime --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index fe245d7837..753162cc89 100644 --- a/composer.lock +++ b/composer.lock @@ -161,16 +161,16 @@ }, { "name": "appwrite/php-runtimes", - "version": "0.19.1", + "version": "0.19.2", "source": { "type": "git", "url": "https://github.com/appwrite/runtimes.git", - "reference": "7bd0cc3cb97de625d7b07230bd91b121f88e72ae" + "reference": "e5c142519df5aced37de9c302971c29c079ce3d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/runtimes/zipball/7bd0cc3cb97de625d7b07230bd91b121f88e72ae", - "reference": "7bd0cc3cb97de625d7b07230bd91b121f88e72ae", + "url": "https://api.github.com/repos/appwrite/runtimes/zipball/e5c142519df5aced37de9c302971c29c079ce3d9", + "reference": "e5c142519df5aced37de9c302971c29c079ce3d9", "shasum": "" }, "require": { @@ -210,9 +210,9 @@ ], "support": { "issues": "https://github.com/appwrite/runtimes/issues", - "source": "https://github.com/appwrite/runtimes/tree/0.19.1" + "source": "https://github.com/appwrite/runtimes/tree/0.19.2" }, - "time": "2025-05-27T07:12:56+00:00" + "time": "2025-11-11T13:44:44+00:00" }, { "name": "beberlei/assert",