From ff06920e2479610bec304e289ad42f4aaad9b67f Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 13 Oct 2025 23:43:54 +0100 Subject: [PATCH 001/118] feat: add support for blocking disposable email addresses - Introduced a new exception for disposable email addresses. - Updated user account creation and email handling to check against a list of disposable email domains. - Added a new API endpoint to enable or disable disposable email checks for projects. - Updated project model to include disposable email settings. - Configured loading of disposable email domains from a separate configuration file. --- app/config/domains/disposable-emails.php | 22 +++++++++++ app/config/errors.php | 5 +++ app/controllers/api/account.php | 32 +++++++++++++++ app/controllers/api/projects.php | 39 +++++++++++++++++++ app/controllers/api/users.php | 2 + app/init/configs.php | 1 + src/Appwrite/Extend/Exception.php | 1 + .../Utopia/Response/Model/Project.php | 1 + 8 files changed, 103 insertions(+) create mode 100644 app/config/domains/disposable-emails.php diff --git a/app/config/domains/disposable-emails.php b/app/config/domains/disposable-emails.php new file mode 100644 index 0000000000..b5516470d8 --- /dev/null +++ b/app/config/domains/disposable-emails.php @@ -0,0 +1,22 @@ + true, + '027168.com' => true, + '062e.com' => true, + '0815.ru' => true, + '0815.su' => true, + '0845.ru' => true, + '0box.eu' => true, + '0cd.cn' => true, + '10minutemail.com' => true, + 'tempmail.org' => true, + 'guerrillamail.com' => true, + 'mailinator.com' => true, + 'temp-mail.org' => true, + 'throwaway.email' => true, + 'yopmail.com' => true, + 'maildrop.cc' => true, + 'getnada.com' => true, + 'tempail.com' => true, +]; diff --git a/app/config/errors.php b/app/config/errors.php index 2e18f05797..58fa0a607e 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -221,6 +221,11 @@ return [ 'description' => 'A user with the same email already exists in the current project.', 'code' => 409, ], + Exception::USER_EMAIL_DISPOSABLE => [ + 'name' => Exception::USER_EMAIL_DISPOSABLE, + 'description' => 'Disposable email addresses are not allowed. Please use a permanent email address.', + 'code' => 400, + ], Exception::USER_PASSWORD_MISMATCH => [ 'name' => Exception::USER_PASSWORD_MISMATCH, 'description' => 'Passwords do not match. Please check the password and confirm password.', diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 5563fc6a59..18d3177251 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -369,6 +369,14 @@ App::post('/v1/account') } } + if ($project->getAttribute('auths', [])['disposableEmails'] ?? false) { + $disposableEmails = Config::getParam('disposableEmails', []); + $emailDomain = substr(strrchr($email, "@"), 1); + if (isset($disposableEmails[$emailDomain])) { + throw new Exception(Exception::USER_EMAIL_DISPOSABLE); + } + } + $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]); $passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0; @@ -1967,6 +1975,14 @@ App::post('/v1/account/tokens/magic-url') throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } + if ($project->getAttribute('auths', [])['disposableEmails'] ?? false) { + $disposableEmails = Config::getParam('disposableEmails', []); + $emailDomain = substr(strrchr($email, "@"), 1); + if (isset($disposableEmails[$emailDomain])) { + throw new Exception(Exception::USER_EMAIL_DISPOSABLE); + } + } + $userId = $userId === 'unique()' ? ID::unique() : $userId; $user->setAttributes([ @@ -2217,6 +2233,14 @@ App::post('/v1/account/tokens/email') throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } + if ($project->getAttribute('auths', [])['disposableEmails'] ?? false) { + $disposableEmails = Config::getParam('disposableEmails', []); + $emailDomain = substr(strrchr($email, "@"), 1); + if (isset($disposableEmails[$emailDomain])) { + throw new Exception(Exception::USER_EMAIL_DISPOSABLE); + } + } + $userId = $userId === 'unique()' ? ID::unique() : $userId; $user->setAttributes([ @@ -3050,6 +3074,14 @@ App::patch('/v1/account/email') throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } + if ($project->getAttribute('auths', [])['disposableEmails'] ?? false) { + $disposableEmails = Config::getParam('disposableEmails', []); + $emailDomain = substr(strrchr($email, "@"), 1); + if (isset($disposableEmails[$emailDomain])) { + throw new Exception(Exception::USER_EMAIL_DISPOSABLE); + } + } + $user ->setAttribute('email', $email) ->setAttribute('emailVerification', false) // After this user needs to confirm mail again diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 80d407322e..c60de52bfb 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -120,6 +120,7 @@ App::post('/v1/projects') 'passwordDictionary' => false, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, 'personalDataCheck' => false, + 'disposableEmails' => false, 'mockNumbers' => [], 'sessionAlerts' => false, 'membershipsUserName' => false, @@ -1021,6 +1022,44 @@ App::patch('/v1/projects/:projectId/auth/personal-data') $response->dynamic($project, Response::MODEL_PROJECT); }); +App::patch('/v1/projects/:projectId/auth/disposable-emails') + ->desc('Update disposable emails check') + ->groups(['api', 'projects']) + ->label('scope', 'projects.write') + ->label('sdk', new Method( + namespace: 'projects', + group: 'auth', + name: 'updateDisposableEmails', + description: '/docs/references/projects/update-disposable-emails.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_PROJECT, + ) + ] + )) + ->param('projectId', '', new UID(), 'Project unique ID.') + ->param('enabled', false, new Boolean(false), 'Set whether or not to block disposable email addresses. Default is false.') + ->inject('response') + ->inject('dbForPlatform') + ->action(function (string $projectId, bool $enabled, Response $response, Database $dbForPlatform) { + + $project = $dbForPlatform->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $auths = $project->getAttribute('auths', []); + $auths['disposableEmails'] = $enabled; + + $dbForPlatform->updateDocument('projects', $project->getId(), $project + ->setAttribute('auths', $auths)); + + $response->dynamic($project, Response::MODEL_PROJECT); + }); + App::patch('/v1/projects/:projectId/auth/max-sessions') ->desc('Update project user sessions limit') ->groups(['api', 'projects']) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 5498a33bf5..2b1e5bafea 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -97,6 +97,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e } } + $password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null; $user = new Document([ '$id' => $userId, @@ -1324,6 +1325,7 @@ App::patch('/v1/users/:userId/password') } } + if (\strlen($password) === 0) { $user ->setAttribute('password', '') diff --git a/app/init/configs.php b/app/init/configs.php index 7572302919..1ec4d60480 100644 --- a/app/init/configs.php +++ b/app/init/configs.php @@ -40,3 +40,4 @@ Config::load('storage-outputs', __DIR__ . '/../config/storage/outputs.php'); Config::load('specifications', __DIR__ . '/../config/specifications.php'); Config::load('templates-function', __DIR__ . '/../config/templates/function.php'); Config::load('templates-site', __DIR__ . '/../config/templates/site.php'); +Config::load('disposableEmails', __DIR__ . '/../config/domains/disposable-emails.php'); // (eldad) following cloud camelCase format. Wish we could be consistent with the rest of the codebase. \ No newline at end of file diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 6f8744568a..21c0dc9ac5 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -81,6 +81,7 @@ class Exception extends \Exception public const string USER_PASSWORD_RECENTLY_USED = 'password_recently_used'; public const string USER_PASSWORD_PERSONAL_DATA = 'password_personal_data'; public const string USER_EMAIL_ALREADY_EXISTS = 'user_email_already_exists'; + public const string USER_EMAIL_DISPOSABLE = 'user_email_disposable'; public const string USER_PASSWORD_MISMATCH = 'user_password_mismatch'; public const string USER_SESSION_NOT_FOUND = 'user_session_not_found'; public const string USER_IDENTITY_NOT_FOUND = 'user_identity_not_found'; diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index abe67e7e86..03c2d5772e 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -377,6 +377,7 @@ class Project extends Model $document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0); $document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false); $document->setAttribute('authPersonalDataCheck', $authValues['personalDataCheck'] ?? false); + $document->setAttribute('authDisposableEmails', $authValues['disposableEmails'] ?? false); $document->setAttribute('authMockNumbers', $authValues['mockNumbers'] ?? []); $document->setAttribute('authSessionAlerts', $authValues['sessionAlerts'] ?? false); $document->setAttribute('authMembershipsUserName', $authValues['membershipsUserName'] ?? true); From 3e58014dbdd785e2bc5c85e0f233dd24bfd7fa43 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 13 Oct 2025 23:48:50 +0100 Subject: [PATCH 002/118] fixes --- app/config/domains/disposable-emails.php | 18 ----------- app/controllers/api/projects.php | 38 ------------------------ app/init/configs.php | 2 +- 3 files changed, 1 insertion(+), 57 deletions(-) diff --git a/app/config/domains/disposable-emails.php b/app/config/domains/disposable-emails.php index b5516470d8..b62512838d 100644 --- a/app/config/domains/disposable-emails.php +++ b/app/config/domains/disposable-emails.php @@ -1,22 +1,4 @@ true, - '027168.com' => true, - '062e.com' => true, - '0815.ru' => true, - '0815.su' => true, - '0845.ru' => true, - '0box.eu' => true, - '0cd.cn' => true, - '10minutemail.com' => true, - 'tempmail.org' => true, - 'guerrillamail.com' => true, - 'mailinator.com' => true, - 'temp-mail.org' => true, - 'throwaway.email' => true, - 'yopmail.com' => true, - 'maildrop.cc' => true, - 'getnada.com' => true, - 'tempail.com' => true, ]; diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index c60de52bfb..aafe867fe2 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1022,44 +1022,6 @@ App::patch('/v1/projects/:projectId/auth/personal-data') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/disposable-emails') - ->desc('Update disposable emails check') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'auth', - name: 'updateDisposableEmails', - description: '/docs/references/projects/update-disposable-emails.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROJECT, - ) - ] - )) - ->param('projectId', '', new UID(), 'Project unique ID.') - ->param('enabled', false, new Boolean(false), 'Set whether or not to block disposable email addresses. Default is false.') - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, bool $enabled, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $auths = $project->getAttribute('auths', []); - $auths['disposableEmails'] = $enabled; - - $dbForPlatform->updateDocument('projects', $project->getId(), $project - ->setAttribute('auths', $auths)); - - $response->dynamic($project, Response::MODEL_PROJECT); - }); - App::patch('/v1/projects/:projectId/auth/max-sessions') ->desc('Update project user sessions limit') ->groups(['api', 'projects']) diff --git a/app/init/configs.php b/app/init/configs.php index 1ec4d60480..81ef2eb583 100644 --- a/app/init/configs.php +++ b/app/init/configs.php @@ -40,4 +40,4 @@ Config::load('storage-outputs', __DIR__ . '/../config/storage/outputs.php'); Config::load('specifications', __DIR__ . '/../config/specifications.php'); Config::load('templates-function', __DIR__ . '/../config/templates/function.php'); Config::load('templates-site', __DIR__ . '/../config/templates/site.php'); -Config::load('disposableEmails', __DIR__ . '/../config/domains/disposable-emails.php'); // (eldad) following cloud camelCase format. Wish we could be consistent with the rest of the codebase. \ No newline at end of file +Config::load('disposableEmails', __DIR__ . '/../config/domains/disposable-emails.php'); // (eldad) following cloud camelCase format. Wish we could be consistent with the rest of the codebase. From ddcfac4f0f7f0ed0a5481d232f78adef4f5db783 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 14 Oct 2025 00:03:11 +0100 Subject: [PATCH 003/118] feat: enhance account handling with plan support for disposable email validation - Added 'plan' injection to account-related API endpoints. - Updated logic to check for disposable email validation support based on the plan. - Improved handling of disposable email checks during account creation and email updates. --- app/controllers/api/account.php | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 18d3177251..59994d9702 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -325,7 +325,8 @@ App::post('/v1/account') ->inject('project') ->inject('dbForProject') ->inject('hooks') - ->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Hooks $hooks) { + ->inject('plan') + ->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Hooks $hooks, array $plan) { $email = \strtolower($email); if ('console' === $project->getId()) { @@ -369,7 +370,7 @@ App::post('/v1/account') } } - if ($project->getAttribute('auths', [])['disposableEmails'] ?? false) { + if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false)) { $disposableEmails = Config::getParam('disposableEmails', []); $emailDomain = substr(strrchr($email, "@"), 1); if (isset($disposableEmails[$emailDomain])) { @@ -1942,7 +1943,8 @@ App::post('/v1/account/tokens/magic-url') ->inject('locale') ->inject('queueForEvents') ->inject('queueForMails') - ->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) { + ->inject('plan') + ->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); } @@ -1975,7 +1977,7 @@ App::post('/v1/account/tokens/magic-url') throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } - if ($project->getAttribute('auths', [])['disposableEmails'] ?? false) { + if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false)) { $disposableEmails = Config::getParam('disposableEmails', []); $emailDomain = substr(strrchr($email, "@"), 1); if (isset($disposableEmails[$emailDomain])) { @@ -2202,7 +2204,8 @@ App::post('/v1/account/tokens/email') ->inject('locale') ->inject('queueForEvents') ->inject('queueForMails') - ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) { + ->inject('plan') + ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); } @@ -2233,7 +2236,7 @@ App::post('/v1/account/tokens/email') throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } - if ($project->getAttribute('auths', [])['disposableEmails'] ?? false) { + if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false)) { $disposableEmails = Config::getParam('disposableEmails', []); $emailDomain = substr(strrchr($email, "@"), 1); if (isset($disposableEmails[$emailDomain])) { @@ -3048,7 +3051,8 @@ App::patch('/v1/account/email') ->inject('queueForEvents') ->inject('project') ->inject('hooks') - ->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks) { + ->inject('plan') + ->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, array $plan) { // passwordUpdate will be empty if the user has never set a password $passwordUpdate = $user->getAttribute('passwordUpdate'); @@ -3074,7 +3078,7 @@ App::patch('/v1/account/email') throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } - if ($project->getAttribute('auths', [])['disposableEmails'] ?? false) { + if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false)) { $disposableEmails = Config::getParam('disposableEmails', []); $emailDomain = substr(strrchr($email, "@"), 1); if (isset($disposableEmails[$emailDomain])) { From 0bd27c41824d8f3ed178b9e883478231545915a8 Mon Sep 17 00:00:00 2001 From: "Eldad A. Fux" Date: Tue, 14 Oct 2025 07:58:24 +0100 Subject: [PATCH 004/118] Update app/controllers/api/users.php Co-authored-by: Jake Barnby --- app/controllers/api/users.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 2b1e5bafea..652ce25d91 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -97,7 +97,6 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e } } - $password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null; $user = new Document([ '$id' => $userId, From 4c6961f7d628cb9d812da158cce17599172ccb18 Mon Sep 17 00:00:00 2001 From: "Eldad A. Fux" Date: Tue, 14 Oct 2025 07:58:30 +0100 Subject: [PATCH 005/118] Update app/controllers/api/users.php Co-authored-by: Jake Barnby --- app/controllers/api/users.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 652ce25d91..5498a33bf5 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -1324,7 +1324,6 @@ App::patch('/v1/users/:userId/password') } } - if (\strlen($password) === 0) { $user ->setAttribute('password', '') From 68b72f316295bde61e9f0de4b242d823dffe8ffd Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 21:44:22 +0100 Subject: [PATCH 006/118] feat: integrate Utopia Emails library for email validation - Added Utopia Emails library as a dependency in composer.json. - Updated email validation references across multiple API controllers to use the new Utopia Emails validator. - Removed the deprecated disposable emails configuration file. - Updated composer.lock to reflect the new library and version changes for existing dependencies. --- app/config/domains/disposable-emails.php | 4 - app/controllers/api/account.php | 2 +- app/controllers/api/messaging.php | 2 +- app/controllers/api/projects.php | 2 +- app/controllers/api/teams.php | 2 +- app/controllers/api/users.php | 2 +- app/init/database/formats.php | 2 +- composer.json | 1 + composer.lock | 86 ++++++++++++++++--- src/Appwrite/GraphQL/Types/Mapper.php | 2 +- src/Appwrite/Network/Validator/Email.php | 51 ++--------- .../Collections/Attributes/Email/Create.php | 2 +- .../Collections/Attributes/Email/Update.php | 2 +- .../TablesDB/Tables/Columns/Email/Create.php | 2 +- .../TablesDB/Tables/Columns/Email/Update.php | 2 +- .../SDK/Specification/Format/OpenAPI3.php | 2 +- .../SDK/Specification/Format/Swagger2.php | 2 +- 17 files changed, 93 insertions(+), 75 deletions(-) delete mode 100644 app/config/domains/disposable-emails.php diff --git a/app/config/domains/disposable-emails.php b/app/config/domains/disposable-emails.php deleted file mode 100644 index b62512838d..0000000000 --- a/app/config/domains/disposable-emails.php +++ /dev/null @@ -1,4 +0,0 @@ -=8.0", + "utopia-php/cli": "^0.15", + "utopia-php/domains": "^0.8", + "utopia-php/fetch": "^0.4", + "utopia-php/framework": "0.33.*" + }, + "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.4.0" + }, + "time": "2025-10-19T20:31:42+00:00" + }, { "name": "utopia-php/fetch", "version": "0.4.2", diff --git a/src/Appwrite/GraphQL/Types/Mapper.php b/src/Appwrite/GraphQL/Types/Mapper.php index b74e2a7549..fcf10ef208 100644 --- a/src/Appwrite/GraphQL/Types/Mapper.php +++ b/src/Appwrite/GraphQL/Types/Mapper.php @@ -273,7 +273,7 @@ class Mapper case 'Appwrite\Event\Validator\Event': case 'Appwrite\Event\Validator\FunctionEvent': case 'Appwrite\Network\Validator\CNAME': - case 'Appwrite\Network\Validator\Email': + case 'Utopia\Emails\Validator\Email': case 'Appwrite\Network\Validator\Redirect': case 'Appwrite\Network\Validator\DNS': case 'Appwrite\Network\Validator\Origin': diff --git a/src/Appwrite/Network/Validator/Email.php b/src/Appwrite/Network/Validator/Email.php index 3209a4aada..8052e2bdd0 100644 --- a/src/Appwrite/Network/Validator/Email.php +++ b/src/Appwrite/Network/Validator/Email.php @@ -2,16 +2,17 @@ namespace Appwrite\Network\Validator; -use Utopia\Validator; +use Utopia\Emails\Validator\Email as UtopiaEmailValidator; /** * Email * * Validate that an variable is a valid email address + * Extends the new Utopia Emails validator to maintain backward compatibility * - * @package Utopia\Validator + * @package Appwrite\Network\Validator */ -class Email extends Validator +class Email extends UtopiaEmailValidator { protected bool $allowEmpty; @@ -20,18 +21,6 @@ class Email extends Validator $this->allowEmpty = $allowEmpty; } - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - return 'Value must be a valid email address'; - } - /** * Is valid * @@ -46,34 +35,6 @@ class Email extends Validator return true; } - if (!\filter_var($value, FILTER_VALIDATE_EMAIL)) { - return false; - } - - return true; + return parent::isValid($value); } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return self::TYPE_STRING; - } -} +} \ No newline at end of file diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php index cbfd66e4d9..0b597ae320 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php @@ -4,7 +4,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attribu use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; -use Appwrite\Network\Validator\Email; +use Utopia\Emails\Validator\Email; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Deprecated; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Update.php index 2446722f7a..d4d9fd95d3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Update.php @@ -3,7 +3,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email; use Appwrite\Event\Event; -use Appwrite\Network\Validator\Email; +use Utopia\Emails\Validator\Email; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php index 3a8d492e4e..5ffe02e9ee 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php @@ -2,7 +2,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Email; -use Appwrite\Network\Validator\Email; +use Utopia\Emails\Validator\Email; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email\Create as EmailCreate; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Update.php index 4d32489357..f67a97207a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Update.php @@ -2,7 +2,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Email; -use Appwrite\Network\Validator\Email; +use Utopia\Emails\Validator\Email; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email\Update as EmailUpdate; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 38613313db..4044df3e0b 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -464,7 +464,7 @@ class OpenAPI3 extends Format Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]', }; break; - case 'Appwrite\Network\Validator\Email': + case 'Utopia\Emails\Validator\Email': $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'email'; $node['schema']['x-example'] = 'email@example.com'; diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index d497369b9a..d73604a327 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -471,7 +471,7 @@ class Swagger2 extends Format Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]', }; break; - case 'Appwrite\Network\Validator\Email': + case 'Utopia\Emails\Validator\Email': $node['type'] = $validator->getType(); $node['format'] = 'email'; $node['x-example'] = 'email@example.com'; From 18a66bae4ec9e63035ec0d97b15848932df9a7ef Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 21:55:52 +0100 Subject: [PATCH 007/118] chore: update Utopia Emails dependency to version 0.5 and remove deprecated email validator - Updated the Utopia Emails dependency version from 0.4.* to 0.5.* in composer.json and composer.lock. - Removed the custom Email validator class and its associated tests as it is no longer needed with the new Utopia Emails library. --- composer.json | 2 +- composer.lock | 14 ++--- src/Appwrite/Network/Validator/Email.php | 40 ------------ tests/unit/Network/Validators/EmailTest.php | 69 --------------------- 4 files changed, 8 insertions(+), 117 deletions(-) delete mode 100644 src/Appwrite/Network/Validator/Email.php delete mode 100755 tests/unit/Network/Validators/EmailTest.php diff --git a/composer.json b/composer.json index 9c6186d8dc..66425323c2 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,7 @@ "utopia-php/domains": "0.8.*", "utopia-php/dns": "0.3.*", "utopia-php/dsn": "0.2.1", - "utopia-php/emails": "0.4.*", + "utopia-php/emails": "0.5.*", "utopia-php/framework": "0.33.*", "utopia-php/fetch": "0.4.*", "utopia-php/image": "0.8.*", diff --git a/composer.lock b/composer.lock index 3f9da4f993..8715283e08 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": "2fbd76a832756d8b21adf27e5b0a7cc7", + "content-hash": "16bb025bc719fc989a11800ec450fd5b", "packages": [ { "name": "adhocore/jwt", @@ -3900,16 +3900,16 @@ }, { "name": "utopia-php/emails", - "version": "0.4.0", + "version": "0.5.0", "source": { "type": "git", "url": "https://github.com/utopia-php/emails.git", - "reference": "48d772c854a176f063e495d0179a1369087fd4f0" + "reference": "aebfd79cd8c39f230fb9a238ca12a4f7a1f7e0cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/emails/zipball/48d772c854a176f063e495d0179a1369087fd4f0", - "reference": "48d772c854a176f063e495d0179a1369087fd4f0", + "url": "https://api.github.com/repos/utopia-php/emails/zipball/aebfd79cd8c39f230fb9a238ca12a4f7a1f7e0cd", + "reference": "aebfd79cd8c39f230fb9a238ca12a4f7a1f7e0cd", "shasum": "" }, "require": { @@ -3954,9 +3954,9 @@ ], "support": { "issues": "https://github.com/utopia-php/emails/issues", - "source": "https://github.com/utopia-php/emails/tree/0.4.0" + "source": "https://github.com/utopia-php/emails/tree/0.5.0" }, - "time": "2025-10-19T20:31:42+00:00" + "time": "2025-10-19T20:51:56+00:00" }, { "name": "utopia-php/fetch", diff --git a/src/Appwrite/Network/Validator/Email.php b/src/Appwrite/Network/Validator/Email.php deleted file mode 100644 index 8052e2bdd0..0000000000 --- a/src/Appwrite/Network/Validator/Email.php +++ /dev/null @@ -1,40 +0,0 @@ -allowEmpty = $allowEmpty; - } - - /** - * Is valid - * - * Validation will pass when $value is valid email address. - * - * @param mixed $value - * @return bool - */ - public function isValid($value): bool - { - if ($this->allowEmpty && \strlen($value) === 0) { - return true; - } - - return parent::isValid($value); - } -} \ No newline at end of file diff --git a/tests/unit/Network/Validators/EmailTest.php b/tests/unit/Network/Validators/EmailTest.php deleted file mode 100755 index f629ed6ddc..0000000000 --- a/tests/unit/Network/Validators/EmailTest.php +++ /dev/null @@ -1,69 +0,0 @@ - - * @version 1.0 RC4 - * @license The MIT License (MIT) - */ - -namespace Tests\Unit\Network\Validators; - -use Appwrite\Network\Validator\Email; -use PHPUnit\Framework\TestCase; - -class EmailTest extends TestCase -{ - protected ?Email $email = null; - - public function setUp(): void - { - $this->email = new Email(); - } - - public function tearDown(): void - { - $this->email = null; - } - - public function testIsValid(): void - { - $this->assertEquals(true, $this->email->isValid('email@domain.com')); - $this->assertEquals(true, $this->email->isValid('firstname.lastname@domain.com')); - $this->assertEquals(true, $this->email->isValid('email@subdomain.domain.com')); - $this->assertEquals(true, $this->email->isValid('firstname+lastname@domain.com')); - $this->assertEquals(true, $this->email->isValid('email@[123.123.123.123]')); - $this->assertEquals(true, $this->email->isValid('"email"@domain.com')); - $this->assertEquals(true, $this->email->isValid('1234567890@domain.com')); - $this->assertEquals(true, $this->email->isValid('email@domain-one.com')); - $this->assertEquals(true, $this->email->isValid('_______@domain.com')); - $this->assertEquals(true, $this->email->isValid('email@domain.name')); - $this->assertEquals(true, $this->email->isValid('email@domain.co.jp')); - $this->assertEquals(true, $this->email->isValid('firstname-lastname@domain.com')); - $this->assertEquals(false, $this->email->isValid(false)); - $this->assertEquals(false, $this->email->isValid(['string', 'string'])); - $this->assertEquals(false, $this->email->isValid(1)); - $this->assertEquals(false, $this->email->isValid(1.2)); - $this->assertEquals(false, $this->email->isValid('plainaddress')); // Missing @ sign and domain - $this->assertEquals(false, $this->email->isValid('@domain.com')); // Missing username - $this->assertEquals(false, $this->email->isValid('#@%^%#$@#$@#.com')); // Garbage - $this->assertEquals(false, $this->email->isValid('Joe Smith ')); // Encoded html within email is invalid - $this->assertEquals(false, $this->email->isValid('email.domain.com')); // Missing @ - $this->assertEquals(false, $this->email->isValid('email@domain@domain.com')); // Two @ sign - $this->assertEquals(false, $this->email->isValid('.email@domain.com')); // Leading dot in address is not allowed - $this->assertEquals(false, $this->email->isValid('email.@domain.com')); // Trailing dot in address is not allowed - $this->assertEquals(false, $this->email->isValid('email..email@domain.com')); // Multiple dots - $this->assertEquals(false, $this->email->isValid('あいうえお@domain.com')); // Unicode char as address - $this->assertEquals(false, $this->email->isValid('email@domain.com (Joe Smith)')); // Text followed email is not allowed - $this->assertEquals(false, $this->email->isValid('email@domain')); // Missing top level domain (.com/.net/.org/etc) - $this->assertEquals(false, $this->email->isValid('email@-domain.com')); // Leading dash in front of domain is invalid - $this->assertEquals(false, $this->email->isValid('email@111.222.333.44444')); // Invalid IP format - $this->assertEquals(false, $this->email->isValid('email@domain..com')); // Multiple dot in the domain portion is invalid - $this->assertEquals($this->email->getType(), 'string'); - } -} From 6c6c71484d72a10666dc02ef4fdf250e73e8a13f Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 22:01:27 +0100 Subject: [PATCH 008/118] refactor: replace disposable email validation logic with Utopia Emails library - Introduced the EmailNotDisposable validator from the Utopia Emails library for improved disposable email validation in the account API. - Removed the deprecated disposable emails configuration loading from the config files to streamline the codebase. --- app/controllers/api/account.php | 11 +++++------ app/init/configs.php | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 3a4858534b..c215aea3d7 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -21,6 +21,7 @@ use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; use Utopia\Emails\Validator\Email; +use Utopia\Emails\Validator\EmailNotDisposable; use Appwrite\Network\Validator\Redirect; use Appwrite\OpenSSL\OpenSSL; use Appwrite\SDK\AuthType; @@ -371,9 +372,8 @@ App::post('/v1/account') } if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false)) { - $disposableEmails = Config::getParam('disposableEmails', []); - $emailDomain = substr(strrchr($email, "@"), 1); - if (isset($disposableEmails[$emailDomain])) { + $emailNotDisposableValidator = new EmailNotDisposable(); + if (!$emailNotDisposableValidator->isValid($email)) { throw new Exception(Exception::USER_EMAIL_DISPOSABLE); } } @@ -3079,9 +3079,8 @@ App::patch('/v1/account/email') } if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false)) { - $disposableEmails = Config::getParam('disposableEmails', []); - $emailDomain = substr(strrchr($email, "@"), 1); - if (isset($disposableEmails[$emailDomain])) { + $emailNotDisposableValidator = new EmailNotDisposable(); + if (!$emailNotDisposableValidator->isValid($email)) { throw new Exception(Exception::USER_EMAIL_DISPOSABLE); } } diff --git a/app/init/configs.php b/app/init/configs.php index 81ef2eb583..7572302919 100644 --- a/app/init/configs.php +++ b/app/init/configs.php @@ -40,4 +40,3 @@ Config::load('storage-outputs', __DIR__ . '/../config/storage/outputs.php'); Config::load('specifications', __DIR__ . '/../config/specifications.php'); Config::load('templates-function', __DIR__ . '/../config/templates/function.php'); Config::load('templates-site', __DIR__ . '/../config/templates/site.php'); -Config::load('disposableEmails', __DIR__ . '/../config/domains/disposable-emails.php'); // (eldad) following cloud camelCase format. Wish we could be consistent with the rest of the codebase. From c89d07520003cae0dec23524ed1423250baec905 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 22:09:26 +0100 Subject: [PATCH 009/118] refactor: streamline disposable email validation in account API - Replaced the previous disposable email validation logic with the EmailNotDisposable validator from the Utopia Emails library for consistency and improved maintainability. - Removed unnecessary configuration loading related to disposable emails, aligning with recent updates to the email validation process. --- app/controllers/api/account.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index c215aea3d7..63252e364e 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1978,9 +1978,8 @@ App::post('/v1/account/tokens/magic-url') } if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false)) { - $disposableEmails = Config::getParam('disposableEmails', []); - $emailDomain = substr(strrchr($email, "@"), 1); - if (isset($disposableEmails[$emailDomain])) { + $emailNotDisposableValidator = new EmailNotDisposable(); + if (!$emailNotDisposableValidator->isValid($email)) { throw new Exception(Exception::USER_EMAIL_DISPOSABLE); } } @@ -2237,9 +2236,8 @@ App::post('/v1/account/tokens/email') } if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false)) { - $disposableEmails = Config::getParam('disposableEmails', []); - $emailDomain = substr(strrchr($email, "@"), 1); - if (isset($disposableEmails[$emailDomain])) { + $emailNotDisposableValidator = new EmailNotDisposable(); + if (!$emailNotDisposableValidator->isValid($email)) { throw new Exception(Exception::USER_EMAIL_DISPOSABLE); } } From 1c465a8cdac8f0f3c08761070801204c088fb4e1 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 22:16:03 +0100 Subject: [PATCH 010/118] refactor: standardize email validation across API controllers - Reintroduced the Utopia Emails validator in multiple API controllers to ensure consistent email validation practices. - Removed deprecated email validation references, streamlining the codebase and aligning with recent updates to the Utopia Emails library. --- app/controllers/api/account.php | 4 ++-- app/controllers/api/messaging.php | 2 +- app/controllers/api/projects.php | 2 +- app/controllers/api/teams.php | 2 +- app/controllers/api/users.php | 2 +- app/init/database/formats.php | 2 +- .../Http/Databases/Collections/Attributes/Email/Create.php | 2 +- .../Http/Databases/Collections/Attributes/Email/Update.php | 2 +- .../Databases/Http/TablesDB/Tables/Columns/Email/Create.php | 2 +- .../Databases/Http/TablesDB/Tables/Columns/Email/Update.php | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 63252e364e..ca3c2b9f3b 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -20,8 +20,6 @@ use Appwrite\Event\Messaging; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; -use Utopia\Emails\Validator\Email; -use Utopia\Emails\Validator\EmailNotDisposable; use Appwrite\Network\Validator\Redirect; use Appwrite\OpenSSL\OpenSSL; use Appwrite\SDK\AuthType; @@ -58,6 +56,8 @@ 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\Validator\Email; +use Utopia\Emails\Validator\EmailNotDisposable; use Utopia\Locale\Locale; use Utopia\Storage\Validator\FileName; use Utopia\System\System; diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index e671366769..1f4e1300c6 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -8,7 +8,6 @@ use Appwrite\Event\Event; use Appwrite\Event\Messaging; use Appwrite\Extend\Exception; use Appwrite\Messaging\Status as MessageStatus; -use Utopia\Emails\Validator\Email; use Appwrite\Permission; use Appwrite\Role; use Appwrite\SDK\AuthType; @@ -43,6 +42,7 @@ use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\Roles; use Utopia\Database\Validator\UID; +use Utopia\Emails\Validator\Email; use Utopia\Locale\Locale; use Utopia\System\System; use Utopia\Validator\ArrayList; diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 8bf05fec45..dd0faab3cc 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -9,7 +9,6 @@ use Appwrite\Event\Validator\Event; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; use Appwrite\Network\Platform; -use Utopia\Emails\Validator\Email; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; @@ -38,6 +37,7 @@ use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Domains\Validator\PublicDomain; use Utopia\DSN\DSN; +use Utopia\Emails\Validator\Email; use Utopia\Locale\Locale; use Utopia\Pools\Group; use Utopia\System\System; diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index bed60a099d..25279c1ae1 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -10,7 +10,6 @@ use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; -use Utopia\Emails\Validator\Email; use Appwrite\Network\Validator\Redirect; use Appwrite\Platform\Workers\Deletes; use Appwrite\SDK\AuthType; @@ -48,6 +47,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\Validator\Email; use Utopia\Locale\Locale; use Utopia\System\System; use Utopia\Validator\ArrayList; diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 4be322596a..bf135646a8 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -16,7 +16,6 @@ use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; -use Utopia\Emails\Validator\Email; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; @@ -49,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\Validator\Email; use Utopia\Locale\Locale; use Utopia\System\System; use Utopia\Validator\ArrayList; diff --git a/app/init/database/formats.php b/app/init/database/formats.php index 6b38d171a0..29a4f0c7d4 100644 --- a/app/init/database/formats.php +++ b/app/init/database/formats.php @@ -1,9 +1,9 @@ Date: Sat, 14 Mar 2026 09:21:22 +0100 Subject: [PATCH 011/118] Implement email validation rules for disposable, canonical, and free emails in user account creation and project settings. Update error handling for invalid email types and adjust related configurations in the console and project models. --- app/config/console.php | 3 + app/config/errors.php | 10 + app/controllers/api/account.php | 291 +++++++++++++----- app/controllers/api/users.php | 133 ++++++-- app/worker.php | 1 - src/Appwrite/Extend/Exception.php | 2 + .../Modules/Projects/Http/Projects/Create.php | 2 + .../Modules/Teams/Http/Memberships/Create.php | 47 ++- .../Utopia/Response/Model/Project.php | 20 ++ .../Projects/ProjectsConsoleClientTest.php | 3 + 10 files changed, 391 insertions(+), 121 deletions(-) diff --git a/app/config/console.php b/app/config/console.php index 24de8a8cbb..4f219fd561 100644 --- a/app/config/console.php +++ b/app/config/console.php @@ -39,6 +39,9 @@ $console = [ 'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user 'duration' => TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds 'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled', + 'disposableEmails' => false, + 'canonicalEmails' => false, + 'freeEmails' => false, 'invalidateSessions' => true ], 'authWhitelistEmails' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [], diff --git a/app/config/errors.php b/app/config/errors.php index 3b359a89b6..391975cb34 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -231,6 +231,16 @@ return [ 'description' => 'Disposable email addresses are not allowed. Please use a permanent email address.', 'code' => 400, ], + Exception::USER_EMAIL_FREE => [ + 'name' => Exception::USER_EMAIL_FREE, + 'description' => 'Free email addresses are not allowed. Please use a business or custom-domain email address.', + 'code' => 400, + ], + Exception::USER_EMAIL_NOT_CANONICAL => [ + 'name' => Exception::USER_EMAIL_NOT_CANONICAL, + 'description' => 'This email address must already be in its canonical form. Please remove aliases, tags, or provider-specific variations and try again.', + 'code' => 400, + ], Exception::USER_PASSWORD_MISMATCH => [ 'name' => Exception::USER_PASSWORD_MISMATCH, 'description' => 'Passwords do not match. Please check the password and confirm password.', diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index e2b7927904..2f66d76e4e 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -60,7 +60,6 @@ use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; use Utopia\Emails\Email; -use Utopia\Emails\Validator\EmailNotDisposable; use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\Storage\Validator\FileName; @@ -427,23 +426,43 @@ Http::post('/v1/account') } } - if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false)) { - $emailNotDisposableValidator = new EmailNotDisposable(); - if (!$emailNotDisposableValidator->isValid($email)) { - throw new Exception(Exception::USER_EMAIL_DISPOSABLE); - } - } - $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]); $passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0; $proof = new ProofsPassword(); $hash = $proof->hash($password); + $emailMetadata = [ + 'emailCanonical' => null, + 'emailIsCanonical' => null, + 'emailIsCorporate' => null, + 'emailIsDisposable' => null, + 'emailIsFree' => null, + ]; try { - $emailCanonical = new Email($email); - } catch (Throwable) { - $emailCanonical = null; + $parsedEmail = new Email($email); + $canonical = $parsedEmail->getCanonical(); + $emailMetadata = [ + 'emailCanonical' => $canonical, + 'emailIsCanonical' => $parsedEmail->get() === $canonical, + 'emailIsCorporate' => $parsedEmail->isCorporate(), + 'emailIsDisposable' => $parsedEmail->isDisposable(), + 'emailIsFree' => $parsedEmail->isFree(), + ]; + } catch (\Throwable) { + throw new Exception(Exception::GENERAL_INVALID_EMAIL); + } + + if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) { + throw new Exception(Exception::USER_EMAIL_DISPOSABLE); + } + + if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) { + throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL); + } + + if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) { + throw new Exception(Exception::USER_EMAIL_FREE); } try { @@ -474,11 +493,11 @@ Http::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' => $emailMetadata['emailCanonical'], + 'emailIsCanonical' => $emailMetadata['emailIsCanonical'], + 'emailIsCorporate' => $emailMetadata['emailIsCorporate'], + 'emailIsDisposable' => $emailMetadata['emailIsDisposable'], + 'emailIsFree' => $emailMetadata['emailIsFree'], ]); $user->removeAttribute('$sequence'); @@ -1491,8 +1510,9 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->inject('store') ->inject('proofForPassword') ->inject('proofForToken') + ->inject('plan') ->inject('authorization') - ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) { + ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, Authorization $authorization) use ($oauthDefaultSuccess) { $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; $port = $request->getPort(); $callbackBase = $protocol . '://' . $request->getHostname(); @@ -1734,10 +1754,38 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') } } + $emailMetadata = [ + 'emailCanonical' => null, + 'emailIsCanonical' => null, + 'emailIsCorporate' => null, + 'emailIsDisposable' => null, + 'emailIsFree' => null, + ]; + try { - $emailCanonical = new Email($email); - } catch (Throwable) { - $emailCanonical = null; + $parsedEmail = new Email($email); + $canonical = $parsedEmail->getCanonical(); + $emailMetadata = [ + 'emailCanonical' => $canonical, + 'emailIsCanonical' => $parsedEmail->get() === $canonical, + 'emailIsCorporate' => $parsedEmail->isCorporate(), + 'emailIsDisposable' => $parsedEmail->isDisposable(), + 'emailIsFree' => $parsedEmail->isFree(), + ]; + } catch (\Throwable) { + $failureRedirect(Exception::GENERAL_INVALID_EMAIL); + } + + if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) { + $failureRedirect(Exception::USER_EMAIL_DISPOSABLE); + } + + if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) { + $failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL); + } + + if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) { + $failureRedirect(Exception::USER_EMAIL_FREE); } try { @@ -1767,11 +1815,11 @@ Http::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' => $emailMetadata['emailCanonical'], + 'emailIsCanonical' => $emailMetadata['emailIsCanonical'], + 'emailIsCorporate' => $emailMetadata['emailIsCorporate'], + 'emailIsDisposable' => $emailMetadata['emailIsDisposable'], + 'emailIsFree' => $emailMetadata['emailIsFree'], ]); $user->removeAttribute('$sequence'); @@ -1846,19 +1894,47 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') } if (empty($user->getAttribute('email'))) { - $user->setAttribute('email', $oauth2->getUserEmail($accessToken)); + $email = $oauth2->getUserEmail($accessToken); + $emailMetadata = [ + 'emailCanonical' => null, + 'emailIsCanonical' => null, + 'emailIsCorporate' => null, + 'emailIsDisposable' => null, + 'emailIsFree' => null, + ]; try { - $emailCanonical = new Email($user->getAttribute('email')); - } catch (Throwable) { - $emailCanonical = null; + $parsedEmail = new Email($email); + $canonical = $parsedEmail->getCanonical(); + $emailMetadata = [ + 'emailCanonical' => $canonical, + 'emailIsCanonical' => $parsedEmail->get() === $canonical, + 'emailIsCorporate' => $parsedEmail->isCorporate(), + 'emailIsDisposable' => $parsedEmail->isDisposable(), + 'emailIsFree' => $parsedEmail->isFree(), + ]; + } catch (\Throwable) { + $failureRedirect(Exception::GENERAL_INVALID_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()); + if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) { + $failureRedirect(Exception::USER_EMAIL_DISPOSABLE); + } + + if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) { + $failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL); + } + + if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) { + $failureRedirect(Exception::USER_EMAIL_FREE); + } + + $user->setAttribute('email', $email); + $user->setAttribute('emailCanonical', $emailMetadata['emailCanonical']); + $user->setAttribute('emailIsCanonical', $emailMetadata['emailIsCanonical']); + $user->setAttribute('emailIsCorporate', $emailMetadata['emailIsCorporate']); + $user->setAttribute('emailIsDisposable', $emailMetadata['emailIsDisposable']); + $user->setAttribute('emailIsFree', $emailMetadata['emailIsFree']); } if (empty($user->getAttribute('name'))) { @@ -2170,17 +2246,38 @@ Http::post('/v1/account/tokens/magic-url') $userId = $userId === 'unique()' ? ID::unique() : $userId; - if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false)) { - $emailNotDisposableValidator = new EmailNotDisposable(); - if (!$emailNotDisposableValidator->isValid($email)) { - throw new Exception(Exception::USER_EMAIL_DISPOSABLE); - } - } + $emailMetadata = [ + 'emailCanonical' => null, + 'emailIsCanonical' => null, + 'emailIsCorporate' => null, + 'emailIsDisposable' => null, + 'emailIsFree' => null, + ]; try { - $emailCanonical = new Email($email); - } catch (Throwable) { - $emailCanonical = null; + $parsedEmail = new Email($email); + $canonical = $parsedEmail->getCanonical(); + $emailMetadata = [ + 'emailCanonical' => $canonical, + 'emailIsCanonical' => $parsedEmail->get() === $canonical, + 'emailIsCorporate' => $parsedEmail->isCorporate(), + 'emailIsDisposable' => $parsedEmail->isDisposable(), + 'emailIsFree' => $parsedEmail->isFree(), + ]; + } catch (\Throwable) { + throw new Exception(Exception::GENERAL_INVALID_EMAIL); + } + + if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) { + throw new Exception(Exception::USER_EMAIL_DISPOSABLE); + } + + if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) { + throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL); + } + + if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) { + throw new Exception(Exception::USER_EMAIL_FREE); } $user->setAttributes([ @@ -2207,11 +2304,11 @@ Http::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' => $emailMetadata['emailCanonical'], + 'emailIsCanonical' => $emailMetadata['emailIsCanonical'], + 'emailIsCorporate' => $emailMetadata['emailIsCorporate'], + 'emailIsDisposable' => $emailMetadata['emailIsDisposable'], + 'emailIsFree' => $emailMetadata['emailIsFree'], ]); $user->removeAttribute('$sequence'); @@ -2456,17 +2553,38 @@ Http::post('/v1/account/tokens/email') $userId = $userId === 'unique()' ? ID::unique() : $userId; - if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false)) { - $emailNotDisposableValidator = new EmailNotDisposable(); - if (!$emailNotDisposableValidator->isValid($email)) { - throw new Exception(Exception::USER_EMAIL_DISPOSABLE); - } - } + $emailMetadata = [ + 'emailCanonical' => null, + 'emailIsCanonical' => null, + 'emailIsCorporate' => null, + 'emailIsDisposable' => null, + 'emailIsFree' => null, + ]; try { - $emailCanonical = new Email($email); - } catch (Throwable) { - $emailCanonical = null; + $parsedEmail = new Email($email); + $canonical = $parsedEmail->getCanonical(); + $emailMetadata = [ + 'emailCanonical' => $canonical, + 'emailIsCanonical' => $parsedEmail->get() === $canonical, + 'emailIsCorporate' => $parsedEmail->isCorporate(), + 'emailIsDisposable' => $parsedEmail->isDisposable(), + 'emailIsFree' => $parsedEmail->isFree(), + ]; + } catch (\Throwable) { + throw new Exception(Exception::GENERAL_INVALID_EMAIL); + } + + if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) { + throw new Exception(Exception::USER_EMAIL_DISPOSABLE); + } + + if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) { + throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL); + } + + if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) { + throw new Exception(Exception::USER_EMAIL_FREE); } $user->setAttributes([ @@ -2491,11 +2609,11 @@ Http::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' => $emailMetadata['emailCanonical'], + 'emailIsCanonical' => $emailMetadata['emailIsCanonical'], + 'emailIsCorporate' => $emailMetadata['emailIsCorporate'], + 'emailIsDisposable' => $emailMetadata['emailIsDisposable'], + 'emailIsFree' => $emailMetadata['emailIsFree'], ]); $user->removeAttribute('$sequence'); @@ -3343,27 +3461,48 @@ Http::patch('/v1/account/email') throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } - if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false)) { - $emailNotDisposableValidator = new EmailNotDisposable(); - if (!$emailNotDisposableValidator->isValid($email)) { - throw new Exception(Exception::USER_EMAIL_DISPOSABLE); - } - } + $emailMetadata = [ + 'emailCanonical' => null, + 'emailIsCanonical' => null, + 'emailIsCorporate' => null, + 'emailIsDisposable' => null, + 'emailIsFree' => null, + ]; try { - $emailCanonical = new Email($email); - } catch (Throwable) { - $emailCanonical = null; + $parsedEmail = new Email($email); + $canonical = $parsedEmail->getCanonical(); + $emailMetadata = [ + 'emailCanonical' => $canonical, + 'emailIsCanonical' => $parsedEmail->get() === $canonical, + 'emailIsCorporate' => $parsedEmail->isCorporate(), + 'emailIsDisposable' => $parsedEmail->isDisposable(), + 'emailIsFree' => $parsedEmail->isFree(), + ]; + } catch (\Throwable) { + throw new Exception(Exception::GENERAL_INVALID_EMAIL); + } + + if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) { + throw new Exception(Exception::USER_EMAIL_DISPOSABLE); + } + + if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) { + throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL); + } + + if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) { + throw new Exception(Exception::USER_EMAIL_FREE); } $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', $emailMetadata['emailCanonical']) + ->setAttribute('emailIsCanonical', $emailMetadata['emailIsCanonical']) + ->setAttribute('emailIsCorporate', $emailMetadata['emailIsCorporate']) + ->setAttribute('emailIsDisposable', $emailMetadata['emailIsDisposable']) + ->setAttribute('emailIsFree', $emailMetadata['emailIsFree']) ; if (empty($passwordUpdate)) { diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 9d04018b10..e436a55300 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -73,7 +73,7 @@ use Utopia\Validator\Text; use Utopia\Validator\WhiteList; /** TODO: Remove function when we move to using utopia/platform */ -function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, ?string $name, Document $project, Database $dbForProject, Hooks $hooks): Document +function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, ?string $name, Document $project, Database $dbForProject, Hooks $hooks, array $plan): Document { $name = $name ?? ''; $plaintextPassword = $password; @@ -110,11 +110,39 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor } } + $emailMetadata = [ + 'emailCanonical' => null, + 'emailIsCanonical' => null, + 'emailIsCorporate' => null, + 'emailIsDisposable' => null, + 'emailIsFree' => null, + ]; + try { - $emailCanonical = new Email($email); - } catch (Throwable) { - $emailCanonical = null; + $parsedEmail = new Email($email ?? ''); + $canonical = $parsedEmail->getCanonical(); + $emailMetadata = [ + 'emailCanonical' => $canonical, + 'emailIsCanonical' => $parsedEmail->get() === $canonical, + 'emailIsCorporate' => $parsedEmail->isCorporate(), + 'emailIsDisposable' => $parsedEmail->isDisposable(), + 'emailIsFree' => $parsedEmail->isFree(), + ]; + } catch (\Throwable) { } + + if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) { + throw new Exception(Exception::USER_EMAIL_DISPOSABLE); + } + + if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) { + throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL); + } + + if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) { + throw new Exception(Exception::USER_EMAIL_FREE); + } + $hashedPassword = null; $isHashed = !$hash instanceof Plaintext; @@ -159,11 +187,11 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor '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' => $emailMetadata['emailCanonical'], + 'emailIsCanonical' => $emailMetadata['emailIsCanonical'], + 'emailIsCorporate' => $emailMetadata['emailIsCorporate'], + 'emailIsDisposable' => $emailMetadata['emailIsDisposable'], + 'emailIsFree' => $emailMetadata['emailIsFree'], ]); if (!$isHashed && !empty($password)) { @@ -256,10 +284,11 @@ Http::post('/v1/users') ->inject('project') ->inject('dbForProject') ->inject('hooks') - ->action(function (string $userId, ?string $email, ?string $phone, ?string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { + ->inject('plan') + ->action(function (string $userId, ?string $email, ?string $phone, ?string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) { $plaintext = new Plaintext(); - $user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks); + $user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks, $plan); $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($user, Response::MODEL_USER); @@ -292,11 +321,12 @@ Http::post('/v1/users/bcrypt') ->inject('project') ->inject('dbForProject') ->inject('hooks') - ->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { + ->inject('plan') + ->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) { $bcrypt = new Bcrypt(); $bcrypt->setCost(8); // Default cost - $user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); + $user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -330,10 +360,11 @@ Http::post('/v1/users/md5') ->inject('project') ->inject('dbForProject') ->inject('hooks') - ->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { + ->inject('plan') + ->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) { $md5 = new MD5(); - $user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); + $user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -367,10 +398,11 @@ Http::post('/v1/users/argon2') ->inject('project') ->inject('dbForProject') ->inject('hooks') - ->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { + ->inject('plan') + ->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) { $argon2 = new Argon2(); - $user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); + $user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -405,13 +437,14 @@ Http::post('/v1/users/sha') ->inject('project') ->inject('dbForProject') ->inject('hooks') - ->action(function (string $userId, string $email, string $password, string $passwordVersion, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { + ->inject('plan') + ->action(function (string $userId, string $email, string $password, string $passwordVersion, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) { $sha = new Sha(); if (!empty($passwordVersion)) { $sha->setVersion($passwordVersion); } - $user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); + $user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -445,10 +478,11 @@ Http::post('/v1/users/phpass') ->inject('project') ->inject('dbForProject') ->inject('hooks') - ->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { + ->inject('plan') + ->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) { $phpass = new PHPass(); - $user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); + $user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -487,7 +521,8 @@ Http::post('/v1/users/scrypt') ->inject('project') ->inject('dbForProject') ->inject('hooks') - ->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { + ->inject('plan') + ->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) { $scrypt = new Scrypt(); $scrypt ->setSalt($passwordSalt) @@ -496,7 +531,7 @@ Http::post('/v1/users/scrypt') ->setParallelCost($passwordParallel) ->setLength($passwordLength); - $user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); + $user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -533,14 +568,15 @@ Http::post('/v1/users/scrypt-modified') ->inject('project') ->inject('dbForProject') ->inject('hooks') - ->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { + ->inject('plan') + ->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) { $scryptModified = new ScryptModified(); $scryptModified ->setSalt($passwordSalt) ->setSaltSeparator($passwordSaltSeparator) ->setSignerKey($passwordSignerKey); - $user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); + $user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -1429,8 +1465,10 @@ Http::patch('/v1/users/:userId/email') ->param('email', '', new EmailValidator(allowEmpty: true), 'User email.') ->inject('response') ->inject('dbForProject') + ->inject('project') + ->inject('plan') ->inject('queueForEvents') - ->action(function (string $userId, string $email, Response $response, Database $dbForProject, Event $queueForEvents) { + ->action(function (string $userId, string $email, Response $response, Database $dbForProject, Document $project, array $plan, Event $queueForEvents) { $user = $dbForProject->getDocument('users', $userId); @@ -1461,20 +1499,47 @@ Http::patch('/v1/users/:userId/email') $oldEmail = $user->getAttribute('email'); + $emailMetadata = [ + 'emailCanonical' => null, + 'emailIsCanonical' => null, + 'emailIsCorporate' => null, + 'emailIsDisposable' => null, + 'emailIsFree' => null, + ]; + try { - $emailCanonical = new Email($email); - } catch (Throwable) { - $emailCanonical = null; + $parsedEmail = new Email($email); + $canonical = $parsedEmail->getCanonical(); + $emailMetadata = [ + 'emailCanonical' => $canonical, + 'emailIsCanonical' => $parsedEmail->get() === $canonical, + 'emailIsCorporate' => $parsedEmail->isCorporate(), + 'emailIsDisposable' => $parsedEmail->isDisposable(), + 'emailIsFree' => $parsedEmail->isFree(), + ]; + } catch (\Throwable) { + } + + if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) { + throw new Exception(Exception::USER_EMAIL_DISPOSABLE); + } + + if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) { + throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL); + } + + if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) { + throw new Exception(Exception::USER_EMAIL_FREE); } $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', $emailMetadata['emailCanonical']) + ->setAttribute('emailIsCanonical', $emailMetadata['emailIsCanonical']) + ->setAttribute('emailIsCorporate', $emailMetadata['emailIsCorporate']) + ->setAttribute('emailIsDisposable', $emailMetadata['emailIsDisposable']) + ->setAttribute('emailIsFree', $emailMetadata['emailIsFree']) ; try { diff --git a/app/worker.php b/app/worker.php index db036b6a99..b591064280 100644 --- a/app/worker.php +++ b/app/worker.php @@ -1,7 +1,6 @@ TOKEN_EXPIRATION_LOGIN_LONG, 'personalDataCheck' => false, 'disposableEmails' => false, + 'canonicalEmails' => false, + 'freeEmails' => false, 'mockNumbers' => [], 'sessionAlerts' => false, 'membershipsUserName' => false, diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php index 221c3aa521..2a139a541c 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php @@ -169,14 +169,41 @@ class Create extends Action throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } + $emailMetadata = [ + 'emailCanonical' => null, + 'emailIsCanonical' => null, + 'emailIsCorporate' => null, + 'emailIsDisposable' => null, + 'emailIsFree' => null, + ]; + try { - $userId = ID::unique(); - $hash = $proofForPassword->hash($proofForPassword->generate()); - $emailCanonical = new Email($email); - } catch (Throwable) { - $emailCanonical = null; + $parsedEmail = new Email($email); + $canonical = $parsedEmail->getCanonical(); + $emailMetadata = [ + 'emailCanonical' => $canonical, + 'emailIsCanonical' => $parsedEmail->get() === $canonical, + 'emailIsCorporate' => $parsedEmail->isCorporate(), + 'emailIsDisposable' => $parsedEmail->isDisposable(), + 'emailIsFree' => $parsedEmail->isFree(), + ]; + } catch (\Throwable) { } + if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) { + throw new Exception(Exception::USER_EMAIL_DISPOSABLE); + } + + if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) { + throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL); + } + + if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) { + throw new Exception(Exception::USER_EMAIL_FREE); + } + + $hash = $proofForPassword->hash($proofForPassword->generate()); + $userId = ID::unique(); $userDocument = new Document([ @@ -209,11 +236,11 @@ class Create extends Action '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' => $emailMetadata['emailCanonical'], + 'emailIsCanonical' => $emailMetadata['emailIsCanonical'], + 'emailIsCorporate' => $emailMetadata['emailIsCorporate'], + 'emailIsDisposable' => $emailMetadata['emailIsDisposable'], + 'emailIsFree' => $emailMetadata['emailIsFree'], ]); try { diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index 9fadca05a4..5902902e9e 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -137,6 +137,24 @@ class Project extends Model 'default' => false, 'example' => true, ]) + ->addRule('authDisposableEmails', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Whether or not to disallow disposable email addresses during signup and email updates.', + 'default' => false, + 'example' => true, + ]) + ->addRule('authCanonicalEmails', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Whether or not to require canonical email addresses during signup and email updates.', + 'default' => false, + 'example' => true, + ]) + ->addRule('authFreeEmails', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Whether or not to disallow free email addresses during signup and email updates.', + 'default' => false, + 'example' => true, + ]) ->addRule('authMockNumbers', [ 'type' => Response::MODEL_MOCK_NUMBER, 'description' => 'An array of mock numbers and their corresponding verification codes (OTPs).', @@ -416,6 +434,8 @@ class Project extends Model $document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false); $document->setAttribute('authPersonalDataCheck', $authValues['personalDataCheck'] ?? false); $document->setAttribute('authDisposableEmails', $authValues['disposableEmails'] ?? false); + $document->setAttribute('authCanonicalEmails', $authValues['canonicalEmails'] ?? false); + $document->setAttribute('authFreeEmails', $authValues['freeEmails'] ?? false); $document->setAttribute('authMockNumbers', $authValues['mockNumbers'] ?? []); $document->setAttribute('authSessionAlerts', $authValues['sessionAlerts'] ?? false); $document->setAttribute('authMembershipsUserName', $authValues['membershipsUserName'] ?? true); diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 52e59f3e72..d059f1cf50 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -60,6 +60,9 @@ class ProjectsConsoleClientTest extends Scope $this->assertArrayHasKey('platforms', $response['body']); $this->assertArrayHasKey('webhooks', $response['body']); $this->assertArrayHasKey('keys', $response['body']); + $this->assertEquals(false, $response['body']['authDisposableEmails']); + $this->assertEquals(false, $response['body']['authCanonicalEmails']); + $this->assertEquals(false, $response['body']['authFreeEmails']); $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', From e5385f75128f102885d00c3eed15bc3ad1a4b469 Mon Sep 17 00:00:00 2001 From: eldadfux Date: Sun, 15 Mar 2026 08:54:11 +0100 Subject: [PATCH 012/118] Removed old validator --- app/controllers/api/account.php | 2 +- app/controllers/api/projects.php | 2 +- app/controllers/api/users.php | 2 +- src/Appwrite/GraphQL/Types/Mapper.php | 1 - src/Appwrite/Network/Validator/Email.php | 79 ------------------- .../Modules/Teams/Http/Memberships/Create.php | 2 +- .../SDK/Specification/Format/OpenAPI3.php | 1 - .../SDK/Specification/Format/Swagger2.php | 1 - 8 files changed, 4 insertions(+), 86 deletions(-) delete mode 100644 src/Appwrite/Network/Validator/Email.php diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 2f66d76e4e..a2c0644fa6 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -16,7 +16,6 @@ use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; -use Appwrite\Network\Validator\Email as EmailValidator; use Appwrite\Network\Validator\Redirect; use Appwrite\OpenSSL\OpenSSL; use Appwrite\SDK\AuthType; @@ -60,6 +59,7 @@ use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; use Utopia\Emails\Email; +use Utopia\Emails\Validator\Email as EmailValidator; use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\Storage\Validator\FileName; diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 24a1b28cdd..2270ff1246 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -7,7 +7,6 @@ use Appwrite\Event\Mail; use Appwrite\Event\Validator\Event; use Appwrite\Extend\Exception; use Appwrite\Network\Platform; -use Appwrite\Network\Validator\Email; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; @@ -31,6 +30,7 @@ use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\UID; use Utopia\Domains\Validator\PublicDomain; +use Utopia\Emails\Validator\Email; use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\System\System; diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index e436a55300..f69832badb 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -15,7 +15,6 @@ use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; -use Appwrite\Network\Validator\Email as EmailValidator; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; @@ -60,6 +59,7 @@ use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; use Utopia\Emails\Email; +use Utopia\Emails\Validator\Email as EmailValidator; use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\System\System; diff --git a/src/Appwrite/GraphQL/Types/Mapper.php b/src/Appwrite/GraphQL/Types/Mapper.php index f288299884..037f80bcf7 100644 --- a/src/Appwrite/GraphQL/Types/Mapper.php +++ b/src/Appwrite/GraphQL/Types/Mapper.php @@ -274,7 +274,6 @@ class Mapper case \Appwrite\Event\Validator\Event::class: case \Appwrite\Event\Validator\FunctionEvent::class: case \Appwrite\Network\Validator\CNAME::class: - case \Appwrite\Network\Validator\Email::class: case \Utopia\Emails\Validator\Email::class: case \Appwrite\Network\Validator\Redirect::class: case \Appwrite\Network\Validator\DNS::class: diff --git a/src/Appwrite/Network/Validator/Email.php b/src/Appwrite/Network/Validator/Email.php deleted file mode 100644 index 3209a4aada..0000000000 --- a/src/Appwrite/Network/Validator/Email.php +++ /dev/null @@ -1,79 +0,0 @@ -allowEmpty = $allowEmpty; - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - return 'Value must be a valid email address'; - } - - /** - * Is valid - * - * Validation will pass when $value is valid email address. - * - * @param mixed $value - * @return bool - */ - public function isValid($value): bool - { - if ($this->allowEmpty && \strlen($value) === 0) { - return true; - } - - if (!\filter_var($value, FILTER_VALIDATE_EMAIL)) { - return false; - } - - return true; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return self::TYPE_STRING; - } -} diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php index 2a139a541c..f2eac72082 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php @@ -7,7 +7,6 @@ use Appwrite\Event\Event; use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Extend\Exception; -use Appwrite\Network\Validator\Email as EmailValidator; use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -32,6 +31,7 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Emails\Email; +use Utopia\Emails\Validator\Email as EmailValidator; use Utopia\Locale\Locale; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index f2a1d080a0..748a288297 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -450,7 +450,6 @@ class OpenAPI3 extends Format Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]', }; break; - case \Appwrite\Network\Validator\Email::class: case \Utopia\Emails\Validator\Email::class: $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'email'; diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index f256ceec91..17ec03f297 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -455,7 +455,6 @@ class Swagger2 extends Format Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]', }; break; - case \Appwrite\Network\Validator\Email::class: case \Utopia\Emails\Validator\Email::class: $node['type'] = $validator->getType(); $node['format'] = 'email'; From ea2b1519d5a68775d063d88a2438f3f42ab3f436 Mon Sep 17 00:00:00 2001 From: eldadfux Date: Sun, 15 Mar 2026 09:26:37 +0100 Subject: [PATCH 013/118] Updated deps --- composer.lock | 227 +++++++++++++++++++++++++------------------------- 1 file changed, 114 insertions(+), 113 deletions(-) diff --git a/composer.lock b/composer.lock index 0e25d3bc5f..81329d3450 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": "b99693284208ff3d006260a089a4f7b9", + "content-hash": "02892029fbc4800e4b7eb6953121efd5", "packages": [ { "name": "adhocore/jwt", @@ -3606,16 +3606,16 @@ }, { "name": "utopia-php/cache", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "7068870c086a6aea16173563a26b93ef3e408439" + "reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/7068870c086a6aea16173563a26b93ef3e408439", - "reference": "7068870c086a6aea16173563a26b93ef3e408439", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/05ceba981436a4022553f7aaa2a05fa049d0f71c", + "reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c", "shasum": "" }, "require": { @@ -3652,9 +3652,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/1.0.0" + "source": "https://github.com/utopia-php/cache/tree/1.0.1" }, - "time": "2026-01-28T10:55:44+00:00" + "time": "2026-03-12T03:39:09+00:00" }, { "name": "utopia-php/cli", @@ -3850,16 +3850,16 @@ }, { "name": "utopia-php/database", - "version": "5.3.8", + "version": "5.3.12", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "4920bb60afb98d4bd81f4d331765716ae1d40255" + "reference": "7e843989e67b1de3bfb27212334c28694bda4e95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/4920bb60afb98d4bd81f4d331765716ae1d40255", - "reference": "4920bb60afb98d4bd81f4d331765716ae1d40255", + "url": "https://api.github.com/repos/utopia-php/database/zipball/7e843989e67b1de3bfb27212334c28694bda4e95", + "reference": "7e843989e67b1de3bfb27212334c28694bda4e95", "shasum": "" }, "require": { @@ -3902,9 +3902,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/5.3.8" + "source": "https://github.com/utopia-php/database/tree/5.3.12" }, - "time": "2026-03-11T01:03:34+00:00" + "time": "2026-03-13T12:16:26+00:00" }, { "name": "utopia-php/detector", @@ -4058,16 +4058,16 @@ }, { "name": "utopia-php/domains", - "version": "1.0.2", + "version": "1.0.5", "source": { "type": "git", "url": "https://github.com/utopia-php/domains.git", - "reference": "b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6" + "reference": "0edf6bb2b07f30db849a267027077bf5abb994c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/domains/zipball/b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6", - "reference": "b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6", + "url": "https://api.github.com/repos/utopia-php/domains/zipball/0edf6bb2b07f30db849a267027077bf5abb994c6", + "reference": "0edf6bb2b07f30db849a267027077bf5abb994c6", "shasum": "" }, "require": { @@ -4114,9 +4114,9 @@ ], "support": { "issues": "https://github.com/utopia-php/domains/issues", - "source": "https://github.com/utopia-php/domains/tree/1.0.2" + "source": "https://github.com/utopia-php/domains/tree/1.0.5" }, - "time": "2026-02-25T08:18:25+00:00" + "time": "2026-03-03T09:20:50+00:00" }, { "name": "utopia-php/dsn", @@ -4167,16 +4167,16 @@ }, { "name": "utopia-php/emails", - "version": "0.6.8", + "version": "0.6.9", "source": { "type": "git", "url": "https://github.com/utopia-php/emails.git", - "reference": "25dfcd46ed47b862d2a7e7c98d92a3a4680b6f1b" + "reference": "3a59fb392a03a88f5497e5fdb0ea84a252a4dfdf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/emails/zipball/25dfcd46ed47b862d2a7e7c98d92a3a4680b6f1b", - "reference": "25dfcd46ed47b862d2a7e7c98d92a3a4680b6f1b", + "url": "https://api.github.com/repos/utopia-php/emails/zipball/3a59fb392a03a88f5497e5fdb0ea84a252a4dfdf", + "reference": "3a59fb392a03a88f5497e5fdb0ea84a252a4dfdf", "shasum": "" }, "require": { @@ -4222,9 +4222,9 @@ ], "support": { "issues": "https://github.com/utopia-php/emails/issues", - "source": "https://github.com/utopia-php/emails/tree/0.6.8" + "source": "https://github.com/utopia-php/emails/tree/0.6.9" }, - "time": "2026-02-09T12:31:56+00:00" + "time": "2026-03-14T13:52:56+00:00" }, { "name": "utopia-php/fetch", @@ -4572,16 +4572,16 @@ }, { "name": "utopia-php/mongo", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/mongo.git", - "reference": "45bedf36c2c946ec7a0a3e59b9f12f772de0b01d" + "reference": "83dbcde768d5fb40241f5ca8aa5ed8ca140a7469" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/mongo/zipball/45bedf36c2c946ec7a0a3e59b9f12f772de0b01d", - "reference": "45bedf36c2c946ec7a0a3e59b9f12f772de0b01d", + "url": "https://api.github.com/repos/utopia-php/mongo/zipball/83dbcde768d5fb40241f5ca8aa5ed8ca140a7469", + "reference": "83dbcde768d5fb40241f5ca8aa5ed8ca140a7469", "shasum": "" }, "require": { @@ -4627,9 +4627,9 @@ ], "support": { "issues": "https://github.com/utopia-php/mongo/issues", - "source": "https://github.com/utopia-php/mongo/tree/1.0.0" + "source": "https://github.com/utopia-php/mongo/tree/1.0.1" }, - "time": "2026-02-12T05:54:06+00:00" + "time": "2026-03-13T07:29:24+00:00" }, { "name": "utopia-php/platform", @@ -5215,29 +5215,28 @@ }, { "name": "utopia-php/vcs", - "version": "2.0.0", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14" + "reference": "5769679308bad498f2777547d48ab332166c4c0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/058049326e04a2a0c2f0ce8ad00c7e84825aba14", - "reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/5769679308bad498f2777547d48ab332166c4c0b", + "reference": "5769679308bad498f2777547d48ab332166c4c0b", "shasum": "" }, "require": { "adhocore/jwt": "^1.1", "php": ">=8.0", - "utopia-php/cache": "1.0.*", - "utopia-php/framework": "0.*.*", - "utopia-php/system": "0.10.*" + "utopia-php/cache": "1.0.*" }, "require-dev": { "laravel/pint": "1.*.*", "phpstan/phpstan": "1.*.*", - "phpunit/phpunit": "^9.4" + "phpunit/phpunit": "^9.4", + "utopia-php/system": "0.10.*" }, "type": "library", "autoload": { @@ -5258,9 +5257,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/2.0.0" + "source": "https://github.com/utopia-php/vcs/tree/2.0.2" }, - "time": "2026-02-25T11:36:45+00:00" + "time": "2026-03-13T15:25:16+00:00" }, { "name": "utopia-php/websocket", @@ -5438,16 +5437,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.11.1", + "version": "1.11.6", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb" + "reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6ff411f26f2750eea05c7598c14bb3a2ada898cb", - "reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38", + "reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38", "shasum": "" }, "require": { @@ -5483,22 +5482,22 @@ "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.11.1" + "source": "https://github.com/appwrite/sdk-generator/tree/1.11.6" }, - "time": "2026-02-25T07:15:19+00:00" + "time": "2026-03-09T07:12:51+00:00" }, { "name": "brianium/paratest", - "version": "v7.19.0", + "version": "v7.19.2", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6" + "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", - "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9", + "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9", "shasum": "" }, "require": { @@ -5512,9 +5511,9 @@ "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", "phpunit/php-file-iterator": "^6.0.1 || ^7", "phpunit/php-timer": "^8 || ^9", - "phpunit/phpunit": "^12.5.9 || ^13", + "phpunit/phpunit": "^12.5.14 || ^13.0.5", "sebastian/environment": "^8.0.3 || ^9", - "symfony/console": "^7.4.4 || ^8.0.4", + "symfony/console": "^7.4.7 || ^8.0.7", "symfony/process": "^7.4.5 || ^8.0.5" }, "require-dev": { @@ -5522,11 +5521,11 @@ "ext-pcntl": "*", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.38", - "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.12", - "phpstan/phpstan-strict-rules": "^2.0.8", - "symfony/filesystem": "^7.4.0 || ^8.0.1" + "phpstan/phpstan": "^2.1.40", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "symfony/filesystem": "^7.4.6 || ^8.0.6" }, "bin": [ "bin/paratest", @@ -5566,7 +5565,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.19.0" + "source": "https://github.com/paratestphp/paratest/tree/v7.19.2" }, "funding": [ { @@ -5578,7 +5577,7 @@ "type": "paypal" } ], - "time": "2026-02-06T10:53:26+00:00" + "time": "2026-03-09T14:33:17+00:00" }, { "name": "czproject/git-php", @@ -5921,16 +5920,16 @@ }, { "name": "laravel/pint", - "version": "v1.27.1", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5" + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/54cca2de13790570c7b6f0f94f37896bee4abcb5", - "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5", + "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", "shasum": "" }, "require": { @@ -5941,13 +5940,14 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.93.1", - "illuminate/view": "^12.51.0", - "larastan/larastan": "^3.9.2", + "friendsofphp/php-cs-fixer": "^3.94.2", + "illuminate/view": "^12.54.1", + "larastan/larastan": "^3.9.3", "laravel-zero/framework": "^12.0.5", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.3", - "pestphp/pest": "^3.8.5" + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.0" }, "bin": [ "builds/pint" @@ -5984,7 +5984,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-02-10T20:00:20+00:00" + "time": "2026-03-12T15:51:39+00:00" }, { "name": "matthiasmullie/minify", @@ -6398,16 +6398,16 @@ }, { "name": "phpbench/phpbench", - "version": "1.4.3", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/phpbench/phpbench.git", - "reference": "b641dde59d969ea42eed70a39f9b51950bc96878" + "reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpbench/phpbench/zipball/b641dde59d969ea42eed70a39f9b51950bc96878", - "reference": "b641dde59d969ea42eed70a39f9b51950bc96878", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/9a28fd0833f11171b949843c6fd663eb69b6d14c", + "reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c", "shasum": "" }, "require": { @@ -6418,7 +6418,7 @@ "ext-reflection": "*", "ext-spl": "*", "ext-tokenizer": "*", - "php": "^8.1", + "php": "^8.2", "phpbench/container": "^2.2", "psr/log": "^1.1 || ^2.0 || ^3.0", "seld/jsonlint": "^1.1", @@ -6438,8 +6438,9 @@ "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^10.4 || ^11.0", + "phpunit/phpunit": "^11.5", "rector/rector": "^1.2", + "sebastian/exporter": "^6.3.2", "symfony/error-handler": "^6.1 || ^7.0 || ^8.0", "symfony/var-dumper": "^6.1 || ^7.0 || ^8.0" }, @@ -6484,7 +6485,7 @@ ], "support": { "issues": "https://github.com/phpbench/phpbench/issues", - "source": "https://github.com/phpbench/phpbench/tree/1.4.3" + "source": "https://github.com/phpbench/phpbench/tree/1.5.1" }, "funding": [ { @@ -6492,15 +6493,15 @@ "type": "github" } ], - "time": "2025-11-06T19:07:31+00:00" + "time": "2026-03-05T08:18:58+00:00" }, { "name": "phpstan/phpstan", - "version": "1.12.32", + "version": "1.12.33", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", - "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1", + "reference": "37982d6fc7cbb746dda7773530cda557cdf119e1", "shasum": "" }, "require": { @@ -6545,7 +6546,7 @@ "type": "github" } ], - "time": "2025-09-30T10:16:31+00:00" + "time": "2026-02-28T20:30:03+00:00" }, { "name": "phpunit/php-code-coverage", @@ -7336,16 +7337,16 @@ }, { "name": "sebastian/environment", - "version": "8.0.3", + "version": "8.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", - "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", "shasum": "" }, "require": { @@ -7388,7 +7389,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.4" }, "funding": [ { @@ -7408,7 +7409,7 @@ "type": "tidelift" } ], - "time": "2025-08-12T14:11:56+00:00" + "time": "2026-03-15T07:05:40+00:00" }, { "name": "sebastian/exporter", @@ -8095,16 +8096,16 @@ }, { "name": "symfony/console", - "version": "v8.0.4", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b" + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b", - "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b", + "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", "shasum": "" }, "require": { @@ -8161,7 +8162,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.4" + "source": "https://github.com/symfony/console/tree/v8.0.7" }, "funding": [ { @@ -8181,20 +8182,20 @@ "type": "tidelift" } ], - "time": "2026-01-13T13:06:50+00:00" + "time": "2026-03-06T14:06:22+00:00" }, { "name": "symfony/filesystem", - "version": "v8.0.1", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d937d400b980523dc9ee946bb69972b5e619058d" + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d", - "reference": "d937d400b980523dc9ee946bb69972b5e619058d", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770", + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770", "shasum": "" }, "require": { @@ -8231,7 +8232,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v8.0.1" + "source": "https://github.com/symfony/filesystem/tree/v8.0.6" }, "funding": [ { @@ -8251,20 +8252,20 @@ "type": "tidelift" } ], - "time": "2025-12-01T09:13:36+00:00" + "time": "2026-02-25T16:59:43+00:00" }, { "name": "symfony/finder", - "version": "v8.0.5", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0" + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0", - "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0", + "url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c", + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c", "shasum": "" }, "require": { @@ -8299,7 +8300,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v8.0.5" + "source": "https://github.com/symfony/finder/tree/v8.0.6" }, "funding": [ { @@ -8319,7 +8320,7 @@ "type": "tidelift" } ], - "time": "2026-01-26T15:08:38+00:00" + "time": "2026-01-29T09:41:02+00:00" }, { "name": "symfony/options-resolver", @@ -8789,16 +8790,16 @@ }, { "name": "symfony/string", - "version": "v8.0.4", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "758b372d6882506821ed666032e43020c4f57194" + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194", - "reference": "758b372d6882506821ed666032e43020c4f57194", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", "shasum": "" }, "require": { @@ -8855,7 +8856,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.4" + "source": "https://github.com/symfony/string/tree/v8.0.6" }, "funding": [ { @@ -8875,7 +8876,7 @@ "type": "tidelift" } ], - "time": "2026-01-12T12:37:40+00:00" + "time": "2026-02-09T10:14:57+00:00" }, { "name": "textalk/websocket", @@ -9131,5 +9132,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } From aaa2a0525f26d539b7b8bb50573e6e03842ad412 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 16 Mar 2026 23:00:36 +0530 Subject: [PATCH 014/118] feat: migrate from static Http::setResource() to DI Container Upgrade utopia-php/framework from 0.33.x to 0.34.x which removes the static Http::setResource() API. Resources are now registered on a Utopia\DI\Container instance. - Replace 81 Http::setResource() calls in resources.php with $container->set() - Refactor http.php to use Swoole HttpServer adapter with shared container - Refactor realtime.php to use FPM adapter with global container - Refactor cli.php to use direct $cli->setResource() calls - Update Specs.php to use local container + FPM adapter - Update Migrate.php to inject console document instead of creating Http instance - Update GraphQL Schema.php to use instance setResource() --- app/cli.php | 58 ++-- app/http.php | 93 +++---- app/init/resources.php | 168 ++++++------ app/realtime.php | 11 +- composer.json | 12 +- composer.lock | 340 +++++++++++++----------- src/Appwrite/GraphQL/Schema.php | 2 +- src/Appwrite/Platform/Tasks/Migrate.php | 7 +- src/Appwrite/Platform/Tasks/Specs.php | 13 +- 9 files changed, 365 insertions(+), 339 deletions(-) diff --git a/app/cli.php b/app/cli.php index ee134b9487..619f700d91 100644 --- a/app/cli.php +++ b/app/cli.php @@ -24,7 +24,6 @@ use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; -use Utopia\DI\Dependency; use Utopia\DSN\DSN; use Utopia\Logger\Log; use Utopia\Platform\Service; @@ -59,18 +58,9 @@ $taskName = $args[0]; $platform->init(Service::TYPE_TASK); $cli = $platform->getCli(); -$setResource = function (string $name, callable $callback, array $injections = []) use ($cli) { - $dependency = new Dependency(); - $dependency->setName($name)->setCallback($callback); - foreach ($injections as $injection) { - $dependency->inject($injection); - } - $cli->setResource($dependency); -}; +$cli->setResource('register', fn () => $register, []); -$setResource('register', fn () => $register, []); - -$setResource('cache', function ($pools) { +$cli->setResource('cache', function ($pools) { $list = Config::getParam('pools-cache', []); $adapters = []; @@ -81,18 +71,18 @@ $setResource('cache', function ($pools) { return new Cache(new Sharding($adapters)); }, ['pools']); -$setResource('pools', function (Registry $register) { +$cli->setResource('pools', function (Registry $register) { return $register->get('pools'); }, ['register']); -$setResource('authorization', function () { +$cli->setResource('authorization', function () { $authorization = new Authorization(); $authorization->disable(); return $authorization; }, []); -$setResource('dbForPlatform', function ($pools, $cache, $authorization) { +$cli->setResource('dbForPlatform', function ($pools, $cache, $authorization) { $sleep = 3; $maxAttempts = 5; $attempts = 0; @@ -135,17 +125,17 @@ $setResource('dbForPlatform', function ($pools, $cache, $authorization) { return $dbForPlatform; }, ['pools', 'cache', 'authorization']); -$setResource('console', function () { +$cli->setResource('console', function () { return new Document(Config::getParam('console')); }, []); -$setResource( +$cli->setResource( 'isResourceBlocked', fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false, [] ); -$setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) { +$cli->setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) { $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) { @@ -207,7 +197,7 @@ $setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $c }; }, ['pools', 'dbForPlatform', 'cache', 'authorization']); -$setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { +$cli->setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { $database = null; return function (?Document $project = null) use ($pools, $cache, $database, $authorization) { @@ -236,41 +226,41 @@ $setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $a return $database; }; }, ['pools', 'cache', 'authorization']); -$setResource('publisher', function (Group $pools) { +$cli->setResource('publisher', function (Group $pools) { return new BrokerPool(publisher: $pools->get('publisher')); }, ['pools']); -$setResource('publisherDatabases', function (BrokerPool $publisher) { +$cli->setResource('publisherDatabases', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$setResource('publisherFunctions', function (BrokerPool $publisher) { +$cli->setResource('publisherFunctions', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$setResource('publisherMigrations', function (BrokerPool $publisher) { +$cli->setResource('publisherMigrations', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$setResource('publisherMessaging', function (BrokerPool $publisher) { +$cli->setResource('publisherMessaging', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$setResource('usage', function () { +$cli->setResource('usage', function () { return new UsageContext(); }, []); -$setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher( +$cli->setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher( $publisher, new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME)) ), ['publisher']); -$setResource('queueForStatsResources', function (Publisher $publisher) { +$cli->setResource('queueForStatsResources', function (Publisher $publisher) { return new StatsResources($publisher); }, ['publisher']); -$setResource('queueForFunctions', function (Publisher $publisher) { +$cli->setResource('queueForFunctions', function (Publisher $publisher) { return new Func($publisher); }, ['publisher']); -$setResource('queueForDeletes', function (Publisher $publisher) { +$cli->setResource('queueForDeletes', function (Publisher $publisher) { return new Delete($publisher); }, ['publisher']); -$setResource('queueForCertificates', function (Publisher $publisher) { +$cli->setResource('queueForCertificates', function (Publisher $publisher) { return new Certificate($publisher); }, ['publisher']); -$setResource('logError', function (Registry $register) { +$cli->setResource('logError', function (Registry $register) { return function (Throwable $error, string $namespace, string $action) use ($register) { Console::error('[Error] Timestamp: ' . date('c', time())); Console::error('[Error] Type: ' . get_class($error)); @@ -322,13 +312,13 @@ $setResource('logError', function (Registry $register) { }; }, ['register']); -$setResource('executor', fn () => new Executor(), []); +$cli->setResource('executor', fn () => new Executor(), []); -$setResource('bus', function (Registry $register) use ($cli) { +$cli->setResource('bus', function (Registry $register) use ($cli) { return $register->get('bus')->setResolver(fn (string $name) => $cli->getResource($name)); }, ['register']); -$setResource('telemetry', fn () => new NoTelemetry(), []); +$cli->setResource('telemetry', fn () => new NoTelemetry(), []); $cli ->error() diff --git a/app/http.php b/app/http.php index 1302940856..5cc7d1560d 100644 --- a/app/http.php +++ b/app/http.php @@ -1,13 +1,11 @@ column('value', Table::TYPE_INT, 1); $certifiedDomains->create(); -Http::setResource('riskyDomains', fn () => $riskyDomains); -Http::setResource('certifiedDomains', fn () => $certifiedDomains); - -$http = new Server( - host: "0.0.0.0", - port: System::getEnv('PORT', 80), - mode: SWOOLE_PROCESS, -); +global $container; +$container->set('riskyDomains', fn () => $riskyDomains); +$container->set('certifiedDomains', fn () => $certifiedDomains); $payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing $totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6)); +$swooleAdapter = new HttpServer( + host: "0.0.0.0", + port: System::getEnv('PORT', 80), + settings: [ + Constant::OPTION_WORKER_NUM => $totalWorkers, + Constant::OPTION_DISPATCH_FUNC => dispatch(...), + Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD, + Constant::OPTION_HTTP_COMPRESSION => false, + Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize, + Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize, + Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background + ], + container: $container, +); + +$http = $swooleAdapter->getServer(); + /** * Assigns HTTP requests to worker threads by analyzing its payload/content. * @@ -160,18 +171,6 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int return $workerId; } - -$http - ->set([ - Constant::OPTION_WORKER_NUM => $totalWorkers, - Constant::OPTION_DISPATCH_FUNC => dispatch(...), - Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD, - Constant::OPTION_HTTP_COMPRESSION => false, - Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize, - Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize, - Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background - ]); - $http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) { }); @@ -188,9 +187,9 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) { Console::success('Reload completed...'); }); -Http::setResource('bus', function ($register, $utopia) { - return $register->get('bus')->setResolver(fn (string $name) => $utopia->getResource($name)); -}, ['register', 'utopia']); +$container->set('bus', function ($register) use ($swooleAdapter) { + return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name)); +}, ['register']); include __DIR__ . '/controllers/general.php'; @@ -286,13 +285,15 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c Span::current()?->finish(); } -$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $totalWorkers, $register) { - $app = new Http('UTC'); +$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $totalWorkers, $register, $swooleAdapter) { + global $container; + $pools = $register->get('pools'); + /** @var Group $pools */ + $container->set('pools', fn () => $pools); - go(function () use ($register, $app) { - $pools = $register->get('pools'); - /** @var Group $pools */ - Http::setResource('pools', fn () => $pools); + $app = new Http($swooleAdapter, 'UTC'); + + go(function () use ($register, $app, $pools) { /** @var array $collections */ $collections = Config::getParam('collections', []); @@ -492,14 +493,11 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot }); }); -$http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register, $files) { +$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($register, $files, $swooleAdapter) { Span::init('http.request'); - Http::setResource('swooleRequest', fn () => $swooleRequest); - Http::setResource('swooleResponse', fn () => $swooleResponse); - - $request = new Request($swooleRequest); - $response = new Response($swooleResponse); + $request = new Request($utopiaRequest->getSwooleRequest()); + $response = new Response($utopiaResponse->getSwooleResponse()); Span::add('http.method', $request->getMethod()); @@ -515,13 +513,13 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool return; } - $app = new Http('UTC'); + $pools = $register->get('pools'); + $swooleAdapter->getContainer()->set('pools', fn () => $pools); + + $app = new Http($swooleAdapter, 'UTC'); $app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled'); $app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB - $pools = $register->get('pools'); - Http::setResource('pools', fn () => $pools); - try { $authorization = $app->getResource('authorization'); @@ -605,6 +603,7 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool } } + $swooleResponse = $utopiaResponse->getSwooleResponse(); $swooleResponse->setStatusCode(500); $output = ((Http::isDevelopment())) ? [ @@ -628,11 +627,13 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool }); // Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory -$http->on(Constant::EVENT_TASK, function () use ($register) { +$http->on(Constant::EVENT_TASK, function () use ($register, $swooleAdapter) { + global $container; $lastSyncUpdate = null; $pools = $register->get('pools'); - Http::setResource('pools', fn () => $pools); - $app = new Http('UTC'); + $container->set('pools', fn () => $pools); + + $app = new Http($swooleAdapter, 'UTC'); /** @var Utopia\Database\Database $dbForPlatform */ $dbForPlatform = $app->getResource('dbForPlatform'); @@ -707,4 +708,4 @@ $http->on(Constant::EVENT_TASK, function () use ($register) { }); }); -$http->start(); +$swooleAdapter->start(); diff --git a/app/init/resources.php b/app/init/resources.php index 1bab4491a4..8556cbeb0f 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -52,6 +52,7 @@ use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\DSN\DSN; +use Utopia\DI\Container; use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\Logger\Log; @@ -76,117 +77,120 @@ use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub as VcsGitHub; // Runtime Execution -Http::setResource('log', fn () => new Log()); -Http::setResource('logger', function ($register) { +global $register; +global $container; +$container = new Container(); + +$container->set('log', fn () => new Log()); +$container->set('logger', function ($register) { return $register->get('logger'); }, ['register']); -Http::setResource('hooks', function ($register) { +$container->set('hooks', function ($register) { return $register->get('hooks'); }, ['register']); -global $register; -Http::setResource('register', fn () => $register); -Http::setResource('locale', function () { +$container->set('register', fn () => $register); +$container->set('locale', function () { $locale = new Locale(System::getEnv('_APP_LOCALE', 'en')); $locale->setFallback(System::getEnv('_APP_LOCALE', 'en')); return $locale; }); -Http::setResource('localeCodes', function () { +$container->set('localeCodes', function () { return array_map(fn ($locale) => $locale['code'], Config::getParam('locale-codes', [])); }); // Queues -Http::setResource('publisher', function (Group $pools) { +$container->set('publisher', function (Group $pools) { return new BrokerPool(publisher: $pools->get('publisher')); }, ['pools']); -Http::setResource('publisherDatabases', function (Publisher $publisher) { +$container->set('publisherDatabases', function (Publisher $publisher) { return $publisher; }, ['publisher']); -Http::setResource('publisherFunctions', function (Publisher $publisher) { +$container->set('publisherFunctions', function (Publisher $publisher) { return $publisher; }, ['publisher']); -Http::setResource('publisherMigrations', function (Publisher $publisher) { +$container->set('publisherMigrations', function (Publisher $publisher) { return $publisher; }, ['publisher']); -Http::setResource('publisherMails', function (Publisher $publisher) { +$container->set('publisherMails', function (Publisher $publisher) { return $publisher; }, ['publisher']); -Http::setResource('publisherDeletes', function (Publisher $publisher) { +$container->set('publisherDeletes', function (Publisher $publisher) { return $publisher; }, ['publisher']); -Http::setResource('publisherMessaging', function (Publisher $publisher) { +$container->set('publisherMessaging', function (Publisher $publisher) { return $publisher; }, ['publisher']); -Http::setResource('publisherWebhooks', function (Publisher $publisher) { +$container->set('publisherWebhooks', function (Publisher $publisher) { return $publisher; }, ['publisher']); -Http::setResource('queueForMessaging', function (Publisher $publisher) { +$container->set('queueForMessaging', function (Publisher $publisher) { return new Messaging($publisher); }, ['publisher']); -Http::setResource('queueForMails', function (Publisher $publisher) { +$container->set('queueForMails', function (Publisher $publisher) { return new Mail($publisher); }, ['publisher']); -Http::setResource('queueForBuilds', function (Publisher $publisher) { +$container->set('queueForBuilds', function (Publisher $publisher) { return new Build($publisher); }, ['publisher']); -Http::setResource('queueForScreenshots', function (Publisher $publisher) { +$container->set('queueForScreenshots', function (Publisher $publisher) { return new Screenshot($publisher); }, ['publisher']); -Http::setResource('queueForDatabase', function (Publisher $publisher) { +$container->set('queueForDatabase', function (Publisher $publisher) { return new EventDatabase($publisher); }, ['publisher']); -Http::setResource('queueForDeletes', function (Publisher $publisher) { +$container->set('queueForDeletes', function (Publisher $publisher) { return new Delete($publisher); }, ['publisher']); -Http::setResource('queueForEvents', function (Publisher $publisher) { +$container->set('queueForEvents', function (Publisher $publisher) { return new Event($publisher); }, ['publisher']); -Http::setResource('queueForWebhooks', function (Publisher $publisher) { +$container->set('queueForWebhooks', function (Publisher $publisher) { return new Webhook($publisher); }, ['publisher']); -Http::setResource('queueForRealtime', function () { +$container->set('queueForRealtime', function () { return new Realtime(); }, []); -Http::setResource('usage', function () { +$container->set('usage', function () { return new UsageContext(); }, []); -Http::setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher( +$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher( $publisher, new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME)) ), ['publisher']); -Http::setResource('queueForAudits', function (Publisher $publisher) { +$container->set('queueForAudits', function (Publisher $publisher) { return new AuditEvent($publisher); }, ['publisher']); -Http::setResource('queueForFunctions', function (Publisher $publisher) { +$container->set('queueForFunctions', function (Publisher $publisher) { return new Func($publisher); }, ['publisher']); -Http::setResource('eventProcessor', function () { +$container->set('eventProcessor', function () { return new EventProcessor(); }, []); -Http::setResource('queueForCertificates', function (Publisher $publisher) { +$container->set('queueForCertificates', function (Publisher $publisher) { return new Certificate($publisher); }, ['publisher']); -Http::setResource('queueForMigrations', function (Publisher $publisher) { +$container->set('queueForMigrations', function (Publisher $publisher) { return new Migration($publisher); }, ['publisher']); -Http::setResource('queueForStatsResources', function (Publisher $publisher) { +$container->set('queueForStatsResources', function (Publisher $publisher) { return new StatsResources($publisher); }, ['publisher']); /** * Platform configuration */ -Http::setResource('platform', function () { +$container->set('platform', function () { return Config::getParam('platform', []); }, []); /** * List of allowed request hostnames for the request. */ -Http::setResource('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) { +$container->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) { $allowed = [...($platform['hostnames'] ?? [])]; /* Add platform configured hostnames */ @@ -230,7 +234,7 @@ Http::setResource('allowedHostnames', function (array $platform, Document $proje /** * List of allowed request schemes for the request. */ -Http::setResource('allowedSchemes', function (array $platform, Document $project) { +$container->set('allowedSchemes', function (array $platform, Document $project) { $allowed = [...($platform['schemas'] ?? [])]; if (! $project->isEmpty() && $project->getId() !== 'console') { @@ -250,7 +254,7 @@ Http::setResource('allowedSchemes', function (array $platform, Document $project /** * Rule associated with a request origin. */ -Http::setResource('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) { +$container->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) { $domain = \parse_url($request->getOrigin(), PHP_URL_HOST); if (empty($domain)) { @@ -300,7 +304,7 @@ Http::setResource('rule', function (Request $request, Database $dbForPlatform, D /** * CORS service */ -Http::setResource('cors', function (array $allowedHostnames) { +$container->set('cors', function (array $allowedHostnames) { $corsConfig = Config::getParam('cors'); return new Cors( @@ -312,7 +316,7 @@ Http::setResource('cors', function (array $allowedHostnames) { ); }, ['allowedHostnames']); -Http::setResource('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) { +$container->set('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) { if (! $devKey->isEmpty()) { return new URL(); } @@ -320,7 +324,7 @@ Http::setResource('originValidator', function (Document $devKey, array $allowedH return new Origin($allowedHostnames, $allowedSchemes); }, ['devKey', 'allowedHostnames', 'allowedSchemes']); -Http::setResource('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) { +$container->set('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) { if (! $devKey->isEmpty()) { return new URL(); } @@ -328,7 +332,7 @@ Http::setResource('redirectValidator', function (Document $devKey, array $allowe return new Redirect($allowedHostnames, $allowedSchemes); }, ['devKey', 'allowedHostnames', 'allowedSchemes']); -Http::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) { +$container->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) { /** * Handles user authentication and session validation. * @@ -476,7 +480,7 @@ Http::setResource('user', function (string $mode, Document $project, Document $c return $user; }, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']); -Http::setResource('project', function ($dbForPlatform, $request, $console, $authorization) { +$container->set('project', function ($dbForPlatform, $request, $console, $authorization) { /** @var Appwrite\Utopia\Request $request */ /** @var Utopia\Database\Database $dbForPlatform */ /** @var Utopia\Database\Document $console */ @@ -495,7 +499,7 @@ Http::setResource('project', function ($dbForPlatform, $request, $console, $auth return $project; }, ['dbForPlatform', 'request', 'console', 'authorization']); -Http::setResource('session', function (User $user, Store $store, Token $proofForToken) { +$container->set('session', function (User $user, Store $store, Token $proofForToken) { if ($user->isEmpty()) { return; } @@ -515,11 +519,11 @@ Http::setResource('session', function (User $user, Store $store, Token $proofFor }, ['user', 'store', 'proofForToken']); -Http::setResource('store', function (): Store { +$container->set('store', function (): Store { return new Store(); }); -Http::setResource('proofForPassword', function (): Password { +$container->set('proofForPassword', function (): Password { $hash = new Argon2(); $hash ->setMemoryCost(7168) @@ -533,29 +537,29 @@ Http::setResource('proofForPassword', function (): Password { return $password; }); -Http::setResource('proofForToken', function (): Token { +$container->set('proofForToken', function (): Token { $token = new Token(); $token->setHash(new Sha()); return $token; }); -Http::setResource('proofForCode', function (): Code { +$container->set('proofForCode', function (): Code { $code = new Code(); $code->setHash(new Sha()); return $code; }); -Http::setResource('console', function () { +$container->set('console', function () { return new Document(Config::getParam('console')); }, []); -Http::setResource('authorization', function () { +$container->set('authorization', function () { return new Authorization(); }, []); -Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization) { +$container->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization) { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForPlatform; } @@ -794,7 +798,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor return $database; }, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization']); -Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) { +$container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) { $adapter = new DatabasePool($pools->get('console')); $database = new Database($adapter, $cache); @@ -813,7 +817,7 @@ Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authori return $database; }, ['pools', 'cache', 'authorization']); -Http::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) { +$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) { $databases = []; return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) { @@ -874,7 +878,7 @@ Http::setResource('getProjectDB', function (Group $pools, Database $dbForPlatfor }; }, ['pools', 'dbForPlatform', 'cache', 'authorization']); -Http::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { +$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { $database = null; return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) { @@ -904,15 +908,15 @@ Http::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizati }; }, ['pools', 'cache', 'authorization']); -Http::setResource('audit', function ($dbForProject) { +$container->set('audit', function ($dbForProject) { $adapter = new AdapterDatabase($dbForProject); return new Audit($adapter); }, ['dbForProject']); -Http::setResource('telemetry', fn () => new NoTelemetry()); +$container->set('telemetry', fn () => new NoTelemetry()); -Http::setResource('cache', function (Group $pools, Telemetry $telemetry) { +$container->set('cache', function (Group $pools, Telemetry $telemetry) { $list = Config::getParam('pools-cache', []); $adapters = []; @@ -926,7 +930,7 @@ Http::setResource('cache', function (Group $pools, Telemetry $telemetry) { return $cache; }, ['pools', 'telemetry']); -Http::setResource('redis', function () { +$container->set('redis', function () { $host = System::getEnv('_APP_REDIS_HOST', 'localhost'); $port = System::getEnv('_APP_REDIS_PORT', 6379); $pass = System::getEnv('_APP_REDIS_PASS', ''); @@ -941,28 +945,28 @@ Http::setResource('redis', function () { return $redis; }); -Http::setResource('timelimit', function (\Redis $redis) { +$container->set('timelimit', function (\Redis $redis) { return function (string $key, int $limit, int $time) use ($redis) { return new TimeLimitRedis($key, $limit, $time, $redis); }; }, ['redis']); -Http::setResource('deviceForLocal', function (Telemetry $telemetry) { +$container->set('deviceForLocal', function (Telemetry $telemetry) { return new Device\Telemetry($telemetry, new Local()); }, ['telemetry']); -Http::setResource('deviceForFiles', function ($project, Telemetry $telemetry) { +$container->set('deviceForFiles', function ($project, Telemetry $telemetry) { return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); }, ['project', 'telemetry']); -Http::setResource('deviceForSites', function ($project, Telemetry $telemetry) { +$container->set('deviceForSites', function ($project, Telemetry $telemetry) { return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())); }, ['project', 'telemetry']); -Http::setResource('deviceForMigrations', function ($project, Telemetry $telemetry) { +$container->set('deviceForMigrations', function ($project, Telemetry $telemetry) { return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())); }, ['project', 'telemetry']); -Http::setResource('deviceForFunctions', function ($project, Telemetry $telemetry) { +$container->set('deviceForFunctions', function ($project, Telemetry $telemetry) { return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())); }, ['project', 'telemetry']); -Http::setResource('deviceForBuilds', function ($project, Telemetry $telemetry) { +$container->set('deviceForBuilds', function ($project, Telemetry $telemetry) { return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); }, ['project', 'telemetry']); @@ -1073,7 +1077,7 @@ function getDevice(string $root, string $connection = ''): Device } } -Http::setResource('mode', function ($request) { +$container->set('mode', function ($request) { /** @var Appwrite\Utopia\Request $request */ /** @@ -1084,17 +1088,17 @@ Http::setResource('mode', function ($request) { return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); }, ['request']); -Http::setResource('geodb', function ($register) { +$container->set('geodb', function ($register) { /** @var Utopia\Registry\Registry $register */ return $register->get('geodb'); }, ['register']); -Http::setResource('passwordsDictionary', function ($register) { +$container->set('passwordsDictionary', function ($register) { /** @var Utopia\Registry\Registry $register */ return $register->get('passwordsDictionary'); }, ['register']); -Http::setResource('servers', function () { +$container->set('servers', function () { $platforms = Config::getParam('sdks'); $server = $platforms[APP_SDK_PLATFORM_SERVER]; @@ -1105,11 +1109,11 @@ Http::setResource('servers', function () { return $languages; }); -Http::setResource('promiseAdapter', function ($register) { +$container->set('promiseAdapter', function ($register) { return $register->get('promiseAdapter'); }, ['register']); -Http::setResource('schema', function ($utopia, $dbForProject, $authorization) { +$container->set('schema', function ($utopia, $dbForProject, $authorization) { $complexity = function (int $complexity, array $args) { $queries = Query::parseQueries($args['queries'] ?? []); @@ -1196,11 +1200,11 @@ Http::setResource('schema', function ($utopia, $dbForProject, $authorization) { ); }, ['utopia', 'dbForProject', 'authorization']); -Http::setResource('gitHub', function (Cache $cache) { +$container->set('gitHub', function (Cache $cache) { return new VcsGitHub($cache); }, ['cache']); -Http::setResource('requestTimestamp', function ($request) { +$container->set('requestTimestamp', function ($request) { // TODO: Move this to the Request class itself $timestampHeader = $request->getHeader('x-appwrite-timestamp'); $requestTimestamp = null; @@ -1215,15 +1219,15 @@ Http::setResource('requestTimestamp', function ($request) { return $requestTimestamp; }, ['request']); -Http::setResource('plan', function (array $plan = []) { +$container->set('plan', function (array $plan = []) { return []; }); -Http::setResource('smsRates', function () { +$container->set('smsRates', function () { return []; }); -Http::setResource('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) { +$container->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) { $devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', '')); // Check if given key match project's development keys @@ -1272,7 +1276,7 @@ Http::setResource('devKey', function (Request $request, Document $project, array return $key; }, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']); -Http::setResource('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) { +$container->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) { $teamInternalId = ''; if ($project->getId() !== 'console') { $teamInternalId = $project->getAttribute('teamInternalId', ''); @@ -1315,12 +1319,12 @@ Http::setResource('team', function (Document $project, Database $dbForPlatform, return $team; }, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']); -Http::setResource( +$container->set( 'isResourceBlocked', fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false ); -Http::setResource('previewHostname', function (Request $request, ?Key $apiKey) { +$container->set('previewHostname', function (Request $request, ?Key $apiKey) { $allowed = false; if (Http::isDevelopment()) { @@ -1339,7 +1343,7 @@ Http::setResource('previewHostname', function (Request $request, ?Key $apiKey) { return ''; }, ['request', 'apiKey']); -Http::setResource('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key { +$container->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key { $key = $request->getHeader('x-appwrite-key'); if (empty($key)) { @@ -1373,9 +1377,9 @@ Http::setResource('apiKey', function (Request $request, Document $project, Docum return $key; }, ['request', 'project', 'team', 'user']); -Http::setResource('executor', fn () => new Executor()); +$container->set('executor', fn () => new Executor()); -Http::setResource('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) { +$container->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) { $tokenJWT = $request->getParam('token'); if (! empty($tokenJWT) && ! $project->isEmpty()) { // JWT authentication @@ -1442,11 +1446,11 @@ Http::setResource('resourceToken', function ($project, $dbForProject, $request, return new Document([]); }, ['project', 'dbForProject', 'request', 'authorization']); -Http::setResource('transactionState', function (Database $dbForProject, Authorization $authorization) { +$container->set('transactionState', function (Database $dbForProject, Authorization $authorization) { return new TransactionState($dbForProject, $authorization); }, ['dbForProject', 'authorization']); -Http::setResource('executionsRetentionCount', function (Document $project, array $plan) { +$container->set('executionsRetentionCount', function (Document $project, array $plan) { if ($project->getId() === 'console' || empty($plan)) { return 0; } diff --git a/app/realtime.php b/app/realtime.php index 5addb2a78f..f05133c875 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -33,6 +33,7 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\DI\Container; use Utopia\DSN\DSN; use Utopia\Http\Http; use Utopia\Logger\Log; @@ -606,15 +607,17 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, }); $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) { - $app = new Http('UTC'); + global $container; $request = new Request($request); $response = new Response(new SwooleResponse()); Console::info("Connection open (user: {$connection})"); - Http::setResource('pools', fn () => $register->get('pools')); - Http::setResource('request', fn () => $request); - Http::setResource('response', fn () => $response); + $container->set('pools', fn () => $register->get('pools')); + $adapter = new \Utopia\Http\Adapter\FPM\Server($container); + $app = new Http($adapter, 'UTC'); + $app->setResource('request', fn () => $request); + $app->setResource('response', fn () => $response); $project = null; $logUser = null; diff --git a/composer.json b/composer.json index d74103d8ee..cab557a1e8 100644 --- a/composer.json +++ b/composer.json @@ -53,29 +53,29 @@ "utopia-php/audit": "2.2.*", "utopia-php/auth": "0.5.*", "utopia-php/cache": "1.0.*", - "utopia-php/cli": "0.22.*", + "utopia-php/cli": "0.23.*", "utopia-php/compression": "0.1.*", "utopia-php/config": "1.*", "utopia-php/console": "0.1.*", - "utopia-php/database": "5.*", + "utopia-php/database": "dev-main as 5.3.15", "utopia-php/detector": "0.2.*", "utopia-php/domains": "1.*", "utopia-php/emails": "0.6.*", "utopia-php/dns": "1.6.*", "utopia-php/dsn": "0.2.1", - "utopia-php/framework": "0.33.*", + "utopia-php/framework": "dev-feat/coroutines-option as 0.34.15", "utopia-php/fetch": "0.5.*", "utopia-php/image": "0.8.*", "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", "utopia-php/migration": "1.7.*", - "utopia-php/platform": "0.7.*", + "utopia-php/platform": "0.9.*", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", "utopia-php/preloader": "0.2.*", - "utopia-php/queue": "0.15.*", - "utopia-php/servers": "0.2.5", + "utopia-php/queue": "0.16.*", + "utopia-php/servers": "0.3.*", "utopia-php/registry": "0.5.*", "utopia-php/storage": "1.0.*", "utopia-php/system": "0.10.*", diff --git a/composer.lock b/composer.lock index d494aa8d4b..4d4f1015eb 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": "ff0d44c80f0ea7ff7d700533e5d81786", + "content-hash": "5848ab9fef2b8aefac2e80f54bd1f146", "packages": [ { "name": "adhocore/jwt", @@ -3606,16 +3606,16 @@ }, { "name": "utopia-php/cache", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "7068870c086a6aea16173563a26b93ef3e408439" + "reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/7068870c086a6aea16173563a26b93ef3e408439", - "reference": "7068870c086a6aea16173563a26b93ef3e408439", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/05ceba981436a4022553f7aaa2a05fa049d0f71c", + "reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c", "shasum": "" }, "require": { @@ -3652,27 +3652,27 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/1.0.0" + "source": "https://github.com/utopia-php/cache/tree/1.0.1" }, - "time": "2026-01-28T10:55:44+00:00" + "time": "2026-03-12T03:39:09+00:00" }, { "name": "utopia-php/cli", - "version": "0.22.0", + "version": "0.23.0", "source": { "type": "git", "url": "https://github.com/utopia-php/cli.git", - "reference": "a7ac387ee626fd27075a87e836fb72c5be38add4" + "reference": "4efef2662b27cdce0d6d09ea5c3a16a1cca2ba6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cli/zipball/a7ac387ee626fd27075a87e836fb72c5be38add4", - "reference": "a7ac387ee626fd27075a87e836fb72c5be38add4", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/4efef2662b27cdce0d6d09ea5c3a16a1cca2ba6c", + "reference": "4efef2662b27cdce0d6d09ea5c3a16a1cca2ba6c", "shasum": "" }, "require": { "php": ">=7.4", - "utopia-php/servers": "0.2.*" + "utopia-php/servers": "0.3.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -3703,9 +3703,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cli/issues", - "source": "https://github.com/utopia-php/cli/tree/0.22.0" + "source": "https://github.com/utopia-php/cli/tree/0.23.0" }, - "time": "2025-10-21T10:42:45+00:00" + "time": "2026-03-13T12:23:18+00:00" }, { "name": "utopia-php/compression", @@ -3850,16 +3850,16 @@ }, { "name": "utopia-php/database", - "version": "5.3.8", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "4920bb60afb98d4bd81f4d331765716ae1d40255" + "reference": "bb89e8c4a5d534fc7e650c4438aa796667fe160a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/4920bb60afb98d4bd81f4d331765716ae1d40255", - "reference": "4920bb60afb98d4bd81f4d331765716ae1d40255", + "url": "https://api.github.com/repos/utopia-php/database/zipball/bb89e8c4a5d534fc7e650c4438aa796667fe160a", + "reference": "bb89e8c4a5d534fc7e650c4438aa796667fe160a", "shasum": "" }, "require": { @@ -3868,9 +3868,10 @@ "ext-pdo": "*", "php": ">=8.4", "utopia-php/cache": "1.*", - "utopia-php/framework": "0.33.*", + "utopia-php/console": "0.1.*", "utopia-php/mongo": "1.*", - "utopia-php/pools": "1.*" + "utopia-php/pools": "1.*", + "utopia-php/validators": "0.2.*" }, "require-dev": { "fakerphp/faker": "1.23.*", @@ -3880,8 +3881,9 @@ "phpunit/phpunit": "9.*", "rregeer/phpunit-coverage-check": "0.3.*", "swoole/ide-helper": "5.1.3", - "utopia-php/cli": "0.14.*" + "utopia-php/cli": "0.22.*" }, + "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -3902,9 +3904,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/5.3.8" + "source": "https://github.com/utopia-php/database/tree/main" }, - "time": "2026-03-11T01:03:34+00:00" + "time": "2026-03-16T11:41:45+00:00" }, { "name": "utopia-php/detector", @@ -3953,25 +3955,26 @@ }, { "name": "utopia-php/di", - "version": "0.1.0", + "version": "0.3.1", "source": { "type": "git", "url": "https://github.com/utopia-php/di.git", - "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31" + "reference": "68873b7267842315d01d82a83b988bae525eab31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/di/zipball/22490c95f7ac3898ed1c33f1b1b5dd577305ee31", - "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31", + "url": "https://api.github.com/repos/utopia-php/di/zipball/68873b7267842315d01d82a83b988bae525eab31", + "reference": "68873b7267842315d01d82a83b988bae525eab31", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.2", + "psr/container": "^2.0" }, "require-dev": { - "laravel/pint": "^1.2", + "laravel/pint": "^1.27", "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^9.5.25", "swoole/ide-helper": "4.8.3" }, @@ -3988,16 +3991,18 @@ ], "description": "A simple and lite library for managing dependency injections", "keywords": [ - "framework", - "http", + "PSR-11", + "container", + "dependency-injection", + "di", "php", - "upf" + "utopia" ], "support": { "issues": "https://github.com/utopia-php/di/issues", - "source": "https://github.com/utopia-php/di/tree/0.1.0" + "source": "https://github.com/utopia-php/di/tree/0.3.1" }, - "time": "2024-08-08T14:35:19+00:00" + "time": "2026-03-13T05:47:23+00:00" }, { "name": "utopia-php/dns", @@ -4058,16 +4063,16 @@ }, { "name": "utopia-php/domains", - "version": "1.0.2", + "version": "1.0.5", "source": { "type": "git", "url": "https://github.com/utopia-php/domains.git", - "reference": "b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6" + "reference": "0edf6bb2b07f30db849a267027077bf5abb994c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/domains/zipball/b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6", - "reference": "b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6", + "url": "https://api.github.com/repos/utopia-php/domains/zipball/0edf6bb2b07f30db849a267027077bf5abb994c6", + "reference": "0edf6bb2b07f30db849a267027077bf5abb994c6", "shasum": "" }, "require": { @@ -4114,9 +4119,9 @@ ], "support": { "issues": "https://github.com/utopia-php/domains/issues", - "source": "https://github.com/utopia-php/domains/tree/1.0.2" + "source": "https://github.com/utopia-php/domains/tree/1.0.5" }, - "time": "2026-02-25T08:18:25+00:00" + "time": "2026-03-03T09:20:50+00:00" }, { "name": "utopia-php/dsn", @@ -4167,16 +4172,16 @@ }, { "name": "utopia-php/emails", - "version": "0.6.8", + "version": "0.6.9", "source": { "type": "git", "url": "https://github.com/utopia-php/emails.git", - "reference": "25dfcd46ed47b862d2a7e7c98d92a3a4680b6f1b" + "reference": "3a59fb392a03a88f5497e5fdb0ea84a252a4dfdf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/emails/zipball/25dfcd46ed47b862d2a7e7c98d92a3a4680b6f1b", - "reference": "25dfcd46ed47b862d2a7e7c98d92a3a4680b6f1b", + "url": "https://api.github.com/repos/utopia-php/emails/zipball/3a59fb392a03a88f5497e5fdb0ea84a252a4dfdf", + "reference": "3a59fb392a03a88f5497e5fdb0ea84a252a4dfdf", "shasum": "" }, "require": { @@ -4222,9 +4227,9 @@ ], "support": { "issues": "https://github.com/utopia-php/emails/issues", - "source": "https://github.com/utopia-php/emails/tree/0.6.8" + "source": "https://github.com/utopia-php/emails/tree/0.6.9" }, - "time": "2026-02-09T12:31:56+00:00" + "time": "2026-03-14T13:52:56+00:00" }, { "name": "utopia-php/fetch", @@ -4267,52 +4272,56 @@ }, { "name": "utopia-php/framework", - "version": "0.33.41", + "version": "dev-feat/coroutines-option", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "0f3bf2377c867e547c929c3733b8224afee6ef06" + "reference": "59570333e41de68c49ab283f55166af755ed5aa8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/0f3bf2377c867e547c929c3733b8224afee6ef06", - "reference": "0f3bf2377c867e547c929c3733b8224afee6ef06", + "url": "https://api.github.com/repos/utopia-php/http/zipball/59570333e41de68c49ab283f55166af755ed5aa8", + "reference": "59570333e41de68c49ab283f55166af755ed5aa8", "shasum": "" }, "require": { - "php": ">=8.3", - "utopia-php/compression": "0.1.*", - "utopia-php/telemetry": "0.2.*", + "ext-swoole": "*", + "php": ">=8.2", + "utopia-php/di": "0.3.*", + "utopia-php/servers": "0.3.*", "utopia-php/validators": "0.2.*" }, "require-dev": { + "doctrine/instantiator": "^1.5", "laravel/pint": "1.*", - "phpbench/phpbench": "1.*", + "phpbench/phpbench": "^1.2", "phpstan/phpstan": "1.*", - "phpunit/phpunit": "9.*", - "swoole/ide-helper": "^6.0" + "phpunit/phpunit": "^9.5.25", + "swoole/ide-helper": "4.8.3" }, "type": "library", "autoload": { "psr-4": { - "Utopia\\": "src/" + "Utopia\\": "src/", + "Tests\\E2E\\": "tests/e2e" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "A simple, light and advanced PHP framework", + "description": "A simple, light and advanced PHP HTTP framework", "keywords": [ "framework", + "http", "php", "upf" ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.41" + "source": "https://github.com/utopia-php/http/tree/feat/coroutines-option" }, - "time": "2026-02-24T12:01:28+00:00" + "time": "2026-03-16T17:28:15+00:00" }, { "name": "utopia-php/image", @@ -4572,16 +4581,16 @@ }, { "name": "utopia-php/mongo", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/mongo.git", - "reference": "45bedf36c2c946ec7a0a3e59b9f12f772de0b01d" + "reference": "83dbcde768d5fb40241f5ca8aa5ed8ca140a7469" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/mongo/zipball/45bedf36c2c946ec7a0a3e59b9f12f772de0b01d", - "reference": "45bedf36c2c946ec7a0a3e59b9f12f772de0b01d", + "url": "https://api.github.com/repos/utopia-php/mongo/zipball/83dbcde768d5fb40241f5ca8aa5ed8ca140a7469", + "reference": "83dbcde768d5fb40241f5ca8aa5ed8ca140a7469", "shasum": "" }, "require": { @@ -4627,31 +4636,32 @@ ], "support": { "issues": "https://github.com/utopia-php/mongo/issues", - "source": "https://github.com/utopia-php/mongo/tree/1.0.0" + "source": "https://github.com/utopia-php/mongo/tree/1.0.1" }, - "time": "2026-02-12T05:54:06+00:00" + "time": "2026-03-13T07:29:24+00:00" }, { "name": "utopia-php/platform", - "version": "0.7.16", + "version": "0.9.2", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "34e67e4b80b5741c380071fe765fbc12a132de4f" + "reference": "490e9aa716e0f8007f9e953150a776f3e107c57e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/34e67e4b80b5741c380071fe765fbc12a132de4f", - "reference": "34e67e4b80b5741c380071fe765fbc12a132de4f", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/490e9aa716e0f8007f9e953150a776f3e107c57e", + "reference": "490e9aa716e0f8007f9e953150a776f3e107c57e", "shasum": "" }, "require": { "ext-json": "*", "ext-redis": "*", - "php": ">=8.0", - "utopia-php/cli": "0.22.*", - "utopia-php/framework": "0.33.*", - "utopia-php/queue": "0.15.*" + "php": ">=8.2", + "utopia-php/cli": "0.23.0", + "utopia-php/framework": "0.34.*", + "utopia-php/queue": "0.16.*", + "utopia-php/validators": "0.2.*" }, "require-dev": { "laravel/pint": "1.*", @@ -4678,9 +4688,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.7.16" + "source": "https://github.com/utopia-php/platform/tree/0.9.2" }, - "time": "2026-02-11T06:36:48+00:00" + "time": "2026-03-15T13:53:33+00:00" }, { "name": "utopia-php/pools", @@ -4790,16 +4800,16 @@ }, { "name": "utopia-php/queue", - "version": "0.15.6", + "version": "0.16.0", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "08e361d69610f371382b344c369eef355ca414b4" + "reference": "ffdc9315d2f5999960c95a5860f067ea2eaa36f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/08e361d69610f371382b344c369eef355ca414b4", - "reference": "08e361d69610f371382b344c369eef355ca414b4", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/ffdc9315d2f5999960c95a5860f067ea2eaa36f7", + "reference": "ffdc9315d2f5999960c95a5860f067ea2eaa36f7", "shasum": "" }, "require": { @@ -4807,7 +4817,7 @@ "php-amqplib/php-amqplib": "^3.7", "utopia-php/fetch": "0.5.*", "utopia-php/pools": "1.*", - "utopia-php/servers": "0.2.*", + "utopia-php/servers": "0.3.*", "utopia-php/telemetry": "0.2.*", "utopia-php/validators": "0.2.*" }, @@ -4850,9 +4860,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.15.6" + "source": "https://github.com/utopia-php/queue/tree/0.16.0" }, - "time": "2026-02-23T13:03:51+00:00" + "time": "2026-03-13T12:23:30+00:00" }, { "name": "utopia-php/registry", @@ -4908,21 +4918,21 @@ }, { "name": "utopia-php/servers", - "version": "0.2.5", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/utopia-php/servers.git", - "reference": "4770e879a90685af4ba14e7e5d95d0a17c7fdf03" + "reference": "235be31200df9437fc96a1c270ffef4c64fafe52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/servers/zipball/4770e879a90685af4ba14e7e5d95d0a17c7fdf03", - "reference": "4770e879a90685af4ba14e7e5d95d0a17c7fdf03", + "url": "https://api.github.com/repos/utopia-php/servers/zipball/235be31200df9437fc96a1c270ffef4c64fafe52", + "reference": "235be31200df9437fc96a1c270ffef4c64fafe52", "shasum": "" }, "require": { - "php": ">=8.0", - "utopia-php/di": "0.1.*", + "php": ">=8.2", + "utopia-php/di": "0.3.*", "utopia-php/validators": "0.*" }, "require-dev": { @@ -4956,9 +4966,9 @@ ], "support": { "issues": "https://github.com/utopia-php/servers/issues", - "source": "https://github.com/utopia-php/servers/tree/0.2.5" + "source": "https://github.com/utopia-php/servers/tree/0.3.0" }, - "time": "2026-02-10T04:21:53+00:00" + "time": "2026-03-13T11:31:42+00:00" }, { "name": "utopia-php/span", @@ -5059,16 +5069,16 @@ }, { "name": "utopia-php/system", - "version": "0.10.0", + "version": "0.10.1", "source": { "type": "git", "url": "https://github.com/utopia-php/system.git", - "reference": "6441a9c180958a373e5ddb330264dd638539dfdb" + "reference": "7c1669533bb9c285de19191270c8c1439161a78a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/system/zipball/6441a9c180958a373e5ddb330264dd638539dfdb", - "reference": "6441a9c180958a373e5ddb330264dd638539dfdb", + "url": "https://api.github.com/repos/utopia-php/system/zipball/7c1669533bb9c285de19191270c8c1439161a78a", + "reference": "7c1669533bb9c285de19191270c8c1439161a78a", "shasum": "" }, "require": { @@ -5109,9 +5119,9 @@ ], "support": { "issues": "https://github.com/utopia-php/system/issues", - "source": "https://github.com/utopia-php/system/tree/0.10.0" + "source": "https://github.com/utopia-php/system/tree/0.10.1" }, - "time": "2025-10-15T19:12:00+00:00" + "time": "2026-03-15T21:07:41+00:00" }, { "name": "utopia-php/telemetry", @@ -5215,29 +5225,28 @@ }, { "name": "utopia-php/vcs", - "version": "2.0.0", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14" + "reference": "5769679308bad498f2777547d48ab332166c4c0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/058049326e04a2a0c2f0ce8ad00c7e84825aba14", - "reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/5769679308bad498f2777547d48ab332166c4c0b", + "reference": "5769679308bad498f2777547d48ab332166c4c0b", "shasum": "" }, "require": { "adhocore/jwt": "^1.1", "php": ">=8.0", - "utopia-php/cache": "1.0.*", - "utopia-php/framework": "0.*.*", - "utopia-php/system": "0.10.*" + "utopia-php/cache": "1.0.*" }, "require-dev": { "laravel/pint": "1.*.*", "phpstan/phpstan": "1.*.*", - "phpunit/phpunit": "^9.4" + "phpunit/phpunit": "^9.4", + "utopia-php/system": "0.10.*" }, "type": "library", "autoload": { @@ -5258,9 +5267,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/2.0.0" + "source": "https://github.com/utopia-php/vcs/tree/2.0.2" }, - "time": "2026-02-25T11:36:45+00:00" + "time": "2026-03-13T15:25:16+00:00" }, { "name": "utopia-php/websocket", @@ -5438,16 +5447,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.11.1", + "version": "1.11.8", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb" + "reference": "bf45bb91419f157e6d539d05f3f2c2d2120c90dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6ff411f26f2750eea05c7598c14bb3a2ada898cb", - "reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/bf45bb91419f157e6d539d05f3f2c2d2120c90dc", + "reference": "bf45bb91419f157e6d539d05f3f2c2d2120c90dc", "shasum": "" }, "require": { @@ -5483,22 +5492,22 @@ "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.11.1" + "source": "https://github.com/appwrite/sdk-generator/tree/1.11.8" }, - "time": "2026-02-25T07:15:19+00:00" + "time": "2026-03-16T11:02:05+00:00" }, { "name": "brianium/paratest", - "version": "v7.19.0", + "version": "v7.19.2", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6" + "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", - "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9", + "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9", "shasum": "" }, "require": { @@ -5512,9 +5521,9 @@ "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", "phpunit/php-file-iterator": "^6.0.1 || ^7", "phpunit/php-timer": "^8 || ^9", - "phpunit/phpunit": "^12.5.9 || ^13", + "phpunit/phpunit": "^12.5.14 || ^13.0.5", "sebastian/environment": "^8.0.3 || ^9", - "symfony/console": "^7.4.4 || ^8.0.4", + "symfony/console": "^7.4.7 || ^8.0.7", "symfony/process": "^7.4.5 || ^8.0.5" }, "require-dev": { @@ -5522,11 +5531,11 @@ "ext-pcntl": "*", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.38", - "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.12", - "phpstan/phpstan-strict-rules": "^2.0.8", - "symfony/filesystem": "^7.4.0 || ^8.0.1" + "phpstan/phpstan": "^2.1.40", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "symfony/filesystem": "^7.4.6 || ^8.0.6" }, "bin": [ "bin/paratest", @@ -5566,7 +5575,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.19.0" + "source": "https://github.com/paratestphp/paratest/tree/v7.19.2" }, "funding": [ { @@ -5578,7 +5587,7 @@ "type": "paypal" } ], - "time": "2026-02-06T10:53:26+00:00" + "time": "2026-03-09T14:33:17+00:00" }, { "name": "czproject/git-php", @@ -5767,16 +5776,16 @@ }, { "name": "laravel/pint", - "version": "v1.27.1", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5" + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/54cca2de13790570c7b6f0f94f37896bee4abcb5", - "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5", + "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", "shasum": "" }, "require": { @@ -5787,13 +5796,14 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.93.1", - "illuminate/view": "^12.51.0", - "larastan/larastan": "^3.9.2", + "friendsofphp/php-cs-fixer": "^3.94.2", + "illuminate/view": "^12.54.1", + "larastan/larastan": "^3.9.3", "laravel-zero/framework": "^12.0.5", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.3", - "pestphp/pest": "^3.8.5" + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.0" }, "bin": [ "builds/pint" @@ -5830,7 +5840,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-02-10T20:00:20+00:00" + "time": "2026-03-12T15:51:39+00:00" }, { "name": "matthiasmullie/minify", @@ -6984,16 +6994,16 @@ }, { "name": "sebastian/environment", - "version": "8.0.3", + "version": "8.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", - "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", "shasum": "" }, "require": { @@ -7036,7 +7046,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.4" }, "funding": [ { @@ -7056,7 +7066,7 @@ "type": "tidelift" } ], - "time": "2025-08-12T14:11:56+00:00" + "time": "2026-03-15T07:05:40+00:00" }, { "name": "sebastian/exporter", @@ -7679,16 +7689,16 @@ }, { "name": "symfony/console", - "version": "v8.0.4", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b" + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b", - "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b", + "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", "shasum": "" }, "require": { @@ -7745,7 +7755,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.4" + "source": "https://github.com/symfony/console/tree/v8.0.7" }, "funding": [ { @@ -7765,7 +7775,7 @@ "type": "tidelift" } ], - "time": "2026-01-13T13:06:50+00:00" + "time": "2026-03-06T14:06:22+00:00" }, { "name": "symfony/polyfill-ctype", @@ -8164,16 +8174,16 @@ }, { "name": "symfony/string", - "version": "v8.0.4", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "758b372d6882506821ed666032e43020c4f57194" + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194", - "reference": "758b372d6882506821ed666032e43020c4f57194", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", "shasum": "" }, "require": { @@ -8230,7 +8240,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.4" + "source": "https://github.com/symfony/string/tree/v8.0.6" }, "funding": [ { @@ -8250,7 +8260,7 @@ "type": "tidelift" } ], - "time": "2026-01-12T12:37:40+00:00" + "time": "2026-02-09T10:14:57+00:00" }, { "name": "textalk/websocket", @@ -8431,9 +8441,25 @@ "time": "2024-11-07T12:36:22+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/database", + "version": "dev-main", + "alias": "5.3.15", + "alias_normalized": "5.3.15.0" + }, + { + "package": "utopia-php/framework", + "version": "dev-feat/coroutines-option", + "alias": "0.34.15", + "alias_normalized": "0.34.15.0" + } + ], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": { + "utopia-php/database": 20, + "utopia-php/framework": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/src/Appwrite/GraphQL/Schema.php b/src/Appwrite/GraphQL/Schema.php index 57115ff027..5446230bd6 100644 --- a/src/Appwrite/GraphQL/Schema.php +++ b/src/Appwrite/GraphQL/Schema.php @@ -32,7 +32,7 @@ class Schema array $urls, array $params, ): GQLSchema { - Http::setResource('utopia:graphql', static function () use ($utopia) { + $utopia->setResource('utopia:graphql', static function () use ($utopia) { return $utopia; }); diff --git a/src/Appwrite/Platform/Tasks/Migrate.php b/src/Appwrite/Platform/Tasks/Migrate.php index cec2f6ec27..d12dca7bb6 100644 --- a/src/Appwrite/Platform/Tasks/Migrate.php +++ b/src/Appwrite/Platform/Tasks/Migrate.php @@ -9,7 +9,6 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception; use Utopia\Database\Validator\Authorization; -use Utopia\Http\Http; use Utopia\Platform\Action; use Utopia\Registry\Registry; use Utopia\Validator\Text; @@ -32,6 +31,7 @@ class Migrate extends Action ->inject('getProjectDB') ->inject('register') ->inject('authorisation') + ->inject('console') ->callback($this->action(...)); } @@ -48,7 +48,8 @@ class Migrate extends Action Database $dbForPlatform, callable $getProjectDB, Registry $register, - Authorization $authorization + Authorization $authorization, + Document $console ): void { if (!\array_key_exists($version, Migration::$versions)) { @@ -85,8 +86,6 @@ class Migrate extends Action Console::log('Migrated ' . ++$count . '/' . $total . ' projects...'); }); - $console = (new Http('UTC'))->getResource('console'); - try { $migration ->setProject($console, $getProjectDB($console), $dbForPlatform, $authorization, $getProjectDB) diff --git a/src/Appwrite/Platform/Tasks/Specs.php b/src/Appwrite/Platform/Tasks/Specs.php index e68656f55d..5dbd6784ae 100644 --- a/src/Appwrite/Platform/Tasks/Specs.php +++ b/src/Appwrite/Platform/Tasks/Specs.php @@ -18,6 +18,8 @@ use Utopia\Config\Config; use Utopia\Console; use Utopia\Database\Adapter\MySQL; use Utopia\Database\Database; +use Utopia\DI\Container; +use Utopia\Http\Adapter\FPM\Server as FPMServer; use Utopia\Http\Http; use Utopia\Http\Request as UtopiaRequest; use Utopia\Http\Response as UtopiaResponse; @@ -283,10 +285,11 @@ class Specs extends Action $mocks = ($mode === 'mocks'); // Mock dependencies - Http::setResource('request', fn () => $this->getRequest()); - Http::setResource('response', fn () => $response); - Http::setResource('dbForPlatform', fn () => new Database(new MySQL(''), new Cache(new None()))); - Http::setResource('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None()))); + $specsContainer = new Container(); + $specsContainer->set('request', fn () => $this->getRequest()); + $specsContainer->set('response', fn () => $response); + $specsContainer->set('dbForPlatform', fn () => new Database(new MySQL(''), new Cache(new None()))); + $specsContainer->set('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None()))); $platforms = static::getPlatforms(); $authCounts = $this->getAuthCounts(); @@ -377,7 +380,7 @@ class Specs extends Action } $arguments = [ - new Http('UTC'), + new Http(new FPMServer($specsContainer), 'UTC'), $services, $routes, $models, From e475a7ac5ad7034a0bc563170082c0916d2a0c03 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 16 Mar 2026 23:19:07 +0530 Subject: [PATCH 015/118] lock file --- composer.json | 7 +++++-- composer.lock | 37 ++++++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 65a70f96ba..f178b6ae87 100644 --- a/composer.json +++ b/composer.json @@ -94,13 +94,16 @@ "spomky-labs/otphp": "11.*", "webonyx/graphql-php": "14.11.*", "league/csv": "9.14.*", - "enshrined/svg-sanitize": "0.22.*", - "utopia-php/di": "0.1.0" + "enshrined/svg-sanitize": "0.22.*" }, "repositories": [ { "type": "vcs", "url": "https://github.com/utopia-php/database" + }, + { + "type": "vcs", + "url": "https://github.com/utopia-php/http" } ], "require-dev": { diff --git a/composer.lock b/composer.lock index fdde1cb0a3..d016efb072 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": "13579de3d747c541fdcce4f709df8e57", + "content-hash": "1c156b2a11a5abb568b44d880d1ddec7", "packages": [ { "name": "adhocore/jwt", @@ -3850,16 +3850,16 @@ }, { "name": "utopia-php/database", - "version": "dev-fix-collection-recreate", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "5208630969dfdfbe8eda9c34c6b28ce711ece7c4" + "reference": "bb89e8c4a5d534fc7e650c4438aa796667fe160a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/5208630969dfdfbe8eda9c34c6b28ce711ece7c4", - "reference": "5208630969dfdfbe8eda9c34c6b28ce711ece7c4", + "url": "https://api.github.com/repos/utopia-php/database/zipball/bb89e8c4a5d534fc7e650c4438aa796667fe160a", + "reference": "bb89e8c4a5d534fc7e650c4438aa796667fe160a", "shasum": "" }, "require": { @@ -3934,10 +3934,10 @@ "utopia" ], "support": { - "source": "https://github.com/utopia-php/database/tree/fix-collection-recreate", + "source": "https://github.com/utopia-php/database/tree/main", "issues": "https://github.com/utopia-php/database/issues" }, - "time": "2026-03-16T11:58:09+00:00" + "time": "2026-03-16T11:41:45+00:00" }, { "name": "utopia-php/detector", @@ -5478,16 +5478,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.11.6", + "version": "1.11.8", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38" + "reference": "bf45bb91419f157e6d539d05f3f2c2d2120c90dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38", - "reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/bf45bb91419f157e6d539d05f3f2c2d2120c90dc", + "reference": "bf45bb91419f157e6d539d05f3f2c2d2120c90dc", "shasum": "" }, "require": { @@ -5523,9 +5523,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.11.6" + "source": "https://github.com/appwrite/sdk-generator/tree/1.11.8" }, - "time": "2026-03-09T07:12:51+00:00" + "time": "2026-03-16T11:02:05+00:00" }, { "name": "brianium/paratest", @@ -8475,14 +8475,21 @@ "aliases": [ { "package": "utopia-php/database", - "version": "dev-fix-collection-recreate", + "version": "dev-main", "alias": "5.3.15", "alias_normalized": "5.3.15.0" + }, + { + "package": "utopia-php/framework", + "version": "dev-feat/coroutines-option", + "alias": "0.34.15", + "alias_normalized": "0.34.15.0" } ], "minimum-stability": "dev", "stability-flags": { - "utopia-php/database": 20 + "utopia-php/database": 20, + "utopia-php/framework": 20 }, "prefer-stable": true, "prefer-lowest": false, From a1bc503ce45a6c2b62aba345b32a62b554d5d5e4 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 16 Mar 2026 23:23:03 +0530 Subject: [PATCH 016/118] formatting --- app/init/resources.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/init/resources.php b/app/init/resources.php index 9edbe3c92c..2167ddec3a 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -51,8 +51,8 @@ use Utopia\Database\DateTime as DatabaseDateTime; use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use Utopia\DSN\DSN; use Utopia\DI\Container; +use Utopia\DSN\DSN; use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\Logger\Log; From b75cd993beae220765084853f5f21a436913fdd7 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 16 Mar 2026 23:27:34 +0530 Subject: [PATCH 017/118] missing trusted ip headers --- composer.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.lock b/composer.lock index d016efb072..30aab90b10 100644 --- a/composer.lock +++ b/composer.lock @@ -4307,12 +4307,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "59570333e41de68c49ab283f55166af755ed5aa8" + "reference": "d785061990b2a0957dce215f9bc6d29fa47f5683" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/59570333e41de68c49ab283f55166af755ed5aa8", - "reference": "59570333e41de68c49ab283f55166af755ed5aa8", + "url": "https://api.github.com/repos/utopia-php/http/zipball/d785061990b2a0957dce215f9bc6d29fa47f5683", + "reference": "d785061990b2a0957dce215f9bc6d29fa47f5683", "shasum": "" }, "require": { @@ -4352,7 +4352,7 @@ "issues": "https://github.com/utopia-php/http/issues", "source": "https://github.com/utopia-php/http/tree/feat/coroutines-option" }, - "time": "2026-03-16T17:28:15+00:00" + "time": "2026-03-16T17:55:57+00:00" }, { "name": "utopia-php/image", From b8e51366d77be37d340a9fa1c71773e488480f4e Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 16 Mar 2026 23:32:31 +0530 Subject: [PATCH 018/118] add missing compression methods --- composer.lock | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/composer.lock b/composer.lock index 30aab90b10..09d7107c70 100644 --- a/composer.lock +++ b/composer.lock @@ -4307,17 +4307,18 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "d785061990b2a0957dce215f9bc6d29fa47f5683" + "reference": "61d9d2cefc06e549c521f5798b20faabe9478b98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/d785061990b2a0957dce215f9bc6d29fa47f5683", - "reference": "d785061990b2a0957dce215f9bc6d29fa47f5683", + "url": "https://api.github.com/repos/utopia-php/http/zipball/61d9d2cefc06e549c521f5798b20faabe9478b98", + "reference": "61d9d2cefc06e549c521f5798b20faabe9478b98", "shasum": "" }, "require": { "ext-swoole": "*", "php": ">=8.2", + "utopia-php/compression": "0.1.*", "utopia-php/di": "0.3.*", "utopia-php/servers": "0.3.*", "utopia-php/validators": "0.2.*" @@ -4352,7 +4353,7 @@ "issues": "https://github.com/utopia-php/http/issues", "source": "https://github.com/utopia-php/http/tree/feat/coroutines-option" }, - "time": "2026-03-16T17:55:57+00:00" + "time": "2026-03-16T18:01:51+00:00" }, { "name": "utopia-php/image", From eb399c7bd0e1894a5eb9f445d736935d894923cf Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 16 Mar 2026 23:45:18 +0530 Subject: [PATCH 019/118] wip --- app/http.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/http.php b/app/http.php index 5cc7d1560d..6a518c8684 100644 --- a/app/http.php +++ b/app/http.php @@ -513,10 +513,16 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($regis return; } + global $container; $pools = $register->get('pools'); - $swooleAdapter->getContainer()->set('pools', fn () => $pools); + $container->set('pools', fn () => $pools); + + $requestContainer = $swooleAdapter->getContainer(); + $requestContainer->set('request', fn () => $request); + $requestContainer->set('response', fn () => $response); $app = new Http($swooleAdapter, 'UTC'); + $container->set('utopia', fn () => $app); $app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled'); $app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB From d9c1b9db2a7552bcb7bdec622884c73ac8a2d9ec Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 08:49:43 +0530 Subject: [PATCH 020/118] chore: register request resources seperately --- app/http.php | 7 +- app/init/resources.php | 1800 ++++++++++++++++++++-------------------- app/realtime.php | 1 + 3 files changed, 911 insertions(+), 897 deletions(-) diff --git a/app/http.php b/app/http.php index 6a518c8684..70dc6f58ce 100644 --- a/app/http.php +++ b/app/http.php @@ -69,6 +69,8 @@ $swooleAdapter = new HttpServer( container: $container, ); +$container->set('container', fn () => fn () => $swooleAdapter->getContainer()); + $http = $swooleAdapter->getServer(); /** @@ -522,7 +524,10 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($regis $requestContainer->set('response', fn () => $response); $app = new Http($swooleAdapter, 'UTC'); - $container->set('utopia', fn () => $app); + $requestContainer->set('utopia', fn () => $app); + + registerRequestResources($requestContainer); + $app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled'); $app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB diff --git a/app/init/resources.php b/app/init/resources.php index 2167ddec3a..e353c61896 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -188,336 +188,942 @@ $container->set('platform', function () { }, []); /** - * List of allowed request hostnames for the request. + * Register per-request resources on the given container. + * These resources depend (directly or transitively) on request/response + * and must be fresh for each HTTP request. */ -$container->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) { - $allowed = [...($platform['hostnames'] ?? [])]; - - /* Add platform configured hostnames */ - if (! $project->isEmpty() && $project->getId() !== 'console') { - $platforms = $project->getAttribute('platforms', []); - $hostnames = Platform::getHostnames($platforms); - $allowed = [...$allowed, ...$hostnames]; - } - - /* Add the request hostname if a dev key is found */ - if (! $devKey->isEmpty()) { - $allowed[] = $request->getHostname(); - } - - $originHostname = parse_url($request->getOrigin(), PHP_URL_HOST); - $refererHostname = parse_url($request->getReferer(), PHP_URL_HOST); - - $hostname = $originHostname; - if (empty($hostname)) { - $hostname = $refererHostname; - } - - /* Add request hostname for preflight requests */ - if ($request->getMethod() === 'OPTIONS') { - $allowed[] = $hostname; - } - - /* Allow the request origin of rule */ - if (! $rule->isEmpty() && ! empty($rule->getAttribute('domain', ''))) { - $allowed[] = $rule->getAttribute('domain', ''); - } - - /* Allow the request origin if a dev key is found */ - if (! $devKey->isEmpty() && ! empty($hostname)) { - $allowed[] = $hostname; - } - - return array_unique($allowed); -}, ['platform', 'project', 'rule', 'devKey', 'request']); - -/** - * List of allowed request schemes for the request. - */ -$container->set('allowedSchemes', function (array $platform, Document $project) { - $allowed = [...($platform['schemas'] ?? [])]; - - if (! $project->isEmpty() && $project->getId() !== 'console') { - /* Add hardcoded schemes */ - $allowed[] = 'exp'; - $allowed[] = 'appwrite-callback-' . $project->getId(); - - /* Add platform configured schemes */ - $platforms = $project->getAttribute('platforms', []); - $schemes = Platform::getSchemes($platforms); - $allowed = [...$allowed, ...$schemes]; - } - - return array_unique($allowed); -}, ['platform', 'project']); - -/** - * Rule associated with a request origin. - */ -$container->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) { - $domain = \parse_url($request->getOrigin(), PHP_URL_HOST); - - if (empty($domain)) { - $domain = \parse_url($request->getReferer(), PHP_URL_HOST); - } - - if (empty($domain)) { - return new Document(); - } - - // TODO: (@Meldiron) Remove after 1.7.x migration - $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) { - if ($isMd5) { - return $dbForPlatform->getDocument('rules', md5($domain)); - } - - return $dbForPlatform->findOne('rules', [ - Query::equal('domain', [$domain]), - ]) ?? new Document(); - }); - - $permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence(); - - // Temporary implementation until custom wildcard domains are an official feature - // Allow trusted projects; Used for Console (website) previews - if (! $permitsCurrentProject && ! $rule->isEmpty() && ! empty($rule->getAttribute('projectId', ''))) { - $trustedProjects = []; - foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) { - if (empty($trustedProject)) { - continue; - } - $trustedProjects[] = $trustedProject; - } - if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects)) { - $permitsCurrentProject = true; - } - } - - if (! $permitsCurrentProject) { - return new Document(); - } - - return $rule; -}, ['request', 'dbForPlatform', 'project', 'authorization']); - -/** - * CORS service - */ -$container->set('cors', function (array $allowedHostnames) { - $corsConfig = Config::getParam('cors'); - - return new Cors( - $allowedHostnames, - allowedMethods: $corsConfig['allowedMethods'], - allowedHeaders: $corsConfig['allowedHeaders'], - allowCredentials: true, - exposedHeaders: $corsConfig['exposedHeaders'], - ); -}, ['allowedHostnames']); - -$container->set('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) { - if (! $devKey->isEmpty()) { - return new URL(); - } - - return new Origin($allowedHostnames, $allowedSchemes); -}, ['devKey', 'allowedHostnames', 'allowedSchemes']); - -$container->set('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) { - if (! $devKey->isEmpty()) { - return new URL(); - } - - return new Redirect($allowedHostnames, $allowedSchemes); -}, ['devKey', 'allowedHostnames', 'allowedSchemes']); - -$container->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) { +function registerRequestResources(Container $container): void +{ /** - * Handles user authentication and session validation. - * - * This function follows a series of steps to determine the appropriate user session - * based on cookies, headers, and JWT tokens. - * - * Process: - * 1. Checks the cookie based on mode: - * - If in admin mode, uses console project id for key. - * - Otherwise, sets the key using the project ID - * 2. If no cookie is found, attempts to retrieve the fallback header `x-fallback-cookies`. - * - If this method is used, returns the header: `X-Debug-Fallback: true`. - * 3. Fetches the user document from the appropriate database based on the mode. - * 4. If the user document is empty or the session key cannot be verified, sets an empty user document. - * 5. Regardless of the results from steps 1-4, attempts to fetch the JWT token. - * 6. If the JWT user has a valid session ID, updates the user variable with the user from `projectDB`, - * overwriting the previous value. - * 7. If account API key is passed, use user of the account API key as long as user ID header matches too + * List of allowed request hostnames for the request. */ - $authorization->setDefaultStatus(true); + $container->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) { + $allowed = [...($platform['hostnames'] ?? [])]; - $store->setKey('a_session_' . $project->getId()); - - if ($mode === APP_MODE_ADMIN) { - $store->setKey('a_session_' . $console->getId()); - } - - $store->decode( - $request->getCookie( - $store->getKey(), // Get sessions - $request->getCookie($store->getKey() . '_legacy', '') - ) - ); - - // Get session from header for SSR clients - if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) { - $sessionHeader = $request->getHeader('x-appwrite-session', ''); - - if (! empty($sessionHeader)) { - $store->decode($sessionHeader); + /* Add platform configured hostnames */ + if (! $project->isEmpty() && $project->getId() !== 'console') { + $platforms = $project->getAttribute('platforms', []); + $hostnames = Platform::getHostnames($platforms); + $allowed = [...$allowed, ...$hostnames]; } - } - // Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies - if ($response) { // if in http context - add debug header - $response->addHeader('X-Debug-Fallback', 'false'); - } - - if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) { - if ($response) { - $response->addHeader('X-Debug-Fallback', 'true'); + /* Add the request hostname if a dev key is found */ + if (! $devKey->isEmpty()) { + $allowed[] = $request->getHostname(); } - $fallback = $request->getHeader('x-fallback-cookies', ''); - $fallback = \json_decode($fallback, true); - $store->decode(((is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : '')); - } - $user = null; - if ($mode === APP_MODE_ADMIN) { - /** @var User $user */ - $user = $dbForPlatform->getDocument('users', $store->getProperty('id', '')); - } else { - if ($project->isEmpty()) { - $user = new User([]); + $originHostname = parse_url($request->getOrigin(), PHP_URL_HOST); + $refererHostname = parse_url($request->getReferer(), PHP_URL_HOST); + + $hostname = $originHostname; + if (empty($hostname)) { + $hostname = $refererHostname; + } + + /* Add request hostname for preflight requests */ + if ($request->getMethod() === 'OPTIONS') { + $allowed[] = $hostname; + } + + /* Allow the request origin of rule */ + if (! $rule->isEmpty() && ! empty($rule->getAttribute('domain', ''))) { + $allowed[] = $rule->getAttribute('domain', ''); + } + + /* Allow the request origin if a dev key is found */ + if (! $devKey->isEmpty() && ! empty($hostname)) { + $allowed[] = $hostname; + } + + return array_unique($allowed); + }, ['platform', 'project', 'rule', 'devKey', 'request']); + + /** + * List of allowed request schemes for the request. + */ + $container->set('allowedSchemes', function (array $platform, Document $project) { + $allowed = [...($platform['schemas'] ?? [])]; + + if (! $project->isEmpty() && $project->getId() !== 'console') { + /* Add hardcoded schemes */ + $allowed[] = 'exp'; + $allowed[] = 'appwrite-callback-' . $project->getId(); + + /* Add platform configured schemes */ + $platforms = $project->getAttribute('platforms', []); + $schemes = Platform::getSchemes($platforms); + $allowed = [...$allowed, ...$schemes]; + } + + return array_unique($allowed); + }, ['platform', 'project']); + + /** + * Rule associated with a request origin. + */ + $container->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) { + $domain = \parse_url($request->getOrigin(), PHP_URL_HOST); + + if (empty($domain)) { + $domain = \parse_url($request->getReferer(), PHP_URL_HOST); + } + + if (empty($domain)) { + return new Document(); + } + + // TODO: (@Meldiron) Remove after 1.7.x migration + $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; + $rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) { + if ($isMd5) { + return $dbForPlatform->getDocument('rules', md5($domain)); + } + + return $dbForPlatform->findOne('rules', [ + Query::equal('domain', [$domain]), + ]) ?? new Document(); + }); + + $permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence(); + + // Temporary implementation until custom wildcard domains are an official feature + // Allow trusted projects; Used for Console (website) previews + if (! $permitsCurrentProject && ! $rule->isEmpty() && ! empty($rule->getAttribute('projectId', ''))) { + $trustedProjects = []; + foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) { + if (empty($trustedProject)) { + continue; + } + $trustedProjects[] = $trustedProject; + } + if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects)) { + $permitsCurrentProject = true; + } + } + + if (! $permitsCurrentProject) { + return new Document(); + } + + return $rule; + }, ['request', 'dbForPlatform', 'project', 'authorization']); + + /** + * CORS service + */ + $container->set('cors', function (array $allowedHostnames) { + $corsConfig = Config::getParam('cors'); + + return new Cors( + $allowedHostnames, + allowedMethods: $corsConfig['allowedMethods'], + allowedHeaders: $corsConfig['allowedHeaders'], + allowCredentials: true, + exposedHeaders: $corsConfig['exposedHeaders'], + ); + }, ['allowedHostnames']); + + $container->set('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) { + if (! $devKey->isEmpty()) { + return new URL(); + } + + return new Origin($allowedHostnames, $allowedSchemes); + }, ['devKey', 'allowedHostnames', 'allowedSchemes']); + + $container->set('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) { + if (! $devKey->isEmpty()) { + return new URL(); + } + + return new Redirect($allowedHostnames, $allowedSchemes); + }, ['devKey', 'allowedHostnames', 'allowedSchemes']); + + $container->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) { + /** + * Handles user authentication and session validation. + * + * This function follows a series of steps to determine the appropriate user session + * based on cookies, headers, and JWT tokens. + * + * Process: + * 1. Checks the cookie based on mode: + * - If in admin mode, uses console project id for key. + * - Otherwise, sets the key using the project ID + * 2. If no cookie is found, attempts to retrieve the fallback header `x-fallback-cookies`. + * - If this method is used, returns the header: `X-Debug-Fallback: true`. + * 3. Fetches the user document from the appropriate database based on the mode. + * 4. If the user document is empty or the session key cannot be verified, sets an empty user document. + * 5. Regardless of the results from steps 1-4, attempts to fetch the JWT token. + * 6. If the JWT user has a valid session ID, updates the user variable with the user from `projectDB`, + * overwriting the previous value. + * 7. If account API key is passed, use user of the account API key as long as user ID header matches too + */ + $authorization->setDefaultStatus(true); + + $store->setKey('a_session_' . $project->getId()); + + if ($mode === APP_MODE_ADMIN) { + $store->setKey('a_session_' . $console->getId()); + } + + $store->decode( + $request->getCookie( + $store->getKey(), // Get sessions + $request->getCookie($store->getKey() . '_legacy', '') + ) + ); + + // Get session from header for SSR clients + if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) { + $sessionHeader = $request->getHeader('x-appwrite-session', ''); + + if (! empty($sessionHeader)) { + $store->decode($sessionHeader); + } + } + + // Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies + if ($response) { // if in http context - add debug header + $response->addHeader('X-Debug-Fallback', 'false'); + } + + if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) { + if ($response) { + $response->addHeader('X-Debug-Fallback', 'true'); + } + $fallback = $request->getHeader('x-fallback-cookies', ''); + $fallback = \json_decode($fallback, true); + $store->decode(((is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : '')); + } + + $user = null; + if ($mode === APP_MODE_ADMIN) { + /** @var User $user */ + $user = $dbForPlatform->getDocument('users', $store->getProperty('id', '')); } else { - if (! empty($store->getProperty('id', ''))) { - if ($project->getId() === 'console') { - /** @var User $user */ - $user = $dbForPlatform->getDocument('users', $store->getProperty('id', '')); - } else { - /** @var User $user */ - $user = $dbForProject->getDocument('users', $store->getProperty('id', '')); + if ($project->isEmpty()) { + $user = new User([]); + } else { + if (! empty($store->getProperty('id', ''))) { + if ($project->getId() === 'console') { + /** @var User $user */ + $user = $dbForPlatform->getDocument('users', $store->getProperty('id', '')); + } else { + /** @var User $user */ + $user = $dbForProject->getDocument('users', $store->getProperty('id', '')); + } } } } - } - if ( - ! $user || - $user->isEmpty() // Check a document has been found in the DB - || ! $user->sessionVerify($store->getProperty('secret', ''), $proofForToken) - ) { // Validate user has valid login token - $user = new User([]); - } - - $authJWT = $request->getHeader('x-appwrite-jwt', ''); - if (! empty($authJWT) && ! $project->isEmpty()) { // JWT authentication - if (! $user->isEmpty()) { - throw new Exception(Exception::USER_JWT_AND_COOKIE_SET); + if ( + ! $user || + $user->isEmpty() // Check a document has been found in the DB + || ! $user->sessionVerify($store->getProperty('secret', ''), $proofForToken) + ) { // Validate user has valid login token + $user = new User([]); + } + + $authJWT = $request->getHeader('x-appwrite-jwt', ''); + if (! empty($authJWT) && ! $project->isEmpty()) { // JWT authentication + if (! $user->isEmpty()) { + throw new Exception(Exception::USER_JWT_AND_COOKIE_SET); + } + + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); + try { + $payload = $jwt->decode($authJWT); + } catch (JWTException $error) { + throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage()); + } + + $jwtUserId = $payload['userId'] ?? ''; + if (! empty($jwtUserId)) { + if ($mode === APP_MODE_ADMIN) { + $user = $dbForPlatform->getDocument('users', $jwtUserId); + } else { + $user = $dbForProject->getDocument('users', $jwtUserId); + } + } + $jwtSessionId = $payload['sessionId'] ?? ''; + if (! empty($jwtSessionId)) { + if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token + $user = new User([]); + } + } + } + + // Account based on account API key + $accountKey = $request->getHeader('x-appwrite-key', ''); + $accountKeyUserId = $request->getHeader('x-appwrite-user', ''); + if (! empty($accountKeyUserId) && ! empty($accountKey)) { + if (! $user->isEmpty()) { + throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET); + } + + $accountKeyUser = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId)); + if (! $accountKeyUser->isEmpty()) { + $key = $accountKeyUser->find( + key: 'secret', + find: $accountKey, + subject: 'keys' + ); + + if (! empty($key)) { + $expire = $key->getAttribute('expire'); + if (! empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) { + throw new Exception(Exception::ACCOUNT_KEY_EXPIRED); + } + + $user = $accountKeyUser; + } + } + } + + $dbForProject->setMetadata('user', $user->getId()); + $dbForPlatform->setMetadata('user', $user->getId()); + + return $user; + }, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']); + + $container->set('project', function ($dbForPlatform, $request, $console, $authorization) { + /** @var Appwrite\Utopia\Request $request */ + /** @var Utopia\Database\Database $dbForPlatform */ + /** @var Utopia\Database\Document $console */ + $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', '')); + // Realtime channel "project" can send project=Query array + if (! \is_string($projectId)) { + $projectId = $request->getHeader('x-appwrite-project', ''); + } + + if (empty($projectId) || $projectId === 'console') { + return $console; + } + + $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); + + return $project; + }, ['dbForPlatform', 'request', 'console', 'authorization']); + + $container->set('session', function (User $user, Store $store, Token $proofForToken) { + if ($user->isEmpty()) { + return; + } + + $sessions = $user->getAttribute('sessions', []); + $sessionId = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken); + + if (! $sessionId) { + return; + } + foreach ($sessions as $session) { + /** @var Document $session */ + if ($sessionId === $session->getId()) { + return $session; + } + } + + }, ['user', 'store', 'proofForToken']); + + $container->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization) { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForPlatform; + } + + $database = $project->getAttribute('database', ''); + if (empty($database)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Project database is not configured'); } - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); try { - $payload = $jwt->decode($authJWT); - } catch (JWTException $error) { - throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage()); + $dsn = new DSN($database); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $database); } - $jwtUserId = $payload['userId'] ?? ''; - if (! empty($jwtUserId)) { - if ($mode === APP_MODE_ADMIN) { - $user = $dbForPlatform->getDocument('users', $jwtUserId); - } else { - $user = $dbForProject->getDocument('users', $jwtUserId); + $adapter = new DatabasePool($pools->get($dsn->getHost())); + $database = new Database($adapter, $cache); + + $database + ->setDatabase(APP_DATABASE) + ->setAuthorization($authorization) + ->setMetadata('host', \gethostname()) + ->setMetadata('project', $project->getId()) + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) + ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); + $database->setDocumentType('users', User::class); + + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + + if (\in_array($dsn->getHost(), $sharedTables)) { + $database + ->setSharedTables(true) + ->setTenant($project->getSequence()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getSequence()); + } + + /** + * This isolated event handling for `users.*.create` which is based on a `Database::EVENT_DOCUMENT_CREATE` listener may look odd, but it is **intentional**. + * + * Accounts can be created in many ways beyond `createAccount` + * (anonymous, OAuth, phone, etc.), and those flows are probably not covered in event tests; so we handle this here. + */ + $eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) { + // Only trigger events for user creation with the database listener. + if ($document->getCollection() !== 'users') { + return; } - } - $jwtSessionId = $payload['sessionId'] ?? ''; - if (! empty($jwtSessionId)) { - if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token - $user = new User([]); + + $queueForEvents + ->setEvent('users.[userId].create') + ->setParam('userId', $document->getId()) + ->setPayload($response->output($document, Response::MODEL_USER)); + + // Trigger functions, webhooks, and realtime events + $queueForFunctions + ->from($queueForEvents) + ->trigger(); + + /** Trigger webhooks events only if a project has them enabled */ + if (! empty($project->getAttribute('webhooks'))) { + $queueForWebhooks + ->from($queueForEvents) + ->trigger(); } - } - } - // Account based on account API key - $accountKey = $request->getHeader('x-appwrite-key', ''); - $accountKeyUserId = $request->getHeader('x-appwrite-user', ''); - if (! empty($accountKeyUserId) && ! empty($accountKey)) { - if (! $user->isEmpty()) { - throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET); - } + /** Trigger realtime events only for non console events */ + if ($queueForEvents->getProject()->getId() !== 'console') { + $queueForRealtime + ->from($queueForEvents) + ->trigger(); + } + }; - $accountKeyUser = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId)); - if (! $accountKeyUser->isEmpty()) { - $key = $accountKeyUser->find( - key: 'secret', - find: $accountKey, - subject: 'keys' + /** + * Purge function events cache when functions are created, updated or deleted. + */ + $functionsEventsCacheListener = function (string $event, Document $document, Document $project, Database $dbForProject) { + + if ($document->getCollection() !== 'functions') { + return; + } + + if ($project->isEmpty() || $project->getId() === 'console') { + return; + } + + $hostname = $dbForProject->getAdapter()->getHostname(); + $cacheKey = \sprintf( + '%s-cache-%s:%s:%s:project:%s:functions:events', + $dbForProject->getCacheName(), + $hostname ?? '', + $dbForProject->getNamespace(), + $dbForProject->getTenant(), + $project->getId() ); - if (! empty($key)) { - $expire = $key->getAttribute('expire'); - if (! empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) { - throw new Exception(Exception::ACCOUNT_KEY_EXPIRED); - } + $dbForProject->getCache()->purge($cacheKey); + }; - $user = $accountKeyUser; + $usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) { + $value = 1; + + switch ($event) { + case Database::EVENT_DOCUMENT_DELETE: + $value = -1; + break; + case Database::EVENT_DOCUMENTS_DELETE: + $value = -1 * $document->getAttribute('modified', 0); + break; + case Database::EVENT_DOCUMENTS_CREATE: + $value = $document->getAttribute('modified', 0); + break; + case Database::EVENT_DOCUMENTS_UPSERT: + $value = $document->getAttribute('created', 0); + break; + } + + switch (true) { + case $document->getCollection() === 'teams': + $usage->addMetric(METRIC_TEAMS, $value); // per project + break; + case $document->getCollection() === 'users': + $usage->addMetric(METRIC_USERS, $value); // per project + if ($event === Database::EVENT_DOCUMENT_DELETE) { + $usage->addReduce($document); + } + break; + case $document->getCollection() === 'sessions': // sessions + $usage->addMetric(METRIC_SESSIONS, $value); // per project + break; + case $document->getCollection() === 'databases': // databases + $usage->addMetric(METRIC_DATABASES, $value); // per project + + if ($event === Database::EVENT_DOCUMENT_DELETE) { + $usage->addReduce($document); + } + break; + case str_starts_with($document->getCollection(), 'database_') && ! str_contains($document->getCollection(), 'collection'): // collections + $parts = explode('_', $document->getCollection()); + $databaseInternalId = $parts[1] ?? 0; + $usage + ->addMetric(METRIC_COLLECTIONS, $value) // per project + ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value); + + if ($event === Database::EVENT_DOCUMENT_DELETE) { + $usage->addReduce($document); + } + break; + case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): // documents + $parts = explode('_', $document->getCollection()); + $databaseInternalId = $parts[1] ?? 0; + $collectionInternalId = $parts[3] ?? 0; + $usage + ->addMetric(METRIC_DOCUMENTS, $value) // per project + ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database + ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection + break; + case $document->getCollection() === 'buckets': // buckets + $usage->addMetric(METRIC_BUCKETS, $value); // per project + if ($event === Database::EVENT_DOCUMENT_DELETE) { + $usage + ->addReduce($document); + } + break; + case str_starts_with($document->getCollection(), 'bucket_'): // files + $parts = explode('_', $document->getCollection()); + $bucketInternalId = $parts[1]; + $usage + ->addMetric(METRIC_FILES, $value) // per project + ->addMetric(METRIC_FILES_STORAGE, $document->getAttribute('sizeOriginal') * $value) // per project + ->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES), $value) // per bucket + ->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES_STORAGE), $document->getAttribute('sizeOriginal') * $value); // per bucket + break; + case $document->getCollection() === 'functions': + $usage->addMetric(METRIC_FUNCTIONS, $value); // per project + + if ($event === Database::EVENT_DOCUMENT_DELETE) { + $usage + ->addReduce($document); + } + break; + case $document->getCollection() === 'sites': + $usage->addMetric(METRIC_SITES, $value); // per project + + if ($event === Database::EVENT_DOCUMENT_DELETE) { + $usage + ->addReduce($document); + } + break; + case $document->getCollection() === 'deployments': + $usage + ->addMetric(METRIC_DEPLOYMENTS, $value) // per project + ->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project + ->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS), $value) // per function + ->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value) + ->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS), $value) // per function + ->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value); + break; + default: + break; + } + }; + + // Clone the queues, to prevent events triggered by the database listener + // from overwriting the events that are supposed to be triggered in the shutdown hook. + $queueForEventsClone = new Event($publisher); + $queueForFunctions = new Func($publisherFunctions); + $queueForWebhooks = new Webhook($publisherWebhooks); + $queueForRealtime = new Realtime(); + + $database + ->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) + ->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) + ->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) + ->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) + ->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) + ->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener( + $project, + $document, + $response, + $queueForEventsClone->from($queueForEvents), + $queueForFunctions->from($queueForEvents), + $queueForWebhooks->from($queueForEvents), + $queueForRealtime->from($queueForEvents) + )) + ->on(Database::EVENT_DOCUMENT_CREATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database)) + ->on(Database::EVENT_DOCUMENT_UPDATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database)) + ->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database)); + + return $database; + }, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization']); + + $container->set('schema', function ($utopia, $dbForProject, $authorization) { + + $complexity = function (int $complexity, array $args) { + $queries = Query::parseQueries($args['queries'] ?? []); + $query = Query::getByType($queries, [Query::TYPE_LIMIT])[0] ?? null; + $limit = $query ? $query->getValue() : APP_LIMIT_LIST_DEFAULT; + + return $complexity * $limit; + }; + + $attributes = function (int $limit, int $offset) use ($dbForProject, $authorization) { + $attrs = $authorization->skip(fn () => $dbForProject->find('attributes', [ + Query::limit($limit), + Query::offset($offset), + ])); + + return \array_map(function ($attr) { + return $attr->getArrayCopy(); + }, $attrs); + }; + + $urls = [ + 'list' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents"; + }, + 'create' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents"; + }, + 'read' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; + }, + 'update' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; + }, + 'delete' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; + }, + ]; + + // NOTE: `params` and `urls` are not used internally in the `Schema::build` function below! + $params = [ + 'list' => function (string $databaseId, string $collectionId, array $args) { + return ['queries' => $args['queries']]; + }, + 'create' => function (string $databaseId, string $collectionId, array $args) { + $id = $args['id'] ?? 'unique()'; + $permissions = $args['permissions'] ?? null; + + unset($args['id']); + unset($args['permissions']); + + // Order must be the same as the route params + return [ + 'databaseId' => $databaseId, + 'documentId' => $id, + 'collectionId' => $collectionId, + 'data' => $args, + 'permissions' => $permissions, + ]; + }, + 'update' => function (string $databaseId, string $collectionId, array $args) { + $documentId = $args['id']; + $permissions = $args['permissions'] ?? null; + + unset($args['id']); + unset($args['permissions']); + + // Order must be the same as the route params + return [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'documentId' => $documentId, + 'data' => $args, + 'permissions' => $permissions, + ]; + }, + ]; + + return Schema::build( + $utopia, + $complexity, + $attributes, + $urls, + $params, + ); + }, ['utopia', 'dbForProject', 'authorization']); + + $container->set('audit', function ($dbForProject) { + $adapter = new AdapterDatabase($dbForProject); + + return new Audit($adapter); + }, ['dbForProject']); + + $container->set('mode', function ($request) { + /** @var Appwrite\Utopia\Request $request */ + + /** + * Defines the mode for the request: + * - 'default' => Requests for Client and Server Side + * - 'admin' => Request from the Console on non-console projects + */ + return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); + }, ['request']); + + $container->set('requestTimestamp', function ($request) { + // TODO: Move this to the Request class itself + $timestampHeader = $request->getHeader('x-appwrite-timestamp'); + $requestTimestamp = null; + if (! empty($timestampHeader)) { + try { + $requestTimestamp = new \DateTime($timestampHeader); + } catch (\Throwable $e) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid X-Appwrite-Timestamp header value'); } } - } - $dbForProject->setMetadata('user', $user->getId()); - $dbForPlatform->setMetadata('user', $user->getId()); + return $requestTimestamp; + }, ['request']); - return $user; -}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']); + $container->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) { + $devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', '')); -$container->set('project', function ($dbForPlatform, $request, $console, $authorization) { - /** @var Appwrite\Utopia\Request $request */ - /** @var Utopia\Database\Database $dbForPlatform */ - /** @var Utopia\Database\Document $console */ - $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', '')); - // Realtime channel "project" can send project=Query array - if (! \is_string($projectId)) { - $projectId = $request->getHeader('x-appwrite-project', ''); - } - - if (empty($projectId) || $projectId === 'console') { - return $console; - } - - $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); - - return $project; -}, ['dbForPlatform', 'request', 'console', 'authorization']); - -$container->set('session', function (User $user, Store $store, Token $proofForToken) { - if ($user->isEmpty()) { - return; - } - - $sessions = $user->getAttribute('sessions', []); - $sessionId = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken); - - if (! $sessionId) { - return; - } - foreach ($sessions as $session) { - /** @var Document $session */ - if ($sessionId === $session->getId()) { - return $session; + // Check if given key match project's development keys + $key = $project->find('secret', $devKey, 'devKeys'); + if (! $key) { + return new Document([]); } - } -}, ['user', 'store', 'proofForToken']); + // check expiration + $expire = $key->getAttribute('expire'); + if (! empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) { + return new Document([]); + } + + // update access time + $accessedAt = $key->getAttribute('accessedAt', 0); + if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) { + $key->setAttribute('accessedAt', DatabaseDateTime::now()); + $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([ + 'accessedAt' => $key->getAttribute('accessedAt') + ]))); + $dbForPlatform->purgeCachedDocument('projects', $project->getId()); + } + + // add sdk to key + $sdkValidator = new WhiteList($servers, true); + $sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN')); + + if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) { + $sdks = $key->getAttribute('sdks', []); + + if (! in_array($sdk, $sdks)) { + $sdks[] = $sdk; + $key->setAttribute('sdks', $sdks); + + /** Update access time as well */ + $key->setAttribute('accessedAt', DatabaseDateTime::now()); + $key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([ + 'sdks' => $key->getAttribute('sdks'), + 'accessedAt' => $key->getAttribute('accessedAt') + ]))); + $dbForPlatform->purgeCachedDocument('projects', $project->getId()); + } + } + + return $key; + }, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']); + + $container->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) { + $teamInternalId = ''; + if ($project->getId() !== 'console') { + $teamInternalId = $project->getAttribute('teamInternalId', ''); + } else { + $route = $utopia->match($request); + $path = ! empty($route) ? $route->getPath() : $request->getURI(); + $orgHeader = $request->getHeader('x-appwrite-organization', ''); + if (str_starts_with($path, '/v1/projects/:projectId')) { + $uri = $request->getURI(); + $pid = explode('/', $uri)[3]; + $p = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $pid)); + $teamInternalId = $p->getAttribute('teamInternalId', ''); + } elseif ($path === '/v1/projects') { + $teamId = $request->getParam('teamId', ''); + + if (empty($teamId)) { + return new Document([]); + } + + $team = $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $teamId)); + + return $team; + } elseif (! empty($orgHeader)) { + return $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $orgHeader)); + } + } + + // if teamInternalId is empty, return an empty document + + if (empty($teamInternalId)) { + return new Document([]); + } + + $team = $authorization->skip(function () use ($dbForPlatform, $teamInternalId) { + return $dbForPlatform->findOne('teams', [ + Query::equal('$sequence', [$teamInternalId]), + ]); + }); + + return $team; + }, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']); + + $container->set('previewHostname', function (Request $request, ?Key $apiKey) { + $allowed = false; + + if (Http::isDevelopment()) { + $allowed = true; + } elseif (! \is_null($apiKey) && $apiKey->getHostnameOverride() === true) { + $allowed = true; + } + + if ($allowed) { + $host = $request->getQuery('appwrite-hostname', $request->getHeader('x-appwrite-hostname', '')) ?? ''; + if (! empty($host)) { + return $host; + } + } + + return ''; + }, ['request', 'apiKey']); + + $container->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key { + $key = $request->getHeader('x-appwrite-key'); + + if (empty($key)) { + return null; + } + + $key = Key::decode($project, $team, $user, $key); + + $userHeader = $request->getHeader('x-appwrite-user'); + $organizationHeader = $request->getHeader('x-appwrite-organization'); + $projectHeader = $request->getHeader('x-appwrite-project'); + + if (! empty($key->getProjectId())) { + if (empty($projectHeader) || $projectHeader !== $key->getProjectId()) { + throw new Exception(Exception::PROJECT_ID_MISSING); + } + } + + if (! empty($key->getUserId())) { + if (empty($userHeader) || $userHeader !== $key->getUserId()) { + throw new Exception(Exception::USER_ID_MISSING); + } + } + + if (! empty($key->getTeamId())) { + if (empty($organizationHeader) || $organizationHeader !== $key->getTeamId()) { + throw new Exception(Exception::ORGANIZATION_ID_MISSING); + } + } + + return $key; + }, ['request', 'project', 'team', 'user']); + + $container->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) { + $tokenJWT = $request->getParam('token'); + + if (! empty($tokenJWT) && ! $project->isEmpty()) { // JWT authentication + // Use a large but reasonable maxAge to avoid auto-exp when token has no expiry + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), RESOURCE_TOKEN_ALGORITHM, RESOURCE_TOKEN_MAX_AGE, RESOURCE_TOKEN_LEEWAY); // Instantiate with key, algo, maxAge and leeway. + + try { + $payload = $jwt->decode($tokenJWT); + } catch (JWTException $error) { + return new Document([]); + } + + $tokenId = $payload['tokenId'] ?? ''; + if (empty($tokenId)) { + return new Document([]); + } + + $token = $authorization->skip(fn () => $dbForProject->getDocument('resourceTokens', $tokenId)); + + if ($token->isEmpty()) { + return new Document([]); + } + + $expiry = $token->getAttribute('expire'); + + if ($expiry !== null) { + $now = new \DateTime(); + $expiryDate = new \DateTime($expiry); + + if ($expiryDate < $now) { + return new Document([]); + } + } + + return match ($token->getAttribute('resourceType')) { + TOKENS_RESOURCE_TYPE_FILES => (function () use ($token, $dbForProject, $authorization) { + $sequences = explode(':', $token->getAttribute('resourceInternalId')); + $ids = explode(':', $token->getAttribute('resourceId')); + + if (count($sequences) !== 2 || count($ids) !== 2) { + return new Document([]); + } + + $accessedAt = $token->getAttribute('accessedAt', 0); + if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) { + $token->setAttribute('accessedAt', DatabaseDateTime::now()); + $authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), new Document([ + 'accessedAt' => $token->getAttribute('accessedAt') + ]))); + } + + return new Document([ + 'bucketId' => $ids[0], + 'fileId' => $ids[1], + 'bucketInternalId' => $sequences[0], + 'fileInternalId' => $sequences[1], + ]); + })(), + + default => throw new Exception(Exception::TOKEN_RESOURCE_TYPE_INVALID), + }; + } + + return new Document([]); + }, ['project', 'dbForProject', 'request', 'authorization']); + + $container->set('transactionState', function (Database $dbForProject, Authorization $authorization) { + return new TransactionState($dbForProject, $authorization); + }, ['dbForProject', 'authorization']); + + $container->set('executionsRetentionCount', function (Document $project, array $plan) { + if ($project->getId() === 'console' || empty($plan)) { + return 0; + } + + return (int) ($plan['executionsRetentionCount'] ?? 100); + }, ['project', 'plan']); + + $container->set('deviceForFiles', function ($project, Telemetry $telemetry) { + return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); + }, ['project', 'telemetry']); + $container->set('deviceForSites', function ($project, Telemetry $telemetry) { + return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())); + }, ['project', 'telemetry']); + $container->set('deviceForMigrations', function ($project, Telemetry $telemetry) { + return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())); + }, ['project', 'telemetry']); + $container->set('deviceForFunctions', function ($project, Telemetry $telemetry) { + return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())); + }, ['project', 'telemetry']); + $container->set('deviceForBuilds', function ($project, Telemetry $telemetry) { + return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); + }, ['project', 'telemetry']); +} $container->set('store', function (): Store { return new Store(); @@ -559,245 +1165,6 @@ $container->set('authorization', function () { return new Authorization(); }, []); -$container->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization) { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForPlatform; - } - - $database = $project->getAttribute('database', ''); - if (empty($database)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Project database is not configured'); - } - - try { - $dsn = new DSN($database); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $database); - } - - $adapter = new DatabasePool($pools->get($dsn->getHost())); - $database = new Database($adapter, $cache); - - $database - ->setDatabase(APP_DATABASE) - ->setAuthorization($authorization) - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()) - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) - ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); - $database->setDocumentType('users', User::class); - - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - - if (\in_array($dsn->getHost(), $sharedTables)) { - $database - ->setSharedTables(true) - ->setTenant($project->getSequence()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getSequence()); - } - - /** - * This isolated event handling for `users.*.create` which is based on a `Database::EVENT_DOCUMENT_CREATE` listener may look odd, but it is **intentional**. - * - * Accounts can be created in many ways beyond `createAccount` - * (anonymous, OAuth, phone, etc.), and those flows are probably not covered in event tests; so we handle this here. - */ - $eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) { - // Only trigger events for user creation with the database listener. - if ($document->getCollection() !== 'users') { - return; - } - - $queueForEvents - ->setEvent('users.[userId].create') - ->setParam('userId', $document->getId()) - ->setPayload($response->output($document, Response::MODEL_USER)); - - // Trigger functions, webhooks, and realtime events - $queueForFunctions - ->from($queueForEvents) - ->trigger(); - - /** Trigger webhooks events only if a project has them enabled */ - if (! empty($project->getAttribute('webhooks'))) { - $queueForWebhooks - ->from($queueForEvents) - ->trigger(); - } - - /** Trigger realtime events only for non console events */ - if ($queueForEvents->getProject()->getId() !== 'console') { - $queueForRealtime - ->from($queueForEvents) - ->trigger(); - } - }; - - /** - * Purge function events cache when functions are created, updated or deleted. - */ - $functionsEventsCacheListener = function (string $event, Document $document, Document $project, Database $dbForProject) { - - if ($document->getCollection() !== 'functions') { - return; - } - - if ($project->isEmpty() || $project->getId() === 'console') { - return; - } - - $hostname = $dbForProject->getAdapter()->getHostname(); - $cacheKey = \sprintf( - '%s-cache-%s:%s:%s:project:%s:functions:events', - $dbForProject->getCacheName(), - $hostname ?? '', - $dbForProject->getNamespace(), - $dbForProject->getTenant(), - $project->getId() - ); - - $dbForProject->getCache()->purge($cacheKey); - }; - - $usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) { - $value = 1; - - switch ($event) { - case Database::EVENT_DOCUMENT_DELETE: - $value = -1; - break; - case Database::EVENT_DOCUMENTS_DELETE: - $value = -1 * $document->getAttribute('modified', 0); - break; - case Database::EVENT_DOCUMENTS_CREATE: - $value = $document->getAttribute('modified', 0); - break; - case Database::EVENT_DOCUMENTS_UPSERT: - $value = $document->getAttribute('created', 0); - break; - } - - switch (true) { - case $document->getCollection() === 'teams': - $usage->addMetric(METRIC_TEAMS, $value); // per project - break; - case $document->getCollection() === 'users': - $usage->addMetric(METRIC_USERS, $value); // per project - if ($event === Database::EVENT_DOCUMENT_DELETE) { - $usage->addReduce($document); - } - break; - case $document->getCollection() === 'sessions': // sessions - $usage->addMetric(METRIC_SESSIONS, $value); // per project - break; - case $document->getCollection() === 'databases': // databases - $usage->addMetric(METRIC_DATABASES, $value); // per project - - if ($event === Database::EVENT_DOCUMENT_DELETE) { - $usage->addReduce($document); - } - break; - case str_starts_with($document->getCollection(), 'database_') && ! str_contains($document->getCollection(), 'collection'): // collections - $parts = explode('_', $document->getCollection()); - $databaseInternalId = $parts[1] ?? 0; - $usage - ->addMetric(METRIC_COLLECTIONS, $value) // per project - ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value); - - if ($event === Database::EVENT_DOCUMENT_DELETE) { - $usage->addReduce($document); - } - break; - case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): // documents - $parts = explode('_', $document->getCollection()); - $databaseInternalId = $parts[1] ?? 0; - $collectionInternalId = $parts[3] ?? 0; - $usage - ->addMetric(METRIC_DOCUMENTS, $value) // per project - ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database - ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection - break; - case $document->getCollection() === 'buckets': // buckets - $usage->addMetric(METRIC_BUCKETS, $value); // per project - if ($event === Database::EVENT_DOCUMENT_DELETE) { - $usage - ->addReduce($document); - } - break; - case str_starts_with($document->getCollection(), 'bucket_'): // files - $parts = explode('_', $document->getCollection()); - $bucketInternalId = $parts[1]; - $usage - ->addMetric(METRIC_FILES, $value) // per project - ->addMetric(METRIC_FILES_STORAGE, $document->getAttribute('sizeOriginal') * $value) // per project - ->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES), $value) // per bucket - ->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES_STORAGE), $document->getAttribute('sizeOriginal') * $value); // per bucket - break; - case $document->getCollection() === 'functions': - $usage->addMetric(METRIC_FUNCTIONS, $value); // per project - - if ($event === Database::EVENT_DOCUMENT_DELETE) { - $usage - ->addReduce($document); - } - break; - case $document->getCollection() === 'sites': - $usage->addMetric(METRIC_SITES, $value); // per project - - if ($event === Database::EVENT_DOCUMENT_DELETE) { - $usage - ->addReduce($document); - } - break; - case $document->getCollection() === 'deployments': - $usage - ->addMetric(METRIC_DEPLOYMENTS, $value) // per project - ->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project - ->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS), $value) // per function - ->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value) - ->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS), $value) // per function - ->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value); - break; - default: - break; - } - }; - - // Clone the queues, to prevent events triggered by the database listener - // from overwriting the events that are supposed to be triggered in the shutdown hook. - $queueForEventsClone = new Event($publisher); - $queueForFunctions = new Func($publisherFunctions); - $queueForWebhooks = new Webhook($publisherWebhooks); - $queueForRealtime = new Realtime(); - - $database - ->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) - ->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) - ->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) - ->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) - ->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) - ->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener( - $project, - $document, - $response, - $queueForEventsClone->from($queueForEvents), - $queueForFunctions->from($queueForEvents), - $queueForWebhooks->from($queueForEvents), - $queueForRealtime->from($queueForEvents) - )) - ->on(Database::EVENT_DOCUMENT_CREATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database)) - ->on(Database::EVENT_DOCUMENT_UPDATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database)) - ->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database)); - - return $database; -}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization']); - $container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) { $adapter = new DatabasePool($pools->get('console')); @@ -907,12 +1274,6 @@ $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization }; }, ['pools', 'cache', 'authorization']); -$container->set('audit', function ($dbForProject) { - $adapter = new AdapterDatabase($dbForProject); - - return new Audit($adapter); -}, ['dbForProject']); - $container->set('telemetry', fn () => new NoTelemetry()); $container->set('cache', function (Group $pools, Telemetry $telemetry) { @@ -953,22 +1314,6 @@ $container->set('timelimit', function (\Redis $redis) { $container->set('deviceForLocal', function (Telemetry $telemetry) { return new Device\Telemetry($telemetry, new Local()); }, ['telemetry']); -$container->set('deviceForFiles', function ($project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); -$container->set('deviceForSites', function ($project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())); -}, ['project', 'telemetry']); -$container->set('deviceForMigrations', function ($project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); -$container->set('deviceForFunctions', function ($project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); -$container->set('deviceForBuilds', function ($project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); - function getDevice(string $root, string $connection = ''): Device { $connection = ! empty($connection) ? $connection : System::getEnv('_APP_CONNECTIONS_STORAGE', ''); @@ -1076,17 +1421,6 @@ function getDevice(string $root, string $connection = ''): Device } } -$container->set('mode', function ($request) { - /** @var Appwrite\Utopia\Request $request */ - - /** - * Defines the mode for the request: - * - 'default' => Requests for Client and Server Side - * - 'admin' => Request from the Console on non-console projects - */ - return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); -}, ['request']); - $container->set('geodb', function ($register) { /** @var Utopia\Registry\Registry $register */ return $register->get('geodb'); @@ -1112,112 +1446,10 @@ $container->set('promiseAdapter', function ($register) { return $register->get('promiseAdapter'); }, ['register']); -$container->set('schema', function ($utopia, $dbForProject, $authorization) { - - $complexity = function (int $complexity, array $args) { - $queries = Query::parseQueries($args['queries'] ?? []); - $query = Query::getByType($queries, [Query::TYPE_LIMIT])[0] ?? null; - $limit = $query ? $query->getValue() : APP_LIMIT_LIST_DEFAULT; - - return $complexity * $limit; - }; - - $attributes = function (int $limit, int $offset) use ($dbForProject, $authorization) { - $attrs = $authorization->skip(fn () => $dbForProject->find('attributes', [ - Query::limit($limit), - Query::offset($offset), - ])); - - return \array_map(function ($attr) { - return $attr->getArrayCopy(); - }, $attrs); - }; - - $urls = [ - 'list' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents"; - }, - 'create' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents"; - }, - 'read' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; - }, - 'update' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; - }, - 'delete' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; - }, - ]; - - // NOTE: `params` and `urls` are not used internally in the `Schema::build` function below! - $params = [ - 'list' => function (string $databaseId, string $collectionId, array $args) { - return ['queries' => $args['queries']]; - }, - 'create' => function (string $databaseId, string $collectionId, array $args) { - $id = $args['id'] ?? 'unique()'; - $permissions = $args['permissions'] ?? null; - - unset($args['id']); - unset($args['permissions']); - - // Order must be the same as the route params - return [ - 'databaseId' => $databaseId, - 'documentId' => $id, - 'collectionId' => $collectionId, - 'data' => $args, - 'permissions' => $permissions, - ]; - }, - 'update' => function (string $databaseId, string $collectionId, array $args) { - $documentId = $args['id']; - $permissions = $args['permissions'] ?? null; - - unset($args['id']); - unset($args['permissions']); - - // Order must be the same as the route params - return [ - 'databaseId' => $databaseId, - 'collectionId' => $collectionId, - 'documentId' => $documentId, - 'data' => $args, - 'permissions' => $permissions, - ]; - }, - ]; - - return Schema::build( - $utopia, - $complexity, - $attributes, - $urls, - $params, - ); -}, ['utopia', 'dbForProject', 'authorization']); - $container->set('gitHub', function (Cache $cache) { return new VcsGitHub($cache); }, ['cache']); -$container->set('requestTimestamp', function ($request) { - // TODO: Move this to the Request class itself - $timestampHeader = $request->getHeader('x-appwrite-timestamp'); - $requestTimestamp = null; - if (! empty($timestampHeader)) { - try { - $requestTimestamp = new \DateTime($timestampHeader); - } catch (\Throwable $e) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid X-Appwrite-Timestamp header value'); - } - } - - return $requestTimestamp; -}, ['request']); - $container->set('plan', function (array $plan = []) { return []; }); @@ -1226,233 +1458,9 @@ $container->set('smsRates', function () { return []; }); -$container->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) { - $devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', '')); - - // Check if given key match project's development keys - $key = $project->find('secret', $devKey, 'devKeys'); - if (! $key) { - return new Document([]); - } - - // check expiration - $expire = $key->getAttribute('expire'); - if (! empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) { - return new Document([]); - } - - // update access time - $accessedAt = $key->getAttribute('accessedAt', 0); - if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) { - $key->setAttribute('accessedAt', DatabaseDateTime::now()); - $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([ - 'accessedAt' => $key->getAttribute('accessedAt') - ]))); - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - } - - // add sdk to key - $sdkValidator = new WhiteList($servers, true); - $sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN')); - - if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) { - $sdks = $key->getAttribute('sdks', []); - - if (! in_array($sdk, $sdks)) { - $sdks[] = $sdk; - $key->setAttribute('sdks', $sdks); - - /** Update access time as well */ - $key->setAttribute('accessedAt', DatabaseDateTime::now()); - $key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([ - 'sdks' => $key->getAttribute('sdks'), - 'accessedAt' => $key->getAttribute('accessedAt') - ]))); - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - } - } - - return $key; -}, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']); - -$container->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) { - $teamInternalId = ''; - if ($project->getId() !== 'console') { - $teamInternalId = $project->getAttribute('teamInternalId', ''); - } else { - $route = $utopia->match($request); - $path = ! empty($route) ? $route->getPath() : $request->getURI(); - $orgHeader = $request->getHeader('x-appwrite-organization', ''); - if (str_starts_with($path, '/v1/projects/:projectId')) { - $uri = $request->getURI(); - $pid = explode('/', $uri)[3]; - $p = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $pid)); - $teamInternalId = $p->getAttribute('teamInternalId', ''); - } elseif ($path === '/v1/projects') { - $teamId = $request->getParam('teamId', ''); - - if (empty($teamId)) { - return new Document([]); - } - - $team = $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $teamId)); - - return $team; - } elseif (! empty($orgHeader)) { - return $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $orgHeader)); - } - } - - // if teamInternalId is empty, return an empty document - - if (empty($teamInternalId)) { - return new Document([]); - } - - $team = $authorization->skip(function () use ($dbForPlatform, $teamInternalId) { - return $dbForPlatform->findOne('teams', [ - Query::equal('$sequence', [$teamInternalId]), - ]); - }); - - return $team; -}, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']); - $container->set( 'isResourceBlocked', fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false ); -$container->set('previewHostname', function (Request $request, ?Key $apiKey) { - $allowed = false; - - if (Http::isDevelopment()) { - $allowed = true; - } elseif (! \is_null($apiKey) && $apiKey->getHostnameOverride() === true) { - $allowed = true; - } - - if ($allowed) { - $host = $request->getQuery('appwrite-hostname', $request->getHeader('x-appwrite-hostname', '')) ?? ''; - if (! empty($host)) { - return $host; - } - } - - return ''; -}, ['request', 'apiKey']); - -$container->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key { - $key = $request->getHeader('x-appwrite-key'); - - if (empty($key)) { - return null; - } - - $key = Key::decode($project, $team, $user, $key); - - $userHeader = $request->getHeader('x-appwrite-user'); - $organizationHeader = $request->getHeader('x-appwrite-organization'); - $projectHeader = $request->getHeader('x-appwrite-project'); - - if (! empty($key->getProjectId())) { - if (empty($projectHeader) || $projectHeader !== $key->getProjectId()) { - throw new Exception(Exception::PROJECT_ID_MISSING); - } - } - - if (! empty($key->getUserId())) { - if (empty($userHeader) || $userHeader !== $key->getUserId()) { - throw new Exception(Exception::USER_ID_MISSING); - } - } - - if (! empty($key->getTeamId())) { - if (empty($organizationHeader) || $organizationHeader !== $key->getTeamId()) { - throw new Exception(Exception::ORGANIZATION_ID_MISSING); - } - } - - return $key; -}, ['request', 'project', 'team', 'user']); - $container->set('executor', fn () => new Executor()); - -$container->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) { - $tokenJWT = $request->getParam('token'); - - if (! empty($tokenJWT) && ! $project->isEmpty()) { // JWT authentication - // Use a large but reasonable maxAge to avoid auto-exp when token has no expiry - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), RESOURCE_TOKEN_ALGORITHM, RESOURCE_TOKEN_MAX_AGE, RESOURCE_TOKEN_LEEWAY); // Instantiate with key, algo, maxAge and leeway. - - try { - $payload = $jwt->decode($tokenJWT); - } catch (JWTException $error) { - return new Document([]); - } - - $tokenId = $payload['tokenId'] ?? ''; - if (empty($tokenId)) { - return new Document([]); - } - - $token = $authorization->skip(fn () => $dbForProject->getDocument('resourceTokens', $tokenId)); - - if ($token->isEmpty()) { - return new Document([]); - } - - $expiry = $token->getAttribute('expire'); - - if ($expiry !== null) { - $now = new \DateTime(); - $expiryDate = new \DateTime($expiry); - - if ($expiryDate < $now) { - return new Document([]); - } - } - - return match ($token->getAttribute('resourceType')) { - TOKENS_RESOURCE_TYPE_FILES => (function () use ($token, $dbForProject, $authorization) { - $sequences = explode(':', $token->getAttribute('resourceInternalId')); - $ids = explode(':', $token->getAttribute('resourceId')); - - if (count($sequences) !== 2 || count($ids) !== 2) { - return new Document([]); - } - - $accessedAt = $token->getAttribute('accessedAt', 0); - if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) { - $token->setAttribute('accessedAt', DatabaseDateTime::now()); - $authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), new Document([ - 'accessedAt' => $token->getAttribute('accessedAt') - ]))); - } - - return new Document([ - 'bucketId' => $ids[0], - 'fileId' => $ids[1], - 'bucketInternalId' => $sequences[0], - 'fileInternalId' => $sequences[1], - ]); - })(), - - default => throw new Exception(Exception::TOKEN_RESOURCE_TYPE_INVALID), - }; - } - - return new Document([]); -}, ['project', 'dbForProject', 'request', 'authorization']); - -$container->set('transactionState', function (Database $dbForProject, Authorization $authorization) { - return new TransactionState($dbForProject, $authorization); -}, ['dbForProject', 'authorization']); - -$container->set('executionsRetentionCount', function (Document $project, array $plan) { - if ($project->getId() === 'console' || empty($plan)) { - return 0; - } - - return (int) ($plan['executionsRetentionCount'] ?? 100); -}, ['project', 'plan']); diff --git a/app/realtime.php b/app/realtime.php index 435b0aaa1d..bee8c02dc9 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -614,6 +614,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, Console::info("Connection open (user: {$connection})"); $container->set('pools', fn () => $register->get('pools')); + registerRequestResources($container); $adapter = new \Utopia\Http\Adapter\FPM\Server($container); $app = new Http($adapter, 'UTC'); $app->setResource('request', fn () => $request); From 27e5ade92b86fac42d5cce9bc572f24fad98b180 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 09:43:54 +0530 Subject: [PATCH 021/118] fix login --- composer.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.lock b/composer.lock index 09d7107c70..cc3d2374e7 100644 --- a/composer.lock +++ b/composer.lock @@ -4307,12 +4307,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "61d9d2cefc06e549c521f5798b20faabe9478b98" + "reference": "6d7f82e50f9517d8d13667edccdafbc3560447d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/61d9d2cefc06e549c521f5798b20faabe9478b98", - "reference": "61d9d2cefc06e549c521f5798b20faabe9478b98", + "url": "https://api.github.com/repos/utopia-php/http/zipball/6d7f82e50f9517d8d13667edccdafbc3560447d0", + "reference": "6d7f82e50f9517d8d13667edccdafbc3560447d0", "shasum": "" }, "require": { @@ -4353,7 +4353,7 @@ "issues": "https://github.com/utopia-php/http/issues", "source": "https://github.com/utopia-php/http/tree/feat/coroutines-option" }, - "time": "2026-03-16T18:01:51+00:00" + "time": "2026-03-17T04:12:13+00:00" }, { "name": "utopia-php/image", @@ -6235,11 +6235,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.40", + "version": "2.1.41", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", - "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a2eae8f20856b3afe74bf1f9726ce8c11438e300", + "reference": "a2eae8f20856b3afe74bf1f9726ce8c11438e300", "shasum": "" }, "require": { @@ -6284,7 +6284,7 @@ "type": "github" } ], - "time": "2026-02-23T15:04:35+00:00" + "time": "2026-03-16T18:24:10+00:00" }, { "name": "phpunit/php-code-coverage", From c900b22dc09254cea1c4edd7f4e99e81f64bef53 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 09:52:55 +0530 Subject: [PATCH 022/118] fix connection container and view class --- app/realtime.php | 7 ++++--- composer.lock | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index bee8c02dc9..70a655117d 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -613,9 +613,10 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, Console::info("Connection open (user: {$connection})"); - $container->set('pools', fn () => $register->get('pools')); - registerRequestResources($container); - $adapter = new \Utopia\Http\Adapter\FPM\Server($container); + $connectionContainer = new Container($container); + $connectionContainer->set('pools', fn () => $register->get('pools')); + registerRequestResources($connectionContainer); + $adapter = new \Utopia\Http\Adapter\FPM\Server($connectionContainer); $app = new Http($adapter, 'UTC'); $app->setResource('request', fn () => $request); $app->setResource('response', fn () => $response); diff --git a/composer.lock b/composer.lock index cc3d2374e7..35bd811100 100644 --- a/composer.lock +++ b/composer.lock @@ -4307,12 +4307,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "6d7f82e50f9517d8d13667edccdafbc3560447d0" + "reference": "ff4be62f2086578babcc671a3f8ede3c9e958b58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/6d7f82e50f9517d8d13667edccdafbc3560447d0", - "reference": "6d7f82e50f9517d8d13667edccdafbc3560447d0", + "url": "https://api.github.com/repos/utopia-php/http/zipball/ff4be62f2086578babcc671a3f8ede3c9e958b58", + "reference": "ff4be62f2086578babcc671a3f8ede3c9e958b58", "shasum": "" }, "require": { @@ -4353,7 +4353,7 @@ "issues": "https://github.com/utopia-php/http/issues", "source": "https://github.com/utopia-php/http/tree/feat/coroutines-option" }, - "time": "2026-03-17T04:12:13+00:00" + "time": "2026-03-17T04:17:19+00:00" }, { "name": "utopia-php/image", From 22bd655ad19eb2973b554dda19bb5709c8ee4b1d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 10:28:08 +0530 Subject: [PATCH 023/118] fix autoload --- composer.lock | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 35bd811100..9ae5187c96 100644 --- a/composer.lock +++ b/composer.lock @@ -4307,12 +4307,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "ff4be62f2086578babcc671a3f8ede3c9e958b58" + "reference": "dc674196b7181067cd798470d1db84dcb5a140ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/ff4be62f2086578babcc671a3f8ede3c9e958b58", - "reference": "ff4be62f2086578babcc671a3f8ede3c9e958b58", + "url": "https://api.github.com/repos/utopia-php/http/zipball/dc674196b7181067cd798470d1db84dcb5a140ec", + "reference": "dc674196b7181067cd798470d1db84dcb5a140ec", "shasum": "" }, "require": { @@ -4334,8 +4334,7 @@ "type": "library", "autoload": { "psr-4": { - "Utopia\\": "src/", - "Tests\\E2E\\": "tests/e2e" + "Utopia\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4353,7 +4352,7 @@ "issues": "https://github.com/utopia-php/http/issues", "source": "https://github.com/utopia-php/http/tree/feat/coroutines-option" }, - "time": "2026-03-17T04:17:19+00:00" + "time": "2026-03-17T04:57:28+00:00" }, { "name": "utopia-php/image", From d60858cd8fcccabf605aeac458b8d2943bc74520 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 10:37:49 +0530 Subject: [PATCH 024/118] fix phpstan --- composer.lock | 8 +- phpstan-baseline.neon | 810 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 727 insertions(+), 91 deletions(-) diff --git a/composer.lock b/composer.lock index 9ae5187c96..a0f74125ce 100644 --- a/composer.lock +++ b/composer.lock @@ -4307,12 +4307,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "dc674196b7181067cd798470d1db84dcb5a140ec" + "reference": "fbcf5a3ca934f59fa4c4929f003ba0d563cc1f02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/dc674196b7181067cd798470d1db84dcb5a140ec", - "reference": "dc674196b7181067cd798470d1db84dcb5a140ec", + "url": "https://api.github.com/repos/utopia-php/http/zipball/fbcf5a3ca934f59fa4c4929f003ba0d563cc1f02", + "reference": "fbcf5a3ca934f59fa4c4929f003ba0d563cc1f02", "shasum": "" }, "require": { @@ -4352,7 +4352,7 @@ "issues": "https://github.com/utopia-php/http/issues", "source": "https://github.com/utopia-php/http/tree/feat/coroutines-option" }, - "time": "2026-03-17T04:57:28+00:00" + "time": "2026-03-17T05:01:57+00:00" }, { "name": "utopia-php/image", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0219836405..86d007432d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -72,12 +72,6 @@ parameters: count: 1 path: app/cli.php - - - message: '#^Parameter \#1 \$name of method Utopia\\DI\\Injection\:\:inject\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: app/cli.php - - message: '#^Parameter \#1 \$pool of class Utopia\\Cache\\Adapter\\Pool constructor expects Utopia\\Pools\\Pool\, mixed given\.$#' identifier: argument.type @@ -109,7 +103,7 @@ parameters: path: app/cli.php - - message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, string\|false given\.$#' + message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string, string\|false given\.$#' identifier: argument.type count: 1 path: app/cli.php @@ -126,12 +120,6 @@ parameters: count: 1 path: app/cli.php - - - message: '#^Variable \$dbForPlatform might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: app/cli.php - - message: '#^Cannot access offset ''files'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible @@ -2070,6 +2058,12 @@ parameters: count: 18 path: app/controllers/api/messaging.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: app/controllers/api/messaging.php + - message: '#^Parameter \#2 \$default of method Utopia\\Locale\\Locale\:\:getText\(\) expects string\|null, false given\.$#' identifier: argument.type @@ -2868,6 +2862,12 @@ parameters: count: 1 path: app/controllers/api/users.php + - + message: '#^Parameter \#1 \$array of function array_unique expects an array of values castable to string, array given\.$#' + identifier: argument.type + count: 1 + path: app/controllers/api/users.php + - message: '#^Parameter \#1 \$dictionary of class Appwrite\\Auth\\Validator\\PasswordDictionary constructor expects array, mixed given\.$#' identifier: argument.type @@ -3487,7 +3487,7 @@ parameters: path: app/controllers/general.php - - message: '#^Parameter \#1 \$value of function intval expects array\|bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function intval expects array\|bool\|float\|GMP\|int\|resource\|SimpleXMLElement\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: app/controllers/general.php @@ -3498,6 +3498,18 @@ parameters: count: 2 path: app/controllers/general.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: app/controllers/general.php + + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 3 + path: app/controllers/general.php + - message: '#^Parameter \#2 \$default of method Utopia\\DSN\\DSN\:\:getParam\(\) expects string, float given\.$#' identifier: argument.type @@ -3505,7 +3517,7 @@ parameters: path: app/controllers/general.php - - message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, string\|false given\.$#' + message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string, string\|false given\.$#' identifier: argument.type count: 1 path: app/controllers/general.php @@ -3666,6 +3678,12 @@ parameters: count: 1 path: app/controllers/general.php + - + message: '#^Unknown parameter \$override in call to method Utopia\\Http\\Response\:\:addHeader\(\)\.$#' + identifier: argument.unknown + count: 1 + path: app/controllers/general.php + - message: '#^Variable \$body on left side of \?\? always exists and is not nullable\.$#' identifier: nullCoalesce.variable @@ -3966,6 +3984,12 @@ parameters: count: 1 path: app/controllers/shared/api.php + - + message: '#^Parameter \#1 \$array of function array_intersect expects an array of values castable to string, array\ given\.$#' + identifier: argument.type + count: 1 + path: app/controllers/shared/api.php + - message: '#^Parameter \#1 \$array of function array_shift expects array, mixed given\.$#' identifier: argument.type @@ -4062,6 +4086,12 @@ parameters: count: 2 path: app/controllers/shared/api.php + - + message: '#^Parameter \#2 \$arrays of function array_intersect expects an array of values castable to string, array\ given\.$#' + identifier: argument.type + count: 1 + path: app/controllers/shared/api.php + - message: '#^Parameter \#2 \$data of method Utopia\\Cache\\Cache\:\:save\(\) expects array\\|string, mixed given\.$#' identifier: argument.type @@ -4278,6 +4308,12 @@ parameters: count: 1 path: app/controllers/web/home.php + - + message: '#^Anonymous function has an unused use \$register\.$#' + identifier: closure.unusedUse + count: 1 + path: app/http.php + - message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#' identifier: foreach.nonIterable @@ -4446,6 +4482,12 @@ parameters: count: 1 path: app/http.php + - + message: '#^Cannot call method end\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: app/http.php + - message: '#^Cannot call method get\(\) on mixed\.$#' identifier: method.nonObject @@ -4464,12 +4506,6 @@ parameters: count: 1 path: app/http.php - - - message: '#^Cannot call method getResource\(\) on mixed\.$#' - identifier: method.nonObject - count: 1 - path: app/http.php - - message: '#^Cannot call method getRoles\(\) on mixed\.$#' identifier: method.nonObject @@ -4482,12 +4518,30 @@ parameters: count: 1 path: app/http.php + - + message: '#^Cannot call method getSwooleRequest\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: app/http.php + + - + message: '#^Cannot call method getSwooleResponse\(\) on mixed\.$#' + identifier: method.nonObject + count: 2 + path: app/http.php + - message: '#^Cannot call method isEmpty\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: app/http.php + - + message: '#^Cannot call method set\(\) on mixed\.$#' + identifier: method.nonObject + count: 7 + path: app/http.php + - message: '#^Cannot call method setAction\(\) on mixed\.$#' identifier: method.nonObject @@ -4524,6 +4578,12 @@ parameters: count: 1 path: app/http.php + - + message: '#^Cannot call method setStatusCode\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: app/http.php + - message: '#^Cannot call method setType\(\) on mixed\.$#' identifier: method.nonObject @@ -4596,12 +4656,6 @@ parameters: count: 1 path: app/http.php - - - message: '#^Parameter \#1 \$content of method Swoole\\Http\\Response\:\:end\(\) expects string\|null, string\|false given\.$#' - identifier: argument.type - count: 1 - path: app/http.php - - message: '#^Parameter \#1 \$haystack of function str_ends_with expects string, mixed given\.$#' identifier: argument.type @@ -4644,6 +4698,18 @@ parameters: count: 1 path: app/http.php + - + message: '#^Parameter \#1 \$request of class Appwrite\\Utopia\\Request constructor expects Swoole\\Http\\Request, mixed given\.$#' + identifier: argument.type + count: 1 + path: app/http.php + + - + message: '#^Parameter \#1 \$response of class Appwrite\\Utopia\\Response constructor expects Swoole\\Http\\Response, mixed given\.$#' + identifier: argument.type + count: 1 + path: app/http.php + - message: '#^Parameter \#1 \$string of function ltrim expects string, string\|false given\.$#' identifier: argument.type @@ -4681,15 +4747,15 @@ parameters: path: app/http.php - - message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, int given\.$#' + message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string, string\|false given\.$#' identifier: argument.type - count: 5 + count: 1 path: app/http.php - - message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, string\|false given\.$#' + message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, int given\.$#' identifier: argument.type - count: 1 + count: 5 path: app/http.php - @@ -4717,17 +4783,11 @@ parameters: path: app/http.php - - message: '#^Parameter \$port of class Swoole\\Http\\Server constructor expects int, string given\.$#' + message: '#^Parameter \$container of class Utopia\\Http\\Adapter\\Swoole\\HttpServer constructor expects Utopia\\DI\\Container\|null, mixed given\.$#' identifier: argument.type count: 1 path: app/http.php - - - message: '#^Variable \$database might not be defined\.$#' - identifier: variable.undefined - count: 4 - path: app/http.php - - message: '#^Variable \$register might not be defined\.$#' identifier: variable.undefined @@ -4848,6 +4908,18 @@ parameters: count: 2 path: app/init/database/filters.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 3 + path: app/init/database/filters.php + + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\, mixed\> given\.$#' + identifier: argument.type + count: 1 + path: app/init/database/filters.php + - message: '#^Parameter \#2 \$haystack of function in_array expects array, mixed given\.$#' identifier: argument.type @@ -5424,6 +5496,12 @@ parameters: count: 1 path: app/init/resources.php + - + message: '#^Parameter \#1 \$array of function array_unique expects an array of values castable to string, array given\.$#' + identifier: argument.type + count: 2 + path: app/init/resources.php + - message: '#^Parameter \#1 \$data of method Utopia\\Auth\\Store\:\:decode\(\) expects string, mixed given\.$#' identifier: argument.type @@ -6006,6 +6084,12 @@ parameters: count: 1 path: app/realtime.php + - + message: '#^Parameter \#1 \$parent of class Utopia\\DI\\Container constructor expects Psr\\Container\\ContainerInterface\|null, mixed given\.$#' + identifier: argument.type + count: 1 + path: app/realtime.php + - message: '#^Parameter \#1 \$pool of class Appwrite\\PubSub\\Adapter\\Pool constructor expects Utopia\\Pools\\Pool, mixed given\.$#' identifier: argument.type @@ -6079,15 +6163,15 @@ parameters: path: app/realtime.php - - message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, int given\.$#' + message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string, string\|false given\.$#' identifier: argument.type - count: 4 + count: 1 path: app/realtime.php - - message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, string\|false given\.$#' + message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, int given\.$#' identifier: argument.type - count: 1 + count: 4 path: app/realtime.php - @@ -6301,15 +6385,15 @@ parameters: path: app/worker.php - - message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, int given\.$#' + message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string, string\|false given\.$#' identifier: argument.type - count: 6 + count: 2 path: app/worker.php - - message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, string\|false given\.$#' + message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, int given\.$#' identifier: argument.type - count: 2 + count: 6 path: app/worker.php - @@ -6666,6 +6750,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Amazon.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Amazon.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Amazon\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -6774,6 +6864,12 @@ parameters: count: 4 path: src/Appwrite/Auth/OAuth2/Apple.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Apple.php + - message: '#^Parameter \#2 \$start of function mb_substr expects int, float\|int given\.$#' identifier: argument.type @@ -6894,6 +6990,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Auth0.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Auth/OAuth2/Auth0.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Auth0\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -6984,6 +7086,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Authentik.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Auth/OAuth2/Authentik.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Authentik\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -7050,6 +7158,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Autodesk.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Autodesk.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Autodesk\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -7140,6 +7254,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Bitbucket.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Bitbucket.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Bitbucket\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -7284,6 +7404,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Box.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Auth/OAuth2/Box.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Box\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -7362,6 +7488,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Dailymotion.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 3 + path: src/Appwrite/Auth/OAuth2/Dailymotion.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Dailymotion\:\:\$fields type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -7434,6 +7566,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Discord.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Auth/OAuth2/Discord.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Discord\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -7512,6 +7650,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Disqus.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Auth/OAuth2/Disqus.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Disqus\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -7734,6 +7878,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Facebook.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Facebook.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Facebook\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -7800,6 +7950,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Figma.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Figma.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Figma\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -7908,6 +8064,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Github.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Github.php + - message: '#^Parameter \#4 \$payload of method Appwrite\\Auth\\OAuth2\:\:request\(\) expects string, string\|false given\.$#' identifier: argument.type @@ -7992,6 +8154,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Gitlab.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Gitlab.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Gitlab\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -8058,6 +8226,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Google.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Google.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Google\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -8142,6 +8316,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Linkedin.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Linkedin.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Linkedin\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -8232,6 +8412,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Microsoft.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Auth/OAuth2/Microsoft.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Microsoft\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -8304,6 +8490,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Mock.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Mock.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Mock\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -8538,6 +8730,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Oidc.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Auth/OAuth2/Oidc.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Oidc\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -8646,6 +8844,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Okta.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Auth/OAuth2/Okta.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Okta\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -8748,6 +8952,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Paypal.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Paypal.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Paypal\:\:\$endpoint type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -8851,7 +9061,7 @@ parameters: path: src/Appwrite/Auth/OAuth2/Podio.php - - message: '#^Parameter \#1 \$value of function strval expects bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function strval expects bool\|float\|GMP\|int\|resource\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: src/Appwrite/Auth/OAuth2/Podio.php @@ -8940,6 +9150,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Salesforce.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Salesforce.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Salesforce\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -9030,6 +9246,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Slack.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Slack.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Slack\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -9096,6 +9318,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Spotify.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Spotify.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Spotify\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -9162,6 +9390,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Stripe.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Stripe.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Stripe\:\:\$grantType type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -9270,6 +9504,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Tradeshift.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Tradeshift.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Tradeshift\:\:\$apiDomain type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -9366,6 +9606,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Twitch.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Twitch.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Twitch\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -9510,6 +9756,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Yahoo.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Yahoo.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Yahoo\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -9648,6 +9900,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Yandex.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Yandex.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Yandex\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -9708,6 +9966,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Zoho.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Auth/OAuth2/Zoho.php + - message: '#^Parameter \#2 \$string of function explode expects string, mixed given\.$#' identifier: argument.type @@ -9786,6 +10050,12 @@ parameters: count: 1 path: src/Appwrite/Auth/OAuth2/Zoom.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Auth/OAuth2/Zoom.php + - message: '#^Property Appwrite\\Auth\\OAuth2\\Zoom\:\:\$scopes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -10542,6 +10812,12 @@ parameters: count: 1 path: src/Appwrite/Docker/Compose/Service.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Docker/Compose/Service.php + - message: '#^Parameter \#2 \$string of function explode expects string, mixed given\.$#' identifier: argument.type @@ -10740,6 +11016,12 @@ parameters: count: 1 path: src/Appwrite/Event/Event.php + - + message: '#^Parameter \#1 \$array of function array_unique expects an array of values castable to string, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Event/Event.php + - message: '#^Parameter \#1 \$haystack of function str_contains expects string, mixed given\.$#' identifier: argument.type @@ -10752,6 +11034,12 @@ parameters: count: 3 path: src/Appwrite/Event/Event.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 11 + path: src/Appwrite/Event/Event.php + - message: '#^Parameter \#2 \$replace of function str_replace expects array\\|string, list given\.$#' identifier: argument.type @@ -11406,6 +11694,18 @@ parameters: count: 2 path: src/Appwrite/Functions/EventProcessor.php + - + message: '#^Parameter \#1 \$array of function array_unique expects an array of values castable to string, list given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Functions/EventProcessor.php + + - + message: '#^Parameter \#1 \$array of function array_unique expects an array of values castable to string, list\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Functions/EventProcessor.php + - message: '#^Parameter \#1 \$json of function json_decode expects string, mixed given\.$#' identifier: argument.type @@ -11562,6 +11862,18 @@ parameters: count: 1 path: src/Appwrite/GraphQL/Resolvers.php + - + message: '#^Method Utopia\\Http\\Http\:\:execute\(\) invoked with 3 parameters, 2 required\.$#' + identifier: arguments.count + count: 1 + path: src/Appwrite/GraphQL/Resolvers.php + + - + message: '#^Method Utopia\\Http\\Http\:\:getResource\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 8 + path: src/Appwrite/GraphQL/Resolvers.php + - message: '#^Parameter \#1 \$route of method Utopia\\Http\\Http\:\:execute\(\) expects Utopia\\Http\\Route, Utopia\\Http\\Route\|null given\.$#' identifier: argument.type @@ -12132,6 +12444,12 @@ parameters: count: 1 path: src/Appwrite/GraphQL/Types/Mapper.php + - + message: '#^Parameter \#1 \$list of method Utopia\\Http\\Http\:\:getResources\(\) expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/GraphQL/Types/Mapper.php + - message: '#^Parameter \#1 \$rule of static method Appwrite\\GraphQL\\Types\\Mapper\:\:getObjectType\(\) expects array, mixed given\.$#' identifier: argument.type @@ -12558,6 +12876,12 @@ parameters: count: 1 path: src/Appwrite/Messaging/Adapter/Realtime.php + - + message: '#^Parameter \#1 \$array of function array_unique expects an array of values castable to string, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Messaging/Adapter/Realtime.php + - message: '#^Parameter \#1 \$compiled of static method Appwrite\\Utopia\\Database\\RuntimeQuery\:\:filter\(\) expects array, mixed given\.$#' identifier: argument.type @@ -13116,6 +13440,12 @@ parameters: count: 1 path: src/Appwrite/Migration/Version/V15.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Migration/Version/V15.php + - message: '#^Parameter \#2 \$callback of function array_reduce expects callable\(array, mixed\)\: array, Closure\(array, array\)\: non\-empty\-array given\.$#' identifier: argument.type @@ -14172,6 +14502,12 @@ parameters: count: 2 path: src/Appwrite/Migration/Version/V22.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Migration/Version/V22.php + - message: '#^Parameter \#2 \$collectionId of method Appwrite\\Migration\\Migration\:\:createAttributesFromCollection\(\) expects string, mixed given\.$#' identifier: argument.type @@ -14970,12 +15306,24 @@ parameters: count: 2 path: src/Appwrite/Platform/Installer/Server.php + - + message: '#^Class Utopia\\Http\\Http constructor invoked with 1 parameter, 2 required\.$#' + identifier: arguments.count + count: 1 + path: src/Appwrite/Platform/Installer/Server.php + - message: '#^Method Appwrite\\Platform\\Installer\\Server\:\:startDockerInstaller\(\) has parameter \$opts with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: src/Appwrite/Platform/Installer/Server.php + - + message: '#^Method Utopia\\Http\\Adapter\\Swoole\\Server@anonymous/src/Appwrite/Platform/Installer/Server\.php\:156\:\:getNativeServer\(\) should return Swoole\\Http\\Server but returns Swoole\\Coroutine\\Http\\Server\.$#' + identifier: return.type + count: 1 + path: src/Appwrite/Platform/Installer/Server.php + - message: '#^Parameter \#1 \$directory of method Utopia\\Http\\Files\:\:load\(\) expects string, mixed given\.$#' identifier: argument.type @@ -14994,6 +15342,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Installer/Server.php + - + message: '#^Parameter \#1 \$server of class Utopia\\Http\\Http constructor expects Utopia\\Http\\Adapter, string given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Installer/Server.php + - message: '#^Parameter \#1 \$value of method Appwrite\\Platform\\Installer\\Runtime\\Config\:\:setLockedDatabase\(\) expects string\|null, list\\|string given\.$#' identifier: argument.type @@ -15030,6 +15384,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Installer/Server.php + - + message: '#^Static call to instance method Utopia\\Http\\Http\:\:setResource\(\)\.$#' + identifier: method.staticCall + count: 4 + path: src/Appwrite/Platform/Installer/Server.php + - message: '#^Offset 1 on array\{0\: non\-falsy\-string, 1\: non\-empty\-string, 2\?\: numeric\-string\} on left side of \?\? always exists and is not nullable\.$#' identifier: nullCoalesce.offset @@ -16176,6 +16536,18 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Compute/Base.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Compute/Base.php + + - + message: '#^Parameter \#2 \$array of function implode expects array\, list\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Compute/Base.php + - message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, int given\.$#' identifier: argument.type @@ -16260,6 +16632,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Compute/Validator/Specification.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Compute/Validator/Specification.php + - message: '#^Parameter \#2 \$haystack of function in_array expects array, mixed given\.$#' identifier: argument.type @@ -16393,7 +16771,7 @@ parameters: path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php - - message: '#^Parameter \#1 \$operator of static method Utopia\\Database\\Operator\:\:parseOperator\(\) expects array\, array given\.$#' + message: '#^Parameter \#1 \$operator of static method Utopia\\Database\\Operator\:\:parseOperator\(\) expects array\, array\ given\.$#' identifier: argument.type count: 1 path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php @@ -16759,7 +17137,7 @@ parameters: path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php - - message: '#^Parameter \#1 \$value of function floatval expects array\|bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function floatval expects array\|bool\|float\|GMP\|int\|resource\|SimpleXMLElement\|string\|null, mixed given\.$#' identifier: argument.type count: 2 path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php @@ -16789,7 +17167,7 @@ parameters: path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Update.php - - message: '#^Parameter \#1 \$value of function floatval expects array\|bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function floatval expects array\|bool\|float\|GMP\|int\|resource\|SimpleXMLElement\|string\|null, mixed given\.$#' identifier: argument.type count: 2 path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Update.php @@ -16879,7 +17257,7 @@ parameters: path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php - - message: '#^Parameter \#1 \$value of function intval expects array\|bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function intval expects array\|bool\|float\|GMP\|int\|resource\|SimpleXMLElement\|string\|null, mixed given\.$#' identifier: argument.type count: 2 path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php @@ -16909,7 +17287,7 @@ parameters: path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Update.php - - message: '#^Parameter \#1 \$value of function intval expects array\|bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function intval expects array\|bool\|float\|GMP\|int\|resource\|SimpleXMLElement\|string\|null, mixed given\.$#' identifier: argument.type count: 2 path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Update.php @@ -17965,7 +18343,7 @@ parameters: path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php - - message: '#^Parameter \#1 \$input of class Utopia\\Database\\Document constructor expects array\, array given\.$#' + message: '#^Parameter \#1 \$input of class Utopia\\Database\\Document constructor expects array\, array\ given\.$#' identifier: argument.type count: 1 path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -18303,7 +18681,13 @@ parameters: - message: '#^Parameter \#1 \$input of class Utopia\\Database\\Document constructor expects array\, array given\.$#' identifier: argument.type - count: 2 + count: 1 + path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php + + - + message: '#^Parameter \#1 \$input of class Utopia\\Database\\Document constructor expects array\, array\ given\.$#' + identifier: argument.type + count: 1 path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php - @@ -18465,7 +18849,13 @@ parameters: - message: '#^Parameter \#1 \$input of class Utopia\\Database\\Document constructor expects array\, array given\.$#' identifier: argument.type - count: 2 + count: 1 + path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php + + - + message: '#^Parameter \#1 \$input of class Utopia\\Database\\Document constructor expects array\, array\ given\.$#' + identifier: argument.type + count: 1 path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php - @@ -18810,6 +19200,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php + - message: '#^Parameter \#2 \$permissions of method Utopia\\Database\\Database\:\:updateCollection\(\) expects array\, array\\|null given\.$#' identifier: argument.type @@ -19674,6 +20070,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/XList.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Update.php + - message: '#^Binary operation "\*" between mixed and mixed results in an error\.$#' identifier: binaryOp.invalid @@ -20317,19 +20719,19 @@ parameters: path: src/Appwrite/Platform/Modules/Databases/Workers/Databases.php - - message: '#^Parameter \#1 \$value of function floatval expects array\|bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function floatval expects array\|bool\|float\|GMP\|int\|resource\|SimpleXMLElement\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: src/Appwrite/Platform/Modules/Databases/Workers/Databases.php - - message: '#^Parameter \#1 \$value of function intval expects array\|bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function intval expects array\|bool\|float\|GMP\|int\|resource\|SimpleXMLElement\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: src/Appwrite/Platform/Modules/Databases/Workers/Databases.php - - message: '#^Parameter \#1 \$value of function strval expects bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function strval expects bool\|float\|GMP\|int\|resource\|string\|null, mixed given\.$#' identifier: argument.type count: 2 path: src/Appwrite/Platform/Modules/Databases/Workers/Databases.php @@ -20346,6 +20748,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Databases/Workers/Databases.php + - + message: '#^Parameter \#2 \$arrays of function array_diff expects an array of values castable to string, list\ given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Platform/Modules/Databases/Workers/Databases.php + - message: '#^Parameter \#2 \$collection of method Appwrite\\Platform\\Modules\\Databases\\Workers\\Databases\:\:deleteCollection\(\) expects Utopia\\Database\\Document, mixed given\.$#' identifier: argument.type @@ -20575,7 +20983,7 @@ parameters: path: src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php - - message: '#^Parameter \#1 \$value of function strval expects bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function strval expects bool\|float\|GMP\|int\|resource\|string\|null, mixed given\.$#' identifier: argument.type count: 2 path: src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php @@ -21061,7 +21469,7 @@ parameters: path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php - - message: '#^Parameter \#1 \$value of function strval expects bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function strval expects bool\|float\|GMP\|int\|resource\|string\|null, mixed given\.$#' identifier: argument.type count: 2 path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -21270,6 +21678,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php + - + message: '#^Parameter \#1 \$array of function array_diff expects an array of values castable to string, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php + - message: '#^Parameter \#1 \$datetime of function strtotime expects string, string\|null given\.$#' identifier: argument.type @@ -21558,6 +21972,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php + - message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, int given\.$#' identifier: argument.type @@ -21810,6 +22230,12 @@ parameters: count: 2 path: src/Appwrite/Platform/Modules/Functions/Http/Templates/XList.php + - + message: '#^Parameter \#1 \$array of function array_intersect expects an array of values castable to string, array given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Platform/Modules/Functions/Http/Templates/XList.php + - message: '#^Parameter \#1 \$array of function array_keys expects array, mixed given\.$#' identifier: argument.type @@ -21834,6 +22260,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Functions/Http/Templates/XList.php + - + message: '#^Parameter \#2 \$arrays of function array_intersect expects an array of values castable to string, list given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Functions/Http/Templates/XList.php + - message: '#^Parameter \#2 \.\.\.\$arrays of function array_intersect expects array, mixed given\.$#' identifier: argument.type @@ -22452,6 +22884,12 @@ parameters: count: 3 path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php + - message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, int given\.$#' identifier: argument.type @@ -22777,7 +23215,7 @@ parameters: path: src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php - - message: '#^Parameter \#1 \$value of function intval expects array\|bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function intval expects array\|bool\|float\|GMP\|int\|resource\|SimpleXMLElement\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php @@ -22836,6 +23274,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Health/Http/Health/Cache/Get.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, list\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Health/Http/Health/Cache/Get.php + - message: '#^Part \$cache \(mixed\) of encapsed string cannot be cast to string\.$#' identifier: encapsedStringPart.nonString @@ -22902,6 +23346,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Health/Http/Health/DB/Get.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, list\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Health/Http/Health/DB/Get.php + - message: '#^Part \$database \(mixed\) of encapsed string cannot be cast to string\.$#' identifier: encapsedStringPart.nonString @@ -22920,6 +23370,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Health/Http/Health/PubSub/Get.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, list\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Health/Http/Health/PubSub/Get.php + - message: '#^Part \$pubsub \(mixed\) of encapsed string cannot be cast to string\.$#' identifier: encapsedStringPart.nonString @@ -23466,6 +23922,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Projects/Http/Schedules/Create.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Projects/Http/Schedules/Create.php + - message: '#^Method Appwrite\\Platform\\Modules\\Projects\\Http\\Schedules\\XList\:\:action\(\) has parameter \$queries with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -23893,7 +24355,13 @@ parameters: path: src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php - - message: '#^Parameter \#1 \$value of function strval expects bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function strval expects bool\|float\|GMP\|int\|resource\|string\|null, mixed given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php + + - + message: '#^Parameter \#2 \$array of function implode expects array\, list\ given\.$#' identifier: argument.type count: 2 path: src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php @@ -24036,6 +24504,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, list\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php + - message: '#^Method Appwrite\\Platform\\Modules\\Sites\\Http\\Deployments\\Get\:\:action\(\) has no return type specified\.$#' identifier: missingType.return @@ -24090,6 +24564,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, list\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php + - message: '#^Parameter \#2 \$id of method Utopia\\Database\\Database\:\:getDocument\(\) expects string, mixed given\.$#' identifier: argument.type @@ -24258,6 +24738,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Sites/Http/Logs/XList.php + - + message: '#^Parameter \#1 \$array of function array_diff expects an array of values castable to string, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Sites/Http/Logs/XList.php + - message: '#^Parameter \#1 \$datetime of function strtotime expects string, string\|null given\.$#' identifier: argument.type @@ -24438,6 +24924,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php + - message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, int given\.$#' identifier: argument.type @@ -24660,6 +25152,12 @@ parameters: count: 2 path: src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php + - + message: '#^Parameter \#1 \$array of function array_intersect expects an array of values castable to string, array given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php + - message: '#^Parameter \#1 \$array of function array_keys expects array, mixed given\.$#' identifier: argument.type @@ -24684,6 +25182,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php + - + message: '#^Parameter \#2 \$arrays of function array_intersect expects an array of values castable to string, list given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php + - message: '#^Parameter \#2 \.\.\.\$arrays of function array_intersect expects array, mixed given\.$#' identifier: argument.type @@ -25212,6 +25716,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php + - message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, int given\.$#' identifier: argument.type @@ -26574,6 +27084,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php + - message: '#^Parameter \#2 \$replace of function str_replace expects array\\|string, int\\|int\<1, max\> given\.$#' identifier: argument.type @@ -27270,6 +27786,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php + - + message: '#^Parameter \#1 \$array of function array_unique expects an array of values castable to string, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php + - message: '#^Parameter \#1 \$deployment of method Appwrite\\Event\\Build\:\:setDeployment\(\) expects Utopia\\Database\\Document, mixed given\.$#' identifier: argument.type @@ -27325,7 +27847,7 @@ parameters: path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php - - message: '#^Parameter \#1 \$value of function strval expects bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function strval expects bool\|float\|GMP\|int\|resource\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php @@ -27360,6 +27882,18 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php + + - + message: '#^Parameter \#2 \$array of function implode expects array\, list\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php + - message: '#^Parameter \#2 \$haystack of function in_array expects array, mixed given\.$#' identifier: argument.type @@ -27534,6 +28068,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php + - + message: '#^Variable \$previewRuleId in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php + - message: '#^Variable \$previewUrl in empty\(\) always exists and is not falsy\.$#' identifier: empty.variable @@ -27925,7 +28465,7 @@ parameters: path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php - - message: '#^Parameter \#1 \$value of function strval expects bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function strval expects bool\|float\|GMP\|int\|resource\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php @@ -27966,6 +28506,24 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php + + - + message: '#^Parameter \#2 \$array of function implode expects array\, list\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php + + - + message: '#^Parameter \#2 \$arrays of function array_diff expects an array of values castable to string, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php + - message: '#^Parameter \#2 \$githubAppId of method Appwrite\\Platform\\Modules\\VCS\\Http\\GitHub\\Events\\Create\:\:handlePushEvent\(\) expects string, string\|null given\.$#' identifier: argument.type @@ -28170,6 +28728,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php + - + message: '#^Variable \$previewRuleId in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php + - message: '#^Variable \$previewUrl in empty\(\) always exists and is not falsy\.$#' identifier: empty.variable @@ -28369,7 +28933,7 @@ parameters: path: src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php - - message: '#^Parameter \#1 \$input of class Utopia\\Database\\Document constructor expects array\, array given\.$#' + message: '#^Parameter \#1 \$input of class Utopia\\Database\\Document constructor expects array\, array\ given\.$#' identifier: argument.type count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php @@ -28393,7 +28957,7 @@ parameters: path: src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php - - message: '#^Parameter \#1 \$value of function strval expects bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function strval expects bool\|float\|GMP\|int\|resource\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php @@ -28513,7 +29077,7 @@ parameters: path: src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php - - message: '#^Parameter \#1 \$input of class Utopia\\Database\\Document constructor expects array\, array given\.$#' + message: '#^Parameter \#1 \$input of class Utopia\\Database\\Document constructor expects array\, array\ given\.$#' identifier: argument.type count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php @@ -28531,7 +29095,7 @@ parameters: path: src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php - - message: '#^Parameter \#1 \$value of function strval expects bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function strval expects bool\|float\|GMP\|int\|resource\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php @@ -28735,7 +29299,7 @@ parameters: path: src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php - - message: '#^Parameter \#1 \$value of function strval expects bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function strval expects bool\|float\|GMP\|int\|resource\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php @@ -29388,18 +29952,6 @@ parameters: count: 2 path: src/Appwrite/Platform/Tasks/Migrate.php - - - message: '#^Parameter \#1 \$project of method Appwrite\\Migration\\Migration\:\:setProject\(\) expects Utopia\\Database\\Document, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Appwrite/Platform/Tasks/Migrate.php - - - - message: '#^Parameter \#1 of callable callable\(Utopia\\Database\\Document\)\: Utopia\\Database\\Database expects Utopia\\Database\\Document, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Appwrite/Platform/Tasks/Migrate.php - - message: '#^Cannot cast mixed to int\.$#' identifier: cast.int @@ -29652,6 +30204,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Tasks/SDKs.php + - + message: '#^Parameter \#1 \$array of function array_unique expects an array of values castable to string, list given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Tasks/SDKs.php + - message: '#^Parameter \#1 \$changelog of method Appwrite\\Platform\\Tasks\\SDKs\:\:extractReleaseNotes\(\) expects string, string\|false given\.$#' identifier: argument.type @@ -29850,6 +30408,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Tasks/SDKs.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Platform/Tasks/SDKs.php + - message: '#^Parameter \#2 \$haystack of function in_array expects array, list\\|string given\.$#' identifier: argument.type @@ -30090,6 +30654,18 @@ parameters: count: 2 path: src/Appwrite/Platform/Tasks/ScheduleBase.php + - + message: '#^Parameter \#1 \$array of function array_unique expects an array of values castable to string, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Tasks/ScheduleBase.php + + - + message: '#^Parameter \#1 \$array of function array_unique expects an array of values castable to string, list\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Tasks/ScheduleBase.php + - message: '#^Parameter \#1 \$datetime of function strtotime expects string, mixed given\.$#' identifier: argument.type @@ -31369,7 +31945,7 @@ parameters: path: src/Appwrite/Platform/Workers/Deletes.php - - message: '#^Parameter \#1 \$value of function strval expects bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function strval expects bool\|float\|GMP\|int\|resource\|string\|null, mixed given\.$#' identifier: argument.type count: 2 path: src/Appwrite/Platform/Workers/Deletes.php @@ -33606,6 +34182,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Workers/Migrations.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Platform/Workers/Migrations.php + - message: '#^Parameter \#2 \$endpoint of class Utopia\\Migration\\Destinations\\Appwrite constructor expects string, mixed given\.$#' identifier: argument.type @@ -34518,6 +35100,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Workers/Webhooks.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array given\.$#' + identifier: argument.type + count: 3 + path: src/Appwrite/Platform/Workers/Webhooks.php + - message: '#^Parameter \#2 \$payload of method Appwrite\\Platform\\Workers\\Webhooks\:\:execute\(\) expects string, string\|false given\.$#' identifier: argument.type @@ -35389,7 +35977,7 @@ parameters: path: src/Appwrite/SDK/Specification/Format/OpenAPI3.php - - message: '#^Parameter \#1 \$list of method Utopia\\Http\\Http\:\:getResources\(\) expects array, mixed given\.$#' + message: '#^Parameter \#1 \$list of method Utopia\\Http\\Http\:\:getResources\(\) expects array\, mixed given\.$#' identifier: argument.type count: 1 path: src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -35400,6 +35988,12 @@ parameters: count: 2 path: src/Appwrite/SDK/Specification/Format/OpenAPI3.php + - + message: '#^Parameter \#2 \$array of function join expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/SDK/Specification/Format/OpenAPI3.php + - message: '#^Parameter \#2 \$excludedValues of method Appwrite\\SDK\\Specification\\Format\:\:parseDescription\(\) expects array, mixed given\.$#' identifier: argument.type @@ -35718,6 +36312,12 @@ parameters: count: 1 path: src/Appwrite/SDK/Specification/Format/Swagger2.php + - + message: '#^Parameter \#1 \$array of function array_unique expects an array of values castable to string, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/SDK/Specification/Format/Swagger2.php + - message: '#^Parameter \#1 \$array of function array_values expects array\, mixed given\.$#' identifier: argument.type @@ -35737,7 +36337,7 @@ parameters: path: src/Appwrite/SDK/Specification/Format/Swagger2.php - - message: '#^Parameter \#1 \$list of method Utopia\\Http\\Http\:\:getResources\(\) expects array, mixed given\.$#' + message: '#^Parameter \#1 \$list of method Utopia\\Http\\Http\:\:getResources\(\) expects array\, mixed given\.$#' identifier: argument.type count: 1 path: src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -35748,6 +36348,12 @@ parameters: count: 2 path: src/Appwrite/SDK/Specification/Format/Swagger2.php + - + message: '#^Parameter \#2 \$array of function join expects array\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/SDK/Specification/Format/Swagger2.php + - message: '#^Parameter \#2 \$excludedValues of method Appwrite\\SDK\\Specification\\Format\:\:parseDescription\(\) expects array, mixed given\.$#' identifier: argument.type @@ -36252,6 +36858,12 @@ parameters: count: 1 path: src/Appwrite/Utopia/Database/RuntimeQuery.php + - + message: '#^Parameter \#1 \$array of function array_unique expects an array of values castable to string, array given\.$#' + identifier: argument.type + count: 1 + path: src/Appwrite/Utopia/Database/RuntimeQuery.php + - message: '#^Parameter \#1 \$condition of static method Appwrite\\Utopia\\Database\\RuntimeQuery\:\:evaluateCondition\(\) expects array, mixed given\.$#' identifier: argument.type @@ -36876,6 +37488,12 @@ parameters: count: 1 path: src/Appwrite/Utopia/Request/Filters/V20.php + - + message: '#^Parameter \#1 \$array of function array_unique expects an array of values castable to string, array given\.$#' + identifier: argument.type + count: 2 + path: src/Appwrite/Utopia/Request/Filters/V20.php + - message: '#^Parameter \#1 \$attributes of static method Utopia\\Database\\Query\:\:select\(\) expects array\, list given\.$#' identifier: argument.type @@ -38724,6 +39342,12 @@ parameters: count: 6 path: src/Executor/Executor.php + - + message: '#^Offset ''content\-type'' on array\ on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: src/Executor/Executor.php + - message: '#^Offset ''headers'' might not exist on array\|string\.$#' identifier: offsetAccess.notFound @@ -38755,13 +39379,13 @@ parameters: path: src/Executor/Executor.php - - message: '#^Parameter \#1 \$value of function floatval expects array\|bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function floatval expects array\|bool\|float\|GMP\|int\|resource\|SimpleXMLElement\|string\|null, mixed given\.$#' identifier: argument.type count: 2 path: src/Executor/Executor.php - - message: '#^Parameter \#1 \$value of function intval expects array\|bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function intval expects array\|bool\|float\|GMP\|int\|resource\|SimpleXMLElement\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: src/Executor/Executor.php @@ -38868,6 +39492,12 @@ parameters: count: 1 path: tests/e2e/Client.php + - + message: '#^Offset ''content\-type'' on array\ on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: tests/e2e/Client.php + - message: '#^Parameter \#1 \$string of function strlen expects string, mixed given\.$#' identifier: argument.type @@ -39295,7 +39925,7 @@ parameters: path: tests/e2e/General/CompressionTest.php - - message: '#^Parameter \#1 \$value of function intval expects array\|bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function intval expects array\|bool\|float\|GMP\|int\|resource\|SimpleXMLElement\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: tests/e2e/General/CompressionTest.php @@ -64290,6 +64920,12 @@ parameters: count: 1 path: tests/e2e/Services/Migrations/MigrationsConsoleClientTest.php + - + message: '#^Offset 1 might not exist on array\{\}\|array\{non\-falsy\-string, non\-falsy\-string\}\.$#' + identifier: offsetAccess.notFound + count: 2 + path: tests/e2e/Services/Migrations/MigrationsConsoleClientTest.php + - message: '#^Parameter \#1 \$datetime of class DateTime constructor expects string, mixed given\.$#' identifier: argument.type From c055c32371bbe4911cf04efca169ae9ecc52abdf Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 10:52:35 +0530 Subject: [PATCH 025/118] enable coroutines --- app/http.php | 1 + src/Appwrite/Platform/Modules/Functions/Workers/Builds.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/http.php b/app/http.php index 70dc6f58ce..1924071c4c 100644 --- a/app/http.php +++ b/app/http.php @@ -67,6 +67,7 @@ $swooleAdapter = new HttpServer( Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background ], container: $container, + coroutines: true, ); $container->set('container', fn () => fn () => $swooleAdapter->getContainer()); diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 443db88a7a..659fc327d9 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -1203,6 +1203,8 @@ class Builds extends Action protected function sendUsage(Document $resource, Document $deployment, Document $project, Context $usage, UsagePublisher $publisherForUsage): void { $spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)]; + $cpus = $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT; + $memory = $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT; switch ($deployment->getAttribute('status')) { case 'ready': From f8e9f71de3dc4752dcaa9f86cf650e0f9f2387d2 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 11:19:17 +0530 Subject: [PATCH 026/118] move resources --- app/init/resources.php | 247 +++++++++++++++++++++++++++++++---------- 1 file changed, 191 insertions(+), 56 deletions(-) diff --git a/app/init/resources.php b/app/init/resources.php index e353c61896..701deca61b 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -81,7 +81,6 @@ global $register; global $container; $container = new Container(); -$container->set('log', fn () => new Log()); $container->set('logger', function ($register) { return $register->get('logger'); }, ['register']); @@ -91,18 +90,12 @@ $container->set('hooks', function ($register) { }, ['register']); $container->set('register', fn () => $register); -$container->set('locale', function () { - $locale = new Locale(System::getEnv('_APP_LOCALE', 'en')); - $locale->setFallback(System::getEnv('_APP_LOCALE', 'en')); - - return $locale; -}); $container->set('localeCodes', function () { return array_map(fn ($locale) => $locale['code'], Config::getParam('locale-codes', [])); }); -// Queues +// Queues - shared infrastructure (stateless pool wrappers) $container->set('publisher', function (Group $pools) { return new BrokerPool(publisher: $pools->get('publisher')); }, ['pools']); @@ -127,58 +120,10 @@ $container->set('publisherMessaging', function (Publisher $publisher) { $container->set('publisherWebhooks', function (Publisher $publisher) { return $publisher; }, ['publisher']); -$container->set('queueForMessaging', function (Publisher $publisher) { - return new Messaging($publisher); -}, ['publisher']); -$container->set('queueForMails', function (Publisher $publisher) { - return new Mail($publisher); -}, ['publisher']); -$container->set('queueForBuilds', function (Publisher $publisher) { - return new Build($publisher); -}, ['publisher']); -$container->set('queueForScreenshots', function (Publisher $publisher) { - return new Screenshot($publisher); -}, ['publisher']); -$container->set('queueForDatabase', function (Publisher $publisher) { - return new EventDatabase($publisher); -}, ['publisher']); -$container->set('queueForDeletes', function (Publisher $publisher) { - return new Delete($publisher); -}, ['publisher']); -$container->set('queueForEvents', function (Publisher $publisher) { - return new Event($publisher); -}, ['publisher']); -$container->set('queueForWebhooks', function (Publisher $publisher) { - return new Webhook($publisher); -}, ['publisher']); -$container->set('queueForRealtime', function () { - return new Realtime(); -}, []); -$container->set('usage', function () { - return new UsageContext(); -}, []); $container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher( $publisher, new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME)) ), ['publisher']); -$container->set('queueForAudits', function (Publisher $publisher) { - return new AuditEvent($publisher); -}, ['publisher']); -$container->set('queueForFunctions', function (Publisher $publisher) { - return new Func($publisher); -}, ['publisher']); -$container->set('eventProcessor', function () { - return new EventProcessor(); -}, []); -$container->set('queueForCertificates', function (Publisher $publisher) { - return new Certificate($publisher); -}, ['publisher']); -$container->set('queueForMigrations', function (Publisher $publisher) { - return new Migration($publisher); -}, ['publisher']); -$container->set('queueForStatsResources', function (Publisher $publisher) { - return new StatsResources($publisher); -}, ['publisher']); /** * Platform configuration @@ -194,6 +139,196 @@ $container->set('platform', function () { */ function registerRequestResources(Container $container): void { + $container->set('log', fn () => new Log(), []); + + $container->set('logger', function ($register) { + return $register->get('logger'); + }, ['register']); + + $container->set('authorization', function () { + return new Authorization(); + }, []); + + $container->set('store', function (): Store { + return new Store(); + }, []); + + $container->set('proofForPassword', function (): Password { + $hash = new Argon2(); + $hash + ->setMemoryCost(7168) + ->setTimeCost(5) + ->setThreads(1); + + $password = new Password(); + $password + ->setHash($hash); + + return $password; + }); + + $container->set('proofForToken', function (): Token { + $token = new Token(); + $token->setHash(new Sha()); + + return $token; + }); + + $container->set('proofForCode', function (): Code { + $code = new Code(); + $code->setHash(new Sha()); + + return $code; + }); + + $container->set('locale', function () { + $locale = new Locale(System::getEnv('_APP_LOCALE', 'en')); + $locale->setFallback(System::getEnv('_APP_LOCALE', 'en')); + + return $locale; + }); + + // Per-request queue resources (stateful, accumulate event data during request) + $container->set('queueForMessaging', function (Publisher $publisher) { + return new Messaging($publisher); + }, ['publisher']); + $container->set('queueForMails', function (Publisher $publisher) { + return new Mail($publisher); + }, ['publisher']); + $container->set('queueForBuilds', function (Publisher $publisher) { + return new Build($publisher); + }, ['publisher']); + $container->set('queueForScreenshots', function (Publisher $publisher) { + return new Screenshot($publisher); + }, ['publisher']); + $container->set('queueForDatabase', function (Publisher $publisher) { + return new EventDatabase($publisher); + }, ['publisher']); + $container->set('queueForDeletes', function (Publisher $publisher) { + return new Delete($publisher); + }, ['publisher']); + $container->set('queueForEvents', function (Publisher $publisher) { + return new Event($publisher); + }, ['publisher']); + $container->set('queueForWebhooks', function (Publisher $publisher) { + return new Webhook($publisher); + }, ['publisher']); + $container->set('queueForRealtime', function () { + return new Realtime(); + }, []); + $container->set('usage', function () { + return new UsageContext(); + }, []); + $container->set('queueForAudits', function (Publisher $publisher) { + return new AuditEvent($publisher); + }, ['publisher']); + $container->set('queueForFunctions', function (Publisher $publisher) { + return new Func($publisher); + }, ['publisher']); + $container->set('eventProcessor', function () { + return new EventProcessor(); + }, []); + $container->set('queueForCertificates', function (Publisher $publisher) { + return new Certificate($publisher); + }, ['publisher']); + $container->set('queueForMigrations', function (Publisher $publisher) { + return new Migration($publisher); + }, ['publisher']); + $container->set('queueForStatsResources', function (Publisher $publisher) { + return new StatsResources($publisher); + }, ['publisher']); + + $container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) { + $adapter = new DatabasePool($pools->get('console')); + $database = new Database($adapter, $cache); + + $database + ->setDatabase(APP_DATABASE) + ->setAuthorization($authorization) + ->setNamespace('_console') + ->setMetadata('host', \gethostname()) + ->setMetadata('project', 'console') + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) + ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); + + $database->setDocumentType('users', User::class); + + return $database; + }, ['pools', 'cache', 'authorization']); + + $container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) { + $adapters = []; + + return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$adapters) { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForPlatform; + } + + $database = $project->getAttribute('database', ''); + if (empty($database)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Project database is not configured'); + } + + try { + $dsn = new DSN($database); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $database); + } + + $adapter = $adapters[$dsn->getHost()] ??= new DatabasePool($pools->get($dsn->getHost())); + $database = new Database($adapter, $cache); + + $database + ->setDatabase(APP_DATABASE) + ->setAuthorization($authorization) + ->setMetadata('host', \gethostname()) + ->setMetadata('project', $project->getId()) + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) + ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES) + ->setDocumentType('users', User::class); + + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + + if (\in_array($dsn->getHost(), $sharedTables)) { + $database + ->setSharedTables(true) + ->setTenant($project->getSequence()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getSequence()); + } + + return $database; + }; + }, ['pools', 'dbForPlatform', 'cache', 'authorization']); + + $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { + $adapter = null; + + return function (?Document $project = null) use ($pools, $cache, $authorization, &$adapter) { + $adapter ??= new DatabasePool($pools->get('logs')); + $database = new Database($adapter, $cache); + + $database + ->setDatabase(APP_DATABASE) + ->setAuthorization($authorization) + ->setSharedTables(true) + ->setNamespace('logsV1') + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) + ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); + + if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') { + $database->setTenant($project->getSequence()); + } + + return $database; + }; + }, ['pools', 'cache', 'authorization']); + /** * List of allowed request hostnames for the request. */ From 87b02de61254f88eaabab0822f58cfb01b30a34c Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 11:32:15 +0530 Subject: [PATCH 027/118] baseline --- phpstan-baseline.neon | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 86d007432d..3a72cd5993 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5319,7 +5319,7 @@ parameters: - message: '#^Binary operation "\." between ''mysql\://'' and mixed results in an error\.$#' identifier: binaryOp.invalid - count: 2 + count: 3 path: app/init/resources.php - @@ -5379,7 +5379,7 @@ parameters: - message: '#^Cannot call method get\(\) on mixed\.$#' identifier: method.nonObject - count: 3 + count: 4 path: app/init/resources.php - @@ -5523,7 +5523,7 @@ parameters: - message: '#^Parameter \#1 \$dsn of class Utopia\\DSN\\DSN constructor expects string, mixed given\.$#' identifier: argument.type - count: 2 + count: 3 path: app/init/resources.php - @@ -5577,7 +5577,7 @@ parameters: - message: '#^Parameter \#1 \$pool of class Utopia\\Database\\Adapter\\Pool constructor expects Utopia\\Pools\\Pool\, Utopia\\Pools\\Pool\ given\.$#' identifier: argument.type - count: 4 + count: 7 path: app/init/resources.php - @@ -22671,7 +22671,7 @@ parameters: - message: '#^Cannot access offset ''cpus'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible - count: 1 + count: 2 path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php - @@ -22683,7 +22683,7 @@ parameters: - message: '#^Cannot access offset ''memory'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible - count: 1 + count: 2 path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php - @@ -23094,18 +23094,6 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php - - - message: '#^Undefined variable\: \$cpus$#' - identifier: variable.undefined - count: 3 - path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php - - - - message: '#^Undefined variable\: \$memory$#' - identifier: variable.undefined - count: 3 - path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php - - message: '#^Variable \$deployment might not be defined\.$#' identifier: variable.undefined From 8a0f0923422eb9c9799791048221b84c74c32d58 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 14:28:37 +0530 Subject: [PATCH 028/118] fix: only throw container lookup errors --- composer.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/composer.lock b/composer.lock index a0f74125ce..dbeb3c15a9 100644 --- a/composer.lock +++ b/composer.lock @@ -4307,12 +4307,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "fbcf5a3ca934f59fa4c4929f003ba0d563cc1f02" + "reference": "dbdb33f73598949615f159dc612196ef26e57a32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/fbcf5a3ca934f59fa4c4929f003ba0d563cc1f02", - "reference": "fbcf5a3ca934f59fa4c4929f003ba0d563cc1f02", + "url": "https://api.github.com/repos/utopia-php/http/zipball/dbdb33f73598949615f159dc612196ef26e57a32", + "reference": "dbdb33f73598949615f159dc612196ef26e57a32", "shasum": "" }, "require": { @@ -4352,7 +4352,7 @@ "issues": "https://github.com/utopia-php/http/issues", "source": "https://github.com/utopia-php/http/tree/feat/coroutines-option" }, - "time": "2026-03-17T05:01:57+00:00" + "time": "2026-03-17T08:55:56+00:00" }, { "name": "utopia-php/image", @@ -5478,16 +5478,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.11.8", + "version": "1.11.9", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "bf45bb91419f157e6d539d05f3f2c2d2120c90dc" + "reference": "2f0f6ec54736ba7efdff188a9451b56f1665f25a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/bf45bb91419f157e6d539d05f3f2c2d2120c90dc", - "reference": "bf45bb91419f157e6d539d05f3f2c2d2120c90dc", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/2f0f6ec54736ba7efdff188a9451b56f1665f25a", + "reference": "2f0f6ec54736ba7efdff188a9451b56f1665f25a", "shasum": "" }, "require": { @@ -5523,9 +5523,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.11.8" + "source": "https://github.com/appwrite/sdk-generator/tree/1.11.9" }, - "time": "2026-03-16T11:02:05+00:00" + "time": "2026-03-17T08:16:49+00:00" }, { "name": "brianium/paratest", From b1edf75c4e9a177d4eeec13a0968919d1ffd2f61 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 14:29:04 +0530 Subject: [PATCH 029/118] phpunit --- phpunit.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 030d89af8d..c2ffe21b81 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -17,7 +17,6 @@ ./tests/unit - ./tests/e2e/Client.php ./tests/e2e/General ./tests/e2e/Scopes ./tests/e2e/Services/Teams @@ -36,7 +35,6 @@ ./tests/e2e/Services/Webhooks ./tests/e2e/Services/Messaging ./tests/e2e/Services/Migrations - ./tests/e2e/Services/Functions/FunctionsBase.php ./tests/e2e/Services/Functions/FunctionsCustomServerTest.php ./tests/e2e/Services/Functions/FunctionsCustomClientTest.php From be87675e1c205ab281e4cdde3cdcac675859da73 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 15:03:55 +0530 Subject: [PATCH 030/118] fix realtime --- app/realtime.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/realtime.php b/app/realtime.php index 70a655117d..f08a2ad6d6 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -613,8 +613,10 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, Console::info("Connection open (user: {$connection})"); + $pools = $register->get('pools'); + $container->set('pools', fn () => $pools); + $connectionContainer = new Container($container); - $connectionContainer->set('pools', fn () => $register->get('pools')); registerRequestResources($connectionContainer); $adapter = new \Utopia\Http\Adapter\FPM\Server($connectionContainer); $app = new Http($adapter, 'UTC'); From fa1404be52b7b477ed0de877865c88d43fbb2400 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 15:20:29 +0530 Subject: [PATCH 031/118] cleanup --- app/init/resources.php | 1162 +------------------------------ app/init/resources.request.php | 1187 ++++++++++++++++++++++++++++++++ app/realtime.php | 4 +- 3 files changed, 1191 insertions(+), 1162 deletions(-) create mode 100644 app/init/resources.request.php diff --git a/app/init/resources.php b/app/init/resources.php index 701deca61b..019e43cd7c 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -1,39 +1,11 @@ set('platform', function () { return Config::getParam('platform', []); }, []); -/** - * Register per-request resources on the given container. - * These resources depend (directly or transitively) on request/response - * and must be fresh for each HTTP request. - */ -function registerRequestResources(Container $container): void -{ - $container->set('log', fn () => new Log(), []); +require_once __DIR__ . '/resources.request.php'; - $container->set('logger', function ($register) { - return $register->get('logger'); - }, ['register']); - - $container->set('authorization', function () { - return new Authorization(); - }, []); - - $container->set('store', function (): Store { - return new Store(); - }, []); - - $container->set('proofForPassword', function (): Password { - $hash = new Argon2(); - $hash - ->setMemoryCost(7168) - ->setTimeCost(5) - ->setThreads(1); - - $password = new Password(); - $password - ->setHash($hash); - - return $password; - }); - - $container->set('proofForToken', function (): Token { - $token = new Token(); - $token->setHash(new Sha()); - - return $token; - }); - - $container->set('proofForCode', function (): Code { - $code = new Code(); - $code->setHash(new Sha()); - - return $code; - }); - - $container->set('locale', function () { - $locale = new Locale(System::getEnv('_APP_LOCALE', 'en')); - $locale->setFallback(System::getEnv('_APP_LOCALE', 'en')); - - return $locale; - }); - - // Per-request queue resources (stateful, accumulate event data during request) - $container->set('queueForMessaging', function (Publisher $publisher) { - return new Messaging($publisher); - }, ['publisher']); - $container->set('queueForMails', function (Publisher $publisher) { - return new Mail($publisher); - }, ['publisher']); - $container->set('queueForBuilds', function (Publisher $publisher) { - return new Build($publisher); - }, ['publisher']); - $container->set('queueForScreenshots', function (Publisher $publisher) { - return new Screenshot($publisher); - }, ['publisher']); - $container->set('queueForDatabase', function (Publisher $publisher) { - return new EventDatabase($publisher); - }, ['publisher']); - $container->set('queueForDeletes', function (Publisher $publisher) { - return new Delete($publisher); - }, ['publisher']); - $container->set('queueForEvents', function (Publisher $publisher) { - return new Event($publisher); - }, ['publisher']); - $container->set('queueForWebhooks', function (Publisher $publisher) { - return new Webhook($publisher); - }, ['publisher']); - $container->set('queueForRealtime', function () { - return new Realtime(); - }, []); - $container->set('usage', function () { - return new UsageContext(); - }, []); - $container->set('queueForAudits', function (Publisher $publisher) { - return new AuditEvent($publisher); - }, ['publisher']); - $container->set('queueForFunctions', function (Publisher $publisher) { - return new Func($publisher); - }, ['publisher']); - $container->set('eventProcessor', function () { - return new EventProcessor(); - }, []); - $container->set('queueForCertificates', function (Publisher $publisher) { - return new Certificate($publisher); - }, ['publisher']); - $container->set('queueForMigrations', function (Publisher $publisher) { - return new Migration($publisher); - }, ['publisher']); - $container->set('queueForStatsResources', function (Publisher $publisher) { - return new StatsResources($publisher); - }, ['publisher']); - - $container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) { - $adapter = new DatabasePool($pools->get('console')); - $database = new Database($adapter, $cache); - - $database - ->setDatabase(APP_DATABASE) - ->setAuthorization($authorization) - ->setNamespace('_console') - ->setMetadata('host', \gethostname()) - ->setMetadata('project', 'console') - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) - ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); - - $database->setDocumentType('users', User::class); - - return $database; - }, ['pools', 'cache', 'authorization']); - - $container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) { - $adapters = []; - - return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$adapters) { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForPlatform; - } - - $database = $project->getAttribute('database', ''); - if (empty($database)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Project database is not configured'); - } - - try { - $dsn = new DSN($database); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $database); - } - - $adapter = $adapters[$dsn->getHost()] ??= new DatabasePool($pools->get($dsn->getHost())); - $database = new Database($adapter, $cache); - - $database - ->setDatabase(APP_DATABASE) - ->setAuthorization($authorization) - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()) - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) - ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES) - ->setDocumentType('users', User::class); - - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - - if (\in_array($dsn->getHost(), $sharedTables)) { - $database - ->setSharedTables(true) - ->setTenant($project->getSequence()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getSequence()); - } - - return $database; - }; - }, ['pools', 'dbForPlatform', 'cache', 'authorization']); - - $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { - $adapter = null; - - return function (?Document $project = null) use ($pools, $cache, $authorization, &$adapter) { - $adapter ??= new DatabasePool($pools->get('logs')); - $database = new Database($adapter, $cache); - - $database - ->setDatabase(APP_DATABASE) - ->setAuthorization($authorization) - ->setSharedTables(true) - ->setNamespace('logsV1') - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) - ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); - - if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') { - $database->setTenant($project->getSequence()); - } - - return $database; - }; - }, ['pools', 'cache', 'authorization']); - - /** - * List of allowed request hostnames for the request. - */ - $container->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) { - $allowed = [...($platform['hostnames'] ?? [])]; - - /* Add platform configured hostnames */ - if (! $project->isEmpty() && $project->getId() !== 'console') { - $platforms = $project->getAttribute('platforms', []); - $hostnames = Platform::getHostnames($platforms); - $allowed = [...$allowed, ...$hostnames]; - } - - /* Add the request hostname if a dev key is found */ - if (! $devKey->isEmpty()) { - $allowed[] = $request->getHostname(); - } - - $originHostname = parse_url($request->getOrigin(), PHP_URL_HOST); - $refererHostname = parse_url($request->getReferer(), PHP_URL_HOST); - - $hostname = $originHostname; - if (empty($hostname)) { - $hostname = $refererHostname; - } - - /* Add request hostname for preflight requests */ - if ($request->getMethod() === 'OPTIONS') { - $allowed[] = $hostname; - } - - /* Allow the request origin of rule */ - if (! $rule->isEmpty() && ! empty($rule->getAttribute('domain', ''))) { - $allowed[] = $rule->getAttribute('domain', ''); - } - - /* Allow the request origin if a dev key is found */ - if (! $devKey->isEmpty() && ! empty($hostname)) { - $allowed[] = $hostname; - } - - return array_unique($allowed); - }, ['platform', 'project', 'rule', 'devKey', 'request']); - - /** - * List of allowed request schemes for the request. - */ - $container->set('allowedSchemes', function (array $platform, Document $project) { - $allowed = [...($platform['schemas'] ?? [])]; - - if (! $project->isEmpty() && $project->getId() !== 'console') { - /* Add hardcoded schemes */ - $allowed[] = 'exp'; - $allowed[] = 'appwrite-callback-' . $project->getId(); - - /* Add platform configured schemes */ - $platforms = $project->getAttribute('platforms', []); - $schemes = Platform::getSchemes($platforms); - $allowed = [...$allowed, ...$schemes]; - } - - return array_unique($allowed); - }, ['platform', 'project']); - - /** - * Rule associated with a request origin. - */ - $container->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) { - $domain = \parse_url($request->getOrigin(), PHP_URL_HOST); - - if (empty($domain)) { - $domain = \parse_url($request->getReferer(), PHP_URL_HOST); - } - - if (empty($domain)) { - return new Document(); - } - - // TODO: (@Meldiron) Remove after 1.7.x migration - $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) { - if ($isMd5) { - return $dbForPlatform->getDocument('rules', md5($domain)); - } - - return $dbForPlatform->findOne('rules', [ - Query::equal('domain', [$domain]), - ]) ?? new Document(); - }); - - $permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence(); - - // Temporary implementation until custom wildcard domains are an official feature - // Allow trusted projects; Used for Console (website) previews - if (! $permitsCurrentProject && ! $rule->isEmpty() && ! empty($rule->getAttribute('projectId', ''))) { - $trustedProjects = []; - foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) { - if (empty($trustedProject)) { - continue; - } - $trustedProjects[] = $trustedProject; - } - if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects)) { - $permitsCurrentProject = true; - } - } - - if (! $permitsCurrentProject) { - return new Document(); - } - - return $rule; - }, ['request', 'dbForPlatform', 'project', 'authorization']); - - /** - * CORS service - */ - $container->set('cors', function (array $allowedHostnames) { - $corsConfig = Config::getParam('cors'); - - return new Cors( - $allowedHostnames, - allowedMethods: $corsConfig['allowedMethods'], - allowedHeaders: $corsConfig['allowedHeaders'], - allowCredentials: true, - exposedHeaders: $corsConfig['exposedHeaders'], - ); - }, ['allowedHostnames']); - - $container->set('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) { - if (! $devKey->isEmpty()) { - return new URL(); - } - - return new Origin($allowedHostnames, $allowedSchemes); - }, ['devKey', 'allowedHostnames', 'allowedSchemes']); - - $container->set('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) { - if (! $devKey->isEmpty()) { - return new URL(); - } - - return new Redirect($allowedHostnames, $allowedSchemes); - }, ['devKey', 'allowedHostnames', 'allowedSchemes']); - - $container->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) { - /** - * Handles user authentication and session validation. - * - * This function follows a series of steps to determine the appropriate user session - * based on cookies, headers, and JWT tokens. - * - * Process: - * 1. Checks the cookie based on mode: - * - If in admin mode, uses console project id for key. - * - Otherwise, sets the key using the project ID - * 2. If no cookie is found, attempts to retrieve the fallback header `x-fallback-cookies`. - * - If this method is used, returns the header: `X-Debug-Fallback: true`. - * 3. Fetches the user document from the appropriate database based on the mode. - * 4. If the user document is empty or the session key cannot be verified, sets an empty user document. - * 5. Regardless of the results from steps 1-4, attempts to fetch the JWT token. - * 6. If the JWT user has a valid session ID, updates the user variable with the user from `projectDB`, - * overwriting the previous value. - * 7. If account API key is passed, use user of the account API key as long as user ID header matches too - */ - $authorization->setDefaultStatus(true); - - $store->setKey('a_session_' . $project->getId()); - - if ($mode === APP_MODE_ADMIN) { - $store->setKey('a_session_' . $console->getId()); - } - - $store->decode( - $request->getCookie( - $store->getKey(), // Get sessions - $request->getCookie($store->getKey() . '_legacy', '') - ) - ); - - // Get session from header for SSR clients - if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) { - $sessionHeader = $request->getHeader('x-appwrite-session', ''); - - if (! empty($sessionHeader)) { - $store->decode($sessionHeader); - } - } - - // Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies - if ($response) { // if in http context - add debug header - $response->addHeader('X-Debug-Fallback', 'false'); - } - - if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) { - if ($response) { - $response->addHeader('X-Debug-Fallback', 'true'); - } - $fallback = $request->getHeader('x-fallback-cookies', ''); - $fallback = \json_decode($fallback, true); - $store->decode(((is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : '')); - } - - $user = null; - if ($mode === APP_MODE_ADMIN) { - /** @var User $user */ - $user = $dbForPlatform->getDocument('users', $store->getProperty('id', '')); - } else { - if ($project->isEmpty()) { - $user = new User([]); - } else { - if (! empty($store->getProperty('id', ''))) { - if ($project->getId() === 'console') { - /** @var User $user */ - $user = $dbForPlatform->getDocument('users', $store->getProperty('id', '')); - } else { - /** @var User $user */ - $user = $dbForProject->getDocument('users', $store->getProperty('id', '')); - } - } - } - } - - if ( - ! $user || - $user->isEmpty() // Check a document has been found in the DB - || ! $user->sessionVerify($store->getProperty('secret', ''), $proofForToken) - ) { // Validate user has valid login token - $user = new User([]); - } - - $authJWT = $request->getHeader('x-appwrite-jwt', ''); - if (! empty($authJWT) && ! $project->isEmpty()) { // JWT authentication - if (! $user->isEmpty()) { - throw new Exception(Exception::USER_JWT_AND_COOKIE_SET); - } - - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); - try { - $payload = $jwt->decode($authJWT); - } catch (JWTException $error) { - throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage()); - } - - $jwtUserId = $payload['userId'] ?? ''; - if (! empty($jwtUserId)) { - if ($mode === APP_MODE_ADMIN) { - $user = $dbForPlatform->getDocument('users', $jwtUserId); - } else { - $user = $dbForProject->getDocument('users', $jwtUserId); - } - } - $jwtSessionId = $payload['sessionId'] ?? ''; - if (! empty($jwtSessionId)) { - if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token - $user = new User([]); - } - } - } - - // Account based on account API key - $accountKey = $request->getHeader('x-appwrite-key', ''); - $accountKeyUserId = $request->getHeader('x-appwrite-user', ''); - if (! empty($accountKeyUserId) && ! empty($accountKey)) { - if (! $user->isEmpty()) { - throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET); - } - - $accountKeyUser = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId)); - if (! $accountKeyUser->isEmpty()) { - $key = $accountKeyUser->find( - key: 'secret', - find: $accountKey, - subject: 'keys' - ); - - if (! empty($key)) { - $expire = $key->getAttribute('expire'); - if (! empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) { - throw new Exception(Exception::ACCOUNT_KEY_EXPIRED); - } - - $user = $accountKeyUser; - } - } - } - - $dbForProject->setMetadata('user', $user->getId()); - $dbForPlatform->setMetadata('user', $user->getId()); - - return $user; - }, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']); - - $container->set('project', function ($dbForPlatform, $request, $console, $authorization) { - /** @var Appwrite\Utopia\Request $request */ - /** @var Utopia\Database\Database $dbForPlatform */ - /** @var Utopia\Database\Document $console */ - $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', '')); - // Realtime channel "project" can send project=Query array - if (! \is_string($projectId)) { - $projectId = $request->getHeader('x-appwrite-project', ''); - } - - if (empty($projectId) || $projectId === 'console') { - return $console; - } - - $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); - - return $project; - }, ['dbForPlatform', 'request', 'console', 'authorization']); - - $container->set('session', function (User $user, Store $store, Token $proofForToken) { - if ($user->isEmpty()) { - return; - } - - $sessions = $user->getAttribute('sessions', []); - $sessionId = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken); - - if (! $sessionId) { - return; - } - foreach ($sessions as $session) { - /** @var Document $session */ - if ($sessionId === $session->getId()) { - return $session; - } - } - - }, ['user', 'store', 'proofForToken']); - - $container->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization) { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForPlatform; - } - - $database = $project->getAttribute('database', ''); - if (empty($database)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Project database is not configured'); - } - - try { - $dsn = new DSN($database); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $database); - } - - $adapter = new DatabasePool($pools->get($dsn->getHost())); - $database = new Database($adapter, $cache); - - $database - ->setDatabase(APP_DATABASE) - ->setAuthorization($authorization) - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()) - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) - ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); - $database->setDocumentType('users', User::class); - - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - - if (\in_array($dsn->getHost(), $sharedTables)) { - $database - ->setSharedTables(true) - ->setTenant($project->getSequence()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getSequence()); - } - - /** - * This isolated event handling for `users.*.create` which is based on a `Database::EVENT_DOCUMENT_CREATE` listener may look odd, but it is **intentional**. - * - * Accounts can be created in many ways beyond `createAccount` - * (anonymous, OAuth, phone, etc.), and those flows are probably not covered in event tests; so we handle this here. - */ - $eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) { - // Only trigger events for user creation with the database listener. - if ($document->getCollection() !== 'users') { - return; - } - - $queueForEvents - ->setEvent('users.[userId].create') - ->setParam('userId', $document->getId()) - ->setPayload($response->output($document, Response::MODEL_USER)); - - // Trigger functions, webhooks, and realtime events - $queueForFunctions - ->from($queueForEvents) - ->trigger(); - - /** Trigger webhooks events only if a project has them enabled */ - if (! empty($project->getAttribute('webhooks'))) { - $queueForWebhooks - ->from($queueForEvents) - ->trigger(); - } - - /** Trigger realtime events only for non console events */ - if ($queueForEvents->getProject()->getId() !== 'console') { - $queueForRealtime - ->from($queueForEvents) - ->trigger(); - } - }; - - /** - * Purge function events cache when functions are created, updated or deleted. - */ - $functionsEventsCacheListener = function (string $event, Document $document, Document $project, Database $dbForProject) { - - if ($document->getCollection() !== 'functions') { - return; - } - - if ($project->isEmpty() || $project->getId() === 'console') { - return; - } - - $hostname = $dbForProject->getAdapter()->getHostname(); - $cacheKey = \sprintf( - '%s-cache-%s:%s:%s:project:%s:functions:events', - $dbForProject->getCacheName(), - $hostname ?? '', - $dbForProject->getNamespace(), - $dbForProject->getTenant(), - $project->getId() - ); - - $dbForProject->getCache()->purge($cacheKey); - }; - - $usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) { - $value = 1; - - switch ($event) { - case Database::EVENT_DOCUMENT_DELETE: - $value = -1; - break; - case Database::EVENT_DOCUMENTS_DELETE: - $value = -1 * $document->getAttribute('modified', 0); - break; - case Database::EVENT_DOCUMENTS_CREATE: - $value = $document->getAttribute('modified', 0); - break; - case Database::EVENT_DOCUMENTS_UPSERT: - $value = $document->getAttribute('created', 0); - break; - } - - switch (true) { - case $document->getCollection() === 'teams': - $usage->addMetric(METRIC_TEAMS, $value); // per project - break; - case $document->getCollection() === 'users': - $usage->addMetric(METRIC_USERS, $value); // per project - if ($event === Database::EVENT_DOCUMENT_DELETE) { - $usage->addReduce($document); - } - break; - case $document->getCollection() === 'sessions': // sessions - $usage->addMetric(METRIC_SESSIONS, $value); // per project - break; - case $document->getCollection() === 'databases': // databases - $usage->addMetric(METRIC_DATABASES, $value); // per project - - if ($event === Database::EVENT_DOCUMENT_DELETE) { - $usage->addReduce($document); - } - break; - case str_starts_with($document->getCollection(), 'database_') && ! str_contains($document->getCollection(), 'collection'): // collections - $parts = explode('_', $document->getCollection()); - $databaseInternalId = $parts[1] ?? 0; - $usage - ->addMetric(METRIC_COLLECTIONS, $value) // per project - ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value); - - if ($event === Database::EVENT_DOCUMENT_DELETE) { - $usage->addReduce($document); - } - break; - case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): // documents - $parts = explode('_', $document->getCollection()); - $databaseInternalId = $parts[1] ?? 0; - $collectionInternalId = $parts[3] ?? 0; - $usage - ->addMetric(METRIC_DOCUMENTS, $value) // per project - ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database - ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection - break; - case $document->getCollection() === 'buckets': // buckets - $usage->addMetric(METRIC_BUCKETS, $value); // per project - if ($event === Database::EVENT_DOCUMENT_DELETE) { - $usage - ->addReduce($document); - } - break; - case str_starts_with($document->getCollection(), 'bucket_'): // files - $parts = explode('_', $document->getCollection()); - $bucketInternalId = $parts[1]; - $usage - ->addMetric(METRIC_FILES, $value) // per project - ->addMetric(METRIC_FILES_STORAGE, $document->getAttribute('sizeOriginal') * $value) // per project - ->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES), $value) // per bucket - ->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES_STORAGE), $document->getAttribute('sizeOriginal') * $value); // per bucket - break; - case $document->getCollection() === 'functions': - $usage->addMetric(METRIC_FUNCTIONS, $value); // per project - - if ($event === Database::EVENT_DOCUMENT_DELETE) { - $usage - ->addReduce($document); - } - break; - case $document->getCollection() === 'sites': - $usage->addMetric(METRIC_SITES, $value); // per project - - if ($event === Database::EVENT_DOCUMENT_DELETE) { - $usage - ->addReduce($document); - } - break; - case $document->getCollection() === 'deployments': - $usage - ->addMetric(METRIC_DEPLOYMENTS, $value) // per project - ->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project - ->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS), $value) // per function - ->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value) - ->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS), $value) // per function - ->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value); - break; - default: - break; - } - }; - - // Clone the queues, to prevent events triggered by the database listener - // from overwriting the events that are supposed to be triggered in the shutdown hook. - $queueForEventsClone = new Event($publisher); - $queueForFunctions = new Func($publisherFunctions); - $queueForWebhooks = new Webhook($publisherWebhooks); - $queueForRealtime = new Realtime(); - - $database - ->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) - ->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) - ->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) - ->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) - ->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) - ->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener( - $project, - $document, - $response, - $queueForEventsClone->from($queueForEvents), - $queueForFunctions->from($queueForEvents), - $queueForWebhooks->from($queueForEvents), - $queueForRealtime->from($queueForEvents) - )) - ->on(Database::EVENT_DOCUMENT_CREATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database)) - ->on(Database::EVENT_DOCUMENT_UPDATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database)) - ->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database)); - - return $database; - }, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization']); - - $container->set('schema', function ($utopia, $dbForProject, $authorization) { - - $complexity = function (int $complexity, array $args) { - $queries = Query::parseQueries($args['queries'] ?? []); - $query = Query::getByType($queries, [Query::TYPE_LIMIT])[0] ?? null; - $limit = $query ? $query->getValue() : APP_LIMIT_LIST_DEFAULT; - - return $complexity * $limit; - }; - - $attributes = function (int $limit, int $offset) use ($dbForProject, $authorization) { - $attrs = $authorization->skip(fn () => $dbForProject->find('attributes', [ - Query::limit($limit), - Query::offset($offset), - ])); - - return \array_map(function ($attr) { - return $attr->getArrayCopy(); - }, $attrs); - }; - - $urls = [ - 'list' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents"; - }, - 'create' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents"; - }, - 'read' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; - }, - 'update' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; - }, - 'delete' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; - }, - ]; - - // NOTE: `params` and `urls` are not used internally in the `Schema::build` function below! - $params = [ - 'list' => function (string $databaseId, string $collectionId, array $args) { - return ['queries' => $args['queries']]; - }, - 'create' => function (string $databaseId, string $collectionId, array $args) { - $id = $args['id'] ?? 'unique()'; - $permissions = $args['permissions'] ?? null; - - unset($args['id']); - unset($args['permissions']); - - // Order must be the same as the route params - return [ - 'databaseId' => $databaseId, - 'documentId' => $id, - 'collectionId' => $collectionId, - 'data' => $args, - 'permissions' => $permissions, - ]; - }, - 'update' => function (string $databaseId, string $collectionId, array $args) { - $documentId = $args['id']; - $permissions = $args['permissions'] ?? null; - - unset($args['id']); - unset($args['permissions']); - - // Order must be the same as the route params - return [ - 'databaseId' => $databaseId, - 'collectionId' => $collectionId, - 'documentId' => $documentId, - 'data' => $args, - 'permissions' => $permissions, - ]; - }, - ]; - - return Schema::build( - $utopia, - $complexity, - $attributes, - $urls, - $params, - ); - }, ['utopia', 'dbForProject', 'authorization']); - - $container->set('audit', function ($dbForProject) { - $adapter = new AdapterDatabase($dbForProject); - - return new Audit($adapter); - }, ['dbForProject']); - - $container->set('mode', function ($request) { - /** @var Appwrite\Utopia\Request $request */ - - /** - * Defines the mode for the request: - * - 'default' => Requests for Client and Server Side - * - 'admin' => Request from the Console on non-console projects - */ - return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); - }, ['request']); - - $container->set('requestTimestamp', function ($request) { - // TODO: Move this to the Request class itself - $timestampHeader = $request->getHeader('x-appwrite-timestamp'); - $requestTimestamp = null; - if (! empty($timestampHeader)) { - try { - $requestTimestamp = new \DateTime($timestampHeader); - } catch (\Throwable $e) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid X-Appwrite-Timestamp header value'); - } - } - - return $requestTimestamp; - }, ['request']); - - $container->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) { - $devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', '')); - - // Check if given key match project's development keys - $key = $project->find('secret', $devKey, 'devKeys'); - if (! $key) { - return new Document([]); - } - - // check expiration - $expire = $key->getAttribute('expire'); - if (! empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) { - return new Document([]); - } - - // update access time - $accessedAt = $key->getAttribute('accessedAt', 0); - if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) { - $key->setAttribute('accessedAt', DatabaseDateTime::now()); - $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([ - 'accessedAt' => $key->getAttribute('accessedAt') - ]))); - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - } - - // add sdk to key - $sdkValidator = new WhiteList($servers, true); - $sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN')); - - if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) { - $sdks = $key->getAttribute('sdks', []); - - if (! in_array($sdk, $sdks)) { - $sdks[] = $sdk; - $key->setAttribute('sdks', $sdks); - - /** Update access time as well */ - $key->setAttribute('accessedAt', DatabaseDateTime::now()); - $key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([ - 'sdks' => $key->getAttribute('sdks'), - 'accessedAt' => $key->getAttribute('accessedAt') - ]))); - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - } - } - - return $key; - }, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']); - - $container->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) { - $teamInternalId = ''; - if ($project->getId() !== 'console') { - $teamInternalId = $project->getAttribute('teamInternalId', ''); - } else { - $route = $utopia->match($request); - $path = ! empty($route) ? $route->getPath() : $request->getURI(); - $orgHeader = $request->getHeader('x-appwrite-organization', ''); - if (str_starts_with($path, '/v1/projects/:projectId')) { - $uri = $request->getURI(); - $pid = explode('/', $uri)[3]; - $p = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $pid)); - $teamInternalId = $p->getAttribute('teamInternalId', ''); - } elseif ($path === '/v1/projects') { - $teamId = $request->getParam('teamId', ''); - - if (empty($teamId)) { - return new Document([]); - } - - $team = $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $teamId)); - - return $team; - } elseif (! empty($orgHeader)) { - return $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $orgHeader)); - } - } - - // if teamInternalId is empty, return an empty document - - if (empty($teamInternalId)) { - return new Document([]); - } - - $team = $authorization->skip(function () use ($dbForPlatform, $teamInternalId) { - return $dbForPlatform->findOne('teams', [ - Query::equal('$sequence', [$teamInternalId]), - ]); - }); - - return $team; - }, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']); - - $container->set('previewHostname', function (Request $request, ?Key $apiKey) { - $allowed = false; - - if (Http::isDevelopment()) { - $allowed = true; - } elseif (! \is_null($apiKey) && $apiKey->getHostnameOverride() === true) { - $allowed = true; - } - - if ($allowed) { - $host = $request->getQuery('appwrite-hostname', $request->getHeader('x-appwrite-hostname', '')) ?? ''; - if (! empty($host)) { - return $host; - } - } - - return ''; - }, ['request', 'apiKey']); - - $container->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key { - $key = $request->getHeader('x-appwrite-key'); - - if (empty($key)) { - return null; - } - - $key = Key::decode($project, $team, $user, $key); - - $userHeader = $request->getHeader('x-appwrite-user'); - $organizationHeader = $request->getHeader('x-appwrite-organization'); - $projectHeader = $request->getHeader('x-appwrite-project'); - - if (! empty($key->getProjectId())) { - if (empty($projectHeader) || $projectHeader !== $key->getProjectId()) { - throw new Exception(Exception::PROJECT_ID_MISSING); - } - } - - if (! empty($key->getUserId())) { - if (empty($userHeader) || $userHeader !== $key->getUserId()) { - throw new Exception(Exception::USER_ID_MISSING); - } - } - - if (! empty($key->getTeamId())) { - if (empty($organizationHeader) || $organizationHeader !== $key->getTeamId()) { - throw new Exception(Exception::ORGANIZATION_ID_MISSING); - } - } - - return $key; - }, ['request', 'project', 'team', 'user']); - - $container->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) { - $tokenJWT = $request->getParam('token'); - - if (! empty($tokenJWT) && ! $project->isEmpty()) { // JWT authentication - // Use a large but reasonable maxAge to avoid auto-exp when token has no expiry - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), RESOURCE_TOKEN_ALGORITHM, RESOURCE_TOKEN_MAX_AGE, RESOURCE_TOKEN_LEEWAY); // Instantiate with key, algo, maxAge and leeway. - - try { - $payload = $jwt->decode($tokenJWT); - } catch (JWTException $error) { - return new Document([]); - } - - $tokenId = $payload['tokenId'] ?? ''; - if (empty($tokenId)) { - return new Document([]); - } - - $token = $authorization->skip(fn () => $dbForProject->getDocument('resourceTokens', $tokenId)); - - if ($token->isEmpty()) { - return new Document([]); - } - - $expiry = $token->getAttribute('expire'); - - if ($expiry !== null) { - $now = new \DateTime(); - $expiryDate = new \DateTime($expiry); - - if ($expiryDate < $now) { - return new Document([]); - } - } - - return match ($token->getAttribute('resourceType')) { - TOKENS_RESOURCE_TYPE_FILES => (function () use ($token, $dbForProject, $authorization) { - $sequences = explode(':', $token->getAttribute('resourceInternalId')); - $ids = explode(':', $token->getAttribute('resourceId')); - - if (count($sequences) !== 2 || count($ids) !== 2) { - return new Document([]); - } - - $accessedAt = $token->getAttribute('accessedAt', 0); - if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) { - $token->setAttribute('accessedAt', DatabaseDateTime::now()); - $authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), new Document([ - 'accessedAt' => $token->getAttribute('accessedAt') - ]))); - } - - return new Document([ - 'bucketId' => $ids[0], - 'fileId' => $ids[1], - 'bucketInternalId' => $sequences[0], - 'fileInternalId' => $sequences[1], - ]); - })(), - - default => throw new Exception(Exception::TOKEN_RESOURCE_TYPE_INVALID), - }; - } - - return new Document([]); - }, ['project', 'dbForProject', 'request', 'authorization']); - - $container->set('transactionState', function (Database $dbForProject, Authorization $authorization) { - return new TransactionState($dbForProject, $authorization); - }, ['dbForProject', 'authorization']); - - $container->set('executionsRetentionCount', function (Document $project, array $plan) { - if ($project->getId() === 'console' || empty($plan)) { - return 0; - } - - return (int) ($plan['executionsRetentionCount'] ?? 100); - }, ['project', 'plan']); - - $container->set('deviceForFiles', function ($project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); - }, ['project', 'telemetry']); - $container->set('deviceForSites', function ($project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())); - }, ['project', 'telemetry']); - $container->set('deviceForMigrations', function ($project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())); - }, ['project', 'telemetry']); - $container->set('deviceForFunctions', function ($project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())); - }, ['project', 'telemetry']); - $container->set('deviceForBuilds', function ($project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); - }, ['project', 'telemetry']); -} $container->set('store', function (): Store { return new Store(); diff --git a/app/init/resources.request.php b/app/init/resources.request.php new file mode 100644 index 0000000000..e2e48196d5 --- /dev/null +++ b/app/init/resources.request.php @@ -0,0 +1,1187 @@ +set('log', fn () => new Log(), []); + + $container->set('logger', function ($register) { + return $register->get('logger'); + }, ['register']); + + $container->set('authorization', function () { + return new Authorization(); + }, []); + + $container->set('store', function (): Store { + return new Store(); + }, []); + + $container->set('proofForPassword', function (): Password { + $hash = new Argon2(); + $hash + ->setMemoryCost(7168) + ->setTimeCost(5) + ->setThreads(1); + + $password = new Password(); + $password + ->setHash($hash); + + return $password; + }); + + $container->set('proofForToken', function (): Token { + $token = new Token(); + $token->setHash(new Sha()); + + return $token; + }); + + $container->set('proofForCode', function (): Code { + $code = new Code(); + $code->setHash(new Sha()); + + return $code; + }); + + $container->set('locale', function () { + $locale = new Locale(System::getEnv('_APP_LOCALE', 'en')); + $locale->setFallback(System::getEnv('_APP_LOCALE', 'en')); + + return $locale; + }); + + // Per-request queue resources (stateful, accumulate event data during request) + $container->set('queueForMessaging', function (Publisher $publisher) { + return new Messaging($publisher); + }, ['publisher']); + $container->set('queueForMails', function (Publisher $publisher) { + return new Mail($publisher); + }, ['publisher']); + $container->set('queueForBuilds', function (Publisher $publisher) { + return new Build($publisher); + }, ['publisher']); + $container->set('queueForScreenshots', function (Publisher $publisher) { + return new Screenshot($publisher); + }, ['publisher']); + $container->set('queueForDatabase', function (Publisher $publisher) { + return new EventDatabase($publisher); + }, ['publisher']); + $container->set('queueForDeletes', function (Publisher $publisher) { + return new Delete($publisher); + }, ['publisher']); + $container->set('queueForEvents', function (Publisher $publisher) { + return new Event($publisher); + }, ['publisher']); + $container->set('queueForWebhooks', function (Publisher $publisher) { + return new Webhook($publisher); + }, ['publisher']); + $container->set('queueForRealtime', function () { + return new Realtime(); + }, []); + $container->set('usage', function () { + return new UsageContext(); + }, []); + $container->set('queueForAudits', function (Publisher $publisher) { + return new AuditEvent($publisher); + }, ['publisher']); + $container->set('queueForFunctions', function (Publisher $publisher) { + return new Func($publisher); + }, ['publisher']); + $container->set('eventProcessor', function () { + return new EventProcessor(); + }, []); + $container->set('queueForCertificates', function (Publisher $publisher) { + return new Certificate($publisher); + }, ['publisher']); + $container->set('queueForMigrations', function (Publisher $publisher) { + return new Migration($publisher); + }, ['publisher']); + $container->set('queueForStatsResources', function (Publisher $publisher) { + return new StatsResources($publisher); + }, ['publisher']); + + $container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) { + $adapter = new DatabasePool($pools->get('console')); + $database = new Database($adapter, $cache); + + $database + ->setDatabase(APP_DATABASE) + ->setAuthorization($authorization) + ->setNamespace('_console') + ->setMetadata('host', \gethostname()) + ->setMetadata('project', 'console') + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) + ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); + + $database->setDocumentType('users', User::class); + + return $database; + }, ['pools', 'cache', 'authorization']); + + $container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) { + $adapters = []; + + return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$adapters) { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForPlatform; + } + + $database = $project->getAttribute('database', ''); + if (empty($database)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Project database is not configured'); + } + + try { + $dsn = new DSN($database); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $database); + } + + $adapter = $adapters[$dsn->getHost()] ??= new DatabasePool($pools->get($dsn->getHost())); + $database = new Database($adapter, $cache); + + $database + ->setDatabase(APP_DATABASE) + ->setAuthorization($authorization) + ->setMetadata('host', \gethostname()) + ->setMetadata('project', $project->getId()) + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) + ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES) + ->setDocumentType('users', User::class); + + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + + if (\in_array($dsn->getHost(), $sharedTables)) { + $database + ->setSharedTables(true) + ->setTenant($project->getSequence()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getSequence()); + } + + return $database; + }; + }, ['pools', 'dbForPlatform', 'cache', 'authorization']); + + $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { + $adapter = null; + + return function (?Document $project = null) use ($pools, $cache, $authorization, &$adapter) { + $adapter ??= new DatabasePool($pools->get('logs')); + $database = new Database($adapter, $cache); + + $database + ->setDatabase(APP_DATABASE) + ->setAuthorization($authorization) + ->setSharedTables(true) + ->setNamespace('logsV1') + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) + ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); + + if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') { + $database->setTenant($project->getSequence()); + } + + return $database; + }; + }, ['pools', 'cache', 'authorization']); + + /** + * List of allowed request hostnames for the request. + */ + $container->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) { + $allowed = [...($platform['hostnames'] ?? [])]; + + /* Add platform configured hostnames */ + if (! $project->isEmpty() && $project->getId() !== 'console') { + $platforms = $project->getAttribute('platforms', []); + $hostnames = Platform::getHostnames($platforms); + $allowed = [...$allowed, ...$hostnames]; + } + + /* Add the request hostname if a dev key is found */ + if (! $devKey->isEmpty()) { + $allowed[] = $request->getHostname(); + } + + $originHostname = parse_url($request->getOrigin(), PHP_URL_HOST); + $refererHostname = parse_url($request->getReferer(), PHP_URL_HOST); + + $hostname = $originHostname; + if (empty($hostname)) { + $hostname = $refererHostname; + } + + /* Add request hostname for preflight requests */ + if ($request->getMethod() === 'OPTIONS') { + $allowed[] = $hostname; + } + + /* Allow the request origin of rule */ + if (! $rule->isEmpty() && ! empty($rule->getAttribute('domain', ''))) { + $allowed[] = $rule->getAttribute('domain', ''); + } + + /* Allow the request origin if a dev key is found */ + if (! $devKey->isEmpty() && ! empty($hostname)) { + $allowed[] = $hostname; + } + + return array_unique($allowed); + }, ['platform', 'project', 'rule', 'devKey', 'request']); + + /** + * List of allowed request schemes for the request. + */ + $container->set('allowedSchemes', function (array $platform, Document $project) { + $allowed = [...($platform['schemas'] ?? [])]; + + if (! $project->isEmpty() && $project->getId() !== 'console') { + /* Add hardcoded schemes */ + $allowed[] = 'exp'; + $allowed[] = 'appwrite-callback-' . $project->getId(); + + /* Add platform configured schemes */ + $platforms = $project->getAttribute('platforms', []); + $schemes = Platform::getSchemes($platforms); + $allowed = [...$allowed, ...$schemes]; + } + + return array_unique($allowed); + }, ['platform', 'project']); + + /** + * Rule associated with a request origin. + */ + $container->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) { + $domain = \parse_url($request->getOrigin(), PHP_URL_HOST); + + if (empty($domain)) { + $domain = \parse_url($request->getReferer(), PHP_URL_HOST); + } + + if (empty($domain)) { + return new Document(); + } + + // TODO: (@Meldiron) Remove after 1.7.x migration + $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; + $rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) { + if ($isMd5) { + return $dbForPlatform->getDocument('rules', md5($domain)); + } + + return $dbForPlatform->findOne('rules', [ + Query::equal('domain', [$domain]), + ]) ?? new Document(); + }); + + $permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence(); + + // Temporary implementation until custom wildcard domains are an official feature + // Allow trusted projects; Used for Console (website) previews + if (! $permitsCurrentProject && ! $rule->isEmpty() && ! empty($rule->getAttribute('projectId', ''))) { + $trustedProjects = []; + foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) { + if (empty($trustedProject)) { + continue; + } + $trustedProjects[] = $trustedProject; + } + if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects)) { + $permitsCurrentProject = true; + } + } + + if (! $permitsCurrentProject) { + return new Document(); + } + + return $rule; + }, ['request', 'dbForPlatform', 'project', 'authorization']); + + /** + * CORS service + */ + $container->set('cors', function (array $allowedHostnames) { + $corsConfig = Config::getParam('cors'); + + return new Cors( + $allowedHostnames, + allowedMethods: $corsConfig['allowedMethods'], + allowedHeaders: $corsConfig['allowedHeaders'], + allowCredentials: true, + exposedHeaders: $corsConfig['exposedHeaders'], + ); + }, ['allowedHostnames']); + + $container->set('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) { + if (! $devKey->isEmpty()) { + return new URL(); + } + + return new Origin($allowedHostnames, $allowedSchemes); + }, ['devKey', 'allowedHostnames', 'allowedSchemes']); + + $container->set('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) { + if (! $devKey->isEmpty()) { + return new URL(); + } + + return new Redirect($allowedHostnames, $allowedSchemes); + }, ['devKey', 'allowedHostnames', 'allowedSchemes']); + + $container->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) { + /** + * Handles user authentication and session validation. + * + * This function follows a series of steps to determine the appropriate user session + * based on cookies, headers, and JWT tokens. + * + * Process: + * 1. Checks the cookie based on mode: + * - If in admin mode, uses console project id for key. + * - Otherwise, sets the key using the project ID + * 2. If no cookie is found, attempts to retrieve the fallback header `x-fallback-cookies`. + * - If this method is used, returns the header: `X-Debug-Fallback: true`. + * 3. Fetches the user document from the appropriate database based on the mode. + * 4. If the user document is empty or the session key cannot be verified, sets an empty user document. + * 5. Regardless of the results from steps 1-4, attempts to fetch the JWT token. + * 6. If the JWT user has a valid session ID, updates the user variable with the user from `projectDB`, + * overwriting the previous value. + * 7. If account API key is passed, use user of the account API key as long as user ID header matches too + */ + $authorization->setDefaultStatus(true); + + $store->setKey('a_session_' . $project->getId()); + + if ($mode === APP_MODE_ADMIN) { + $store->setKey('a_session_' . $console->getId()); + } + + $store->decode( + $request->getCookie( + $store->getKey(), // Get sessions + $request->getCookie($store->getKey() . '_legacy', '') + ) + ); + + // Get session from header for SSR clients + if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) { + $sessionHeader = $request->getHeader('x-appwrite-session', ''); + + if (! empty($sessionHeader)) { + $store->decode($sessionHeader); + } + } + + // Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies + if ($response) { // if in http context - add debug header + $response->addHeader('X-Debug-Fallback', 'false'); + } + + if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) { + if ($response) { + $response->addHeader('X-Debug-Fallback', 'true'); + } + $fallback = $request->getHeader('x-fallback-cookies', ''); + $fallback = \json_decode($fallback, true); + $store->decode(((is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : '')); + } + + $user = null; + if ($mode === APP_MODE_ADMIN) { + /** @var User $user */ + $user = $dbForPlatform->getDocument('users', $store->getProperty('id', '')); + } else { + if ($project->isEmpty()) { + $user = new User([]); + } else { + if (! empty($store->getProperty('id', ''))) { + if ($project->getId() === 'console') { + /** @var User $user */ + $user = $dbForPlatform->getDocument('users', $store->getProperty('id', '')); + } else { + /** @var User $user */ + $user = $dbForProject->getDocument('users', $store->getProperty('id', '')); + } + } + } + } + + if ( + ! $user || + $user->isEmpty() // Check a document has been found in the DB + || ! $user->sessionVerify($store->getProperty('secret', ''), $proofForToken) + ) { // Validate user has valid login token + $user = new User([]); + } + + $authJWT = $request->getHeader('x-appwrite-jwt', ''); + if (! empty($authJWT) && ! $project->isEmpty()) { // JWT authentication + if (! $user->isEmpty()) { + throw new Exception(Exception::USER_JWT_AND_COOKIE_SET); + } + + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); + try { + $payload = $jwt->decode($authJWT); + } catch (JWTException $error) { + throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage()); + } + + $jwtUserId = $payload['userId'] ?? ''; + if (! empty($jwtUserId)) { + if ($mode === APP_MODE_ADMIN) { + $user = $dbForPlatform->getDocument('users', $jwtUserId); + } else { + $user = $dbForProject->getDocument('users', $jwtUserId); + } + } + $jwtSessionId = $payload['sessionId'] ?? ''; + if (! empty($jwtSessionId)) { + if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token + $user = new User([]); + } + } + } + + // Account based on account API key + $accountKey = $request->getHeader('x-appwrite-key', ''); + $accountKeyUserId = $request->getHeader('x-appwrite-user', ''); + if (! empty($accountKeyUserId) && ! empty($accountKey)) { + if (! $user->isEmpty()) { + throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET); + } + + $accountKeyUser = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId)); + if (! $accountKeyUser->isEmpty()) { + $key = $accountKeyUser->find( + key: 'secret', + find: $accountKey, + subject: 'keys' + ); + + if (! empty($key)) { + $expire = $key->getAttribute('expire'); + if (! empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) { + throw new Exception(Exception::ACCOUNT_KEY_EXPIRED); + } + + $user = $accountKeyUser; + } + } + } + + $dbForProject->setMetadata('user', $user->getId()); + $dbForPlatform->setMetadata('user', $user->getId()); + + return $user; + }, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']); + + $container->set('project', function ($dbForPlatform, $request, $console, $authorization) { + /** @var Appwrite\Utopia\Request $request */ + /** @var Utopia\Database\Database $dbForPlatform */ + /** @var Utopia\Database\Document $console */ + $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', '')); + // Realtime channel "project" can send project=Query array + if (! \is_string($projectId)) { + $projectId = $request->getHeader('x-appwrite-project', ''); + } + + if (empty($projectId) || $projectId === 'console') { + return $console; + } + + $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); + + return $project; + }, ['dbForPlatform', 'request', 'console', 'authorization']); + + $container->set('session', function (User $user, Store $store, Token $proofForToken) { + if ($user->isEmpty()) { + return; + } + + $sessions = $user->getAttribute('sessions', []); + $sessionId = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken); + + if (! $sessionId) { + return; + } + foreach ($sessions as $session) { + /** @var Document $session */ + if ($sessionId === $session->getId()) { + return $session; + } + } + + }, ['user', 'store', 'proofForToken']); + + $container->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization) { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForPlatform; + } + + $database = $project->getAttribute('database', ''); + if (empty($database)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Project database is not configured'); + } + + try { + $dsn = new DSN($database); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $database); + } + + $adapter = new DatabasePool($pools->get($dsn->getHost())); + $database = new Database($adapter, $cache); + + $database + ->setDatabase(APP_DATABASE) + ->setAuthorization($authorization) + ->setMetadata('host', \gethostname()) + ->setMetadata('project', $project->getId()) + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) + ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); + $database->setDocumentType('users', User::class); + + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + + if (\in_array($dsn->getHost(), $sharedTables)) { + $database + ->setSharedTables(true) + ->setTenant($project->getSequence()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getSequence()); + } + + /** + * This isolated event handling for `users.*.create` which is based on a `Database::EVENT_DOCUMENT_CREATE` listener may look odd, but it is **intentional**. + * + * Accounts can be created in many ways beyond `createAccount` + * (anonymous, OAuth, phone, etc.), and those flows are probably not covered in event tests; so we handle this here. + */ + $eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) { + // Only trigger events for user creation with the database listener. + if ($document->getCollection() !== 'users') { + return; + } + + $queueForEvents + ->setEvent('users.[userId].create') + ->setParam('userId', $document->getId()) + ->setPayload($response->output($document, Response::MODEL_USER)); + + // Trigger functions, webhooks, and realtime events + $queueForFunctions + ->from($queueForEvents) + ->trigger(); + + /** Trigger webhooks events only if a project has them enabled */ + if (! empty($project->getAttribute('webhooks'))) { + $queueForWebhooks + ->from($queueForEvents) + ->trigger(); + } + + /** Trigger realtime events only for non console events */ + if ($queueForEvents->getProject()->getId() !== 'console') { + $queueForRealtime + ->from($queueForEvents) + ->trigger(); + } + }; + + /** + * Purge function events cache when functions are created, updated or deleted. + */ + $functionsEventsCacheListener = function (string $event, Document $document, Document $project, Database $dbForProject) { + + if ($document->getCollection() !== 'functions') { + return; + } + + if ($project->isEmpty() || $project->getId() === 'console') { + return; + } + + $hostname = $dbForProject->getAdapter()->getHostname(); + $cacheKey = \sprintf( + '%s-cache-%s:%s:%s:project:%s:functions:events', + $dbForProject->getCacheName(), + $hostname ?? '', + $dbForProject->getNamespace(), + $dbForProject->getTenant(), + $project->getId() + ); + + $dbForProject->getCache()->purge($cacheKey); + }; + + $usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) { + $value = 1; + + switch ($event) { + case Database::EVENT_DOCUMENT_DELETE: + $value = -1; + break; + case Database::EVENT_DOCUMENTS_DELETE: + $value = -1 * $document->getAttribute('modified', 0); + break; + case Database::EVENT_DOCUMENTS_CREATE: + $value = $document->getAttribute('modified', 0); + break; + case Database::EVENT_DOCUMENTS_UPSERT: + $value = $document->getAttribute('created', 0); + break; + } + + switch (true) { + case $document->getCollection() === 'teams': + $usage->addMetric(METRIC_TEAMS, $value); // per project + break; + case $document->getCollection() === 'users': + $usage->addMetric(METRIC_USERS, $value); // per project + if ($event === Database::EVENT_DOCUMENT_DELETE) { + $usage->addReduce($document); + } + break; + case $document->getCollection() === 'sessions': // sessions + $usage->addMetric(METRIC_SESSIONS, $value); // per project + break; + case $document->getCollection() === 'databases': // databases + $usage->addMetric(METRIC_DATABASES, $value); // per project + + if ($event === Database::EVENT_DOCUMENT_DELETE) { + $usage->addReduce($document); + } + break; + case str_starts_with($document->getCollection(), 'database_') && ! str_contains($document->getCollection(), 'collection'): // collections + $parts = explode('_', $document->getCollection()); + $databaseInternalId = $parts[1] ?? 0; + $usage + ->addMetric(METRIC_COLLECTIONS, $value) // per project + ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value); + + if ($event === Database::EVENT_DOCUMENT_DELETE) { + $usage->addReduce($document); + } + break; + case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): // documents + $parts = explode('_', $document->getCollection()); + $databaseInternalId = $parts[1] ?? 0; + $collectionInternalId = $parts[3] ?? 0; + $usage + ->addMetric(METRIC_DOCUMENTS, $value) // per project + ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database + ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection + break; + case $document->getCollection() === 'buckets': // buckets + $usage->addMetric(METRIC_BUCKETS, $value); // per project + if ($event === Database::EVENT_DOCUMENT_DELETE) { + $usage + ->addReduce($document); + } + break; + case str_starts_with($document->getCollection(), 'bucket_'): // files + $parts = explode('_', $document->getCollection()); + $bucketInternalId = $parts[1]; + $usage + ->addMetric(METRIC_FILES, $value) // per project + ->addMetric(METRIC_FILES_STORAGE, $document->getAttribute('sizeOriginal') * $value) // per project + ->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES), $value) // per bucket + ->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES_STORAGE), $document->getAttribute('sizeOriginal') * $value); // per bucket + break; + case $document->getCollection() === 'functions': + $usage->addMetric(METRIC_FUNCTIONS, $value); // per project + + if ($event === Database::EVENT_DOCUMENT_DELETE) { + $usage + ->addReduce($document); + } + break; + case $document->getCollection() === 'sites': + $usage->addMetric(METRIC_SITES, $value); // per project + + if ($event === Database::EVENT_DOCUMENT_DELETE) { + $usage + ->addReduce($document); + } + break; + case $document->getCollection() === 'deployments': + $usage + ->addMetric(METRIC_DEPLOYMENTS, $value) // per project + ->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project + ->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS), $value) // per function + ->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value) + ->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS), $value) // per function + ->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value); + break; + default: + break; + } + }; + + // Clone the queues, to prevent events triggered by the database listener + // from overwriting the events that are supposed to be triggered in the shutdown hook. + $queueForEventsClone = new Event($publisher); + $queueForFunctions = new Func($publisherFunctions); + $queueForWebhooks = new Webhook($publisherWebhooks); + $queueForRealtime = new Realtime(); + + $database + ->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) + ->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) + ->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) + ->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) + ->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage)) + ->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener( + $project, + $document, + $response, + $queueForEventsClone->from($queueForEvents), + $queueForFunctions->from($queueForEvents), + $queueForWebhooks->from($queueForEvents), + $queueForRealtime->from($queueForEvents) + )) + ->on(Database::EVENT_DOCUMENT_CREATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database)) + ->on(Database::EVENT_DOCUMENT_UPDATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database)) + ->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database)); + + return $database; + }, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization']); + + $container->set('schema', function ($utopia, $dbForProject, $authorization) { + + $complexity = function (int $complexity, array $args) { + $queries = Query::parseQueries($args['queries'] ?? []); + $query = Query::getByType($queries, [Query::TYPE_LIMIT])[0] ?? null; + $limit = $query ? $query->getValue() : APP_LIMIT_LIST_DEFAULT; + + return $complexity * $limit; + }; + + $attributes = function (int $limit, int $offset) use ($dbForProject, $authorization) { + $attrs = $authorization->skip(fn () => $dbForProject->find('attributes', [ + Query::limit($limit), + Query::offset($offset), + ])); + + return \array_map(function ($attr) { + return $attr->getArrayCopy(); + }, $attrs); + }; + + $urls = [ + 'list' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents"; + }, + 'create' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents"; + }, + 'read' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; + }, + 'update' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; + }, + 'delete' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; + }, + ]; + + // NOTE: `params` and `urls` are not used internally in the `Schema::build` function below! + $params = [ + 'list' => function (string $databaseId, string $collectionId, array $args) { + return ['queries' => $args['queries']]; + }, + 'create' => function (string $databaseId, string $collectionId, array $args) { + $id = $args['id'] ?? 'unique()'; + $permissions = $args['permissions'] ?? null; + + unset($args['id']); + unset($args['permissions']); + + // Order must be the same as the route params + return [ + 'databaseId' => $databaseId, + 'documentId' => $id, + 'collectionId' => $collectionId, + 'data' => $args, + 'permissions' => $permissions, + ]; + }, + 'update' => function (string $databaseId, string $collectionId, array $args) { + $documentId = $args['id']; + $permissions = $args['permissions'] ?? null; + + unset($args['id']); + unset($args['permissions']); + + // Order must be the same as the route params + return [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'documentId' => $documentId, + 'data' => $args, + 'permissions' => $permissions, + ]; + }, + ]; + + return Schema::build( + $utopia, + $complexity, + $attributes, + $urls, + $params, + ); + }, ['utopia', 'dbForProject', 'authorization']); + + $container->set('audit', function ($dbForProject) { + $adapter = new AdapterDatabase($dbForProject); + + return new Audit($adapter); + }, ['dbForProject']); + + $container->set('mode', function ($request) { + /** @var Appwrite\Utopia\Request $request */ + + /** + * Defines the mode for the request: + * - 'default' => Requests for Client and Server Side + * - 'admin' => Request from the Console on non-console projects + */ + return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); + }, ['request']); + + $container->set('requestTimestamp', function ($request) { + // TODO: Move this to the Request class itself + $timestampHeader = $request->getHeader('x-appwrite-timestamp'); + $requestTimestamp = null; + if (! empty($timestampHeader)) { + try { + $requestTimestamp = new \DateTime($timestampHeader); + } catch (\Throwable $e) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid X-Appwrite-Timestamp header value'); + } + } + + return $requestTimestamp; + }, ['request']); + + $container->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) { + $devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', '')); + + // Check if given key match project's development keys + $key = $project->find('secret', $devKey, 'devKeys'); + if (! $key) { + return new Document([]); + } + + // check expiration + $expire = $key->getAttribute('expire'); + if (! empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) { + return new Document([]); + } + + // update access time + $accessedAt = $key->getAttribute('accessedAt', 0); + if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) { + $key->setAttribute('accessedAt', DatabaseDateTime::now()); + $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([ + 'accessedAt' => $key->getAttribute('accessedAt') + ]))); + $dbForPlatform->purgeCachedDocument('projects', $project->getId()); + } + + // add sdk to key + $sdkValidator = new WhiteList($servers, true); + $sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN')); + + if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) { + $sdks = $key->getAttribute('sdks', []); + + if (! in_array($sdk, $sdks)) { + $sdks[] = $sdk; + $key->setAttribute('sdks', $sdks); + + /** Update access time as well */ + $key->setAttribute('accessedAt', DatabaseDateTime::now()); + $key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([ + 'sdks' => $key->getAttribute('sdks'), + 'accessedAt' => $key->getAttribute('accessedAt') + ]))); + $dbForPlatform->purgeCachedDocument('projects', $project->getId()); + } + } + + return $key; + }, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']); + + $container->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) { + $teamInternalId = ''; + if ($project->getId() !== 'console') { + $teamInternalId = $project->getAttribute('teamInternalId', ''); + } else { + $route = $utopia->match($request); + $path = ! empty($route) ? $route->getPath() : $request->getURI(); + $orgHeader = $request->getHeader('x-appwrite-organization', ''); + if (str_starts_with($path, '/v1/projects/:projectId')) { + $uri = $request->getURI(); + $pid = explode('/', $uri)[3]; + $p = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $pid)); + $teamInternalId = $p->getAttribute('teamInternalId', ''); + } elseif ($path === '/v1/projects') { + $teamId = $request->getParam('teamId', ''); + + if (empty($teamId)) { + return new Document([]); + } + + $team = $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $teamId)); + + return $team; + } elseif (! empty($orgHeader)) { + return $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $orgHeader)); + } + } + + // if teamInternalId is empty, return an empty document + + if (empty($teamInternalId)) { + return new Document([]); + } + + $team = $authorization->skip(function () use ($dbForPlatform, $teamInternalId) { + return $dbForPlatform->findOne('teams', [ + Query::equal('$sequence', [$teamInternalId]), + ]); + }); + + return $team; + }, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']); + + $container->set('previewHostname', function (Request $request, ?Key $apiKey) { + $allowed = false; + + if (Http::isDevelopment()) { + $allowed = true; + } elseif (! \is_null($apiKey) && $apiKey->getHostnameOverride() === true) { + $allowed = true; + } + + if ($allowed) { + $host = $request->getQuery('appwrite-hostname', $request->getHeader('x-appwrite-hostname', '')) ?? ''; + if (! empty($host)) { + return $host; + } + } + + return ''; + }, ['request', 'apiKey']); + + $container->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key { + $key = $request->getHeader('x-appwrite-key'); + + if (empty($key)) { + return null; + } + + $key = Key::decode($project, $team, $user, $key); + + $userHeader = $request->getHeader('x-appwrite-user'); + $organizationHeader = $request->getHeader('x-appwrite-organization'); + $projectHeader = $request->getHeader('x-appwrite-project'); + + if (! empty($key->getProjectId())) { + if (empty($projectHeader) || $projectHeader !== $key->getProjectId()) { + throw new Exception(Exception::PROJECT_ID_MISSING); + } + } + + if (! empty($key->getUserId())) { + if (empty($userHeader) || $userHeader !== $key->getUserId()) { + throw new Exception(Exception::USER_ID_MISSING); + } + } + + if (! empty($key->getTeamId())) { + if (empty($organizationHeader) || $organizationHeader !== $key->getTeamId()) { + throw new Exception(Exception::ORGANIZATION_ID_MISSING); + } + } + + return $key; + }, ['request', 'project', 'team', 'user']); + + $container->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) { + $tokenJWT = $request->getParam('token'); + + if (! empty($tokenJWT) && ! $project->isEmpty()) { // JWT authentication + // Use a large but reasonable maxAge to avoid auto-exp when token has no expiry + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), RESOURCE_TOKEN_ALGORITHM, RESOURCE_TOKEN_MAX_AGE, RESOURCE_TOKEN_LEEWAY); // Instantiate with key, algo, maxAge and leeway. + + try { + $payload = $jwt->decode($tokenJWT); + } catch (JWTException $error) { + return new Document([]); + } + + $tokenId = $payload['tokenId'] ?? ''; + if (empty($tokenId)) { + return new Document([]); + } + + $token = $authorization->skip(fn () => $dbForProject->getDocument('resourceTokens', $tokenId)); + + if ($token->isEmpty()) { + return new Document([]); + } + + $expiry = $token->getAttribute('expire'); + + if ($expiry !== null) { + $now = new \DateTime(); + $expiryDate = new \DateTime($expiry); + + if ($expiryDate < $now) { + return new Document([]); + } + } + + return match ($token->getAttribute('resourceType')) { + TOKENS_RESOURCE_TYPE_FILES => (function () use ($token, $dbForProject, $authorization) { + $sequences = explode(':', $token->getAttribute('resourceInternalId')); + $ids = explode(':', $token->getAttribute('resourceId')); + + if (count($sequences) !== 2 || count($ids) !== 2) { + return new Document([]); + } + + $accessedAt = $token->getAttribute('accessedAt', 0); + if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) { + $token->setAttribute('accessedAt', DatabaseDateTime::now()); + $authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), new Document([ + 'accessedAt' => $token->getAttribute('accessedAt') + ]))); + } + + return new Document([ + 'bucketId' => $ids[0], + 'fileId' => $ids[1], + 'bucketInternalId' => $sequences[0], + 'fileInternalId' => $sequences[1], + ]); + })(), + + default => throw new Exception(Exception::TOKEN_RESOURCE_TYPE_INVALID), + }; + } + + return new Document([]); + }, ['project', 'dbForProject', 'request', 'authorization']); + + $container->set('transactionState', function (Database $dbForProject, Authorization $authorization) { + return new TransactionState($dbForProject, $authorization); + }, ['dbForProject', 'authorization']); + + $container->set('executionsRetentionCount', function (Document $project, array $plan) { + if ($project->getId() === 'console' || empty($plan)) { + return 0; + } + + return (int) ($plan['executionsRetentionCount'] ?? 100); + }, ['project', 'plan']); + + $container->set('deviceForFiles', function ($project, Telemetry $telemetry) { + return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); + }, ['project', 'telemetry']); + $container->set('deviceForSites', function ($project, Telemetry $telemetry) { + return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())); + }, ['project', 'telemetry']); + $container->set('deviceForMigrations', function ($project, Telemetry $telemetry) { + return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())); + }, ['project', 'telemetry']); + $container->set('deviceForFunctions', function ($project, Telemetry $telemetry) { + return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())); + }, ['project', 'telemetry']); + $container->set('deviceForBuilds', function ($project, Telemetry $telemetry) { + return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); + }, ['project', 'telemetry']); +} diff --git a/app/realtime.php b/app/realtime.php index f08a2ad6d6..63f9420201 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -35,6 +35,7 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\DI\Container; use Utopia\DSN\DSN; +use Utopia\Http\Adapter\FPM\Server as HttpServer; use Utopia\Http\Http; use Utopia\Logger\Log; use Utopia\Pools\Group; @@ -618,7 +619,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $connectionContainer = new Container($container); registerRequestResources($connectionContainer); - $adapter = new \Utopia\Http\Adapter\FPM\Server($connectionContainer); + + $adapter = new HttpServer($connectionContainer); $app = new Http($adapter, 'UTC'); $app->setResource('request', fn () => $request); $app->setResource('response', fn () => $response); From c2795376d8b6574fe516567c23d7f828a5fd3eff Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 16:25:52 +0530 Subject: [PATCH 032/118] update server --- app/http.php | 8 +++----- composer.lock | 8 ++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/http.php b/app/http.php index 1924071c4c..c81f49bfd1 100644 --- a/app/http.php +++ b/app/http.php @@ -6,7 +6,6 @@ require_once __DIR__ . '/init/span.php'; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Swoole\Constant; -use Swoole\Http\Server; use Swoole\Process; use Swoole\Table; use Swoole\Timer; @@ -25,7 +24,7 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; -use Utopia\Http\Adapter\Swoole\HttpServer; +use Utopia\Http\Adapter\Swoole\Server; use Utopia\Http\Files; use Utopia\Http\Http; use Utopia\Logger\Log; @@ -54,7 +53,7 @@ $container->set('certifiedDomains', fn () => $certifiedDomains); $payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing $totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6)); -$swooleAdapter = new HttpServer( +$swooleAdapter = new Server( host: "0.0.0.0", port: System::getEnv('PORT', 80), settings: [ @@ -67,7 +66,6 @@ $swooleAdapter = new HttpServer( Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background ], container: $container, - coroutines: true, ); $container->set('container', fn () => fn () => $swooleAdapter->getContainer()); @@ -288,7 +286,7 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c Span::current()?->finish(); } -$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $totalWorkers, $register, $swooleAdapter) { +$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $register, $swooleAdapter) { global $container; $pools = $register->get('pools'); /** @var Group $pools */ diff --git a/composer.lock b/composer.lock index dbeb3c15a9..76425881ca 100644 --- a/composer.lock +++ b/composer.lock @@ -4307,12 +4307,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "dbdb33f73598949615f159dc612196ef26e57a32" + "reference": "af22ce593c05f7c7210eda7d3df2cf8054a1bb4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/dbdb33f73598949615f159dc612196ef26e57a32", - "reference": "dbdb33f73598949615f159dc612196ef26e57a32", + "url": "https://api.github.com/repos/utopia-php/http/zipball/af22ce593c05f7c7210eda7d3df2cf8054a1bb4b", + "reference": "af22ce593c05f7c7210eda7d3df2cf8054a1bb4b", "shasum": "" }, "require": { @@ -4352,7 +4352,7 @@ "issues": "https://github.com/utopia-php/http/issues", "source": "https://github.com/utopia-php/http/tree/feat/coroutines-option" }, - "time": "2026-03-17T08:55:56+00:00" + "time": "2026-03-17T10:46:12+00:00" }, { "name": "utopia-php/image", From 0564936c4f8391b4690de43f3097989af3044fc2 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 16:29:21 +0530 Subject: [PATCH 033/118] update file name --- app/init/resources.php | 2 +- app/init/{resources.request.php => resources/request.php} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/init/{resources.request.php => resources/request.php} (100%) diff --git a/app/init/resources.php b/app/init/resources.php index 019e43cd7c..2b834a8396 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -97,7 +97,7 @@ $container->set('platform', function () { return Config::getParam('platform', []); }, []); -require_once __DIR__ . '/resources.request.php'; +require_once __DIR__ . '/resources/request.php'; $container->set('store', function (): Store { diff --git a/app/init/resources.request.php b/app/init/resources/request.php similarity index 100% rename from app/init/resources.request.php rename to app/init/resources/request.php From 5ba19ec76329da89f4d3c0b1227a5b074076b684 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 17:14:35 +0530 Subject: [PATCH 034/118] fix alias --- composer.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.lock b/composer.lock index 76425881ca..0faae8d9a1 100644 --- a/composer.lock +++ b/composer.lock @@ -4307,12 +4307,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "af22ce593c05f7c7210eda7d3df2cf8054a1bb4b" + "reference": "27c6fcbf3d10932ea20a067e8476e82cf5a6afb4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/af22ce593c05f7c7210eda7d3df2cf8054a1bb4b", - "reference": "af22ce593c05f7c7210eda7d3df2cf8054a1bb4b", + "url": "https://api.github.com/repos/utopia-php/http/zipball/27c6fcbf3d10932ea20a067e8476e82cf5a6afb4", + "reference": "27c6fcbf3d10932ea20a067e8476e82cf5a6afb4", "shasum": "" }, "require": { @@ -4352,7 +4352,7 @@ "issues": "https://github.com/utopia-php/http/issues", "source": "https://github.com/utopia-php/http/tree/feat/coroutines-option" }, - "time": "2026-03-17T10:46:12+00:00" + "time": "2026-03-17T11:42:30+00:00" }, { "name": "utopia-php/image", From 14a9aa890fe7266442417faed05de35e3c9c430b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 17:18:57 +0530 Subject: [PATCH 035/118] fix issues --- phpstan-baseline.neon | 80 +++++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a5e90748c2..e5dd78525e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,11 +1,5 @@ parameters: ignoreErrors: - - - message: '#^Variable \$dbForPlatform might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: app/cli.php - - message: '#^PHPDoc tag @var above a method has no effect\.$#' identifier: varTag.misplaced @@ -84,6 +78,12 @@ parameters: count: 1 path: app/controllers/general.php + - + message: '#^Unknown parameter \$override in call to method Utopia\\Http\\Response\:\:addHeader\(\)\.$#' + identifier: argument.unknown + count: 1 + path: app/controllers/general.php + - message: '#^Variable \$body on left side of \?\? always exists and is not nullable\.$#' identifier: nullCoalesce.variable @@ -127,9 +127,21 @@ parameters: path: app/controllers/shared/api.php - - message: '#^Variable \$database might not be defined\.$#' - identifier: variable.undefined - count: 4 + message: '#^Anonymous function has an unused use \$register\.$#' + identifier: closure.unusedUse + count: 1 + path: app/http.php + + - + message: '#^Call to an undefined method Utopia\\Http\\Adapter\\Swoole\\Server\:\:bind\(\)\.$#' + identifier: method.notFound + count: 1 + path: app/http.php + + - + message: '#^Call to an undefined method Utopia\\Http\\Adapter\\Swoole\\Server\:\:getWorkerStatus\(\)\.$#' + identifier: method.notFound + count: 3 path: app/http.php - @@ -178,7 +190,7 @@ parameters: message: '#^Variable \$hostname on left side of \?\? always exists and is not nullable\.$#' identifier: nullCoalesce.variable count: 1 - path: app/init/resources.php + path: app/init/resources/request.php - message: '#^Anonymous function has an unused use \$register\.$#' @@ -330,6 +342,18 @@ parameters: count: 5 path: src/Appwrite/GraphQL/Resolvers.php + - + message: '#^Method Utopia\\Http\\Http\:\:execute\(\) invoked with 3 parameters, 2 required\.$#' + identifier: arguments.count + count: 1 + path: src/Appwrite/GraphQL/Resolvers.php + + - + message: '#^Method Utopia\\Http\\Http\:\:getResource\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 8 + path: src/Appwrite/GraphQL/Resolvers.php + - message: '#^Variable \$request in PHPDoc tag @var does not exist\.$#' identifier: varTag.variableNotFound @@ -480,6 +504,18 @@ parameters: count: 1 path: src/Appwrite/Platform/Action.php + - + message: '#^Class Utopia\\Http\\Http constructor invoked with 1 parameter, 2 required\.$#' + identifier: arguments.count + count: 1 + path: src/Appwrite/Platform/Installer/Server.php + + - + message: '#^Static call to instance method Utopia\\Http\\Http\:\:setResource\(\)\.$#' + identifier: method.staticCall + count: 4 + path: src/Appwrite/Platform/Installer/Server.php + - message: '#^Variable \$output in empty\(\) always exists and is not falsy\.$#' identifier: empty.variable @@ -714,18 +750,6 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php - - - message: '#^Undefined variable\: \$cpus$#' - identifier: variable.undefined - count: 3 - path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php - - - - message: '#^Undefined variable\: \$memory$#' - identifier: variable.undefined - count: 3 - path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php - - message: '#^Variable \$deployment might not be defined\.$#' identifier: variable.undefined @@ -846,6 +870,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php + - + message: '#^Variable \$previewRuleId in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php + - message: '#^Variable \$previewUrl in empty\(\) always exists and is not falsy\.$#' identifier: empty.variable @@ -894,6 +924,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php + - + message: '#^Variable \$previewRuleId in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php + - message: '#^Variable \$previewUrl in empty\(\) always exists and is not falsy\.$#' identifier: empty.variable From cdb301a2935d17a20b2404e39b0c57a8b9125fab Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 17:30:42 +0530 Subject: [PATCH 036/118] fix PHPStan errors without regenerating baseline - Fix dispatch() type hint to use \Swoole\Http\Server instead of Utopia adapter - Remove unused $register from go() closure in http.php - Remove unnecessary ?? '' on non-nullable $hostname - Remove unsupported override: param from addHeader() call - Update Resolvers.php for new getResource()/execute() signatures - Migrate Installer/Server.php from static Http::setResource() to container - Remove stale baseline entries, add 1 for pre-existing Deployment.php issue --- app/controllers/general.php | 5 +- app/http.php | 8 +-- app/init/resources/request.php | 2 +- phpstan-baseline.neon | 72 ++-------------------- src/Appwrite/GraphQL/Resolvers.php | 38 ++++++------ src/Appwrite/Platform/Installer/Server.php | 32 +++++----- 6 files changed, 47 insertions(+), 110 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 1a099c4bde..511e124ea9 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -748,11 +748,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S } if (\is_array($values)) { - $count = 0; foreach ($values as $value) { - $override = $count === 0; - $response->addHeader($name, $value, override: $override); - $count++; + $response->addHeader($name, $value); } } else { $response->addHeader($name, $values); diff --git a/app/http.php b/app/http.php index c81f49bfd1..ae63ea4f6b 100644 --- a/app/http.php +++ b/app/http.php @@ -80,16 +80,16 @@ $http = $swooleAdapter->getServer(); * riskier tasks to a dedicated worker subset. Prefers idle workers, with fallback to random selection if necessary. * doc: https://openswoole.com/docs/modules/swoole-server/configuration#dispatch_func * - * @param Server $server Swoole server instance. + * @param \Swoole\Http\Server $server Swoole server instance. * @param int $fd client ID * @param int $type the type of data and its current state * @param string|null $data Request content for categorization. * @global int $totalThreads Total number of workers. * @return int Chosen worker ID for the request. */ -function dispatch(Server $server, int $fd, int $type, $data = null): int +function dispatch(\Swoole\Http\Server $server, int $fd, int $type, $data = null): int { - $resolveWorkerId = function (Server $server, $data = null) { + $resolveWorkerId = function (\Swoole\Http\Server $server, $data = null) { global $totalWorkers, $riskyDomains; // If data is not set we can send request to any worker @@ -294,7 +294,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke $app = new Http($swooleAdapter, 'UTC'); - go(function () use ($register, $app, $pools) { + go(function () use ($app, $pools) { /** @var array $collections */ $collections = Config::getParam('collections', []); diff --git a/app/init/resources/request.php b/app/init/resources/request.php index e2e48196d5..24605097be 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -684,7 +684,7 @@ function registerRequestResources(Container $container): void $cacheKey = \sprintf( '%s-cache-%s:%s:%s:project:%s:functions:events', $dbForProject->getCacheName(), - $hostname ?? '', + $hostname, $dbForProject->getNamespace(), $dbForProject->getTenant(), $project->getId() diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e5dd78525e..73f5639776 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,5 +1,11 @@ parameters: ignoreErrors: + - + message: '#^Variable \$previewRuleId in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 2 + path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php + - message: '#^PHPDoc tag @var above a method has no effect\.$#' identifier: varTag.misplaced @@ -78,12 +84,6 @@ parameters: count: 1 path: app/controllers/general.php - - - message: '#^Unknown parameter \$override in call to method Utopia\\Http\\Response\:\:addHeader\(\)\.$#' - identifier: argument.unknown - count: 1 - path: app/controllers/general.php - - message: '#^Variable \$body on left side of \?\? always exists and is not nullable\.$#' identifier: nullCoalesce.variable @@ -126,24 +126,6 @@ parameters: count: 1 path: app/controllers/shared/api.php - - - message: '#^Anonymous function has an unused use \$register\.$#' - identifier: closure.unusedUse - count: 1 - path: app/http.php - - - - message: '#^Call to an undefined method Utopia\\Http\\Adapter\\Swoole\\Server\:\:bind\(\)\.$#' - identifier: method.notFound - count: 1 - path: app/http.php - - - - message: '#^Call to an undefined method Utopia\\Http\\Adapter\\Swoole\\Server\:\:getWorkerStatus\(\)\.$#' - identifier: method.notFound - count: 3 - path: app/http.php - - message: '#^Variable \$register might not be defined\.$#' identifier: variable.undefined @@ -186,12 +168,6 @@ parameters: count: 1 path: app/init/registers.php - - - message: '#^Variable \$hostname on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: app/init/resources/request.php - - message: '#^Anonymous function has an unused use \$register\.$#' identifier: closure.unusedUse @@ -342,18 +318,6 @@ parameters: count: 5 path: src/Appwrite/GraphQL/Resolvers.php - - - message: '#^Method Utopia\\Http\\Http\:\:execute\(\) invoked with 3 parameters, 2 required\.$#' - identifier: arguments.count - count: 1 - path: src/Appwrite/GraphQL/Resolvers.php - - - - message: '#^Method Utopia\\Http\\Http\:\:getResource\(\) invoked with 2 parameters, 1 required\.$#' - identifier: arguments.count - count: 8 - path: src/Appwrite/GraphQL/Resolvers.php - - message: '#^Variable \$request in PHPDoc tag @var does not exist\.$#' identifier: varTag.variableNotFound @@ -504,18 +468,6 @@ parameters: count: 1 path: src/Appwrite/Platform/Action.php - - - message: '#^Class Utopia\\Http\\Http constructor invoked with 1 parameter, 2 required\.$#' - identifier: arguments.count - count: 1 - path: src/Appwrite/Platform/Installer/Server.php - - - - message: '#^Static call to instance method Utopia\\Http\\Http\:\:setResource\(\)\.$#' - identifier: method.staticCall - count: 4 - path: src/Appwrite/Platform/Installer/Server.php - - message: '#^Variable \$output in empty\(\) always exists and is not falsy\.$#' identifier: empty.variable @@ -870,12 +822,6 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php - - - message: '#^Variable \$previewRuleId in empty\(\) always exists and is not falsy\.$#' - identifier: empty.variable - count: 1 - path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php - - message: '#^Variable \$previewUrl in empty\(\) always exists and is not falsy\.$#' identifier: empty.variable @@ -924,12 +870,6 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php - - - message: '#^Variable \$previewRuleId in empty\(\) always exists and is not falsy\.$#' - identifier: empty.variable - count: 1 - path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php - - message: '#^Variable \$previewUrl in empty\(\) always exists and is not falsy\.$#' identifier: empty.variable diff --git a/src/Appwrite/GraphQL/Resolvers.php b/src/Appwrite/GraphQL/Resolvers.php index 484cafb0ab..afecd7c0f1 100644 --- a/src/Appwrite/GraphQL/Resolvers.php +++ b/src/Appwrite/GraphQL/Resolvers.php @@ -30,9 +30,9 @@ class Resolvers /** @var Response $response */ /** @var Request $request */ - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); $path = $route->getPath(); foreach ($args as $key => $value) { @@ -97,9 +97,9 @@ class Resolvers ): callable { return static fn ($type, $args, $context, $info) => new Swoole( function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); $request->setMethod('GET'); $request->setURI($url($databaseId, $collectionId, $args)); @@ -128,9 +128,9 @@ class Resolvers ): callable { return static fn ($type, $args, $context, $info) => new Swoole( function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); $request->setMethod('GET'); $request->setURI($url($databaseId, $collectionId, $args)); @@ -164,9 +164,9 @@ class Resolvers ): callable { return static fn ($type, $args, $context, $info) => new Swoole( function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); $request->setMethod('POST'); $request->setURI($url($databaseId, $collectionId, $args)); @@ -196,9 +196,9 @@ class Resolvers ): callable { return static fn ($type, $args, $context, $info) => new Swoole( function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); $request->setMethod('PATCH'); $request->setURI($url($databaseId, $collectionId, $args)); @@ -226,9 +226,9 @@ class Resolvers ): callable { return static fn ($type, $args, $context, $info) => new Swoole( function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); $request->setMethod('DELETE'); $request->setURI($url($databaseId, $collectionId, $args)); @@ -270,7 +270,7 @@ class Resolvers try { $route = $utopia->match($request, fresh: true); - $utopia->execute($route, $request, $response); + $utopia->execute($route, $request); } catch (\Throwable $e) { if ($beforeReject) { $e = $beforeReject($e); diff --git a/src/Appwrite/Platform/Installer/Server.php b/src/Appwrite/Platform/Installer/Server.php index 5c07b416a7..70aab77b96 100644 --- a/src/Appwrite/Platform/Installer/Server.php +++ b/src/Appwrite/Platform/Installer/Server.php @@ -138,9 +138,20 @@ class Server $paths = $this->paths; $state = $this->state; - Http::setResource('installerState', fn () => $state); - Http::setResource('installerConfig', fn () => $config); - Http::setResource('installerPaths', fn () => $paths); + $adapter = new class ($host, $port, ['worker_num' => 1]) extends SwooleAdapter { + public function getNativeServer(): SwooleServer + { + return $this->server; + } + }; + + $nativeServer = $adapter->getNativeServer(); + + $container = $adapter->getContainer(); + $container->set('installerState', fn () => $state); + $container->set('installerConfig', fn () => $config); + $container->set('installerPaths', fn () => $paths); + $container->set('swooleServer', fn () => $nativeServer); // Register routes via Utopia Platform $platform = new Installer(); @@ -153,17 +164,6 @@ class Server ->inject('response') ->action($errorHandler->action(...)); - $adapter = new class ($host, $port, ['worker_num' => 1]) extends SwooleAdapter { - public function getNativeServer(): SwooleServer - { - return $this->server; - } - }; - - $nativeServer = $adapter->getNativeServer(); - - Http::setResource('swooleServer', fn () => $nativeServer); - $nativeServer->on('start', function () use ($nativeServer, $port, $readyFile) { \Swoole\Process::signal(SIGTERM, fn () => $nativeServer->shutdown()); \Swoole\Process::signal(SIGINT, fn () => $nativeServer->shutdown()); @@ -173,7 +173,7 @@ class Server } }); - $adapter->onRequest(function (Request $request, Response $response) use ($files) { + $adapter->onRequest(function (Request $request, Response $response) use ($adapter, $files) { // Serve static files from memory $uri = $request->getURI(); if ($files->isFileLoaded($uri)) { @@ -183,7 +183,7 @@ class Server return; } - $app = new Http('UTC'); + $app = new Http($adapter, 'UTC'); $app->run($request, $response); }); From 60939da80124fa0a03bd2938d2dd5b4d52346af1 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 17 Mar 2026 21:39:50 +0530 Subject: [PATCH 037/118] fix graphql --- app/init/resources/request.php | 4 ++++ src/Appwrite/GraphQL/Schema.php | 4 ---- src/Appwrite/Promises/Swoole.php | 13 ++++++++++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/init/resources/request.php b/app/init/resources/request.php index 24605097be..e21ee0e5ae 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -65,6 +65,10 @@ use Utopia\Validator\WhiteList; */ function registerRequestResources(Container $container): void { + $container->set('utopia:graphql', function ($utopia) { + return $utopia; + }, ['utopia']); + $container->set('log', fn () => new Log(), []); $container->set('logger', function ($register) { diff --git a/src/Appwrite/GraphQL/Schema.php b/src/Appwrite/GraphQL/Schema.php index 5446230bd6..d885b55522 100644 --- a/src/Appwrite/GraphQL/Schema.php +++ b/src/Appwrite/GraphQL/Schema.php @@ -32,10 +32,6 @@ class Schema array $urls, array $params, ): GQLSchema { - $utopia->setResource('utopia:graphql', static function () use ($utopia) { - return $utopia; - }); - if (!empty(self::$schema)) { return self::$schema; } diff --git a/src/Appwrite/Promises/Swoole.php b/src/Appwrite/Promises/Swoole.php index c258ef6a5e..9c06fbda2f 100644 --- a/src/Appwrite/Promises/Swoole.php +++ b/src/Appwrite/Promises/Swoole.php @@ -2,10 +2,14 @@ namespace Appwrite\Promises; +use Swoole\Coroutine; use Swoole\Coroutine\Channel; +use Utopia\DI\Container; class Swoole extends Promise { + private const REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container'; + public function __construct(?callable $executor = null) { parent::__construct($executor); @@ -16,7 +20,14 @@ class Swoole extends Promise callable $resolve, callable $reject ): void { - \go(function () use ($executor, $resolve, $reject) { + $parentContainer = (Coroutine::getCid() !== -1) + ? (Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? null) + : null; + + \go(function () use ($executor, $resolve, $reject, $parentContainer) { + if ($parentContainer !== null) { + Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] = new Container($parentContainer); + } try { $executor($resolve, $reject); } catch (\Throwable $exception) { From d04ac5c3e6f658c7b5bff33ba67bb40f9cecd3f8 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 18 Mar 2026 09:29:45 +0530 Subject: [PATCH 038/118] fix pool issue --- composer.lock | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/composer.lock b/composer.lock index 0faae8d9a1..e17cd2b38d 100644 --- a/composer.lock +++ b/composer.lock @@ -3934,7 +3934,7 @@ "utopia" ], "support": { - "source": "https://github.com/utopia-php/database/tree/main", + "source": "https://github.com/utopia-php/database/tree/5.3.15", "issues": "https://github.com/utopia-php/database/issues" }, "time": "2026-03-16T11:41:45+00:00" @@ -4307,12 +4307,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "27c6fcbf3d10932ea20a067e8476e82cf5a6afb4" + "reference": "2f1b5ab33e56736ee9f061b0438cc7eede5b3acf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/27c6fcbf3d10932ea20a067e8476e82cf5a6afb4", - "reference": "27c6fcbf3d10932ea20a067e8476e82cf5a6afb4", + "url": "https://api.github.com/repos/utopia-php/http/zipball/2f1b5ab33e56736ee9f061b0438cc7eede5b3acf", + "reference": "2f1b5ab33e56736ee9f061b0438cc7eede5b3acf", "shasum": "" }, "require": { @@ -4352,7 +4352,7 @@ "issues": "https://github.com/utopia-php/http/issues", "source": "https://github.com/utopia-php/http/tree/feat/coroutines-option" }, - "time": "2026-03-17T11:42:30+00:00" + "time": "2026-03-18T04:09:48+00:00" }, { "name": "utopia-php/image", @@ -4612,16 +4612,16 @@ }, { "name": "utopia-php/mongo", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/utopia-php/mongo.git", - "reference": "83dbcde768d5fb40241f5ca8aa5ed8ca140a7469" + "reference": "677a21c53f7a1316c528b4b45b3fce886cee7223" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/mongo/zipball/83dbcde768d5fb40241f5ca8aa5ed8ca140a7469", - "reference": "83dbcde768d5fb40241f5ca8aa5ed8ca140a7469", + "url": "https://api.github.com/repos/utopia-php/mongo/zipball/677a21c53f7a1316c528b4b45b3fce886cee7223", + "reference": "677a21c53f7a1316c528b4b45b3fce886cee7223", "shasum": "" }, "require": { @@ -4667,9 +4667,9 @@ ], "support": { "issues": "https://github.com/utopia-php/mongo/issues", - "source": "https://github.com/utopia-php/mongo/tree/1.0.1" + "source": "https://github.com/utopia-php/mongo/tree/1.0.2" }, - "time": "2026-03-13T07:29:24+00:00" + "time": "2026-03-18T02:45:50+00:00" }, { "name": "utopia-php/platform", @@ -6234,11 +6234,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.41", + "version": "2.1.42", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a2eae8f20856b3afe74bf1f9726ce8c11438e300", - "reference": "a2eae8f20856b3afe74bf1f9726ce8c11438e300", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1279e1ce86ba768f0780c9d889852b4e02ff40d0", + "reference": "1279e1ce86ba768f0780c9d889852b4e02ff40d0", "shasum": "" }, "require": { @@ -6283,7 +6283,7 @@ "type": "github" } ], - "time": "2026-03-16T18:24:10+00:00" + "time": "2026-03-17T14:58:32+00:00" }, { "name": "phpunit/php-code-coverage", From d177e3adfc940e86acbd4e10c4e0f1d1736aad8f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 18 Mar 2026 10:06:53 +0530 Subject: [PATCH 039/118] fix pool issue --- app/init/registers.php | 15 ++------------- docker-compose.yml | 4 ++-- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/app/init/registers.php b/app/init/registers.php index 7b68c2af9a..ab24c847a6 100644 --- a/app/init/registers.php +++ b/app/init/registers.php @@ -217,19 +217,8 @@ $register->set('pools', function () { $maxConnections = System::getEnv('_APP_CONNECTIONS_MAX', 151); $instanceConnections = $maxConnections / System::getEnv('_APP_POOL_CLIENTS', 14); - $multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled'; - - if ($multiprocessing) { - $workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6)); - } else { - $workerCount = 1; - } - - if ($workerCount > $instanceConnections) { - throw new \Exception('Pool size is too small. Increase the number of allowed database connections or decrease the number of workers.', 500); - } - - $poolSize = (int)($instanceConnections / $workerCount); + $workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6)); + $poolSize = max(1, (int)($instanceConnections / $workerCount)); foreach ($connections as $key => $connection) { $type = $connection['type'] ?? ''; diff --git a/docker-compose.yml b/docker-compose.yml index 7d64dfa867..e93146c779 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1244,7 +1244,7 @@ services: - MYSQL_USER=${_APP_DB_USER} - MYSQL_PASSWORD=${_APP_DB_PASS} - MARIADB_AUTO_UPGRADE=1 - command: "mysqld --innodb-flush-method=fsync" + command: "mysqld --innodb-flush-method=fsync --max-connections=500" healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 5s @@ -1308,7 +1308,7 @@ services: - POSTGRES_DB=${_APP_DB_SCHEMA} - POSTGRES_USER=${_APP_DB_USER} - POSTGRES_PASSWORD=${_APP_DB_PASS} - command: "postgres" + command: "postgres -N 500" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER}"] interval: 5s From df7796d5cb39d77e68b88b048fceb748a94a8986 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 18 Mar 2026 14:19:43 +0530 Subject: [PATCH 040/118] restore phpunit --- phpunit.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/phpunit.xml b/phpunit.xml index c2ffe21b81..030d89af8d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -17,6 +17,7 @@ ./tests/unit + ./tests/e2e/Client.php ./tests/e2e/General ./tests/e2e/Scopes ./tests/e2e/Services/Teams @@ -35,6 +36,7 @@ ./tests/e2e/Services/Webhooks ./tests/e2e/Services/Messaging ./tests/e2e/Services/Migrations + ./tests/e2e/Services/Functions/FunctionsBase.php ./tests/e2e/Services/Functions/FunctionsCustomServerTest.php ./tests/e2e/Services/Functions/FunctionsCustomClientTest.php From 03b2755aaf14005d383ea891d4ad5facde8bbe5f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 18 Mar 2026 16:43:14 +0530 Subject: [PATCH 041/118] fix async execution --- composer.json | 6 +----- composer.lock | 20 ++++++-------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/composer.json b/composer.json index f178b6ae87..382f1b52fc 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,7 @@ "utopia-php/compression": "0.1.*", "utopia-php/config": "1.*", "utopia-php/console": "0.1.*", - "utopia-php/database": "dev-main as 5.3.15", + "utopia-php/database": "5.3.*", "utopia-php/detector": "0.2.*", "utopia-php/domains": "1.*", "utopia-php/emails": "0.6.*", @@ -100,10 +100,6 @@ { "type": "vcs", "url": "https://github.com/utopia-php/database" - }, - { - "type": "vcs", - "url": "https://github.com/utopia-php/http" } ], "require-dev": { diff --git a/composer.lock b/composer.lock index e17cd2b38d..975368629d 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": "1c156b2a11a5abb568b44d880d1ddec7", + "content-hash": "6efdf0d212038b3d65b12cf693276936", "packages": [ { "name": "adhocore/jwt", @@ -3850,7 +3850,7 @@ }, { "name": "utopia-php/database", - "version": "dev-main", + "version": "5.3.15", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", @@ -3883,7 +3883,6 @@ "swoole/ide-helper": "5.1.3", "utopia-php/cli": "0.22.*" }, - "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -4307,12 +4306,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "2f1b5ab33e56736ee9f061b0438cc7eede5b3acf" + "reference": "8420a83e4e21f606da34897a07589db247b90acf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/2f1b5ab33e56736ee9f061b0438cc7eede5b3acf", - "reference": "2f1b5ab33e56736ee9f061b0438cc7eede5b3acf", + "url": "https://api.github.com/repos/utopia-php/http/zipball/8420a83e4e21f606da34897a07589db247b90acf", + "reference": "8420a83e4e21f606da34897a07589db247b90acf", "shasum": "" }, "require": { @@ -4352,7 +4351,7 @@ "issues": "https://github.com/utopia-php/http/issues", "source": "https://github.com/utopia-php/http/tree/feat/coroutines-option" }, - "time": "2026-03-18T04:09:48+00:00" + "time": "2026-03-18T11:08:21+00:00" }, { "name": "utopia-php/image", @@ -8473,12 +8472,6 @@ } ], "aliases": [ - { - "package": "utopia-php/database", - "version": "dev-main", - "alias": "5.3.15", - "alias_normalized": "5.3.15.0" - }, { "package": "utopia-php/framework", "version": "dev-feat/coroutines-option", @@ -8488,7 +8481,6 @@ ], "minimum-stability": "dev", "stability-flags": { - "utopia-php/database": 20, "utopia-php/framework": 20 }, "prefer-stable": true, From ea11af1e1501e03b93a344f9f2602a9c78c830df Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 18 Mar 2026 17:44:03 +0530 Subject: [PATCH 042/118] chore: worker changes --- app/init/worker/job.php | 382 +++++++++++++++++++++++++++++ app/worker.php | 516 ++++------------------------------------ composer.json | 2 +- composer.lock | 24 +- 4 files changed, 440 insertions(+), 484 deletions(-) create mode 100644 app/init/worker/job.php diff --git a/app/init/worker/job.php b/app/init/worker/job.php new file mode 100644 index 0000000000..a58d0c21ef --- /dev/null +++ b/app/init/worker/job.php @@ -0,0 +1,382 @@ +set('log', fn () => new Log(), []); + + $container->set('usage', fn () => new Context(), []); + + $container->set('authorization', function () { + $authorization = new Authorization(); + $authorization->disable(); + + return $authorization; + }, []); + + $container->set('dbForPlatform', function (Cache $cache, Group $pools, Authorization $authorization) { + $adapter = new DatabasePool($pools->get('console')); + $dbForPlatform = new Database($adapter, $cache); + + $dbForPlatform + ->setDatabase(APP_DATABASE) + ->setAuthorization($authorization) + ->setNamespace('_console') + ->setDocumentType('users', User::class); + + return $dbForPlatform; + }, ['cache', 'pools', 'authorization']); + + $container->set('project', function ($message, Database $dbForPlatform) { + $payload = $message->getPayload() ?? []; + $project = new Document($payload['project'] ?? []); + + if ($project->isEmpty() || $project->getId() === 'console') { + return $project; + } + + return $dbForPlatform->getDocument('projects', $project->getId()); + }, ['message', 'dbForPlatform']); + + $container->set('dbForProject', function (Cache $cache, Group $pools, Document $project, Database $dbForPlatform, Authorization $authorization) { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForPlatform; + } + + try { + $dsn = new DSN($project->getAttribute('database')); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $project->getAttribute('database')); + } + + $adapter = new DatabasePool($pools->get($dsn->getHost())); + $database = new Database($adapter, $cache); + $database->setDocumentType('users', User::class); + + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + + if (\in_array($dsn->getHost(), $sharedTables)) { + $database + ->setSharedTables(true) + ->setTenant($project->getSequence()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getSequence()); + } + + $database + ->setDatabase(APP_DATABASE) + ->setAuthorization($authorization) + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER); + + return $database; + }, ['cache', 'pools', 'project', 'dbForPlatform', 'authorization']); + + $container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) { + $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools + + return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForPlatform; + } + + try { + $dsn = new DSN($project->getAttribute('database')); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $project->getAttribute('database')); + } + + if (isset($databases[$dsn->getHost()])) { + $database = $databases[$dsn->getHost()]; + $database->setAuthorization($authorization); + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + + if (\in_array($dsn->getHost(), $sharedTables)) { + $database + ->setSharedTables(true) + ->setTenant($project->getSequence()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getSequence()); + } + + return $database; + } + + $adapter = new DatabasePool($pools->get($dsn->getHost())); + $database = new Database($adapter, $cache); + + $databases[$dsn->getHost()] = $database; + + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + + if (\in_array($dsn->getHost(), $sharedTables)) { + $database + ->setSharedTables(true) + ->setTenant($project->getSequence()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getSequence()); + } + + $database + ->setDatabase(APP_DATABASE) + ->setAuthorization($authorization) + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER); + + return $database; + }; + }, ['pools', 'dbForPlatform', 'cache', 'authorization']); + + $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { + $database = null; + + return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) { + if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') { + $database->setTenant($project->getSequence()); + + return $database; + } + + $adapter = new DatabasePool($pools->get('logs')); + $database = new Database($adapter, $cache); + + $database + ->setDatabase(APP_DATABASE) + ->setAuthorization($authorization) + ->setSharedTables(true) + ->setNamespace('logsV1') + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER) + ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER); + + if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') { + $database->setTenant($project->getSequence()); + } + + return $database; + }; + }, ['pools', 'cache', 'authorization']); + + $container->set('abuseRetention', function () { + return \time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day + }, []); + + $container->set('auditRetention', function (Document $project) { + if ($project->getId() === 'console') { + return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months + } + + return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days + }, ['project']); + + $container->set('executionRetention', function () { + return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days + }, []); + + $container->set('queueForDatabase', function (Publisher $publisher) { + return new EventDatabase($publisher); + }, ['publisher']); + + $container->set('queueForMessaging', function (Publisher $publisher) { + return new Messaging($publisher); + }, ['publisher']); + + $container->set('queueForMails', function (Publisher $publisher) { + return new Mail($publisher); + }, ['publisher']); + + $container->set('queueForBuilds', function (Publisher $publisher) { + return new Build($publisher); + }, ['publisher']); + + $container->set('queueForScreenshots', function (Publisher $publisher) { + return new Screenshot($publisher); + }, ['publisher']); + + $container->set('queueForDeletes', function (Publisher $publisher) { + return new Delete($publisher); + }, ['publisher']); + + $container->set('queueForEvents', function (Publisher $publisher) { + return new Event($publisher); + }, ['publisher']); + + $container->set('queueForAudits', function (Publisher $publisher) { + return new Audit($publisher); + }, ['publisher']); + + $container->set('queueForWebhooks', function (Publisher $publisher) { + return new Webhook($publisher); + }, ['publisher']); + + $container->set('queueForFunctions', function (Publisher $publisher) { + return new Func($publisher); + }, ['publisher']); + + $container->set('queueForRealtime', function () { + return new Realtime(); + }, []); + + $container->set('queueForCertificates', function (Publisher $publisher) { + return new Certificate($publisher); + }, ['publisher']); + + $container->set('queueForMigrations', function (Publisher $publisher) { + return new Migration($publisher); + }, ['publisher']); + + $container->set('deviceForSites', function (Document $project, Telemetry $telemetry) { + return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())); + }, ['project', 'telemetry']); + + $container->set('deviceForMigrations', function (Document $project, Telemetry $telemetry) { + return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())); + }, ['project', 'telemetry']); + + $container->set('deviceForFunctions', function (Document $project, Telemetry $telemetry) { + return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())); + }, ['project', 'telemetry']); + + $container->set('deviceForFiles', function (Document $project, Telemetry $telemetry) { + return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); + }, ['project', 'telemetry']); + + $container->set('deviceForBuilds', function (Document $project, Telemetry $telemetry) { + return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); + }, ['project', 'telemetry']); + + $container->set('deviceForCache', function (Document $project, Telemetry $telemetry) { + return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId())); + }, ['project', 'telemetry']); + + $container->set('logError', function (Registry $register, Document $project) { + return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $project) { + $logger = $register->get('logger'); + + if ($logger) { + $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); + + $log = new Log(); + $log->setNamespace($namespace); + $log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname())); + $log->setVersion($version); + $log->setType(Log::TYPE_ERROR); + $log->setMessage($error->getMessage()); + + $log->addTag('code', $error->getCode()); + $log->addTag('verboseType', \get_class($error)); + $log->addTag('projectId', $project->getId() ?? ''); + + $log->addExtra('file', $error->getFile()); + $log->addExtra('line', $error->getLine()); + $log->addExtra('trace', $error->getTraceAsString()); + + if ($error->getPrevious() !== null) { + if ($error->getPrevious()->getMessage() != $error->getMessage()) { + $log->addExtra('previousMessage', $error->getPrevious()->getMessage()); + } + $log->addExtra('previousFile', $error->getPrevious()->getFile()); + $log->addExtra('previousLine', $error->getPrevious()->getLine()); + } + + foreach (($extras ?? []) as $key => $value) { + $log->addExtra($key, $value); + } + + $log->setAction($action); + + $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; + $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); + + try { + $responseCode = $logger->addLog($log); + Console::info('Error log pushed with status code: ' . $responseCode); + } catch (Throwable $th) { + Console::error('Error pushing log: ' . $th->getMessage()); + } + } + + Console::warning("Failed: {$error->getMessage()}"); + Console::warning($error->getTraceAsString()); + + if ($error->getPrevious() !== null) { + if ($error->getPrevious()->getMessage() != $error->getMessage()) { + Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}"); + } + Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}"); + } + }; + }, ['register', 'project']); + + $container->set('getAudit', function (Database $dbForPlatform, callable $getProjectDB) { + return function (Document $project) use ($dbForPlatform, $getProjectDB) { + if ($project->isEmpty() || $project->getId() === 'console') { + $adapter = new AdapterDatabase($dbForPlatform); + + return new UtopiaAudit($adapter); + } + + $dbForProject = $getProjectDB($project); + $adapter = new AdapterDatabase($dbForProject); + + return new UtopiaAudit($adapter); + }; + }, ['dbForPlatform', 'getProjectDB']); + + $container->set('executionsRetentionCount', function (Document $project, array $plan) { + if ($project->getId() === 'console' || empty($plan)) { + return 0; + } + + return (int) ($plan['executionsRetentionCount'] ?? 100); + }, ['project', 'plan']); +} diff --git a/app/worker.php b/app/worker.php index 840231f16c..6500a6a570 100644 --- a/app/worker.php +++ b/app/worker.php @@ -1,509 +1,67 @@ $register); +global $container; +$container->set('pools', function ($register) { + return $register->get('pools'); +}, ['register']); -Server::setResource('authorization', function () { +$container->set('authorization', function () { $authorization = new Authorization(); $authorization->disable(); return $authorization; }, []); -Server::setResource('dbForPlatform', function (Cache $cache, Registry $register, Authorization $authorization) { - $pools = $register->get('pools'); - $adapter = new DatabasePool($pools->get('console')); - $dbForPlatform = new Database($adapter, $cache); +$container->set('project', fn () => new Document([]), []); - $dbForPlatform - ->setDatabase(APP_DATABASE) - ->setAuthorization($authorization) - ->setNamespace('_console') - ->setDocumentType('users', User::class); +$container->set('log', fn () => new Log(), []); - return $dbForPlatform; -}, ['cache', 'register', 'authorization']); - -Server::setResource('project', function (Message $message, Database $dbForPlatform) { - $payload = $message->getPayload() ?? []; - $project = new Document($payload['project'] ?? []); - - if ($project->getId() === 'console') { - return $project; - } - - return $dbForPlatform->getDocument('projects', $project->getId()); -}, ['message', 'dbForPlatform']); - -Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform, Authorization $authorization) { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForPlatform; - } - - $pools = $register->get('pools'); - - try { - $dsn = new DSN($project->getAttribute('database')); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $project->getAttribute('database')); - } - - $adapter = new DatabasePool($pools->get($dsn->getHost())); - $database = new Database($adapter, $cache); - $database->setDocumentType('users', User::class); - - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - - if (\in_array($dsn->getHost(), $sharedTables)) { - $database - ->setSharedTables(true) - ->setTenant($project->getSequence()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getSequence()); - } - - $database - ->setDatabase(APP_DATABASE) - ->setAuthorization($authorization) - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER); - - return $database; -}, ['cache', 'register', 'message', 'project', 'dbForPlatform', 'authorization']); - -Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) { - $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools - - return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForPlatform; - } - - try { - $dsn = new DSN($project->getAttribute('database')); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $project->getAttribute('database')); - } - - if (isset($databases[$dsn->getHost()])) { - $database = $databases[$dsn->getHost()]; - $database->setAuthorization($authorization); - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - - if (\in_array($dsn->getHost(), $sharedTables)) { - $database - ->setSharedTables(true) - ->setTenant($project->getSequence()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getSequence()); - } - - return $database; - } - - $adapter = new DatabasePool($pools->get($dsn->getHost())); - $database = new Database($adapter, $cache); - - $databases[$dsn->getHost()] = $database; - - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - - if (\in_array($dsn->getHost(), $sharedTables)) { - $database - ->setSharedTables(true) - ->setTenant($project->getSequence()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getSequence()); - } - - $database - ->setDatabase(APP_DATABASE) - ->setAuthorization($authorization) - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER); - - return $database; - }; -}, ['pools', 'dbForPlatform', 'cache', 'authorization']); - -Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { - $database = null; - - return function (?Document $project = null) use ($pools, $cache, $database, $authorization) { - if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') { - $database->setTenant($project->getSequence()); - return $database; - } - - $adapter = new DatabasePool($pools->get('logs')); - $database = new Database($adapter, $cache); - - $database - ->setDatabase(APP_DATABASE) - ->setAuthorization($authorization) - ->setSharedTables(true) - ->setNamespace('logsV1') - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER) - ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER); - - if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') { - $database->setTenant($project->getSequence()); - } - - return $database; - }; -}, ['pools', 'cache', 'authorization']); - -Server::setResource('abuseRetention', function () { - return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day -}); - -Server::setResource('auditRetention', function (Document $project) { - if ($project->getId() === 'console') { - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months - } - - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days -}, ['project']); - -Server::setResource('executionRetention', function () { - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days -}); - -Server::setResource('cache', function (Registry $register) { - $pools = $register->get('pools'); - $list = Config::getParam('pools-cache', []); - $adapters = []; - - foreach ($list as $value) { - $adapters[] = new CachePool($pools->get($value)); - } - - return new Cache(new Sharding($adapters)); -}, ['register']); - -Server::setResource('redis', function () { - $host = System::getEnv('_APP_REDIS_HOST', 'localhost'); - $port = System::getEnv('_APP_REDIS_PORT', 6379); - $pass = System::getEnv('_APP_REDIS_PASS', ''); - - $redis = new \Redis(); - @$redis->pconnect($host, (int) $port); - if ($pass) { - $redis->auth($pass); - } - $redis->setOption(\Redis::OPT_READ_TIMEOUT, -1); - - return $redis; -}); - -Server::setResource('timelimit', function (\Redis $redis) { - return function (string $key, int $limit, int $time) use ($redis) { - return new TimeLimitRedis($key, $limit, $time, $redis); - }; -}, ['redis']); - -Server::setResource('log', fn () => new Log()); - -Server::setResource('publisher', function (Group $pools) { - return new BrokerPool(publisher: $pools->get('publisher')); -}, ['pools']); - -Server::setResource('publisherDatabases', function (BrokerPool $publisher) { - return $publisher; -}, ['publisher']); - -Server::setResource('publisherFunctions', function (BrokerPool $publisher) { - return $publisher; -}, ['publisher']); - -Server::setResource('publisherMigrations', function (BrokerPool $publisher) { - return $publisher; -}, ['publisher']); - -Server::setResource('publisherMessaging', function (BrokerPool $publisher) { - return $publisher; -}, ['publisher']); - -Server::setResource('consumer', function (Group $pools) { +$container->set('consumer', function (Group $pools) { return new BrokerPool(consumer: $pools->get('consumer')); }, ['pools']); -Server::setResource('consumerDatabases', function (BrokerPool $consumer) { +$container->set('consumerDatabases', function (BrokerPool $consumer) { return $consumer; }, ['consumer']); -Server::setResource('consumerMigrations', function (BrokerPool $consumer) { +$container->set('consumerMigrations', function (BrokerPool $consumer) { return $consumer; }, ['consumer']); -Server::setResource('consumerStatsUsage', function (BrokerPool $consumer) { +$container->set('consumerStatsUsage', function (BrokerPool $consumer) { return $consumer; }, ['consumer']); -Server::setResource('usage', function () { - return new Context(); -}, []); -Server::setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher( - $publisher, - new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME)) -), ['publisher']); - -Server::setResource('queueForDatabase', function (Publisher $publisher) { - return new EventDatabase($publisher); -}, ['publisher']); - -Server::setResource('queueForMessaging', function (Publisher $publisher) { - return new Messaging($publisher); -}, ['publisher']); - -Server::setResource('queueForMails', function (Publisher $publisher) { - return new Mail($publisher); -}, ['publisher']); - -Server::setResource('queueForBuilds', function (Publisher $publisher) { - return new Build($publisher); -}, ['publisher']); - -Server::setResource('queueForScreenshots', function (Publisher $publisher) { - return new Screenshot($publisher); -}, ['publisher']); - -Server::setResource('queueForDeletes', function (Publisher $publisher) { - return new Delete($publisher); -}, ['publisher']); - -Server::setResource('queueForEvents', function (Publisher $publisher) { - return new Event($publisher); -}, ['publisher']); - -Server::setResource('queueForAudits', function (Publisher $publisher) { - return new Audit($publisher); -}, ['publisher']); - -Server::setResource('queueForWebhooks', function (Publisher $publisher) { - return new Webhook($publisher); -}, ['publisher']); - -Server::setResource('queueForFunctions', function (Publisher $publisher) { - return new Func($publisher); -}, ['publisher']); - -Server::setResource('queueForRealtime', function () { - return new Realtime(); -}, []); - -Server::setResource('queueForCertificates', function (Publisher $publisher) { - return new Certificate($publisher); -}, ['publisher']); - -Server::setResource('queueForMigrations', function (Publisher $publisher) { - return new Migration($publisher); -}, ['publisher']); - -Server::setResource('logger', function (Registry $register) { - return $register->get('logger'); -}, ['register']); - -Server::setResource('pools', function (Registry $register) { - return $register->get('pools'); -}, ['register']); - -Server::setResource('telemetry', fn () => new NoTelemetry()); - -Server::setResource('deviceForSites', function (Document $project, Telemetry $telemetry) { - return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())); -}, ['project', 'telemetry']); - -Server::setResource('deviceForMigrations', function (Document $project, Telemetry $telemetry) { - return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); - -Server::setResource('deviceForFunctions', function (Document $project, Telemetry $telemetry) { - return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); - -Server::setResource('deviceForFiles', function (Document $project, Telemetry $telemetry) { - return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); - -Server::setResource('deviceForBuilds', function (Document $project, Telemetry $telemetry) { - return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); - -Server::setResource('deviceForCache', function (Document $project, Telemetry $telemetry) { - return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId())); -}, ['project', 'telemetry']); - -Server::setResource( - 'isResourceBlocked', - fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false -); - -Server::setResource('plan', function (array $plan = []) { - return []; -}); - -Server::setResource('certificates', function () { +$container->set('certificates', function () { $email = System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS')); if (empty($email)) { throw new Exception('You must set a valid security email address (_APP_EMAIL_CERTIFICATES) to issue a LetsEncrypt SSL certificate.'); } return new LetsEncrypt($email); -}); +}, []); -Server::setResource('logError', function (Registry $register, Document $project) { - return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $project) { - $logger = $register->get('logger'); - - if ($logger) { - $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); - - $log = new Log(); - $log->setNamespace($namespace); - $log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname())); - $log->setVersion($version); - $log->setType(Log::TYPE_ERROR); - $log->setMessage($error->getMessage()); - - $log->addTag('code', $error->getCode()); - $log->addTag('verboseType', get_class($error)); - $log->addTag('projectId', $project->getId() ?? ''); - - $log->addExtra('file', $error->getFile()); - $log->addExtra('line', $error->getLine()); - $log->addExtra('trace', $error->getTraceAsString()); - - if ($error->getPrevious() !== null) { - if ($error->getPrevious()->getMessage() != $error->getMessage()) { - $log->addExtra('previousMessage', $error->getPrevious()->getMessage()); - } - $log->addExtra('previousFile', $error->getPrevious()->getFile()); - $log->addExtra('previousLine', $error->getPrevious()->getLine()); - } - - foreach (($extras ?? []) as $key => $value) { - $log->addExtra($key, $value); - } - - $log->setAction($action); - - $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; - $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); - - try { - $responseCode = $logger->addLog($log); - Console::info('Error log pushed with status code: ' . $responseCode); - } catch (Throwable $th) { - Console::error('Error pushing log: ' . $th->getMessage()); - } - } - - Console::warning("Failed: {$error->getMessage()}"); - Console::warning($error->getTraceAsString()); - - if ($error->getPrevious() !== null) { - if ($error->getPrevious()->getMessage() != $error->getMessage()) { - Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}"); - } - Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}"); - } - }; -}, ['register', 'project']); - -Server::setResource('executor', fn () => new Executor()); - -Server::setResource('getAudit', function (Database $dbForPlatform, callable $getProjectDB) { - return function (Document $project) use ($dbForPlatform, $getProjectDB) { - if ($project->isEmpty() || $project->getId() === 'console') { - $adapter = new AdapterDatabase($dbForPlatform); - - return new UtopiaAudit($adapter); - } - - $dbForProject = $getProjectDB($project); - $adapter = new AdapterDatabase($dbForProject); - - return new UtopiaAudit($adapter); - }; -}, ['dbForPlatform', 'getProjectDB']); - -Server::setResource('executionsRetentionCount', function (Document $project, array $plan) { - if ($project->getId() === 'console' || empty($plan)) { - return 0; - } - - return (int) ($plan['executionsRetentionCount'] ?? 100); -}, ['project', 'plan']); - -$pools = $register->get('pools'); $platform = new Appwrite(); $args = $platform->getEnv('argv'); @@ -522,37 +80,45 @@ if (\str_starts_with($workerName, 'databases')) { } try { - /** - * Any worker can be configured with the following env vars: - * - _APP_WORKERS_NUM The total number of worker processes - * - _APP_WORKER_PER_CORE The number of worker processes per core (ignored if _APP_WORKERS_NUM is set) - * - _APP_QUEUE_NAME The name of the queue to read for database events - */ + /** @var Group $pools */ + $pools = $container->get('pools'); + + $adapter = new Swoole( + $pools->get('consumer')->pop()->getResource(), + System::getEnv('_APP_WORKERS_NUM', 1), + $queueName + ); + + $worker = new Server($adapter, $container); + $worker->setCoroutines(true); + + $worker->init()->action(function () use ($worker) { + registerWorkerJobResources($worker->getContainer()); + }); + + $container->set('bus', function ($register) use ($worker) { + return $register->get('bus')->setResolver( + fn (string $name) => $worker->getContainer()->get($name) + ); + }, ['register']); + + $platform->setWorker($worker); $platform->init(Service::TYPE_WORKER, [ - 'workersNum' => System::getEnv('_APP_WORKERS_NUM', 1), - 'connection' => $pools->get('consumer')->pop()->getResource(), - 'workerName' => strtolower($workerName) ?? null, - 'queueName' => $queueName, + 'workerName' => strtolower($workerName), ]); } catch (\Throwable $e) { Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine()); + Console::exit(1); } -$worker = $platform->getWorker(); - -Server::setResource('bus', function ($register) use ($worker) { - return $register->get('bus')->setResolver(fn (string $name) => $worker->getResource($name)); -}, ['register']); - $worker ->error() ->inject('error') ->inject('logger') ->inject('log') - ->inject('pools') ->inject('project') ->inject('authorization') - ->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($queueName) { + ->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project, Authorization $authorization) use ($queueName) { $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); if ($logger) { diff --git a/composer.json b/composer.json index 382f1b52fc..fb26e0bd7f 100644 --- a/composer.json +++ b/composer.json @@ -78,7 +78,7 @@ "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", "utopia-php/preloader": "0.2.*", - "utopia-php/queue": "0.16.*", + "utopia-php/queue": "dev-feat/di-container-refactor as 0.16.0", "utopia-php/servers": "0.3.*", "utopia-php/registry": "0.5.*", "utopia-php/storage": "1.0.*", diff --git a/composer.lock b/composer.lock index 975368629d..137ac52714 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": "6efdf0d212038b3d65b12cf693276936", + "content-hash": "f2035503cd8a83da2b41a975311d41f7", "packages": [ { "name": "adhocore/jwt", @@ -4830,21 +4830,22 @@ }, { "name": "utopia-php/queue", - "version": "0.16.0", + "version": "dev-feat/di-container-refactor", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "ffdc9315d2f5999960c95a5860f067ea2eaa36f7" + "reference": "0467c190f0f73f9a3170f2ee21b393a04edaa588" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/ffdc9315d2f5999960c95a5860f067ea2eaa36f7", - "reference": "ffdc9315d2f5999960c95a5860f067ea2eaa36f7", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/0467c190f0f73f9a3170f2ee21b393a04edaa588", + "reference": "0467c190f0f73f9a3170f2ee21b393a04edaa588", "shasum": "" }, "require": { "php": ">=8.3", "php-amqplib/php-amqplib": "^3.7", + "utopia-php/di": "0.3.*", "utopia-php/fetch": "0.5.*", "utopia-php/pools": "1.*", "utopia-php/servers": "0.3.*", @@ -4890,9 +4891,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.16.0" + "source": "https://github.com/utopia-php/queue/tree/feat/di-container-refactor" }, - "time": "2026-03-13T12:23:30+00:00" + "time": "2026-03-17T15:20:45+00:00" }, { "name": "utopia-php/registry", @@ -8477,11 +8478,18 @@ "version": "dev-feat/coroutines-option", "alias": "0.34.15", "alias_normalized": "0.34.15.0" + }, + { + "package": "utopia-php/queue", + "version": "dev-feat/di-container-refactor", + "alias": "0.16.0", + "alias_normalized": "0.16.0.0" } ], "minimum-stability": "dev", "stability-flags": { - "utopia-php/framework": 20 + "utopia-php/framework": 20, + "utopia-php/queue": 20 }, "prefer-stable": true, "prefer-lowest": false, From fa2b7955d05a62f7194221b0d8d6f167340eec3d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 19 Mar 2026 10:04:47 +0530 Subject: [PATCH 043/118] update dependencies --- composer.json | 4 ++-- composer.lock | 61 ++++++++++++++++++++++++++++----------------------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/composer.json b/composer.json index fb26e0bd7f..b99733f548 100644 --- a/composer.json +++ b/composer.json @@ -61,13 +61,13 @@ "utopia-php/compression": "0.1.*", "utopia-php/config": "1.*", "utopia-php/console": "0.1.*", - "utopia-php/database": "5.3.*", + "utopia-php/database": "dev-fix-shared-table-reconciliation as 5.3.15", "utopia-php/detector": "0.2.*", "utopia-php/domains": "1.*", "utopia-php/emails": "0.6.*", "utopia-php/dns": "1.6.*", "utopia-php/dsn": "0.2.1", - "utopia-php/framework": "dev-feat/coroutines-option as 0.34.15", + "utopia-php/framework": "dev-feat/swoole-adapters-and-compression as 0.34.15", "utopia-php/fetch": "0.5.*", "utopia-php/image": "0.8.*", "utopia-php/locale": "0.8.*", diff --git a/composer.lock b/composer.lock index 137ac52714..cd26db5b56 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": "f2035503cd8a83da2b41a975311d41f7", + "content-hash": "23dd96af7065dd3f35083db63b756224", "packages": [ { "name": "adhocore/jwt", @@ -686,23 +686,23 @@ }, { "name": "google/protobuf", - "version": "v4.33.5", + "version": "v4.33.6", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "ebe8010a61b2ae0cff0d246fe1c4d44e9f7dfa6d" + "reference": "84b008c23915ed94536737eae46f41ba3bccfe67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/ebe8010a61b2ae0cff0d246fe1c4d44e9f7dfa6d", - "reference": "ebe8010a61b2ae0cff0d246fe1c4d44e9f7dfa6d", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/84b008c23915ed94536737eae46f41ba3bccfe67", + "reference": "84b008c23915ed94536737eae46f41ba3bccfe67", "shasum": "" }, "require": { "php": ">=8.1.0" }, "require-dev": { - "phpunit/phpunit": ">=5.0.0 <8.5.27" + "phpunit/phpunit": ">=10.5.62 <11.0.0" }, "suggest": { "ext-bcmath": "Need to support JSON deserialization" @@ -724,9 +724,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.5" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.6" }, - "time": "2026-01-29T20:49:00+00:00" + "time": "2026-03-18T17:32:05+00:00" }, { "name": "halaxa/json-machine", @@ -1996,16 +1996,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.49", + "version": "3.0.50", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9" + "reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9", - "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/aa6ad8321ed103dc3624fb600a25b66ebf78ec7b", + "reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b", "shasum": "" }, "require": { @@ -2086,7 +2086,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.49" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.50" }, "funding": [ { @@ -2102,7 +2102,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T09:17:28+00:00" + "time": "2026-03-19T02:57:58+00:00" }, { "name": "psr/clock", @@ -3850,16 +3850,16 @@ }, { "name": "utopia-php/database", - "version": "5.3.15", + "version": "dev-fix-shared-table-reconciliation", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "bb89e8c4a5d534fc7e650c4438aa796667fe160a" + "reference": "602f8deef7c07e224573eb7e7ef22d9cfd9588b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/bb89e8c4a5d534fc7e650c4438aa796667fe160a", - "reference": "bb89e8c4a5d534fc7e650c4438aa796667fe160a", + "url": "https://api.github.com/repos/utopia-php/database/zipball/602f8deef7c07e224573eb7e7ef22d9cfd9588b2", + "reference": "602f8deef7c07e224573eb7e7ef22d9cfd9588b2", "shasum": "" }, "require": { @@ -3933,10 +3933,10 @@ "utopia" ], "support": { - "source": "https://github.com/utopia-php/database/tree/5.3.15", + "source": "https://github.com/utopia-php/database/tree/fix-shared-table-reconciliation", "issues": "https://github.com/utopia-php/database/issues" }, - "time": "2026-03-16T11:41:45+00:00" + "time": "2026-03-19T02:40:25+00:00" }, { "name": "utopia-php/detector", @@ -4302,16 +4302,16 @@ }, { "name": "utopia-php/framework", - "version": "dev-feat/coroutines-option", + "version": "dev-feat/swoole-adapters-and-compression", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "8420a83e4e21f606da34897a07589db247b90acf" + "reference": "57fc53774a21c9ea78e407ad3f1c144cdccff5fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/8420a83e4e21f606da34897a07589db247b90acf", - "reference": "8420a83e4e21f606da34897a07589db247b90acf", + "url": "https://api.github.com/repos/utopia-php/http/zipball/57fc53774a21c9ea78e407ad3f1c144cdccff5fa", + "reference": "57fc53774a21c9ea78e407ad3f1c144cdccff5fa", "shasum": "" }, "require": { @@ -4349,9 +4349,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/feat/coroutines-option" + "source": "https://github.com/utopia-php/http/tree/feat/swoole-adapters-and-compression" }, - "time": "2026-03-18T11:08:21+00:00" + "time": "2026-03-19T04:32:54+00:00" }, { "name": "utopia-php/image", @@ -8473,9 +8473,15 @@ } ], "aliases": [ + { + "package": "utopia-php/database", + "version": "dev-fix-shared-table-reconciliation", + "alias": "5.3.15", + "alias_normalized": "5.3.15.0" + }, { "package": "utopia-php/framework", - "version": "dev-feat/coroutines-option", + "version": "dev-feat/swoole-adapters-and-compression", "alias": "0.34.15", "alias_normalized": "0.34.15.0" }, @@ -8488,6 +8494,7 @@ ], "minimum-stability": "dev", "stability-flags": { + "utopia-php/database": 20, "utopia-php/framework": 20, "utopia-php/queue": 20 }, From fdbc5b673799466fff7d894606686186c5e5fe5f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 19 Mar 2026 10:07:08 +0530 Subject: [PATCH 044/118] update queue --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index cd26db5b56..488fcf91c5 100644 --- a/composer.lock +++ b/composer.lock @@ -4834,12 +4834,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "0467c190f0f73f9a3170f2ee21b393a04edaa588" + "reference": "21b6e385d022631cc45f252186b0610254393e69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/0467c190f0f73f9a3170f2ee21b393a04edaa588", - "reference": "0467c190f0f73f9a3170f2ee21b393a04edaa588", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/21b6e385d022631cc45f252186b0610254393e69", + "reference": "21b6e385d022631cc45f252186b0610254393e69", "shasum": "" }, "require": { @@ -4854,9 +4854,9 @@ }, "require-dev": { "ext-redis": "*", - "laravel/pint": "^0.2.3", + "laravel/pint": "^1.0", "phpstan/phpstan": "^1.8", - "phpunit/phpunit": "^9.5.5", + "phpunit/phpunit": "^11.0", "swoole/ide-helper": "4.8.8", "workerman/workerman": "^4.0" }, @@ -4893,7 +4893,7 @@ "issues": "https://github.com/utopia-php/queue/issues", "source": "https://github.com/utopia-php/queue/tree/feat/di-container-refactor" }, - "time": "2026-03-17T15:20:45+00:00" + "time": "2026-03-19T04:36:39+00:00" }, { "name": "utopia-php/registry", From e38a4e43471c6fb12abd4d29d8bd2697542ebeb2 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 19 Mar 2026 10:08:40 +0530 Subject: [PATCH 045/118] update queue --- app/init/worker/{job.php => message.php} | 2 +- app/worker.php | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) rename app/init/worker/{job.php => message.php} (99%) diff --git a/app/init/worker/job.php b/app/init/worker/message.php similarity index 99% rename from app/init/worker/job.php rename to app/init/worker/message.php index a58d0c21ef..fbe99ea72e 100644 --- a/app/init/worker/job.php +++ b/app/init/worker/message.php @@ -39,7 +39,7 @@ use Utopia\Telemetry\Adapter as Telemetry; * These resources depend on the queue message or keep mutable state and * must be fresh for each worker job. */ -function registerWorkerJobResources(Container $container): void +function registerWorkerMessageResources(Container $container): void { $container->set('log', fn () => new Log(), []); diff --git a/app/worker.php b/app/worker.php index 6500a6a570..2660052597 100644 --- a/app/worker.php +++ b/app/worker.php @@ -1,7 +1,7 @@ setCoroutines(true); $worker->init()->action(function () use ($worker) { - registerWorkerJobResources($worker->getContainer()); + registerWorkerMessageResources($worker->getContainer()); }); $container->set('bus', function ($register) use ($worker) { From 9f1d0927171f962edb62d965f317790b0f30a6ca Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 19 Mar 2026 10:17:25 +0530 Subject: [PATCH 046/118] analyze fixes --- app/init/worker/message.php | 6 +++--- app/worker.php | 22 +++++++++++----------- phpstan-baseline.neon | 6 ------ tests/e2e/Services/Functions/junit.xml | 6 ++++++ 4 files changed, 20 insertions(+), 20 deletions(-) create mode 100644 tests/e2e/Services/Functions/junit.xml diff --git a/app/init/worker/message.php b/app/init/worker/message.php index fbe99ea72e..8404b38343 100644 --- a/app/init/worker/message.php +++ b/app/init/worker/message.php @@ -212,14 +212,14 @@ function registerWorkerMessageResources(Container $container): void $container->set('auditRetention', function (Document $project) { if ($project->getId() === 'console') { - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months + return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months } - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days + return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days }, ['project']); $container->set('executionRetention', function () { - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days + return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days }, []); $container->set('queueForDatabase', function (Publisher $publisher) { diff --git a/app/worker.php b/app/worker.php index 2660052597..382ef1281f 100644 --- a/app/worker.php +++ b/app/worker.php @@ -79,18 +79,18 @@ if (\str_starts_with($workerName, 'databases')) { $queueName = System::getEnv('_APP_QUEUE_NAME', 'v1-' . strtolower($workerName)); } +/** @var Group $pools */ +$pools = $container->get('pools'); + +$adapter = new Swoole( + $pools->get('consumer')->pop()->getResource(), + System::getEnv('_APP_WORKERS_NUM', 1), + $queueName +); + +$worker = new Server($adapter, $container); + try { - /** @var Group $pools */ - $pools = $container->get('pools'); - - $adapter = new Swoole( - $pools->get('consumer')->pop()->getResource(), - System::getEnv('_APP_WORKERS_NUM', 1), - $queueName - ); - - $worker = new Server($adapter, $container); - $worker->init()->action(function () use ($worker) { registerWorkerMessageResources($worker->getContainer()); }); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 73f5639776..8f66f1c36a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -186,12 +186,6 @@ parameters: count: 1 path: app/realtime.php - - - message: '#^Binary operation "\*" between \-1 and string results in an error\.$#' - identifier: binaryOp.invalid - count: 3 - path: app/worker.php - - message: '#^PHPDoc tag @return with type string is incompatible with native type int\.$#' identifier: return.phpDocType diff --git a/tests/e2e/Services/Functions/junit.xml b/tests/e2e/Services/Functions/junit.xml new file mode 100644 index 0000000000..7c1b11a7f2 --- /dev/null +++ b/tests/e2e/Services/Functions/junit.xml @@ -0,0 +1,6 @@ + + + + + + From 5339592b3fca30c42ad8a4da4ed71fae67a683f4 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 19 Mar 2026 10:20:47 +0530 Subject: [PATCH 047/118] update http --- composer.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.lock b/composer.lock index 488fcf91c5..1dca984932 100644 --- a/composer.lock +++ b/composer.lock @@ -3854,12 +3854,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "602f8deef7c07e224573eb7e7ef22d9cfd9588b2" + "reference": "10cf13d0dcfcd2aee13880945e5bc04fbbdbfcf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/602f8deef7c07e224573eb7e7ef22d9cfd9588b2", - "reference": "602f8deef7c07e224573eb7e7ef22d9cfd9588b2", + "url": "https://api.github.com/repos/utopia-php/database/zipball/10cf13d0dcfcd2aee13880945e5bc04fbbdbfcf1", + "reference": "10cf13d0dcfcd2aee13880945e5bc04fbbdbfcf1", "shasum": "" }, "require": { @@ -3936,7 +3936,7 @@ "source": "https://github.com/utopia-php/database/tree/fix-shared-table-reconciliation", "issues": "https://github.com/utopia-php/database/issues" }, - "time": "2026-03-19T02:40:25+00:00" + "time": "2026-03-19T04:40:36+00:00" }, { "name": "utopia-php/detector", @@ -4306,12 +4306,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "57fc53774a21c9ea78e407ad3f1c144cdccff5fa" + "reference": "01c863fbc3f911e6a8e6e0aaa703213257153767" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/57fc53774a21c9ea78e407ad3f1c144cdccff5fa", - "reference": "57fc53774a21c9ea78e407ad3f1c144cdccff5fa", + "url": "https://api.github.com/repos/utopia-php/http/zipball/01c863fbc3f911e6a8e6e0aaa703213257153767", + "reference": "01c863fbc3f911e6a8e6e0aaa703213257153767", "shasum": "" }, "require": { @@ -4351,7 +4351,7 @@ "issues": "https://github.com/utopia-php/http/issues", "source": "https://github.com/utopia-php/http/tree/feat/swoole-adapters-and-compression" }, - "time": "2026-03-19T04:32:54+00:00" + "time": "2026-03-19T04:49:45+00:00" }, { "name": "utopia-php/image", From 625ec4ce915ef351ce5e97d63f49dcfe579c4308 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 19 Mar 2026 14:09:04 +0530 Subject: [PATCH 048/118] sync changes --- composer.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/composer.lock b/composer.lock index 1dca984932..c48bba5ed1 100644 --- a/composer.lock +++ b/composer.lock @@ -3854,12 +3854,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "10cf13d0dcfcd2aee13880945e5bc04fbbdbfcf1" + "reference": "3be72b7b6fa25743b53ec41a037b672bee22bad9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/10cf13d0dcfcd2aee13880945e5bc04fbbdbfcf1", - "reference": "10cf13d0dcfcd2aee13880945e5bc04fbbdbfcf1", + "url": "https://api.github.com/repos/utopia-php/database/zipball/3be72b7b6fa25743b53ec41a037b672bee22bad9", + "reference": "3be72b7b6fa25743b53ec41a037b672bee22bad9", "shasum": "" }, "require": { @@ -3936,7 +3936,7 @@ "source": "https://github.com/utopia-php/database/tree/fix-shared-table-reconciliation", "issues": "https://github.com/utopia-php/database/issues" }, - "time": "2026-03-19T04:40:36+00:00" + "time": "2026-03-19T06:49:33+00:00" }, { "name": "utopia-php/detector", @@ -4306,12 +4306,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "01c863fbc3f911e6a8e6e0aaa703213257153767" + "reference": "578cdec596c8aa8fda16752ba024fef36c6ca127" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/01c863fbc3f911e6a8e6e0aaa703213257153767", - "reference": "01c863fbc3f911e6a8e6e0aaa703213257153767", + "url": "https://api.github.com/repos/utopia-php/http/zipball/578cdec596c8aa8fda16752ba024fef36c6ca127", + "reference": "578cdec596c8aa8fda16752ba024fef36c6ca127", "shasum": "" }, "require": { @@ -4351,7 +4351,7 @@ "issues": "https://github.com/utopia-php/http/issues", "source": "https://github.com/utopia-php/http/tree/feat/swoole-adapters-and-compression" }, - "time": "2026-03-19T04:49:45+00:00" + "time": "2026-03-19T08:35:00+00:00" }, { "name": "utopia-php/image", @@ -5478,16 +5478,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.11.9", + "version": "1.11.10", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "2f0f6ec54736ba7efdff188a9451b56f1665f25a" + "reference": "96e6b79a241fc615627a820107ca64bbd1b550a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/2f0f6ec54736ba7efdff188a9451b56f1665f25a", - "reference": "2f0f6ec54736ba7efdff188a9451b56f1665f25a", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/96e6b79a241fc615627a820107ca64bbd1b550a6", + "reference": "96e6b79a241fc615627a820107ca64bbd1b550a6", "shasum": "" }, "require": { @@ -5523,9 +5523,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.11.9" + "source": "https://github.com/appwrite/sdk-generator/tree/1.11.10" }, - "time": "2026-03-17T08:16:49+00:00" + "time": "2026-03-19T05:27:36+00:00" }, { "name": "brianium/paratest", From e6090800e2560dbd96f05e7a3a774bbc2cb0aa59 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 19 Mar 2026 17:35:31 +0530 Subject: [PATCH 049/118] register resources --- src/Appwrite/Platform/Tasks/Specs.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Appwrite/Platform/Tasks/Specs.php b/src/Appwrite/Platform/Tasks/Specs.php index 5dbd6784ae..cb22457460 100644 --- a/src/Appwrite/Platform/Tasks/Specs.php +++ b/src/Appwrite/Platform/Tasks/Specs.php @@ -290,6 +290,7 @@ class Specs extends Action $specsContainer->set('response', fn () => $response); $specsContainer->set('dbForPlatform', fn () => new Database(new MySQL(''), new Cache(new None()))); $specsContainer->set('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None()))); + registerRequestResources($specsContainer); $platforms = static::getPlatforms(); $authCounts = $this->getAuthCounts(); From ecb8104340be2a71a3956126c839f2331b242d35 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 19 Mar 2026 17:37:47 +0530 Subject: [PATCH 050/118] register resources --- src/Appwrite/Platform/Tasks/Specs.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/Specs.php b/src/Appwrite/Platform/Tasks/Specs.php index cb22457460..cafaeda247 100644 --- a/src/Appwrite/Platform/Tasks/Specs.php +++ b/src/Appwrite/Platform/Tasks/Specs.php @@ -286,11 +286,11 @@ class Specs extends Action // Mock dependencies $specsContainer = new Container(); + registerRequestResources($specsContainer); $specsContainer->set('request', fn () => $this->getRequest()); $specsContainer->set('response', fn () => $response); $specsContainer->set('dbForPlatform', fn () => new Database(new MySQL(''), new Cache(new None()))); $specsContainer->set('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None()))); - registerRequestResources($specsContainer); $platforms = static::getPlatforms(); $authCounts = $this->getAuthCounts(); From c002f4afe3ddfcc883b001398f9b8c75b483c774 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 19 Mar 2026 21:23:00 +0530 Subject: [PATCH 051/118] fix specs generation --- app/init/models.php | 2 +- src/Appwrite/Platform/Tasks/Specs.php | 10 ++++++++-- src/Appwrite/Utopia/Response/Model/Webhook.php | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/init/models.php b/app/init/models.php index d432852660..6c90f08199 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -185,7 +185,7 @@ Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, ' Response::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_LIST, 'deployments', Response::MODEL_DEPLOYMENT)); Response::setModel(new BaseList('Executions List', Response::MODEL_EXECUTION_LIST, 'executions', Response::MODEL_EXECUTION)); Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, false)); -Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, false)); +Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, true)); Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true)); Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false)); Response::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false)); diff --git a/src/Appwrite/Platform/Tasks/Specs.php b/src/Appwrite/Platform/Tasks/Specs.php index cafaeda247..02869124d1 100644 --- a/src/Appwrite/Platform/Tasks/Specs.php +++ b/src/Appwrite/Platform/Tasks/Specs.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Tasks; +use Appwrite\Network\Validator\Redirect; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Specification\Format\OpenAPI3; @@ -18,6 +19,7 @@ use Utopia\Config\Config; use Utopia\Console; use Utopia\Database\Adapter\MySQL; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\DI\Container; use Utopia\Http\Adapter\FPM\Server as FPMServer; use Utopia\Http\Http; @@ -284,13 +286,17 @@ class Specs extends Action $mocks = ($mode === 'mocks'); - // Mock dependencies + // Mock dependencies needed by param validator injections in route definitions $specsContainer = new Container(); - registerRequestResources($specsContainer); $specsContainer->set('request', fn () => $this->getRequest()); $specsContainer->set('response', fn () => $response); $specsContainer->set('dbForPlatform', fn () => new Database(new MySQL(''), new Cache(new None()))); $specsContainer->set('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None()))); + $specsContainer->set('redirectValidator', fn () => new Redirect([], [])); + $specsContainer->set('project', fn () => new Document([])); + $specsContainer->set('passwordsDictionary', fn () => []); + $specsContainer->set('localeCodes', fn () => \array_map(fn ($locale) => $locale['code'], Config::getParam('locale-codes', []))); + $specsContainer->set('plan', fn () => []); $platforms = static::getPlatforms(); $authCounts = $this->getAuthCounts(); diff --git a/src/Appwrite/Utopia/Response/Model/Webhook.php b/src/Appwrite/Utopia/Response/Model/Webhook.php index af1e23447e..517ad4807d 100644 --- a/src/Appwrite/Utopia/Response/Model/Webhook.php +++ b/src/Appwrite/Utopia/Response/Model/Webhook.php @@ -10,7 +10,7 @@ class Webhook extends Model /** * @var bool */ - protected bool $public = false; + protected bool $public = true; public function __construct() { From 8d925f3670ad5a60c40cbf5935bdf394a64e8f1a Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 19 Mar 2026 21:35:19 +0530 Subject: [PATCH 052/118] merge conficts --- app/init/resources/request.php | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/app/init/resources/request.php b/app/init/resources/request.php index e21ee0e5ae..c15e462449 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -552,7 +552,7 @@ function registerRequestResources(Container $container): void return $user; }, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']); - $container->set('project', function ($dbForPlatform, $request, $console, $authorization) { + $container->set('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) { /** @var Appwrite\Utopia\Request $request */ /** @var Utopia\Database\Database $dbForPlatform */ /** @var Utopia\Database\Document $console */ @@ -562,6 +562,20 @@ function registerRequestResources(Container $container): void $projectId = $request->getHeader('x-appwrite-project', ''); } + // Backwards compatibility for new services, originally project resources + // These endpoints moved from /v1/projects/:projectId/ to /v1/ + // When accessed via the old alias path, extract projectId from the URI + $deprecatedProjectPathPrefix = '/v1/projects/'; + $route = $utopia->match($request); + if (!empty($route)) { + $isDeprecatedAlias = \str_starts_with($request->getURI(), $deprecatedProjectPathPrefix) && + !\str_starts_with($route->getPath(), $deprecatedProjectPathPrefix); + + if ($isDeprecatedAlias) { + $projectId = \explode('/', $request->getURI(), 5)[3] ?? ''; + } + } + if (empty($projectId) || $projectId === 'console') { return $console; } @@ -569,7 +583,7 @@ function registerRequestResources(Container $container): void $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); return $project; - }, ['dbForPlatform', 'request', 'console', 'authorization']); + }, ['dbForPlatform', 'request', 'console', 'authorization', 'utopia']); $container->set('session', function (User $user, Store $store, Token $proofForToken) { if ($user->isEmpty()) { @@ -923,7 +937,7 @@ function registerRequestResources(Container $container): void return new Audit($adapter); }, ['dbForProject']); - $container->set('mode', function ($request) { + $container->set('mode', function ($request, Document $project) { /** @var Appwrite\Utopia\Request $request */ /** @@ -931,8 +945,15 @@ function registerRequestResources(Container $container): void * - 'default' => Requests for Client and Server Side * - 'admin' => Request from the Console on non-console projects */ - return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); - }, ['request']); + $mode = $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); + + $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', '')); + if (!empty($projectId) && $project->getId() !== $projectId) { + $mode = APP_MODE_ADMIN; + } + + return $mode; + }, ['request', 'project']); $container->set('requestTimestamp', function ($request) { // TODO: Move this to the Request class itself From 4224f6ea5af9b8269c9c4cc5bf108ae6b4875805 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 19 Mar 2026 21:36:06 +0530 Subject: [PATCH 053/118] merge conficts --- composer.lock | 34 +++++++++++++++++++------- tests/e2e/Services/Functions/junit.xml | 6 ----- 2 files changed, 25 insertions(+), 15 deletions(-) delete mode 100644 tests/e2e/Services/Functions/junit.xml diff --git a/composer.lock b/composer.lock index 48d51284a2..6650626e94 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": "1404c8821e43b3fe92e06a8ed658ed26", + "content-hash": "5b7f63ff3136da1b7db44c6e5f8da030", "packages": [ { "name": "adhocore/jwt", @@ -5478,16 +5478,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.11.10", + "version": "1.11.11", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "96e6b79a241fc615627a820107ca64bbd1b550a6" + "reference": "cfc37c85161a5515af4cd2f9885a811f51a2483a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/96e6b79a241fc615627a820107ca64bbd1b550a6", - "reference": "96e6b79a241fc615627a820107ca64bbd1b550a6", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/cfc37c85161a5515af4cd2f9885a811f51a2483a", + "reference": "cfc37c85161a5515af4cd2f9885a811f51a2483a", "shasum": "" }, "require": { @@ -5523,9 +5523,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.11.10" + "source": "https://github.com/appwrite/sdk-generator/tree/1.11.11" }, - "time": "2026-03-19T05:27:36+00:00" + "time": "2026-03-19T16:21:03+00:00" }, { "name": "brianium/paratest", @@ -8472,9 +8472,25 @@ "time": "2024-11-07T12:36:22+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/framework", + "version": "dev-feat/swoole-adapters-and-compression", + "alias": "0.34.15", + "alias_normalized": "0.34.15.0" + }, + { + "package": "utopia-php/queue", + "version": "dev-feat/di-container-refactor", + "alias": "0.16.0", + "alias_normalized": "0.16.0.0" + } + ], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": { + "utopia-php/framework": 20, + "utopia-php/queue": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/tests/e2e/Services/Functions/junit.xml b/tests/e2e/Services/Functions/junit.xml deleted file mode 100644 index 7c1b11a7f2..0000000000 --- a/tests/e2e/Services/Functions/junit.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - From 6700340ef3339b0f18ae33f0b84925700964aa9e Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 19 Mar 2026 23:26:49 +0530 Subject: [PATCH 054/118] fix realtime --- app/realtime.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index ec66857d01..ffae48ea2e 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -628,12 +628,14 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $container->set('pools', fn () => $pools); $connectionContainer = new Container($container); - registerRequestResources($connectionContainer); $adapter = new HttpServer($connectionContainer); $app = new Http($adapter, 'UTC'); - $app->setResource('request', fn () => $request); - $app->setResource('response', fn () => $response); + $connectionContainer->set('utopia', fn () => $app); + $connectionContainer->set('request', fn () => $request); + $connectionContainer->set('response', fn () => $response); + + registerRequestResources($connectionContainer); $project = null; $logUser = null; From 55b436c67bb10549f3ceea51a9da7149d0e8e068 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 20 Mar 2026 12:17:28 +0530 Subject: [PATCH 055/118] lock file --- composer.lock | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/composer.lock b/composer.lock index 098988791b..2e825d964f 100644 --- a/composer.lock +++ b/composer.lock @@ -4306,12 +4306,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "578cdec596c8aa8fda16752ba024fef36c6ca127" + "reference": "c91012caa001e4cccb1133f13938ae77478b3a28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/578cdec596c8aa8fda16752ba024fef36c6ca127", - "reference": "578cdec596c8aa8fda16752ba024fef36c6ca127", + "url": "https://api.github.com/repos/utopia-php/http/zipball/c91012caa001e4cccb1133f13938ae77478b3a28", + "reference": "c91012caa001e4cccb1133f13938ae77478b3a28", "shasum": "" }, "require": { @@ -4320,6 +4320,7 @@ "utopia-php/compression": "0.1.*", "utopia-php/di": "0.3.*", "utopia-php/servers": "0.3.*", + "utopia-php/telemetry": "0.2.*", "utopia-php/validators": "0.2.*" }, "require-dev": { @@ -4351,7 +4352,7 @@ "issues": "https://github.com/utopia-php/http/issues", "source": "https://github.com/utopia-php/http/tree/feat/swoole-adapters-and-compression" }, - "time": "2026-03-19T08:35:00+00:00" + "time": "2026-03-19T17:31:18+00:00" }, { "name": "utopia-php/image", @@ -5478,16 +5479,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.11.11", + "version": "1.11.13", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "cfc37c85161a5515af4cd2f9885a811f51a2483a" + "reference": "c97527030060798129f2cb7e1e767671bf09f3bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/cfc37c85161a5515af4cd2f9885a811f51a2483a", - "reference": "cfc37c85161a5515af4cd2f9885a811f51a2483a", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/c97527030060798129f2cb7e1e767671bf09f3bd", + "reference": "c97527030060798129f2cb7e1e767671bf09f3bd", "shasum": "" }, "require": { @@ -5523,9 +5524,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.11.11" + "source": "https://github.com/appwrite/sdk-generator/tree/1.11.13" }, - "time": "2026-03-19T16:21:03+00:00" + "time": "2026-03-20T04:48:54+00:00" }, { "name": "brianium/paratest", From 9ecdbf595042cf2ac325430b23c388658453beae Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 20 Mar 2026 13:13:07 +0530 Subject: [PATCH 056/118] func exists --- app/realtime.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/realtime.php b/app/realtime.php index ffae48ea2e..8e6ac90e53 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -635,7 +635,11 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $connectionContainer->set('request', fn () => $request); $connectionContainer->set('response', fn () => $response); - registerRequestResources($connectionContainer); + if (function_exists('registerCloudRequestResources')) { + registerCloudRequestResources($connectionContainer); + } else { + registerRequestResources($connectionContainer); + } $project = null; $logUser = null; From 10cc6a8040d1c350aea05009966548e7dc9d6f77 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 20 Mar 2026 14:09:43 +0530 Subject: [PATCH 057/118] fix global pools state --- app/http.php | 24 +++++++++--------------- app/realtime.php | 8 +++++--- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/app/http.php b/app/http.php index ae63ea4f6b..0dd1dd5df1 100644 --- a/app/http.php +++ b/app/http.php @@ -49,6 +49,9 @@ $certifiedDomains->create(); global $container; $container->set('riskyDomains', fn () => $riskyDomains); $container->set('certifiedDomains', fn () => $certifiedDomains); +$container->set('pools', function ($register) { + return $register->get('pools'); +}, ['register']); $payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing $totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6)); @@ -286,14 +289,12 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c Span::current()?->finish(); } -$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $register, $swooleAdapter) { - global $container; - $pools = $register->get('pools'); - /** @var Group $pools */ - $container->set('pools', fn () => $pools); - +$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) { $app = new Http($swooleAdapter, 'UTC'); + /** @var Group $pools */ + $pools = $app->getResource('pools'); + go(function () use ($app, $pools) { /** @var array $collections */ @@ -494,7 +495,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke }); }); -$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($register, $files, $swooleAdapter) { +$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter) { Span::init('http.request'); $request = new Request($utopiaRequest->getSwooleRequest()); @@ -514,10 +515,6 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($regis return; } - global $container; - $pools = $register->get('pools'); - $container->set('pools', fn () => $pools); - $requestContainer = $swooleAdapter->getContainer(); $requestContainer->set('request', fn () => $request); $requestContainer->set('response', fn () => $response); @@ -637,11 +634,8 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($regis }); // Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory -$http->on(Constant::EVENT_TASK, function () use ($register, $swooleAdapter) { - global $container; +$http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) { $lastSyncUpdate = null; - $pools = $register->get('pools'); - $container->set('pools', fn () => $pools); $app = new Http($swooleAdapter, 'UTC'); diff --git a/app/realtime.php b/app/realtime.php index 8e6ac90e53..155bd87c2b 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -243,6 +243,11 @@ if (!function_exists('triggerStats')) { } } +global $container; +$container->set('pools', function ($register) { + return $register->get('pools'); +}, ['register']); + $realtime = getRealtime(); /** @@ -624,9 +629,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, Console::info("Connection open (user: {$connection})"); - $pools = $register->get('pools'); - $container->set('pools', fn () => $pools); - $connectionContainer = new Container($container); $adapter = new HttpServer($connectionContainer); From 032638e896bd5bb8c36109e970fb8a7ea9f6cfba Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 20 Mar 2026 15:35:28 +0530 Subject: [PATCH 058/118] fix --- app/init/resources.php | 101 ----------------------------------------- phpstan-baseline.neon | 6 --- 2 files changed, 107 deletions(-) diff --git a/app/init/resources.php b/app/init/resources.php index 2b834a8396..8711a37801 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -2,16 +2,9 @@ use Appwrite\Event\Event; use Appwrite\Event\Publisher\Usage as UsagePublisher; -use Appwrite\Extend\Exception; use Appwrite\Utopia\Database\Documents\User; use Executor\Executor; use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis; -use Utopia\Auth\Hashes\Argon2; -use Utopia\Auth\Hashes\Sha; -use Utopia\Auth\Proofs\Code; -use Utopia\Auth\Proofs\Password; -use Utopia\Auth\Proofs\Token; -use Utopia\Auth\Store; use Utopia\Cache\Adapter\Pool as CachePool; use Utopia\Cache\Adapter\Sharding; use Utopia\Cache\Cache; @@ -99,39 +92,6 @@ $container->set('platform', function () { require_once __DIR__ . '/resources/request.php'; - -$container->set('store', function (): Store { - return new Store(); -}); - -$container->set('proofForPassword', function (): Password { - $hash = new Argon2(); - $hash - ->setMemoryCost(7168) - ->setTimeCost(5) - ->setThreads(1); - - $password = new Password(); - $password - ->setHash($hash); - - return $password; -}); - -$container->set('proofForToken', function (): Token { - $token = new Token(); - $token->setHash(new Sha()); - - return $token; -}); - -$container->set('proofForCode', function (): Code { - $code = new Code(); - $code->setHash(new Sha()); - - return $code; -}); - $container->set('console', function () { return new Document(Config::getParam('console')); }, []); @@ -159,67 +119,6 @@ $container->set('dbForPlatform', function (Group $pools, Cache $cache, Authoriza return $database; }, ['pools', 'cache', 'authorization']); -$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) { - $databases = []; - - return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForPlatform; - } - - $database = $project->getAttribute('database', ''); - if (empty($database)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Project database is not configured'); - } - - try { - $dsn = new DSN($database); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $database); - } - - $configure = (function (Database $database) use ($project, $dsn, $authorization) { - $database - ->setDatabase(APP_DATABASE) - ->setAuthorization($authorization) - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()) - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) - ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES) - ->setDocumentType('users', User::class); - - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - - if (\in_array($dsn->getHost(), $sharedTables)) { - $database - ->setSharedTables(true) - ->setTenant($project->getSequence()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getSequence()); - } - }); - - if (isset($databases[$dsn->getHost()])) { - $database = $databases[$dsn->getHost()]; - $configure($database); - - return $database; - } - - $adapter = new DatabasePool($pools->get($dsn->getHost())); - $database = new Database($adapter, $cache); - $databases[$dsn->getHost()] = $database; - $configure($database); - - return $database; - }; -}, ['pools', 'dbForPlatform', 'cache', 'authorization']); - $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { $database = null; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 03b56939f5..9ac23e59f7 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -120,12 +120,6 @@ parameters: count: 1 path: app/controllers/shared/api.php - - - message: '#^Variable \$register might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: app/http.php - - message: '#^Variable \$tag on left side of \?\? always exists and is always null\.$#' identifier: nullCoalesce.variable From d008d9bff0dc1a059a1d5a88079b69e4c9e4049a Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 23 Mar 2026 10:01:27 +0530 Subject: [PATCH 059/118] merge conficts --- app/init/resources/request.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/init/resources/request.php b/app/init/resources/request.php index c15e462449..260d943dea 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -546,6 +546,31 @@ function registerRequestResources(Container $container): void } } + // Impersonation: if current user has impersonator capability and headers are set, act as another user + $impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', ''); + $impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', ''); + $impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', ''); + if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) { + $userDb = (APP_MODE_ADMIN === $mode || $project->getId() === 'console') ? $dbForPlatform : $dbForProject; + $targetUser = null; + if (!empty($impersonateUserId)) { + $targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->getDocument('users', $impersonateUserId)); + } elseif (!empty($impersonateEmail)) { + $targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->findOne('users', [Query::equal('email', [\strtolower($impersonateEmail)])])); + } elseif (!empty($impersonatePhone)) { + $targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->findOne('users', [Query::equal('phone', [$impersonatePhone])])); + } + if ($targetUser !== null && !$targetUser->isEmpty()) { + $impersonator = clone $user; + $user = clone $targetUser; + $user->setAttribute('impersonatorUserId', $impersonator->getId()); + $user->setAttribute('impersonatorUserInternalId', $impersonator->getSequence()); + $user->setAttribute('impersonatorUserName', $impersonator->getAttribute('name', '')); + $user->setAttribute('impersonatorUserEmail', $impersonator->getAttribute('email', '')); + $user->setAttribute('impersonatorAccessedAt', $impersonator->getAttribute('accessedAt', 0)); + } + } + $dbForProject->setMetadata('user', $user->getId()); $dbForPlatform->setMetadata('user', $user->getId()); From 6421bc86899675501bdbd71738cb2e625ea2e374 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 23 Mar 2026 10:06:58 +0530 Subject: [PATCH 060/118] fn name --- app/http.php | 6 ++++-- app/init/resources.php | 2 -- app/init/resources/request.php | 5 ++--- app/init/worker/message.php | 5 ++--- app/realtime.php | 10 ++++------ app/worker.php | 7 ++++--- composer.lock | 24 ++++++++++++------------ 7 files changed, 28 insertions(+), 31 deletions(-) diff --git a/app/http.php b/app/http.php index 0dd1dd5df1..7d78db55e1 100644 --- a/app/http.php +++ b/app/http.php @@ -3,6 +3,8 @@ require_once __DIR__ . '/init.php'; require_once __DIR__ . '/init/span.php'; +$registerRequestResources = require __DIR__ . '/init/resources/request.php'; + use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Swoole\Constant; @@ -495,7 +497,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke }); }); -$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter) { +$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) { Span::init('http.request'); $request = new Request($utopiaRequest->getSwooleRequest()); @@ -522,7 +524,7 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files $app = new Http($swooleAdapter, 'UTC'); $requestContainer->set('utopia', fn () => $app); - registerRequestResources($requestContainer); + $registerRequestResources($requestContainer); $app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled'); $app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB diff --git a/app/init/resources.php b/app/init/resources.php index 8711a37801..75837f985b 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -90,8 +90,6 @@ $container->set('platform', function () { return Config::getParam('platform', []); }, []); -require_once __DIR__ . '/resources/request.php'; - $container->set('console', function () { return new Document(Config::getParam('console')); }, []); diff --git a/app/init/resources/request.php b/app/init/resources/request.php index 260d943dea..82fdc0670e 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -63,8 +63,7 @@ use Utopia\Validator\WhiteList; * These resources depend (directly or transitively) on request/response * and must be fresh for each HTTP request. */ -function registerRequestResources(Container $container): void -{ +return function (Container $container): void { $container->set('utopia:graphql', function ($utopia) { return $utopia; }, ['utopia']); @@ -1234,4 +1233,4 @@ function registerRequestResources(Container $container): void $container->set('deviceForBuilds', function ($project, Telemetry $telemetry) { return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); }, ['project', 'telemetry']); -} +}; diff --git a/app/init/worker/message.php b/app/init/worker/message.php index 8404b38343..69e08cdf80 100644 --- a/app/init/worker/message.php +++ b/app/init/worker/message.php @@ -39,8 +39,7 @@ use Utopia\Telemetry\Adapter as Telemetry; * These resources depend on the queue message or keep mutable state and * must be fresh for each worker job. */ -function registerWorkerMessageResources(Container $container): void -{ +return function (Container $container): void { $container->set('log', fn () => new Log(), []); $container->set('usage', fn () => new Context(), []); @@ -379,4 +378,4 @@ function registerWorkerMessageResources(Container $container): void return (int) ($plan['executionsRetentionCount'] ?? 100); }, ['project', 'plan']); -} +}; diff --git a/app/realtime.php b/app/realtime.php index 155bd87c2b..05e7fac899 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -50,6 +50,8 @@ use Utopia\WebSocket\Server; */ require_once __DIR__ . '/init.php'; +$registerRequestResources = require __DIR__ . '/init/resources/request.php'; + Runtime::enableCoroutine(SWOOLE_HOOK_ALL); // Log uncaught exceptions in one line instead of relying on Swoole's full backtrace dump @@ -622,7 +624,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, Console::error('Failed to restart pub/sub...'); }); -$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) { +$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $registerRequestResources) { global $container; $request = new Request($request); $response = new Response(new SwooleResponse()); @@ -637,11 +639,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $connectionContainer->set('request', fn () => $request); $connectionContainer->set('response', fn () => $response); - if (function_exists('registerCloudRequestResources')) { - registerCloudRequestResources($connectionContainer); - } else { - registerRequestResources($connectionContainer); - } + $registerRequestResources($connectionContainer); $project = null; $logUser = null; diff --git a/app/worker.php b/app/worker.php index 382ef1281f..a9c0bbe21d 100644 --- a/app/worker.php +++ b/app/worker.php @@ -1,7 +1,8 @@ init()->action(function () use ($worker) { - registerWorkerMessageResources($worker->getContainer()); + $worker->init()->action(function () use ($worker, $registerWorkerMessageResources) { + $registerWorkerMessageResources($worker->getContainer()); }); $container->set('bus', function ($register) use ($worker) { diff --git a/composer.lock b/composer.lock index 2e825d964f..e63659f012 100644 --- a/composer.lock +++ b/composer.lock @@ -3985,16 +3985,16 @@ }, { "name": "utopia-php/di", - "version": "0.3.1", + "version": "0.3.2", "source": { "type": "git", "url": "https://github.com/utopia-php/di.git", - "reference": "68873b7267842315d01d82a83b988bae525eab31" + "reference": "07025d721ed5d9be27932e8e640acf1467fc4b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/di/zipball/68873b7267842315d01d82a83b988bae525eab31", - "reference": "68873b7267842315d01d82a83b988bae525eab31", + "url": "https://api.github.com/repos/utopia-php/di/zipball/07025d721ed5d9be27932e8e640acf1467fc4b9d", + "reference": "07025d721ed5d9be27932e8e640acf1467fc4b9d", "shasum": "" }, "require": { @@ -4030,9 +4030,9 @@ ], "support": { "issues": "https://github.com/utopia-php/di/issues", - "source": "https://github.com/utopia-php/di/tree/0.3.1" + "source": "https://github.com/utopia-php/di/tree/0.3.2" }, - "time": "2026-03-13T05:47:23+00:00" + "time": "2026-03-21T07:42:10+00:00" }, { "name": "utopia-php/dns", @@ -5479,16 +5479,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.11.13", + "version": "1.11.14", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "c97527030060798129f2cb7e1e767671bf09f3bd" + "reference": "ed4faf10fafa1930ed0be3dfe43e41561f2de75b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/c97527030060798129f2cb7e1e767671bf09f3bd", - "reference": "c97527030060798129f2cb7e1e767671bf09f3bd", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/ed4faf10fafa1930ed0be3dfe43e41561f2de75b", + "reference": "ed4faf10fafa1930ed0be3dfe43e41561f2de75b", "shasum": "" }, "require": { @@ -5524,9 +5524,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.11.13" + "source": "https://github.com/appwrite/sdk-generator/tree/1.11.14" }, - "time": "2026-03-20T04:48:54+00:00" + "time": "2026-03-20T10:55:13+00:00" }, { "name": "brianium/paratest", From 89c072e2235e7a23bd2ad760c5dd22bb41b762bb Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 23 Mar 2026 10:20:45 +0530 Subject: [PATCH 061/118] fix analyze --- app/http.php | 5 ++--- app/worker.php | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/http.php b/app/http.php index 7d78db55e1..177c19792d 100644 --- a/app/http.php +++ b/app/http.php @@ -31,7 +31,6 @@ use Utopia\Http\Files; use Utopia\Http\Http; use Utopia\Logger\Log; use Utopia\Logger\Log\User; -use Utopia\Pools\Group; use Utopia\Span\Span; use Utopia\System\System; @@ -294,7 +293,7 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) { $app = new Http($swooleAdapter, 'UTC'); - /** @var Group $pools */ + /** @var \Utopia\Pools\Group $pools */ $pools = $app->getResource('pools'); go(function () use ($app, $pools) { @@ -644,7 +643,7 @@ $http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) { /** @var Utopia\Database\Database $dbForPlatform */ $dbForPlatform = $app->getResource('dbForPlatform'); - /** @var Table $riskyDomains */ + /** @var \Swoole\Table $riskyDomains */ $riskyDomains = $app->getResource('riskyDomains'); Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) { diff --git a/app/worker.php b/app/worker.php index a9c0bbe21d..42a0023bc4 100644 --- a/app/worker.php +++ b/app/worker.php @@ -80,7 +80,7 @@ if (\str_starts_with($workerName, 'databases')) { $queueName = System::getEnv('_APP_QUEUE_NAME', 'v1-' . strtolower($workerName)); } -/** @var Group $pools */ +/** @var \Utopia\Pools\Group $pools */ $pools = $container->get('pools'); $adapter = new Swoole( From d932527561ce9d81bb7c4dfc11a638149cff5a6a Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 23 Mar 2026 10:27:10 +0530 Subject: [PATCH 062/118] add null collacing --- app/realtime.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/realtime.php b/app/realtime.php index 05e7fac899..67abed73c5 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -50,7 +50,7 @@ use Utopia\WebSocket\Server; */ require_once __DIR__ . '/init.php'; -$registerRequestResources = require __DIR__ . '/init/resources/request.php'; +$registerRequestResources ??= require __DIR__ . '/init/resources/request.php'; Runtime::enableCoroutine(SWOOLE_HOOK_ALL); From 4641596a6d0a6fc39da918e2dad733eb6da49f7c Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 23 Mar 2026 10:28:26 +0530 Subject: [PATCH 063/118] use stable --- composer.json | 2 +- composer.lock | 21 +++++++-------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index 145dc75e6c..f4581f285d 100644 --- a/composer.json +++ b/composer.json @@ -67,7 +67,7 @@ "utopia-php/emails": "0.6.*", "utopia-php/dns": "1.6.*", "utopia-php/dsn": "0.2.1", - "utopia-php/framework": "dev-feat/swoole-adapters-and-compression as 0.34.15", + "utopia-php/framework": "0.34.*", "utopia-php/fetch": "0.5.*", "utopia-php/image": "0.8.*", "utopia-php/locale": "0.8.*", diff --git a/composer.lock b/composer.lock index e63659f012..9611d41b88 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": "5b7f63ff3136da1b7db44c6e5f8da030", + "content-hash": "bd82f1acdc462cbe9f3e87769e435ea8", "packages": [ { "name": "adhocore/jwt", @@ -4302,16 +4302,16 @@ }, { "name": "utopia-php/framework", - "version": "dev-feat/swoole-adapters-and-compression", + "version": "0.34.16", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "c91012caa001e4cccb1133f13938ae77478b3a28" + "reference": "2b4021ba3f9d476264ce9fd6703d6c79de9add7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/c91012caa001e4cccb1133f13938ae77478b3a28", - "reference": "c91012caa001e4cccb1133f13938ae77478b3a28", + "url": "https://api.github.com/repos/utopia-php/http/zipball/2b4021ba3f9d476264ce9fd6703d6c79de9add7f", + "reference": "2b4021ba3f9d476264ce9fd6703d6c79de9add7f", "shasum": "" }, "require": { @@ -4350,9 +4350,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/feat/swoole-adapters-and-compression" + "source": "https://github.com/utopia-php/http/tree/0.34.16" }, - "time": "2026-03-19T17:31:18+00:00" + "time": "2026-03-20T10:39:07+00:00" }, { "name": "utopia-php/image", @@ -8474,12 +8474,6 @@ } ], "aliases": [ - { - "package": "utopia-php/framework", - "version": "dev-feat/swoole-adapters-and-compression", - "alias": "0.34.15", - "alias_normalized": "0.34.15.0" - }, { "package": "utopia-php/queue", "version": "dev-feat/di-container-refactor", @@ -8489,7 +8483,6 @@ ], "minimum-stability": "dev", "stability-flags": { - "utopia-php/framework": 20, "utopia-php/queue": 20 }, "prefer-stable": true, From c2988efa086fc42936a99c87b449a846fb1b838e Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 23 Mar 2026 22:51:49 +0530 Subject: [PATCH 064/118] chore: use stable --- composer.json | 16 +----- composer.lock | 145 +++++++++++++++++++++++++++----------------------- 2 files changed, 81 insertions(+), 80 deletions(-) diff --git a/composer.json b/composer.json index f4581f285d..c81e427445 100644 --- a/composer.json +++ b/composer.json @@ -74,11 +74,11 @@ "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", "utopia-php/migration": "1.7.*", - "utopia-php/platform": "0.9.*", + "utopia-php/platform": "0.11.*", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", "utopia-php/preloader": "0.2.*", - "utopia-php/queue": "dev-feat/di-container-refactor as 0.16.0", + "utopia-php/queue": "0.17.*", "utopia-php/servers": "0.3.*", "utopia-php/registry": "0.5.*", "utopia-php/storage": "1.0.*", @@ -96,12 +96,6 @@ "league/csv": "9.14.*", "enshrined/svg-sanitize": "0.22.*" }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/utopia-php/database" - } - ], "require-dev": { "ext-fileinfo": "*", "appwrite/sdk-generator": "*", @@ -113,12 +107,6 @@ "czproject/git-php": "4.*", "laravel/pint": "1.*" }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/utopia-php/database" - } - ], "provide": { "ext-phpiredis": "*" }, diff --git a/composer.lock b/composer.lock index 9611d41b88..69d5c6eeb1 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": "bd82f1acdc462cbe9f3e87769e435ea8", + "content-hash": "e46f86b9588a755147f93cf97b27a3b4", "packages": [ { "name": "adhocore/jwt", @@ -3889,38 +3889,7 @@ "Utopia\\Database\\": "src/Database" } }, - "autoload-dev": { - "psr-4": { - "Tests\\E2E\\": "tests/e2e", - "Tests\\Unit\\": "tests/unit" - } - }, - "scripts": { - "build": [ - "Composer\\Config::disableProcessTimeout", - "docker compose build" - ], - "start": [ - "Composer\\Config::disableProcessTimeout", - "docker compose up -d" - ], - "test": [ - "Composer\\Config::disableProcessTimeout", - "docker compose exec tests vendor/bin/phpunit --configuration phpunit.xml" - ], - "lint": [ - "php -d memory_limit=2G ./vendor/bin/pint --test" - ], - "format": [ - "php -d memory_limit=2G ./vendor/bin/pint" - ], - "check": [ - "./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 2G" - ], - "coverage": [ - "./vendor/bin/coverage-check ./tmp/clover.xml 90" - ] - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -3933,8 +3902,8 @@ "utopia" ], "support": { - "source": "https://github.com/utopia-php/database/tree/5.3.17", - "issues": "https://github.com/utopia-php/database/issues" + "issues": "https://github.com/utopia-php/database/issues", + "source": "https://github.com/utopia-php/database/tree/5.3.17" }, "time": "2026-03-20T01:18:52+00:00" }, @@ -4354,6 +4323,60 @@ }, "time": "2026-03-20T10:39:07+00:00" }, + { + "name": "utopia-php/http", + "version": "0.34.16", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/http.git", + "reference": "2b4021ba3f9d476264ce9fd6703d6c79de9add7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/http/zipball/2b4021ba3f9d476264ce9fd6703d6c79de9add7f", + "reference": "2b4021ba3f9d476264ce9fd6703d6c79de9add7f", + "shasum": "" + }, + "require": { + "ext-swoole": "*", + "php": ">=8.2", + "utopia-php/compression": "0.1.*", + "utopia-php/di": "0.3.*", + "utopia-php/servers": "0.3.*", + "utopia-php/telemetry": "0.2.*", + "utopia-php/validators": "0.2.*" + }, + "require-dev": { + "doctrine/instantiator": "^1.5", + "laravel/pint": "1.*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "1.*", + "phpunit/phpunit": "^9.5.25", + "swoole/ide-helper": "4.8.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple, light and advanced PHP HTTP framework", + "keywords": [ + "framework", + "http", + "php", + "upf" + ], + "support": { + "issues": "https://github.com/utopia-php/http/issues", + "source": "https://github.com/utopia-php/http/tree/0.34.16" + }, + "time": "2026-03-20T10:39:07+00:00" + }, { "name": "utopia-php/image", "version": "0.8.4", @@ -4673,31 +4696,30 @@ }, { "name": "utopia-php/platform", - "version": "0.9.2", + "version": "0.11.0", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "490e9aa716e0f8007f9e953150a776f3e107c57e" + "reference": "cfe3dc32038345e99989101e88450f36abc449ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/490e9aa716e0f8007f9e953150a776f3e107c57e", - "reference": "490e9aa716e0f8007f9e953150a776f3e107c57e", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/cfe3dc32038345e99989101e88450f36abc449ca", + "reference": "cfe3dc32038345e99989101e88450f36abc449ca", "shasum": "" }, "require": { "ext-json": "*", "ext-redis": "*", - "php": ">=8.2", - "utopia-php/cli": "0.23.0", - "utopia-php/framework": "0.34.*", - "utopia-php/queue": "0.16.*", - "utopia-php/validators": "0.2.*" + "php": ">=8.1", + "utopia-php/cli": "0.23.*", + "utopia-php/http": "0.34.*", + "utopia-php/queue": "0.17.*", + "utopia-php/servers": "0.3.*" }, "require-dev": { - "laravel/pint": "1.*", - "phpstan/phpstan": "2.*", - "phpunit/phpunit": "9.*" + "laravel/pint": "1.2.*", + "phpunit/phpunit": "^9.3" }, "type": "library", "autoload": { @@ -4719,9 +4741,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.9.2" + "source": "https://github.com/utopia-php/platform/tree/0.11.0" }, - "time": "2026-03-15T13:53:33+00:00" + "time": "2026-03-23T17:20:38+00:00" }, { "name": "utopia-php/pools", @@ -4831,16 +4853,16 @@ }, { "name": "utopia-php/queue", - "version": "dev-feat/di-container-refactor", + "version": "0.17.0", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "21b6e385d022631cc45f252186b0610254393e69" + "reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/21b6e385d022631cc45f252186b0610254393e69", - "reference": "21b6e385d022631cc45f252186b0610254393e69", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/0fbc7d7312f5cf76ec112513fb93317000901f5f", + "reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f", "shasum": "" }, "require": { @@ -4892,9 +4914,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/feat/di-container-refactor" + "source": "https://github.com/utopia-php/queue/tree/0.17.0" }, - "time": "2026-03-19T04:36:39+00:00" + "time": "2026-03-23T16:21:31+00:00" }, { "name": "utopia-php/registry", @@ -8473,18 +8495,9 @@ "time": "2024-11-07T12:36:22+00:00" } ], - "aliases": [ - { - "package": "utopia-php/queue", - "version": "dev-feat/di-container-refactor", - "alias": "0.16.0", - "alias_normalized": "0.16.0.0" - } - ], + "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "utopia-php/queue": 20 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { From ea6a05be4fe7c624b5074df2921d81d64298353b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 24 Mar 2026 07:51:23 +0530 Subject: [PATCH 065/118] fix analyze --- tests/unit/Event/MockPublisher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/Event/MockPublisher.php b/tests/unit/Event/MockPublisher.php index 0b812e7032..6b29965334 100644 --- a/tests/unit/Event/MockPublisher.php +++ b/tests/unit/Event/MockPublisher.php @@ -9,7 +9,7 @@ class MockPublisher implements Publisher { private array $events = []; - public function enqueue(Queue $queue, array $payload): bool + public function enqueue(Queue $queue, array $payload, bool $priority = false): bool { if (!isset($this->events[$queue->name])) { $this->events[$queue->name] = []; From fbce66d50041977d03b0e39dca3afe7f0f875390 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 24 Mar 2026 10:19:34 +0530 Subject: [PATCH 066/118] fix merge conflict --- app/init/resources/request.php | 4 +- docker-compose.yml | 70 +++++++++++++++++++++++++++------- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/app/init/resources/request.php b/app/init/resources/request.php index 8e3c39e9dc..d526ecbd40 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -29,6 +29,8 @@ use Appwrite\Usage\Context as UsageContext; use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; +use Utopia\Agents\Adapters\Ollama; +use Utopia\Agents\Agent; use Utopia\Audit\Adapter\Database as AdapterDatabase; use Utopia\Audit\Audit; use Utopia\Auth\Hashes\Argon2; @@ -55,8 +57,6 @@ use Utopia\Queue\Publisher; use Utopia\Storage\Device; use Utopia\System\System; use Utopia\Telemetry\Adapter as Telemetry; -use Utopia\Agents\Adapters\Ollama; -use Utopia\Agents\Agent; use Utopia\Validator\URL; use Utopia\Validator\WhiteList; diff --git a/docker-compose.yml b/docker-compose.yml index e93146c779..f3cb982526 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -112,6 +112,8 @@ services: condition: service_healthy coredns: condition: service_started + ollama: + condition: service_started entrypoint: - php - -e @@ -159,6 +161,12 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_DB_ADAPTER_VECTORSDB + - _APP_DB_HOST_VECTORSDB + - _APP_DB_PORT_VECTORSDB + - _APP_DB_SCHEMA_VECTORSDB + - _APP_DB_USER_VECTORSDB + - _APP_DB_PASS_VECTORSDB - _APP_SMTP_HOST - _APP_SMTP_PORT - _APP_SMTP_SECURE @@ -295,6 +303,7 @@ services: depends_on: - ${_APP_DB_HOST:-mongodb} - redis + - ollama environment: - _APP_ENV - _APP_WORKER_PER_CORE @@ -311,6 +320,12 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_DB_ADAPTER_VECTORSDB + - _APP_DB_HOST_VECTORSDB + - _APP_DB_PORT_VECTORSDB + - _APP_DB_SCHEMA_VECTORSDB + - _APP_DB_USER_VECTORSDB + - _APP_DB_PASS_VECTORSDB - _APP_USAGE_STATS - _APP_LOGGING_CONFIG - _APP_LOGGING_CONFIG_REALTIME @@ -330,6 +345,7 @@ services: depends_on: - redis - ${_APP_DB_HOST:-mongodb} + - ollama environment: - _APP_ENV - _APP_WORKER_PER_CORE @@ -363,6 +379,7 @@ services: - ${_APP_DB_HOST:-mongodb} - request-catcher-sms - request-catcher-webhook + - ollama environment: - _APP_ENV - _APP_WORKER_PER_CORE @@ -393,6 +410,7 @@ services: depends_on: - redis - ${_APP_DB_HOST:-mongodb} + - ollama volumes: - appwrite-uploads:/storage/uploads:rw - appwrite-cache:/storage/cache:rw @@ -402,6 +420,7 @@ services: - appwrite-certificates:/storage/certificates:rw - ./app:/usr/src/code/app - ./src:/usr/src/code/src + environment: - _APP_ENV - _APP_WORKER_PER_CORE @@ -458,9 +477,11 @@ services: volumes: - ./app:/usr/src/code/app - ./src:/usr/src/code/src + depends_on: - redis - ${_APP_DB_HOST:-mongodb} + - ollama environment: - _APP_ENV - _APP_WORKER_PER_CORE @@ -476,6 +497,12 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_DB_ADAPTER_VECTORSDB + - _APP_DB_HOST_VECTORSDB + - _APP_DB_PORT_VECTORSDB + - _APP_DB_SCHEMA_VECTORSDB + - _APP_DB_USER_VECTORSDB + - _APP_DB_PASS_VECTORSDB - _APP_LOGGING_CONFIG - _APP_WORKERS_NUM - _APP_QUEUE_NAME @@ -497,6 +524,7 @@ services: depends_on: - redis - ${_APP_DB_HOST:-mongodb} + - ollama environment: - _APP_ENV - _APP_WORKER_PER_CORE @@ -629,6 +657,7 @@ services: depends_on: - redis - ${_APP_DB_HOST:-mongodb} + - ollama volumes: - appwrite-config:/storage/config:rw - appwrite-certificates:/storage/certificates:rw @@ -848,6 +877,7 @@ services: - ./app:/usr/src/code/app - ./src:/usr/src/code/src - ./tests:/usr/src/code/tests + depends_on: - ${_APP_DB_HOST:-mongodb} environment: @@ -1044,6 +1074,7 @@ services: depends_on: - redis - ${_APP_DB_HOST:-mongodb} + - ollama environment: - _APP_ENV - _APP_WORKER_PER_CORE @@ -1077,6 +1108,7 @@ services: depends_on: - ${_APP_DB_HOST:-mongodb} - redis + - ollama environment: - _APP_ENV - _APP_WORKER_PER_CORE @@ -1107,6 +1139,7 @@ services: depends_on: - ${_APP_DB_HOST:-mongodb} - redis + - ollama environment: - _APP_ENV - _APP_WORKER_PER_CORE @@ -1137,6 +1170,7 @@ services: depends_on: - ${_APP_DB_HOST:-mongodb} - redis + - ollama environment: - _APP_ENV - _APP_WORKER_PER_CORE @@ -1228,7 +1262,6 @@ services: start_period: 5s mariadb: - profiles: ["mariadb"] image: mariadb:10.11 # fix issues when upgrading using: mysql_upgrade -u root -p container_name: appwrite-mariadb <<: *x-logging @@ -1244,7 +1277,7 @@ services: - MYSQL_USER=${_APP_DB_USER} - MYSQL_PASSWORD=${_APP_DB_PASS} - MARIADB_AUTO_UPGRADE=1 - command: "mysqld --innodb-flush-method=fsync --max-connections=500" + command: "mysqld --innodb-flush-method=fsync" healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 5s @@ -1252,7 +1285,6 @@ services: retries: 12 mongodb: - profiles: ["mongodb"] image: mongo:8.2.5 container_name: appwrite-mongodb <<: *x-logging @@ -1288,32 +1320,41 @@ services: retries: 10 start_period: 30s - - postgresql: - profiles: ["postgresql"] - build: - context: ./tests/resources/postgresql - args: - POSTGRES_VERSION: 17 + image: appwrite/postgres:0.1.0 container_name: appwrite-postgresql <<: *x-logging networks: - appwrite volumes: - - appwrite-postgresql:/var/lib/postgresql:rw + - appwrite-postgresql:/var/lib/postgresql/18/data:rw ports: - "5432:5432" environment: - POSTGRES_DB=${_APP_DB_SCHEMA} - POSTGRES_USER=${_APP_DB_USER} - POSTGRES_PASSWORD=${_APP_DB_PASS} - command: "postgres -N 500" healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER}"] + test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER} -d ${_APP_DB_SCHEMA}"] interval: 5s timeout: 5s - retries: 12 + retries: 10 + start_period: 10s + command: "postgres" + + ollama: + image: appwrite/ollama:0.1.1 + container_name: ollama + ports: + - "11434:11434" + restart: unless-stopped + environment: + MODELS: ${_APP_EMBEDDING_MODELS:-embeddinggemma} + OLLAMA_KEEP_ALIVE: 24h + volumes: + - appwrite-models:/root/.ollama + networks: + - appwrite redis: image: redis:7.4.7-alpine @@ -1436,3 +1477,4 @@ volumes: appwrite-sites: appwrite-builds: appwrite-config: + appwrite-models: \ No newline at end of file From 36d8ad6e7c0cc75a199376e087f8d198d95c44fe Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 24 Mar 2026 10:21:40 +0530 Subject: [PATCH 067/118] lock file --- composer.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.lock b/composer.lock index e0da00e087..9751160bc5 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": "0f4fb4d8b4c1f365de25036ebbe3d6ac", + "content-hash": "314daf5f15ce3755487bd8044be1cf95", "packages": [ { "name": "adhocore/jwt", @@ -4580,16 +4580,16 @@ }, { "name": "utopia-php/migration", - "version": "1.7.0", + "version": "1.8.3", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "97583ae502e40621ea91a71de19d053c5ae2e558" + "reference": "8633523b3343d492427331b6eec53f020f6ab7a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/97583ae502e40621ea91a71de19d053c5ae2e558", - "reference": "97583ae502e40621ea91a71de19d053c5ae2e558", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/8633523b3343d492427331b6eec53f020f6ab7a7", + "reference": "8633523b3343d492427331b6eec53f020f6ab7a7", "shasum": "" }, "require": { @@ -4629,9 +4629,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.7.0" + "source": "https://github.com/utopia-php/migration/tree/1.8.3" }, - "time": "2026-03-10T06:36:27+00:00" + "time": "2026-03-19T09:18:47+00:00" }, { "name": "utopia-php/mongo", From 0c33d981a7ff38c846e9ea647948d5a7c5c3db27 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 24 Mar 2026 10:47:41 +0530 Subject: [PATCH 068/118] fix analyze --- app/init/resources/request.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/init/resources/request.php b/app/init/resources/request.php index d526ecbd40..f3afe05baf 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -728,7 +728,7 @@ return function (Container $container): void { $cacheKey = \sprintf( '%s-cache-%s:%s:%s:project:%s:functions:events', $dbForProject->getCacheName(), - $hostname ?? '', + $hostname, $dbForProject->getNamespace(), $dbForProject->getTenant(), $project->getId() From ff903e7cbb7cd36171328b8656a3cd81519c3342 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sat, 28 Mar 2026 20:39:08 +0530 Subject: [PATCH 069/118] lock file --- composer.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/composer.lock b/composer.lock index a5b382d53f..01fe296886 100644 --- a/composer.lock +++ b/composer.lock @@ -3403,16 +3403,16 @@ }, { "name": "utopia-php/agents", - "version": "1.3.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/utopia-php/agents.git", - "reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30" + "reference": "052227953678a30ecc4b5467401fcb0b2386471e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/agents/zipball/06064fd9fb19b77ae45a12ec7bcbc17670912c30", - "reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30", + "url": "https://api.github.com/repos/utopia-php/agents/zipball/052227953678a30ecc4b5467401fcb0b2386471e", + "reference": "052227953678a30ecc4b5467401fcb0b2386471e", "shasum": "" }, "require": { @@ -3450,9 +3450,9 @@ ], "support": { "issues": "https://github.com/utopia-php/agents/issues", - "source": "https://github.com/utopia-php/agents/tree/1.3.0" + "source": "https://github.com/utopia-php/agents/tree/1.2.1" }, - "time": "2026-03-26T03:51:11+00:00" + "time": "2026-02-24T06:03:55+00:00" }, { "name": "utopia-php/analytics", @@ -5502,16 +5502,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.14.0", + "version": "1.14.1", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e" + "reference": "876f8fea0388b31c1896c967d3346d308fbd911b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/7e7e257b10a8c1384a237e7d8d73452e2108901e", - "reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/876f8fea0388b31c1896c967d3346d308fbd911b", + "reference": "876f8fea0388b31c1896c967d3346d308fbd911b", "shasum": "" }, "require": { @@ -5547,9 +5547,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.14.0" + "source": "https://github.com/appwrite/sdk-generator/tree/1.14.1" }, - "time": "2026-03-26T12:50:11+00:00" + "time": "2026-03-28T14:52:08+00:00" }, { "name": "brianium/paratest", From 66ba483b6aed8ebb7da86ad54c1f22a05d2d3957 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 11:48:41 +0530 Subject: [PATCH 070/118] chore: remove inapplicable phpstan baseline entries from 1.9.x merge $register variable.undefined (app/http.php) and binary op (app/worker.php) suppressions don't apply to this branch's rewritten DI container code. --- phpstan-baseline.neon | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 450eac9b31..0a731aa511 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -120,12 +120,6 @@ parameters: count: 1 path: app/controllers/shared/api.php - - - message: '#^Variable \$register might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: app/http.php - - message: '#^Variable \$tag on left side of \?\? always exists and is always null\.$#' identifier: nullCoalesce.variable @@ -282,12 +276,6 @@ parameters: count: 1 path: src/Appwrite/Functions/EventProcessor.php - - - message: '#^Binary operation "\*" between \-1 and string results in an error\.$#' - identifier: binaryOp.invalid - count: 3 - path: app/worker.php - - message: '#^Anonymous function has an unused use \$context\.$#' identifier: closure.unusedUse From cc04c682b05b2815d5013fa2032fb1eae9bf12c5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 11:49:43 +0530 Subject: [PATCH 071/118] chore: use phpstan-baseline.neon from 1.9.x --- phpstan-baseline.neon | 130 +++++++++--------------------------------- 1 file changed, 26 insertions(+), 104 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0a731aa511..5da64a1c97 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -115,10 +115,10 @@ parameters: path: app/controllers/mock.php - - message: '#^Call to an undefined method Utopia\\Database\\Document\:\:getRoles\(\)\.$#' - identifier: method.notFound - count: 1 - path: app/controllers/shared/api.php + message: '#^Variable \$register might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: app/http.php - message: '#^Variable \$tag on left side of \?\? always exists and is always null\.$#' @@ -156,6 +156,12 @@ parameters: count: 1 path: app/init/registers.php + - + message: '#^Variable \$hostname on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.variable + count: 1 + path: app/init/resources.php + - message: '#^Anonymous function has an unused use \$register\.$#' identifier: closure.unusedUse @@ -175,106 +181,10 @@ parameters: path: app/realtime.php - - message: '#^PHPDoc tag @return with type string is incompatible with native type int\.$#' - identifier: return.phpDocType - count: 1 - path: src/Appwrite/Auth/OAuth2.php - - - - message: '#^PHPDoc tag @param references unknown parameter\: \$token$#' - identifier: parameter.notFound - count: 1 - path: src/Appwrite/Auth/OAuth2/Disqus.php - - - - message: '#^PHPDoc tag @param references unknown parameter\: \$value$#' - identifier: parameter.notFound - count: 1 - path: src/Appwrite/Auth/Validator/PersonalData.php - - - - message: '#^PHPDoc tag @param has invalid value \(DeviceDetector\)\: Unexpected token "\\n ", expected variable at offset 32 on line 2$#' - identifier: phpDoc.parseError - count: 1 - path: src/Appwrite/Detector/Detector.php - - - - message: '#^PHPDoc tag @param has invalid value \(string\)\: Unexpected token "\\n ", expected variable at offset 24 on line 2$#' - identifier: phpDoc.parseError - count: 1 - path: src/Appwrite/Detector/Detector.php - - - - message: '#^PHPDoc tag @var above a method has no effect\.$#' - identifier: varTag.misplaced - count: 1 - path: src/Appwrite/Docker/Compose.php - - - - message: '#^PHPDoc tag @var above a method has no effect\.$#' - identifier: varTag.misplaced - count: 1 - path: src/Appwrite/Docker/Compose/Service.php - - - - message: '#^PHPDoc tag @var above a method has no effect\.$#' - identifier: varTag.misplaced - count: 1 - path: src/Appwrite/Docker/Env.php - - - - message: '#^PHPDoc tag @param has invalid value \(int port\)\: Unexpected token "port", expected variable at offset 50 on line 4$#' - identifier: phpDoc.parseError - count: 1 - path: src/Appwrite/Event/Mail.php - - - - message: '#^PHPDoc tag @param references unknown parameter\: \$password$#' - identifier: parameter.notFound - count: 1 - path: src/Appwrite/Event/Mail.php - - - - message: '#^PHPDoc tag @return with type string is incompatible with native type Appwrite\\Event\\Mail\.$#' - identifier: return.phpDocType - count: 1 - path: src/Appwrite/Event/Mail.php - - - - message: '#^Method Appwrite\\Event\\Message\\Usage\:\:fromArray\(\) should return static\(Appwrite\\Event\\Message\\Usage\) but returns Appwrite\\Event\\Message\\Usage\.$#' - identifier: return.type - count: 1 - path: src/Appwrite/Event/Message/Usage.php - - - - message: '#^PHPDoc tag @param references unknown parameter\: \$message$#' - identifier: parameter.notFound - count: 1 - path: src/Appwrite/Event/Messaging.php - - - - message: '#^PHPDoc tag @return with type string is incompatible with native type Utopia\\Database\\Document\.$#' - identifier: return.phpDocType - count: 1 - path: src/Appwrite/Event/Messaging.php - - - - message: '#^Method Appwrite\\Functions\\EventProcessor\:\:getFunctionsEvents\(\) should return array\ but returns array\\>\.$#' - identifier: return.type - count: 1 - path: src/Appwrite/Functions/EventProcessor.php - - - - message: '#^Method Appwrite\\Functions\\EventProcessor\:\:getWebhooksEvents\(\) should return array\ but returns array\\>\.$#' - identifier: return.type - count: 1 - path: src/Appwrite/Functions/EventProcessor.php - - - - message: '#^Variable \$hostname on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: src/Appwrite/Functions/EventProcessor.php + message: '#^Binary operation "\*" between \-1 and string results in an error\.$#' + identifier: binaryOp.invalid + count: 3 + path: app/worker.php - message: '#^Anonymous function has an unused use \$context\.$#' @@ -642,6 +552,18 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php + - + message: '#^Undefined variable\: \$cpus$#' + identifier: variable.undefined + count: 3 + path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php + + - + message: '#^Undefined variable\: \$memory$#' + identifier: variable.undefined + count: 3 + path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php + - message: '#^Variable \$deployment might not be defined\.$#' identifier: variable.undefined From eb8455bd767cd572c0f625a4464d0fd5d104adec Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 14:22:42 +0530 Subject: [PATCH 072/118] revert --- app/controllers/general.php | 5 +- app/init/resources.php | 406 ------------------------------------ 2 files changed, 4 insertions(+), 407 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 96a6db9d00..79929816d9 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -748,8 +748,11 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S } if (\is_array($values)) { + $count = 0; foreach ($values as $value) { - $response->addHeader($name, $value); + $override = $count === 0; + $response->addHeader($name, $value, override: $override); + $count++; } } else { $response->addHeader($name, $values); diff --git a/app/init/resources.php b/app/init/resources.php index 2c106f0677..75837f985b 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -90,412 +90,6 @@ $container->set('platform', function () { return Config::getParam('platform', []); }, []); -/** - * List of allowed request hostnames for the request. - */ -Http::setResource('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) { - $allowed = [...($platform['hostnames'] ?? [])]; - - /* Add platform configured hostnames */ - if (! $project->isEmpty() && $project->getId() !== 'console') { - $platforms = $project->getAttribute('platforms', []); - $hostnames = Platform::getHostnames($platforms); - $allowed = [...$allowed, ...$hostnames]; - } - - /* Add the request hostname if a dev key is found */ - if (! $devKey->isEmpty()) { - $allowed[] = $request->getHostname(); - } - - $originHostname = parse_url($request->getOrigin(), PHP_URL_HOST); - $refererHostname = parse_url($request->getReferer(), PHP_URL_HOST); - - $hostname = $originHostname; - if (empty($hostname)) { - $hostname = $refererHostname; - } - - /* Add request hostname for preflight requests */ - if ($request->getMethod() === 'OPTIONS') { - $allowed[] = $hostname; - } - - /* Allow the request origin of rule */ - if (! $rule->isEmpty() && ! empty($rule->getAttribute('domain', ''))) { - $allowed[] = $rule->getAttribute('domain', ''); - } - - /* Allow the request origin if a dev key is found */ - if (! $devKey->isEmpty() && ! empty($hostname)) { - $allowed[] = $hostname; - } - - return array_unique($allowed); -}, ['platform', 'project', 'rule', 'devKey', 'request']); - -/** - * List of allowed request schemes for the request. - */ -Http::setResource('allowedSchemes', function (array $platform, Document $project) { - $allowed = [...($platform['schemas'] ?? [])]; - - if (! $project->isEmpty() && $project->getId() !== 'console') { - /* Add hardcoded schemes */ - $allowed[] = 'exp'; - $allowed[] = 'appwrite-callback-' . $project->getId(); - - /* Add platform configured schemes */ - $platforms = $project->getAttribute('platforms', []); - $schemes = Platform::getSchemes($platforms); - $allowed = [...$allowed, ...$schemes]; - } - - return array_unique($allowed); -}, ['platform', 'project']); - -/** - * Rule associated with a request origin. - */ -Http::setResource('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) { - $domain = \parse_url($request->getOrigin(), PHP_URL_HOST); - - if (empty($domain)) { - $domain = \parse_url($request->getReferer(), PHP_URL_HOST); - } - - if (empty($domain)) { - return new Document(); - } - - // TODO: (@Meldiron) Remove after 1.7.x migration - $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) { - if ($isMd5) { - return $dbForPlatform->getDocument('rules', md5($domain)); - } - - return $dbForPlatform->findOne('rules', [ - Query::equal('domain', [$domain]), - ]) ?? new Document(); - }); - - $permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence(); - - // Temporary implementation until custom wildcard domains are an official feature - // Allow trusted projects; Used for Console (website) previews - if (! $permitsCurrentProject && ! $rule->isEmpty() && ! empty($rule->getAttribute('projectId', ''))) { - $trustedProjects = []; - foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) { - if (empty($trustedProject)) { - continue; - } - $trustedProjects[] = $trustedProject; - } - if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects)) { - $permitsCurrentProject = true; - } - } - - if (! $permitsCurrentProject) { - return new Document(); - } - - return $rule; -}, ['request', 'dbForPlatform', 'project', 'authorization']); - -/** - * CORS service - */ -Http::setResource('cors', function (array $allowedHostnames) { - $corsConfig = Config::getParam('cors'); - - return new Cors( - $allowedHostnames, - allowedMethods: $corsConfig['allowedMethods'], - allowedHeaders: $corsConfig['allowedHeaders'], - allowCredentials: true, - exposedHeaders: $corsConfig['exposedHeaders'], - ); -}, ['allowedHostnames']); - -Http::setResource('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) { - if (! $devKey->isEmpty()) { - return new URL(); - } - - return new Origin($allowedHostnames, $allowedSchemes); -}, ['devKey', 'allowedHostnames', 'allowedSchemes']); - -Http::setResource('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) { - if (! $devKey->isEmpty()) { - return new URL(); - } - - return new Redirect($allowedHostnames, $allowedSchemes); -}, ['devKey', 'allowedHostnames', 'allowedSchemes']); - -Http::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) { - /** - * Handles user authentication and session validation. - * - * This function follows a series of steps to determine the appropriate user session - * based on cookies, headers, and JWT tokens. - * - * Process: - * 1. Checks the cookie based on mode: - * - If in admin mode, uses console project id for key. - * - Otherwise, sets the key using the project ID - * 2. If no cookie is found, attempts to retrieve the fallback header `x-fallback-cookies`. - * - If this method is used, returns the header: `X-Debug-Fallback: true`. - * 3. Fetches the user document from the appropriate database based on the mode. - * 4. If the user document is empty or the session key cannot be verified, sets an empty user document. - * 5. Regardless of the results from steps 1-4, attempts to fetch the JWT token. - * 6. If the JWT user has a valid session ID, updates the user variable with the user from `projectDB`, - * overwriting the previous value. - * 7. If account API key is passed, use user of the account API key as long as user ID header matches too - */ - $authorization->setDefaultStatus(true); - - $store->setKey('a_session_' . $project->getId()); - - if ($mode === APP_MODE_ADMIN) { - $store->setKey('a_session_' . $console->getId()); - } - - $store->decode( - $request->getCookie( - $store->getKey(), // Get sessions - $request->getCookie($store->getKey() . '_legacy', '') - ) - ); - - // Get session from header for SSR clients - if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) { - $sessionHeader = $request->getHeader('x-appwrite-session', ''); - - if (! empty($sessionHeader)) { - $store->decode($sessionHeader); - } - } - - // Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies - if ($response) { // if in http context - add debug header - $response->addHeader('X-Debug-Fallback', 'false'); - } - - if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) { - if ($response) { - $response->addHeader('X-Debug-Fallback', 'true'); - } - $fallback = $request->getHeader('x-fallback-cookies', ''); - $fallback = \json_decode($fallback, true); - $store->decode(((is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : '')); - } - - $user = null; - if ($mode === APP_MODE_ADMIN) { - /** @var User $user */ - $user = $dbForPlatform->getDocument('users', $store->getProperty('id', '')); - } else { - if ($project->isEmpty()) { - $user = new User([]); - } else { - if (! empty($store->getProperty('id', ''))) { - if ($project->getId() === 'console') { - /** @var User $user */ - $user = $dbForPlatform->getDocument('users', $store->getProperty('id', '')); - } else { - /** @var User $user */ - $user = $dbForProject->getDocument('users', $store->getProperty('id', '')); - } - } - } - } - - if ( - ! $user || - $user->isEmpty() // Check a document has been found in the DB - || ! $user->sessionVerify($store->getProperty('secret', ''), $proofForToken) - ) { // Validate user has valid login token - $user = new User([]); - } - - $authJWT = $request->getHeader('x-appwrite-jwt', ''); - if (! empty($authJWT) && ! $project->isEmpty()) { // JWT authentication - if (! $user->isEmpty()) { - throw new Exception(Exception::USER_JWT_AND_COOKIE_SET); - } - - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); - try { - $payload = $jwt->decode($authJWT); - } catch (JWTException $error) { - throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage()); - } - - $jwtUserId = $payload['userId'] ?? ''; - if (! empty($jwtUserId)) { - if ($mode === APP_MODE_ADMIN) { - /** @var User $user */ - $user = $dbForPlatform->getDocument('users', $jwtUserId); - } else { - /** @var User $user */ - $user = $dbForProject->getDocument('users', $jwtUserId); - } - } - $jwtSessionId = $payload['sessionId'] ?? ''; - if (! empty($jwtSessionId)) { - if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token - $user = new User([]); - } - } - } - - // Account based on account API key - $accountKey = $request->getHeader('x-appwrite-key', ''); - $accountKeyUserId = $request->getHeader('x-appwrite-user', ''); - if (! empty($accountKeyUserId) && ! empty($accountKey)) { - if (! $user->isEmpty()) { - throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET); - } - - /** @var User $accountKeyUser */ - $accountKeyUser = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId)); - if (! $accountKeyUser->isEmpty()) { - $key = $accountKeyUser->find( - key: 'secret', - find: $accountKey, - subject: 'keys' - ); - - if (! empty($key)) { - $expire = $key->getAttribute('expire'); - if (! empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) { - throw new Exception(Exception::ACCOUNT_KEY_EXPIRED); - } - - $user = $accountKeyUser; - } - } - } - - // Impersonation: if current user has impersonator capability and headers are set, act as another user - $impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', ''); - $impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', ''); - $impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', ''); - if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) { - $userDb = (APP_MODE_ADMIN === $mode || $project->getId() === 'console') ? $dbForPlatform : $dbForProject; - $targetUser = null; - if (!empty($impersonateUserId)) { - $targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->getDocument('users', $impersonateUserId)); - } elseif (!empty($impersonateEmail)) { - $targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->findOne('users', [Query::equal('email', [\strtolower($impersonateEmail)])])); - } elseif (!empty($impersonatePhone)) { - $targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->findOne('users', [Query::equal('phone', [$impersonatePhone])])); - } - if ($targetUser !== null && !$targetUser->isEmpty()) { - $impersonator = clone $user; - $user = clone $targetUser; - $user->setAttribute('impersonatorUserId', $impersonator->getId()); - $user->setAttribute('impersonatorUserInternalId', $impersonator->getSequence()); - $user->setAttribute('impersonatorUserName', $impersonator->getAttribute('name', '')); - $user->setAttribute('impersonatorUserEmail', $impersonator->getAttribute('email', '')); - $user->setAttribute('impersonatorAccessedAt', $impersonator->getAttribute('accessedAt', 0)); - } - } - - $dbForProject->setMetadata('user', $user->getId()); - $dbForPlatform->setMetadata('user', $user->getId()); - - return $user; -}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']); - -Http::setResource('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) { - /** @var Appwrite\Utopia\Request $request */ - /** @var Utopia\Database\Database $dbForPlatform */ - /** @var Utopia\Database\Document $console */ - $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', '')); - // Realtime channel "project" can send project=Query array - if (! \is_string($projectId)) { - $projectId = $request->getHeader('x-appwrite-project', ''); - } - - // Backwards compatibility for new services, originally project resources - // These endpoints moved from /v1/projects/:projectId/ to /v1/ - // When accessed via the old alias path, extract projectId from the URI - $deprecatedProjectPathPrefix = '/v1/projects/'; - $route = $utopia->match($request); - if (!empty($route)) { - $isDeprecatedAlias = \str_starts_with($request->getURI(), $deprecatedProjectPathPrefix) && - !\str_starts_with($route->getPath(), $deprecatedProjectPathPrefix); - - if ($isDeprecatedAlias) { - $projectId = \explode('/', $request->getURI(), 5)[3] ?? ''; - } - } - - if (empty($projectId) || $projectId === 'console') { - return $console; - } - - $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); - - return $project; -}, ['dbForPlatform', 'request', 'console', 'authorization', 'utopia']); - -Http::setResource('session', function (User $user, Store $store, Token $proofForToken) { - if ($user->isEmpty()) { - return; - } - - $sessions = $user->getAttribute('sessions', []); - $sessionId = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken); - - if (! $sessionId) { - return; - } - foreach ($sessions as $session) { - /** @var Document $session */ - if ($sessionId === $session->getId()) { - return $session; - } - } - -}, ['user', 'store', 'proofForToken']); - -Http::setResource('store', function (): Store { - return new Store(); -}); - -Http::setResource('proofForPassword', function (): Password { - $hash = new Argon2(); - $hash - ->setMemoryCost(7168) - ->setTimeCost(5) - ->setThreads(1); - - $password = new Password(); - $password - ->setHash($hash); - - return $password; -}); - -Http::setResource('proofForToken', function (): Token { - $token = new Token(); - $token->setHash(new Sha()); - - return $token; -}); - -Http::setResource('proofForCode', function (): Code { - $code = new Code(); - $code->setHash(new Sha()); - - return $code; -}); - $container->set('console', function () { return new Document(Config::getParam('console')); }, []); From d9c606a1c259b9751d524d82bfde26d3b9d17c15 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 1 Apr 2026 13:15:22 +0400 Subject: [PATCH 073/118] fix(oauth): update Yahoo OAuth scopes from deprecated Social Directory API to OIDC The Yahoo OAuth provider was using deprecated Social Directory API scopes ('sdct-r' and 'sdpp-w') which are no longer valid and causing authentication failures with the error: invalid_scope Changes: - Replace deprecated scopes 'sdct-r' (Social Directory Contacts Read) and 'sdpp-w' (Social Directory Profile Write) with standard OIDC scopes - Add 'openid' scope for OpenID Connect authentication - Add 'profile' scope for basic profile information - Add 'email' scope for email address access These new scopes align with Yahoo's OpenID Connect implementation and are listed in their discovery document at: https://api.login.yahoo.com/.well-known/openid-configuration The Yahoo adapter already uses the OIDC userinfo endpoint (https://api.login.yahoo.com/openid/v1/userinfo), so these scopes are the correct choice for authentication. Custom scopes passed via the API are still supported and will be merged with these defaults via the base OAuth2 class constructor. Fixes: Yahoo OAuth authentication returning 'invalid_scope' error --- src/Appwrite/Auth/OAuth2/Yahoo.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Auth/OAuth2/Yahoo.php b/src/Appwrite/Auth/OAuth2/Yahoo.php index c70a2fb6c9..1a6c6b860d 100644 --- a/src/Appwrite/Auth/OAuth2/Yahoo.php +++ b/src/Appwrite/Auth/OAuth2/Yahoo.php @@ -23,8 +23,9 @@ class Yahoo extends OAuth2 * @var array */ protected array $scopes = [ - 'sdct-r', - 'sdpp-w', + 'openid', + 'profile', + 'email', ]; /** From fb26da5df15ef9799fe1babe6ecdff7b319ef457 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 15:15:48 +0530 Subject: [PATCH 074/118] analyze fixes --- app/controllers/general.php | 7 +------ phpstan-baseline.neon | 30 ------------------------------ 2 files changed, 1 insertion(+), 36 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 79929816d9..c3653ee3d5 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -748,12 +748,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S } if (\is_array($values)) { - $count = 0; - foreach ($values as $value) { - $override = $count === 0; - $response->addHeader($name, $value, override: $override); - $count++; - } + $response->addHeader($name, \implode(', ', $values)); } else { $response->addHeader($name, $values); } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8e9d8a5a38..29a3b44b35 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -114,12 +114,6 @@ parameters: count: 1 path: app/controllers/mock.php - - - message: '#^Variable \$register might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: app/http.php - - message: '#^Variable \$tag on left side of \?\? always exists and is always null\.$#' identifier: nullCoalesce.variable @@ -156,12 +150,6 @@ parameters: count: 1 path: app/init/registers.php - - - message: '#^Variable \$hostname on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: app/init/resources.php - - message: '#^Anonymous function has an unused use \$register\.$#' identifier: closure.unusedUse @@ -180,12 +168,6 @@ parameters: count: 1 path: app/realtime.php - - - message: '#^Binary operation "\*" between \-1 and string results in an error\.$#' - identifier: binaryOp.invalid - count: 3 - path: app/worker.php - - message: '#^Anonymous function has an unused use \$context\.$#' identifier: closure.unusedUse @@ -438,18 +420,6 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php - - - message: '#^Undefined variable\: \$cpus$#' - identifier: variable.undefined - count: 3 - path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php - - - - message: '#^Undefined variable\: \$memory$#' - identifier: variable.undefined - count: 3 - path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php - - message: '#^Variable \$deployment might not be defined\.$#' identifier: variable.undefined From cba7e538984d17bd5e484a62eb2e17f9a08d0459 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 15:37:34 +0530 Subject: [PATCH 075/118] chore: fix composer --- app/cli.php | 4 ++-- app/worker.php | 2 +- composer.json | 2 +- composer.lock | 26 +++++++++++++------------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/cli.php b/app/cli.php index 9af261bb1a..cf655143a4 100644 --- a/app/cli.php +++ b/app/cli.php @@ -46,7 +46,7 @@ require_once __DIR__ . '/controllers/general.php'; global $register; $platform = new Appwrite(); -$args = $platform->getEnv('argv'); +$args = $_SERVER['argv'] ?? []; \array_shift($args); if (! isset($args[0])) { @@ -200,7 +200,7 @@ $cli->setResource('getProjectDB', function (Group $pools, Database $dbForPlatfor $cli->setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { $database = null; - return function (?Document $project = null) use ($pools, $cache, $database, $authorization) { + return function (?Document $project = null) use ($pools, $cache, &$database, $authorization) { if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') { $database->setTenant($project->getSequence()); return $database; diff --git a/app/worker.php b/app/worker.php index 42a0023bc4..c1315546b9 100644 --- a/app/worker.php +++ b/app/worker.php @@ -64,7 +64,7 @@ $container->set('certificates', function () { }, []); $platform = new Appwrite(); -$args = $platform->getEnv('argv'); +$args = $_SERVER['argv'] ?? []; if (! isset($args[1])) { Console::error('Missing worker name'); diff --git a/composer.json b/composer.json index 92e379357b..afd20dd88c 100644 --- a/composer.json +++ b/composer.json @@ -74,7 +74,7 @@ "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", "utopia-php/migration": "1.9.*", - "utopia-php/platform": "0.11.*", + "utopia-php/platform": "0.12.*", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", "utopia-php/preloader": "0.2.*", diff --git a/composer.lock b/composer.lock index 92e20dae50..65d85d8ba7 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": "314daf5f15ce3755487bd8044be1cf95", + "content-hash": "6c527ed14720268ceaa300a7fa6d3715", "packages": [ { "name": "adhocore/jwt", @@ -2708,16 +2708,16 @@ }, { "name": "symfony/http-client", - "version": "v7.4.7", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "1010624285470eb60e88ed10035102c75b4ea6af" + "reference": "01933e626c3de76bea1e22641e205e78f6a34342" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/1010624285470eb60e88ed10035102c75b4ea6af", - "reference": "1010624285470eb60e88ed10035102c75b4ea6af", + "url": "https://api.github.com/repos/symfony/http-client/zipball/01933e626c3de76bea1e22641e205e78f6a34342", + "reference": "01933e626c3de76bea1e22641e205e78f6a34342", "shasum": "" }, "require": { @@ -2785,7 +2785,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.7" + "source": "https://github.com/symfony/http-client/tree/v7.4.8" }, "funding": [ { @@ -2805,7 +2805,7 @@ "type": "tidelift" } ], - "time": "2026-03-05T11:16:58+00:00" + "time": "2026-03-30T12:55:43+00:00" }, { "name": "symfony/http-client-contracts", @@ -4696,16 +4696,16 @@ }, { "name": "utopia-php/platform", - "version": "0.11.0", + "version": "0.12.0", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "cfe3dc32038345e99989101e88450f36abc449ca" + "reference": "068ee46228f0c3972e6b569f2c86b6c80fe583d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/cfe3dc32038345e99989101e88450f36abc449ca", - "reference": "cfe3dc32038345e99989101e88450f36abc449ca", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/068ee46228f0c3972e6b569f2c86b6c80fe583d8", + "reference": "068ee46228f0c3972e6b569f2c86b6c80fe583d8", "shasum": "" }, "require": { @@ -4741,9 +4741,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.11.0" + "source": "https://github.com/utopia-php/platform/tree/0.12.0" }, - "time": "2026-03-23T17:20:38+00:00" + "time": "2026-03-31T14:44:23+00:00" }, { "name": "utopia-php/pools", From c9f7b7f0d904a2b31fc99bd8a7d484e43ae66c1d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 15:42:15 +0530 Subject: [PATCH 076/118] fix: address PR review findings from code review - Add Console::error() fallback in Bus::dispatch() so listener failures are visible even without telemetry (C1/M7) - Remove duplicate $max/$sleep assignments in createDatabase (M1) - Remove duplicate @param in Event::generateEvents docblock (M2) - Remove unused $plan parameter from plan resource factory (M3) - Fix inconsistent indentation in certificate init block (L2) - Add explicit return null in session resource factory (M6) --- app/controllers/general.php | 174 ++++++++++++++++----------------- app/http.php | 2 - app/init/resources.php | 2 +- app/init/resources/request.php | 1 + src/Appwrite/Event/Event.php | 1 - src/Utopia/Bus/Bus.php | 2 + 6 files changed, 91 insertions(+), 91 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index c3653ee3d5..5fd6eb2a57 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1019,107 +1019,107 @@ Http::init() * Automatic certificate generation */ Http::init() - ->groups(['api', 'web']) - ->inject('request') - ->inject('console') - ->inject('dbForPlatform') - ->inject('queueForCertificates') - ->inject('platform') + ->groups(['api', 'web']) + ->inject('request') + ->inject('console') + ->inject('dbForPlatform') + ->inject('queueForCertificates') + ->inject('platform') ->inject('authorization') ->inject('certifiedDomains') - ->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) { - $hostname = $request->getHostname(); - $platformHostnames = $platform['hostnames'] ?? []; + ->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) { + $hostname = $request->getHostname(); + $platformHostnames = $platform['hostnames'] ?? []; - // 1. Cache hit - if ($certifiedDomains->exists(md5($hostname))) { - return; - } + // 1. Cache hit + if ($certifiedDomains->exists(md5($hostname))) { + return; + } - // 2. Domain validation - $domain = new Domain(!empty($hostname) ? $hostname : ''); - if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) { - $certifiedDomains->set(md5($domain->get()), ['value' => 0]); - return; - } + // 2. Domain validation + $domain = new Domain(!empty($hostname) ? $hostname : ''); + if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) { + $certifiedDomains->set(md5($domain->get()), ['value' => 0]); + return; + } - if (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) { - return; - } + if (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) { + return; + } - // 3. Check if domain is a main domain - if (!in_array($domain->get(), $platformHostnames)) { - return; - } + // 3. Check if domain is a main domain + if (!in_array($domain->get(), $platformHostnames)) { + return; + } - // 4. Check/create rule (requires DB access) - $authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, $certifiedDomains) { - try { - // TODO: (@Meldiron) Remove after 1.7.x migration - $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $document = $isMd5 - ? $dbForPlatform->getDocument('rules', md5($domain->get())) - : $dbForPlatform->findOne('rules', [ - Query::equal('domain', [$domain->get()]), - ]); + // 4. Check/create rule (requires DB access) + $authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, $certifiedDomains) { + try { + // TODO: (@Meldiron) Remove after 1.7.x migration + $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; + $document = $isMd5 + ? $dbForPlatform->getDocument('rules', md5($domain->get())) + : $dbForPlatform->findOne('rules', [ + Query::equal('domain', [$domain->get()]), + ]); - if (!$document->isEmpty()) { - return; - } + if (!$document->isEmpty()) { + return; + } - // 5. Create new rule - $owner = ''; + // 5. Create new rule + $owner = ''; - // Mark owner as Appwrite if its appwrite-owned domain - $appwriteDomains = []; - $appwriteDomainEnvs = [ - System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''), - System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), - System::getEnv('_APP_DOMAIN_SITES', ''), - ]; - foreach ($appwriteDomainEnvs as $appwriteDomainEnv) { - foreach (\explode(',', $appwriteDomainEnv) as $appwriteDomain) { - if (empty($appwriteDomain)) { - continue; - } - $appwriteDomains[] = $appwriteDomain; - } - } + // Mark owner as Appwrite if its appwrite-owned domain + $appwriteDomains = []; + $appwriteDomainEnvs = [ + System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''), + System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), + System::getEnv('_APP_DOMAIN_SITES', ''), + ]; + foreach ($appwriteDomainEnvs as $appwriteDomainEnv) { + foreach (\explode(',', $appwriteDomainEnv) as $appwriteDomain) { + if (empty($appwriteDomain)) { + continue; + } + $appwriteDomains[] = $appwriteDomain; + } + } - foreach ($appwriteDomains as $appwriteDomain) { - if (\str_ends_with($domain->get(), $appwriteDomain)) { - $owner = 'Appwrite'; - break; - } - } + foreach ($appwriteDomains as $appwriteDomain) { + if (\str_ends_with($domain->get(), $appwriteDomain)) { + $owner = 'Appwrite'; + break; + } + } - $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); - $document = new Document([ - '$id' => $ruleId, - 'domain' => $domain->get(), - 'type' => 'api', - 'status' => 'verifying', - 'projectId' => $console->getId(), - 'projectInternalId' => $console->getSequence(), - 'search' => implode(' ', [$ruleId, $domain->get()]), - 'owner' => $owner, - 'region' => $console->getAttribute('region') - ]); + $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); + $document = new Document([ + '$id' => $ruleId, + 'domain' => $domain->get(), + 'type' => 'api', + 'status' => 'verifying', + 'projectId' => $console->getId(), + 'projectInternalId' => $console->getSequence(), + 'search' => implode(' ', [$ruleId, $domain->get()]), + 'owner' => $owner, + 'region' => $console->getAttribute('region') + ]); - $dbForPlatform->createDocument('rules', $document); + $dbForPlatform->createDocument('rules', $document); - Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...'); - $queueForCertificates - ->setDomain($document) - ->setSkipRenewCheck(true) - ->trigger(); - } catch (Duplicate $e) { - Console::info('Certificate already exists'); - } finally { - $certifiedDomains->set(md5($domain->get()), ['value' => 1]); - } - }); - }); + Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...'); + $queueForCertificates + ->setDomain($document) + ->setSkipRenewCheck(true) + ->trigger(); + } catch (Duplicate $e) { + Console::info('Certificate already exists'); + } finally { + $certifiedDomains->set(md5($domain->get()), ['value' => 1]); + } + }); + }); Http::options() ->inject('utopia') diff --git a/app/http.php b/app/http.php index ba1d81267d..67da67376d 100644 --- a/app/http.php +++ b/app/http.php @@ -200,8 +200,6 @@ include __DIR__ . '/controllers/general.php'; function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void { - $max = 15; - $sleep = 2; $max = 15; $sleep = 2; $attempts = 0; diff --git a/app/init/resources.php b/app/init/resources.php index 75837f985b..fdca88c30e 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -322,7 +322,7 @@ $container->set('gitHub', function (Cache $cache) { return new VcsGitHub($cache); }, ['cache']); -$container->set('plan', function (array $plan = []) { +$container->set('plan', function () { return []; }); diff --git a/app/init/resources/request.php b/app/init/resources/request.php index f3afe05baf..c90ac0dd1d 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -629,6 +629,7 @@ return function (Container $container): void { } } + return; }, ['user', 'store', 'proofForToken']); $container->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) { diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index bf6339f8a0..6722a07dc4 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -519,7 +519,6 @@ class Event * @param string $pattern * @param array $params * @param ?Document $database - * @param ?Document $database * @return array * @throws \InvalidArgumentException */ diff --git a/src/Utopia/Bus/Bus.php b/src/Utopia/Bus/Bus.php index bef39f0481..0ff95205be 100644 --- a/src/Utopia/Bus/Bus.php +++ b/src/Utopia/Bus/Bus.php @@ -2,6 +2,7 @@ namespace Utopia\Bus; +use Utopia\Console; use Utopia\Span\Span; class Bus @@ -43,6 +44,7 @@ class Bus ($listener->getCallback())($event, ...$deps); } catch (\Throwable $e) { Span::error($e); + Console::error('[Bus] Listener ' . $listener::getName() . ' failed: ' . $e->getMessage()); } finally { Span::current()?->finish(); } From 789870b5457c9234e4dc66d11eb181bdc94d3a99 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 15:43:14 +0530 Subject: [PATCH 077/118] fix: preserve multi-value headers like Set-Cookie instead of comma-joining addHeader() already accumulates multiple values for the same key into an array internally, so calling it once per value is the correct approach. Comma-joining violates RFC 6265 for Set-Cookie headers. --- app/controllers/general.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 5fd6eb2a57..6fdeabb3a8 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -748,7 +748,9 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S } if (\is_array($values)) { - $response->addHeader($name, \implode(', ', $values)); + foreach ($values as $value) { + $response->addHeader($name, $value); + } } else { $response->addHeader($name, $values); } From 15b2ab321e03a1f9ff79c691cd8a23354f2565da Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 16:34:58 +0530 Subject: [PATCH 078/118] fix: restore pool size validation to prevent silent connection exhaustion The old guard that threw when workerCount > instanceConnections was removed during the DI migration, causing pool size to silently floor to 1. This can lead to connection exhaustion on multi-core hosts. --- app/init/registers.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/init/registers.php b/app/init/registers.php index 3a6c1423f4..d834a56154 100644 --- a/app/init/registers.php +++ b/app/init/registers.php @@ -246,7 +246,12 @@ $register->set('pools', function () { $instanceConnections = $maxConnections / System::getEnv('_APP_POOL_CLIENTS', 14); $workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6)); - $poolSize = max(1, (int)($instanceConnections / $workerCount)); + + if ($workerCount > $instanceConnections) { + throw new \Exception('Pool size is too small. Increase the number of allowed database connections (_APP_CONNECTIONS_MAX) or decrease the number of workers (_APP_WORKER_PER_CORE).', 500); + } + + $poolSize = (int)($instanceConnections / $workerCount); foreach ($connections as $key => $connection) { $type = $connection['type'] ?? ''; From 80d5d4716a180f1b7662bc0538a2113b78123e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 1 Apr 2026 15:54:26 +0200 Subject: [PATCH 079/118] Remove base errors --- phpstan-baseline.neon | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8e9d8a5a38..ac308a14b5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -498,24 +498,12 @@ parameters: count: 1 path: src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php - - - message: '#^Caught class Appwrite\\Platform\\Modules\\Teams\\Http\\Memberships\\Throwable not found\.$#' - identifier: class.notFound - count: 1 - path: src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php - - message: '#^Variable \$email in empty\(\) always exists and is always falsy\.$#' identifier: empty.variable count: 1 path: src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php - - - message: '#^Variable \$hash might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php - - message: '#^Variable \$invitee might not be defined\.$#' identifier: variable.undefined From d13644a47a1b4267074436b4bd545970031a64d1 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 11:38:19 +0530 Subject: [PATCH 080/118] fix: make pool sizing runtime-aware --- app/init/registers.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/init/registers.php b/app/init/registers.php index 6a49c2b37d..c697115c00 100644 --- a/app/init/registers.php +++ b/app/init/registers.php @@ -245,13 +245,24 @@ $register->set('pools', function () { $maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151); $instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14); - $workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6)); + $entrypoint = \basename($_SERVER['SCRIPT_NAME'] ?? $_SERVER['argv'][0] ?? ''); + $workerCount = match ($entrypoint) { + 'http.php', + 'realtime.php' => intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6)), + 'worker.php' => max(1, (int) System::getEnv('_APP_WORKERS_NUM', 1)), + default => 1, + }; if ($workerCount > $instanceConnections) { - throw new \Exception('Pool size is too small. Increase the number of allowed database connections (_APP_CONNECTIONS_MAX) or decrease the number of workers (_APP_WORKER_PER_CORE).', 500); + Console::warning( + 'Pool size is too small for ' . $entrypoint . + '. Falling back to a minimum size of 1. ' . + 'Increase _APP_CONNECTIONS_MAX or decrease _APP_WORKER_PER_CORE ' . + '(and _APP_WORKERS_NUM for worker.php) to avoid connection contention.' + ); } - $poolSize = (int)($instanceConnections / $workerCount); + $poolSize = max(1, (int)($instanceConnections / $workerCount)); foreach ($connections as $key => $connection) { $type = $connection['type'] ?? ''; From 30befc6a600e8a63c459e66df1a6b4ea381bf903 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 11:41:35 +0530 Subject: [PATCH 081/118] fix: remove strict pool size exception --- app/init/registers.php | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/app/init/registers.php b/app/init/registers.php index c697115c00..2f6f214bc7 100644 --- a/app/init/registers.php +++ b/app/init/registers.php @@ -245,23 +245,7 @@ $register->set('pools', function () { $maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151); $instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14); - $entrypoint = \basename($_SERVER['SCRIPT_NAME'] ?? $_SERVER['argv'][0] ?? ''); - $workerCount = match ($entrypoint) { - 'http.php', - 'realtime.php' => intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6)), - 'worker.php' => max(1, (int) System::getEnv('_APP_WORKERS_NUM', 1)), - default => 1, - }; - - if ($workerCount > $instanceConnections) { - Console::warning( - 'Pool size is too small for ' . $entrypoint . - '. Falling back to a minimum size of 1. ' . - 'Increase _APP_CONNECTIONS_MAX or decrease _APP_WORKER_PER_CORE ' . - '(and _APP_WORKERS_NUM for worker.php) to avoid connection contention.' - ); - } - + $workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6)); $poolSize = max(1, (int)($instanceConnections / $workerCount)); foreach ($connections as $key => $connection) { From e8bcc9418732224590b896b8797ccec0d58e3619 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 13:58:55 +0530 Subject: [PATCH 082/118] fix: keep composer lock scoped to intended updates --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index c1bc7dbf7e..2e06261f87 100644 --- a/composer.lock +++ b/composer.lock @@ -2708,16 +2708,16 @@ }, { "name": "symfony/http-client", - "version": "v7.4.8", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "01933e626c3de76bea1e22641e205e78f6a34342" + "reference": "1010624285470eb60e88ed10035102c75b4ea6af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/01933e626c3de76bea1e22641e205e78f6a34342", - "reference": "01933e626c3de76bea1e22641e205e78f6a34342", + "url": "https://api.github.com/repos/symfony/http-client/zipball/1010624285470eb60e88ed10035102c75b4ea6af", + "reference": "1010624285470eb60e88ed10035102c75b4ea6af", "shasum": "" }, "require": { @@ -2785,7 +2785,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.8" + "source": "https://github.com/symfony/http-client/tree/v7.4.7" }, "funding": [ { @@ -2805,7 +2805,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T12:55:43+00:00" + "time": "2026-03-05T11:16:58+00:00" }, { "name": "symfony/http-client-contracts", From 4df5f4a18f3f9276dc15b64e495885feeb871f54 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 14:06:45 +0530 Subject: [PATCH 083/118] fix: scope composer lock to intended package updates --- composer.lock | 116 ++++++++++++++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 50 deletions(-) diff --git a/composer.lock b/composer.lock index 2e06261f87..29643c7b08 100644 --- a/composer.lock +++ b/composer.lock @@ -3850,16 +3850,16 @@ }, { "name": "utopia-php/database", - "version": "5.3.19", + "version": "5.3.17", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691" + "reference": "cff2b6ed63d3291b74110d086e16ff089fe05993" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691", - "reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691", + "url": "https://api.github.com/repos/utopia-php/database/zipball/cff2b6ed63d3291b74110d086e16ff089fe05993", + "reference": "cff2b6ed63d3291b74110d086e16ff089fe05993", "shasum": "" }, "require": { @@ -3903,9 +3903,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/5.3.19" + "source": "https://github.com/utopia-php/database/tree/5.3.17" }, - "time": "2026-03-31T15:52:08+00:00" + "time": "2026-03-20T01:18:52+00:00" }, { "name": "utopia-php/detector", @@ -5502,16 +5502,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.16.5", + "version": "1.14.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "d2a93863ec907cdcae283c3062d9a24192b909fc" + "reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/d2a93863ec907cdcae283c3062d9a24192b909fc", - "reference": "d2a93863ec907cdcae283c3062d9a24192b909fc", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/7e7e257b10a8c1384a237e7d8d73452e2108901e", + "reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e", "shasum": "" }, "require": { @@ -5547,22 +5547,22 @@ "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.16.5" + "source": "https://github.com/appwrite/sdk-generator/tree/1.14.0" }, - "time": "2026-04-01T03:01:19+00:00" + "time": "2026-03-26T12:50:11+00:00" }, { "name": "brianium/paratest", - "version": "v7.20.0", + "version": "v7.19.2", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d" + "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/81c80677c9ec0ed4ef16b246167f11dec81a6e3d", - "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9", + "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9", "shasum": "" }, "require": { @@ -5586,7 +5586,7 @@ "ext-pcntl": "*", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.44", + "phpstan/phpstan": "^2.1.40", "phpstan/phpstan-deprecation-rules": "^2.0.4", "phpstan/phpstan-phpunit": "^2.0.16", "phpstan/phpstan-strict-rules": "^2.0.10", @@ -5630,7 +5630,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.20.0" + "source": "https://github.com/paratestphp/paratest/tree/v7.19.2" }, "funding": [ { @@ -5642,7 +5642,7 @@ "type": "paypal" } ], - "time": "2026-03-29T15:46:14+00:00" + "time": "2026-03-09T14:33:17+00:00" }, { "name": "czproject/git-php", @@ -6258,11 +6258,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.46", + "version": "2.1.44", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", - "reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/4a88c083c668b2c364a425c9b3171b2d9ea5d218", + "reference": "4a88c083c668b2c364a425c9b3171b2d9ea5d218", "shasum": "" }, "require": { @@ -6307,7 +6307,7 @@ "type": "github" } ], - "time": "2026-04-01T09:25:14+00:00" + "time": "2026-03-25T17:34:21+00:00" }, { "name": "phpunit/php-code-coverage", @@ -6657,16 +6657,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.15", + "version": "12.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "aeb6899ffdbbf4b4ff5e6b6ebb77b35c51bb6d9a" + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/aeb6899ffdbbf4b4ff5e6b6ebb77b35c51bb6d9a", - "reference": "aeb6899ffdbbf4b4ff5e6b6ebb77b35c51bb6d9a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/47283cfd98d553edcb1353591f4e255dc1bb61f0", + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0", "shasum": "" }, "require": { @@ -6688,7 +6688,7 @@ "sebastian/cli-parser": "^4.2.0", "sebastian/comparator": "^7.1.4", "sebastian/diff": "^7.0.0", - "sebastian/environment": "^8.0.4", + "sebastian/environment": "^8.0.3", "sebastian/exporter": "^7.0.2", "sebastian/global-state": "^8.0.2", "sebastian/object-enumerator": "^7.0.0", @@ -6735,15 +6735,31 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.15" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.14" }, "funding": [ { - "url": "https://phpunit.de/sponsoring.html", - "type": "other" + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2026-03-31T06:41:33+00:00" + "time": "2026-02-18T12:38:40+00:00" }, { "name": "sebastian/cli-parser", @@ -7728,16 +7744,16 @@ }, { "name": "symfony/console", - "version": "v8.0.8", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7" + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5b66d385dc58f69652e56f78a4184615e3f2b7f7", - "reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7", + "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", "shasum": "" }, "require": { @@ -7794,7 +7810,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.8" + "source": "https://github.com/symfony/console/tree/v8.0.7" }, "funding": [ { @@ -7814,7 +7830,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-03-06T14:06:22+00:00" }, { "name": "symfony/polyfill-ctype", @@ -8148,16 +8164,16 @@ }, { "name": "symfony/process", - "version": "v8.0.8", + "version": "v8.0.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc" + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc", - "reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc", + "url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", "shasum": "" }, "require": { @@ -8189,7 +8205,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v8.0.8" + "source": "https://github.com/symfony/process/tree/v8.0.5" }, "funding": [ { @@ -8209,20 +8225,20 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-01-26T15:08:38+00:00" }, { "name": "symfony/string", - "version": "v8.0.8", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ae9488f874d7603f9d2dfbf120203882b645d963" + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963", - "reference": "ae9488f874d7603f9d2dfbf120203882b645d963", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", "shasum": "" }, "require": { @@ -8279,7 +8295,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.8" + "source": "https://github.com/symfony/string/tree/v8.0.6" }, "funding": [ { @@ -8299,7 +8315,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-02-09T10:14:57+00:00" }, { "name": "textalk/websocket", From 17076e4a0080c8613dfd801a5bbd6b4fe7d28843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 2 Apr 2026 11:55:32 +0200 Subject: [PATCH 084/118] Fix formatting --- src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php index 305a6c1f63..5edc69f445 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php @@ -17,7 +17,6 @@ use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Response; use libphonenumber\NumberParseException; use libphonenumber\PhoneNumberUtil; -use Throwable; use Utopia\Auth\Proofs\Password; use Utopia\Auth\Proofs\Token; use Utopia\Database\Database; From 90e705f8c56f7b1ad2a0bd78da712c2d1a7137a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 2 Apr 2026 12:26:03 +0200 Subject: [PATCH 085/118] Improve docs --- app/config/console.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/config/console.php b/app/config/console.php index 4f219fd561..0b0d6c5881 100644 --- a/app/config/console.php +++ b/app/config/console.php @@ -39,6 +39,7 @@ $console = [ 'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user 'duration' => TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds 'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled', + // For email configuration, false means feature is disabled; false means these emails are allowed during sign-ups 'disposableEmails' => false, 'canonicalEmails' => false, 'freeEmails' => false, From 3018b478ba281f998cac8bf3f701de1cdc5723da Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 17:25:13 +0530 Subject: [PATCH 086/118] Fix database transaction and vectors migration flakiness --- .../Http/Databases/Transactions/Update.php | 59 +++++++++++-------- .../Http/VectorsDB/Collections/Create.php | 35 +++++++++++ 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 0c8c6a8520..9f0839a14b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -180,21 +180,35 @@ class Update extends Action } $dbForDatabases = $getDatabasesDB($databaseDoc); + $collections = []; try { - $dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $authorization) { - $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'committing', - ]))); + $transaction = $authorization->skip(fn () => $dbForProject->updateDocument( + 'transactions', + $transactionId, + new Document(['status' => 'committing']) + )); - $operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [ - Query::equal('transactionInternalId', [$transaction->getSequence()]), - Query::orderAsc(), - Query::limit(PHP_INT_MAX), - ])); + $operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + Query::orderAsc(), + Query::limit(PHP_INT_MAX), + ])); + foreach ($operations as $operation) { + $databaseInternalId = $operation['databaseInternalId']; + $collectionInternalId = $operation['collectionInternalId']; + $collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}"; + + if (!isset($collections[$collectionId])) { + $collections[$collectionId] = $authorization->skip( + fn () => $dbForProject->getCollection($collectionId) + ); + } + } + + $dbForDatabases->withTransaction(function () use ($dbForDatabases, $transactionState, $operations, $collections, &$totalOperations, &$databaseOperations, &$currentDocumentId) { $state = []; - $collections = []; foreach ($operations as $operation) { $databaseInternalId = $operation['databaseInternalId']; @@ -210,11 +224,6 @@ class Update extends Action $data = $data->getArrayCopy(); } - if (!isset($collections[$collectionId])) { - $collections[$collectionId] = $authorization->skip( - fn () => $dbForProject->getCollection($collectionId) - ); - } $collection = $collections[$collectionId]; if (\is_array($data) && !empty($data)) { @@ -275,17 +284,17 @@ class Update extends Action break; } } - - $transaction = $authorization->skip(fn () => $dbForProject->updateDocument( - 'transactions', - $transactionId, - new Document(['status' => 'committed']) - )); - - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($transaction); }); + + $transaction = $authorization->skip(fn () => $dbForProject->updateDocument( + 'transactions', + $transactionId, + new Document(['status' => 'committed']) + )); + + $queueForDeletes + ->setType(DELETE_TYPE_DOCUMENT) + ->setDocument($transaction); } catch (NotFoundException $e) { $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php index a7e2d68eac..baa31c4ef7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php @@ -20,6 +20,7 @@ use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; +use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\UID; @@ -116,6 +117,30 @@ class Create extends CollectionAction } /** @var Database $dbForDatabases */ $dbForDatabases = $getDatabasesDB($database); + $cleanupCollection = function () use ($authorization, $dbForProject, $database, $collection): void { + try { + $authorization->skip(fn () => $dbForProject->deleteDocument( + 'database_' . $database->getSequence(), + $collection->getId() + )); + } catch (\Throwable) { + } + + $queries = [ + Query::equal('databaseInternalId', [$database->getSequence()]), + Query::equal('collectionInternalId', [$collection->getSequence()]), + ]; + + try { + $authorization->skip(fn () => $dbForProject->deleteDocuments('attributes', $queries)); + } catch (\Throwable) { + } + + try { + $authorization->skip(fn () => $dbForProject->deleteDocuments('indexes', $queries)); + } catch (\Throwable) { + } + }; $attributes = []; $indexes = []; @@ -134,6 +159,10 @@ class Create extends CollectionAction try { $dbForDatabases->create(); } catch (DuplicateException) { + } catch (\Throwable $e) { + if (!$dbForDatabases->exists(null, Database::METADATA)) { + throw $e; + } } } $dbForDatabases->createCollection( @@ -191,11 +220,17 @@ class Create extends CollectionAction $dbForProject->createDocuments('indexes', $indexDocs); } } catch (DuplicateException) { + $cleanupCollection(); throw new Exception($this->getDuplicateException()); } catch (IndexException) { + $cleanupCollection(); throw new Exception($this->getInvalidIndexException()); } catch (LimitException) { + $cleanupCollection(); throw new Exception($this->getLimitException()); + } catch (\Throwable $e) { + $cleanupCollection(); + throw $e; } $queueForEvents From 130c2221ecfa3b183c4a51acd38fbff0dc52d23f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 3 Apr 2026 22:12:21 +0530 Subject: [PATCH 087/118] Fix VectorsDB metadata bootstrap race --- .../Http/VectorsDB/Collections/Create.php | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php index a7e2d68eac..787c7ae0d9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php @@ -62,7 +62,7 @@ class Create extends CollectionAction new SDKResponse( code: SwooleResponse::STATUS_CODE_CREATED, model: $this->getResponseModel(), - ) + ), ], contentType: ContentType::JSON )) @@ -72,7 +72,7 @@ class Create extends CollectionAction ->param('dimension', null, new Range(MIN_VECTOR_DIMENSION, MAX_VECTOR_DIMENSION), 'Embedding dimension.') ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permissions strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) ->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) - ->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true) + ->param('enabled', true, new Boolean, 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true) ->inject('response') ->inject('dbForProject') ->inject('getDatabasesDB') @@ -95,7 +95,7 @@ class Create extends CollectionAction $permissions = Permission::aggregate($permissions) ?? []; try { - $collection = $dbForProject->createDocument('database_' . $database->getSequence(), new Document([ + $collection = $dbForProject->createDocument('database_'.$database->getSequence(), new Document([ '$id' => $collectionId, 'databaseInternalId' => $database->getSequence(), 'databaseId' => $databaseId, @@ -130,25 +130,27 @@ class Create extends CollectionAction $indexes[] = new Document($index); } try { - if (!$dbForDatabases->exists(null, Database::METADATA)) { - try { - $dbForDatabases->create(); - } catch (DuplicateException) { - } + // Bootstrap the database metadata without a separate existence + // check to avoid races when multiple first collections are created + // concurrently for the same VectorsDB database. + try { + $dbForDatabases->create(); + } catch (DuplicateException) { } $dbForDatabases->createCollection( - id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + id: 'database_'.$database->getSequence().'_collection_'.$collection->getSequence(), permissions: $permissions, documentSecurity: $documentSecurity, - attributes:$attributes, - indexes:$indexes + attributes: $attributes, + indexes: $indexes ); // Create attribute and indexes metadata documents in the attributes and indexes collections // needed for the get and list calls $attributeDocs = array_map(function ($attributeConfig) use ($database, $collection, $databaseId, $collectionId, $dimension) { $key = \is_string($attributeConfig['$id']) ? $attributeConfig['$id'] : (string) $attributeConfig['$id']; + return new Document([ - '$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key), + '$id' => ID::custom($database->getSequence().'_'.$collection->getSequence().'_'.$key), 'key' => $key, 'databaseInternalId' => $database->getSequence(), 'databaseId' => $databaseId, @@ -173,7 +175,7 @@ class Create extends CollectionAction $key = \is_string($indexConfig['$id']) ? $indexConfig['$id'] : (string) $indexConfig['$id']; return new Document([ - '$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key), + '$id' => ID::custom($database->getSequence().'_'.$collection->getSequence().'_'.$key), 'key' => $key, 'status' => 'available', 'databaseInternalId' => $database->getSequence(), @@ -187,7 +189,7 @@ class Create extends CollectionAction ]); }, $collections['defaultIndexes']); - if (!empty($indexDocs)) { + if (! empty($indexDocs)) { $dbForProject->createDocuments('indexes', $indexDocs); } } catch (DuplicateException) { From a5f45b46e9dfbff8ec640cc9dd3ba7e4b708ec88 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 3 Apr 2026 23:41:44 +0530 Subject: [PATCH 088/118] Handle raced VectorsDB metadata bootstrap errors --- .../Http/VectorsDB/Collections/Create.php | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php index 787c7ae0d9..d03213d4b8 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php @@ -72,7 +72,7 @@ class Create extends CollectionAction ->param('dimension', null, new Range(MIN_VECTOR_DIMENSION, MAX_VECTOR_DIMENSION), 'Embedding dimension.') ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permissions strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) ->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) - ->param('enabled', true, new Boolean, 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true) + ->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true) ->inject('response') ->inject('dbForProject') ->inject('getDatabasesDB') @@ -133,9 +133,23 @@ class Create extends CollectionAction // Bootstrap the database metadata without a separate existence // check to avoid races when multiple first collections are created // concurrently for the same VectorsDB database. - try { - $dbForDatabases->create(); - } catch (DuplicateException) { + for ($attempt = 0; $attempt < 5; $attempt++) { + try { + $dbForDatabases->create(); + break; + } catch (DuplicateException) { + break; + } catch (\Throwable $e) { + if ($dbForDatabases->exists(null, Database::METADATA)) { + break; + } + + if ($attempt === 4) { + throw $e; + } + + \usleep(100_000); + } } $dbForDatabases->createCollection( id: 'database_'.$database->getSequence().'_collection_'.$collection->getSequence(), From 3cb53f06047e3411893b45b49ef4671e3fe2ea85 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 3 Apr 2026 23:43:51 +0530 Subject: [PATCH 089/118] Drop unrelated formatting churn from VectorsDB fix --- .../Http/VectorsDB/Collections/Create.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php index d03213d4b8..0294790a9e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php @@ -62,7 +62,7 @@ class Create extends CollectionAction new SDKResponse( code: SwooleResponse::STATUS_CODE_CREATED, model: $this->getResponseModel(), - ), + ) ], contentType: ContentType::JSON )) @@ -95,7 +95,7 @@ class Create extends CollectionAction $permissions = Permission::aggregate($permissions) ?? []; try { - $collection = $dbForProject->createDocument('database_'.$database->getSequence(), new Document([ + $collection = $dbForProject->createDocument('database_' . $database->getSequence(), new Document([ '$id' => $collectionId, 'databaseInternalId' => $database->getSequence(), 'databaseId' => $databaseId, @@ -152,11 +152,11 @@ class Create extends CollectionAction } } $dbForDatabases->createCollection( - id: 'database_'.$database->getSequence().'_collection_'.$collection->getSequence(), + id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), permissions: $permissions, documentSecurity: $documentSecurity, - attributes: $attributes, - indexes: $indexes + attributes:$attributes, + indexes:$indexes ); // Create attribute and indexes metadata documents in the attributes and indexes collections // needed for the get and list calls @@ -164,7 +164,7 @@ class Create extends CollectionAction $key = \is_string($attributeConfig['$id']) ? $attributeConfig['$id'] : (string) $attributeConfig['$id']; return new Document([ - '$id' => ID::custom($database->getSequence().'_'.$collection->getSequence().'_'.$key), + '$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key), 'key' => $key, 'databaseInternalId' => $database->getSequence(), 'databaseId' => $databaseId, @@ -189,7 +189,7 @@ class Create extends CollectionAction $key = \is_string($indexConfig['$id']) ? $indexConfig['$id'] : (string) $indexConfig['$id']; return new Document([ - '$id' => ID::custom($database->getSequence().'_'.$collection->getSequence().'_'.$key), + '$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key), 'key' => $key, 'status' => 'available', 'databaseInternalId' => $database->getSequence(), @@ -203,7 +203,7 @@ class Create extends CollectionAction ]); }, $collections['defaultIndexes']); - if (! empty($indexDocs)) { + if (!empty($indexDocs)) { $dbForProject->createDocuments('indexes', $indexDocs); } } catch (DuplicateException) { From f3f2855fe5e74917cba658799ae3250fcecf4516 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 3 Apr 2026 23:44:19 +0530 Subject: [PATCH 090/118] Remove final formatting-only diff --- .../Modules/Databases/Http/VectorsDB/Collections/Create.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php index 0294790a9e..58433c7deb 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php @@ -162,7 +162,6 @@ class Create extends CollectionAction // needed for the get and list calls $attributeDocs = array_map(function ($attributeConfig) use ($database, $collection, $databaseId, $collectionId, $dimension) { $key = \is_string($attributeConfig['$id']) ? $attributeConfig['$id'] : (string) $attributeConfig['$id']; - return new Document([ '$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key), 'key' => $key, From c978b6f34f7a18a740cdec915f2de0b895f1ab81 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 3 Apr 2026 23:58:25 +0530 Subject: [PATCH 091/118] Stabilize function deployment activation in tests --- tests/e2e/Services/Functions/FunctionsBase.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index af426d5221..42976cda84 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -100,6 +100,24 @@ trait FunctionsBase 'x-appwrite-key' => $this->getProject()['apiKey'], ])); $this->assertNotEquals(401, $function['headers']['status-code'], 'Auth failed while polling function activation'); + + if ( + ($function['body']['deploymentId'] ?? '') !== $deploymentId + && ($function['body']['latestDeploymentId'] ?? '') === $deploymentId + && ($function['body']['latestDeploymentStatus'] ?? '') === 'ready' + ) { + $activation = $this->updateFunctionDeployment($functionId, $deploymentId); + $this->assertContains( + $activation['headers']['status-code'], + [200, 409], + 'Deployment activation request failed: ' . json_encode($activation['body'], JSON_PRETTY_PRINT) + ); + + if ($activation['headers']['status-code'] === 200) { + $function = $activation; + } + } + $this->assertEquals($deploymentId, $function['body']['deploymentId'] ?? '', 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT)); }, 120000, 500); } From 860cf8cd5741f791ce3314b5ae56601fc374a8b6 Mon Sep 17 00:00:00 2001 From: "Eldad A. Fux" Date: Sat, 4 Apr 2026 08:32:46 +0200 Subject: [PATCH 092/118] Revise README content and structure Updated the README to enhance clarity and remove outdated information. --- README.md | 61 ++++++++++++++++++------------------------------------- 1 file changed, 20 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 9815229e43..fe34cba676 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,16 @@ -> We just announced DB operators for Appwrite Databases - [Learn more](https://appwrite.io/blog/post/announcing-db-operators) - -> Appwrite Cloud is now Generally Available - [Learn more](https://appwrite.io/cloud-ga) - -> [Get started with Appwrite](https://apwr.dev/appcloud) +image

- Appwrite banner, with logo and text saying -
-
- Appwrite is a best-in-class, developer-first platform that gives builders everything they need to create scalable, stable, and production-ready software, fast. +

Appwrite

+ Appwrite is an open-source, all-in-one development platform. Use built-in backend infrastructure and web hosting, all from a single place.

- - -[![We're Hiring label](https://img.shields.io/static/v1?label=We're&message=Hiring&color=blue&style=flat-square)](https://appwrite.io/company/careers) -[![Hacktoberfest label](https://img.shields.io/static/v1?label=hacktoberfest&message=ready&color=191120&style=flat-square)](https://hacktoberfest.appwrite.io) -[![Discord label](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord?r=Github) -[![Build Status label](https://img.shields.io/github/actions/workflow/status/appwrite/appwrite/tests.yml?branch=master&label=tests&style=flat-square)](https://github.com/appwrite/appwrite/actions) -[![X Account label](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) - - - - +[![Discord](https://img.shields.io/badge/chat-5865F2?style=flat-square&logo=discord&logoColor=white)](https://appwrite.io/discord) +[![X](https://img.shields.io/badge/follow-000000?style=flat-square&logo=x&logoColor=white)](https://x.com/appwrite) +[![Appwrite Cloud](https://img.shields.io/badge/Cloud-F02E65?style=flat-square&logo=icloud&logoColor=white)](https://cloud.appwrite.io) English | [简体中文](README-CN.md) @@ -32,8 +18,6 @@ Appwrite is an end-to-end platform for building Web, Mobile, Native, or Backend Using Appwrite, you can easily integrate your app with user authentication and multiple sign-in methods, a database for storing and querying users and team data, storage and file management, image manipulation, Cloud Functions, messaging, and [more services](https://appwrite.io/docs). -![Appwrite project dashboard showing various Appwrite features](public/images/github.png) - Find out more at: [https://appwrite.io](https://appwrite.io). Table of Contents: @@ -191,28 +175,23 @@ Below is a list of currently supported platforms and languages. If you would lik #### Client -- :white_check_mark:   [Web](https://github.com/appwrite/sdk-for-web) (Maintained by the Appwrite Team) -- :white_check_mark:   [Flutter](https://github.com/appwrite/sdk-for-flutter) (Maintained by the Appwrite Team) -- :white_check_mark:   [Apple](https://github.com/appwrite/sdk-for-apple) (Maintained by the Appwrite Team) -- :white_check_mark:   [Android](https://github.com/appwrite/sdk-for-android) (Maintained by the Appwrite Team) -- :white_check_mark:   [React Native](https://github.com/appwrite/sdk-for-react-native) - **Beta** (Maintained by the Appwrite Team) +- :white_check_mark:   [Web](https://github.com/appwrite/sdk-for-web) +- :white_check_mark:   [Flutter](https://github.com/appwrite/sdk-for-flutter) +- :white_check_mark:   [Apple](https://github.com/appwrite/sdk-for-apple) +- :white_check_mark:   [Android](https://github.com/appwrite/sdk-for-android) +- :white_check_mark:   [React Native](https://github.com/appwrite/sdk-for-react-native) #### Server -- :white_check_mark:   [NodeJS](https://github.com/appwrite/sdk-for-node) (Maintained by the Appwrite Team) -- :white_check_mark:   [PHP](https://github.com/appwrite/sdk-for-php) (Maintained by the Appwrite Team) -- :white_check_mark:   [Dart](https://github.com/appwrite/sdk-for-dart) (Maintained by the Appwrite Team) -- :white_check_mark:   [Deno](https://github.com/appwrite/sdk-for-deno) (Maintained by the Appwrite Team) -- :white_check_mark:   [Ruby](https://github.com/appwrite/sdk-for-ruby) (Maintained by the Appwrite Team) -- :white_check_mark:   [Python](https://github.com/appwrite/sdk-for-python) (Maintained by the Appwrite Team) -- :white_check_mark:   [Kotlin](https://github.com/appwrite/sdk-for-kotlin) (Maintained by the Appwrite Team) -- :white_check_mark:   [Swift](https://github.com/appwrite/sdk-for-swift) (Maintained by the Appwrite Team) -- :white_check_mark:   [.NET](https://github.com/appwrite/sdk-for-dotnet) - **Beta** (Maintained by the Appwrite Team) - -#### Community - -- :white_check_mark:   [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (Maintained by [Michael Gangolf](https://github.com/m1ga/)) -- :white_check_mark:   [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (Maintained by [fenix-hub @GodotNuts](https://github.com/fenix-hub)) +- :white_check_mark:   [NodeJS](https://github.com/appwrite/sdk-for-node) +- :white_check_mark:   [PHP](https://github.com/appwrite/sdk-for-php) +- :white_check_mark:   [Dart](https://github.com/appwrite/sdk-for-dart) +- :white_check_mark:   [Deno](https://github.com/appwrite/sdk-for-deno) +- :white_check_mark:   [Ruby](https://github.com/appwrite/sdk-for-ruby) +- :white_check_mark:   [Python](https://github.com/appwrite/sdk-for-python) +- :white_check_mark:   [Kotlin](https://github.com/appwrite/sdk-for-kotlin) +- :white_check_mark:   [Swift](https://github.com/appwrite/sdk-for-swift) +- :white_check_mark:   [.NET](https://github.com/appwrite/sdk-for-dotnet) Looking for more SDKs? - Help us by contributing a pull request to our [SDK Generator](https://github.com/appwrite/sdk-generator)! From cf8519bfa51dcc9a6925dcb4d4b6dda50948ea4d Mon Sep 17 00:00:00 2001 From: "Eldad A. Fux" Date: Sat, 4 Apr 2026 08:41:23 +0200 Subject: [PATCH 093/118] Revise Appwrite description and product details Updated the description of Appwrite to emphasize its open-source nature and capabilities. Added detailed product descriptions for Appwrite services. --- README.md | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index fe34cba676..ed83252e2f 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,15 @@ English | [简体中文](README-CN.md) -Appwrite is an end-to-end platform for building Web, Mobile, Native, or Backend apps, packaged as a set of Docker microservices. It includes both a backend server and a fully integrated hosting solution for deploying static and server-side rendered frontends. Appwrite abstracts the complexity and repetitiveness required to build modern apps from scratch and allows you to build secure, full-stack applications faster. +Appwrite is an open-source development platform for building web, mobile, and AI applications. It brings together backend infrastructure and web hosting in one place, so teams can build, ship, and scale without stitching together a fragmented stack. Appwrite is available as a managed cloud platform and can also be self-hosted on infrastructure you control. -Using Appwrite, you can easily integrate your app with user authentication and multiple sign-in methods, a database for storing and querying users and team data, storage and file management, image manipulation, Cloud Functions, messaging, and [more services](https://appwrite.io/docs). +With Appwrite, you can add authentication, databases, storage, functions, messaging, realtime capabilities, and integrated web app hosting through Sites. It is designed to reduce the repetitive backend work required to launch modern products while giving developers secure primitives and flexible APIs to build production-ready applications faster. -Find out more at: [https://appwrite.io](https://appwrite.io). +Find out more at [https://appwrite.io](https://appwrite.io). Table of Contents: +- [Products](#products) - [Installation \& Setup](#installation--setup) - [Self-Hosting](#self-hosting) - [Unix](#unix) @@ -31,17 +32,31 @@ Table of Contents: - [Upgrade from an Older Version](#upgrade-from-an-older-version) - [One-Click Setups](#one-click-setups) - [Getting Started](#getting-started) - - [Products](#products) - [SDKs](#sdks) - [Client](#client) - [Server](#server) - - [Community](#community) - [Architecture](#architecture) - [Contributing](#contributing) - [Security](#security) - [Follow Us](#follow-us) - [License](#license) + +## Products + +- **[Appwrite Auth](https://appwrite.io/docs/products/authentication)** - Secure user authentication with multiple login methods including email/password, SMS, OAuth, anonymous sessions, and magic links. Includes session management, multi-factor authentication, and user verification flows. + +- **[Appwrite Databases](https://appwrite.io/docs/products/databases)** - Scalable structured data storage with support for databases, tables, and rows. Includes querying, pagination, indexing, and relationships to model complex application data. + +- **[Appwrite Storage](https://appwrite.io/docs/products/storage)** - Secure file storage with support for uploads, downloads, encryption, compression, and file transformations for media and assets. + +- **[Appwrite Functions](https://appwrite.io/docs/products/functions)** - Serverless compute platform to run custom backend logic in isolated runtimes, triggered by events or scheduled jobs.15 runtimes supported. + +- **[Appwrite Messaging](https://appwrite.io/docs/products/messaging)** - Multi-channel messaging system for sending emails, SMS, and push notifications to users for engagement, alerts, and transactional workflows. + +- **[Appwrite Sites](https://appwrite.io/docs/products/sites)** - Integrated hosting platform to deploy and scale web applications with support for custom domains, SSR, and seamless backend integration. Git integration and previews are supported. + + ## Installation & Setup The easiest way to get started with Appwrite is by [signing up for Appwrite Cloud](https://cloud.appwrite.io/). While Appwrite Cloud is in public beta, you can build with Appwrite completely free, and we won't collect your credit card information. @@ -152,23 +167,6 @@ Getting started with Appwrite is as easy as creating a new project, choosing you | | [Quick start for Kotlin](https://appwrite.io/docs/quick-starts/kotlin) | | | [Quick start for Swift](https://appwrite.io/docs/quick-starts/swift) | -### Products - -- [**Account**](https://appwrite.io/docs/references/cloud/client-web/account) - Manage current user authentication and account. Track and manage the user sessions, devices, sign-in methods, and security logs. -- [**Users**](https://appwrite.io/docs/server/users) - Manage and list all project users when building backend integrations with Server SDKs. -- [**Teams**](https://appwrite.io/docs/references/cloud/client-web/teams) - Manage and group users in teams. Manage memberships, invites, and user roles within a team. -- [**Databases**](https://appwrite.io/docs/references/cloud/client-web/databases) - Manage databases, collections, and documents. Read, create, update, and delete documents and filter lists of document collections using advanced filters. -- [**Storage**](https://appwrite.io/docs/references/cloud/client-web/storage) - Manage storage files. Read, create, delete, and preview files. Manipulate the preview of your files to perfectly fit your app. All files are scanned by ClamAV and stored in a secure and encrypted way. -- [**Functions**](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Customize your Appwrite project by executing your custom code in a secure, isolated environment. You can trigger your code on any Appwrite system event either manually or using a CRON schedule. -- [**Messaging**](https://appwrite.io/docs/references/cloud/client-web/messaging) - Communicate with your users through push notifications, emails, and SMS text messages using Appwrite Messaging. -- [**Realtime**](https://appwrite.io/docs/realtime) - Listen to real-time events for any of your Appwrite services including users, storage, functions, databases, and more. -- [**Locale**](https://appwrite.io/docs/references/cloud/client-web/locale) - Track your user's location and manage your app locale-based data. -- [**Avatars**](https://appwrite.io/docs/references/cloud/client-web/avatars) - Manage your users' avatars, countries' flags, browser icons, and credit card symbols. Generate QR codes from links or plaintext strings. -- [**MCP**](https://appwrite.io/docs/tooling/mcp) - Use Appwrite's Model Context Protocol (MCP) server to allow LLMs and AI tools like Claude Desktop, Cursor, and Windsurf Editor to directly interact with your Appwrite project through natural language. -- [**Sites**](https://appwrite.io/docs/products/sites) - Develop, deploy, and scale your web applications directly from Appwrite, alongside your backend. - -For the complete API documentation, visit [https://appwrite.io/docs](https://appwrite.io/docs). For more tutorials, news and announcements check out our [blog](https://medium.com/appwrite-io) and [Discord Server](https://discord.gg/GSeTUeA). - ### SDKs Below is a list of currently supported platforms and languages. If you would like to help us add support to your platform of choice, you can go over to our [SDK Generator](https://github.com/appwrite/sdk-generator) project and view our [contribution guide](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md). From 66e68aea143eda00130c3b793960940b788eb4cd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 5 Apr 2026 19:37:29 +0530 Subject: [PATCH 094/118] fix: fail specs when docs are missing --- app/cli.php | 6 +++++- src/Appwrite/SDK/Specification/Format.php | 19 +++++++++++++++++++ .../SDK/Specification/Format/OpenAPI3.php | 4 ++-- .../SDK/Specification/Format/Swagger2.php | 4 ++-- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/app/cli.php b/app/cli.php index b8721320be..b636707f1c 100644 --- a/app/cli.php +++ b/app/cli.php @@ -329,17 +329,20 @@ $setResource('bus', function (Registry $register) use ($cli) { $setResource('telemetry', fn () => new NoTelemetry(), []); +$exitCode = 0; + $cli ->error() ->inject('error') ->inject('logError') - ->action(function (Throwable $error, callable $logError) use ($taskName) { + ->action(function (Throwable $error, callable $logError) use ($taskName, &$exitCode) { call_user_func_array($logError, [ $error, 'Task', $taskName, ]); + $exitCode = 1; Timer::clearAll(); }); @@ -348,3 +351,4 @@ $cli->shutdown()->action(fn () => Timer::clearAll()); Runtime::enableCoroutine(SWOOLE_HOOK_ALL); require_once __DIR__ . '/init/span.php'; run($cli->run(...)); +Console::exit($exitCode); diff --git a/src/Appwrite/SDK/Specification/Format.php b/src/Appwrite/SDK/Specification/Format.php index 7a867c5b91..dd4d378345 100644 --- a/src/Appwrite/SDK/Specification/Format.php +++ b/src/Appwrite/SDK/Specification/Format.php @@ -210,6 +210,25 @@ abstract class Format return $this->services; } + protected function getDescriptionContents(?string $description): string + { + if ($description === null || $description === '') { + return ''; + } + + if (!\str_ends_with($description, '.md')) { + return $description; + } + + $contents = @\file_get_contents($description); + + if ($contents === false) { + throw new \RuntimeException('Documentation file not found or unreadable: ' . $description); + } + + return $contents; + } + protected function getRequestEnumName(string $service, string $method, string $param): ?string { /* `$service` is `$namespace` */ diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 753a0dc52f..41ed386e30 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -128,7 +128,7 @@ class OpenAPI3 extends Format if ($desc === null) { $desc = ''; } - $descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc; + $descContents = $this->getDescriptionContents($desc); $temp = [ 'summary' => $route->getDesc(), @@ -193,7 +193,7 @@ class OpenAPI3 extends Format 'parameters' => [], 'required' => [], 'responses' => [], - 'description' => ($desc) ? \file_get_contents($desc) : '', + 'description' => $this->getDescriptionContents($desc), 'demo' => \strtolower($namespace) . '/' . Template::fromCamelCaseToDash($methodObj->getMethodName()) . '.md', 'public' => $methodObj->isPublic(), ]; diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index 3e9ac891fa..dc65bea215 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -129,7 +129,7 @@ class Swagger2 extends Format if ($desc === null) { $desc = ''; } - $descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc; + $descContents = $this->getDescriptionContents($desc); $temp = [ 'summary' => $route->getDesc(), @@ -201,7 +201,7 @@ class Swagger2 extends Format 'parameters' => [], 'required' => [], 'responses' => [], - 'description' => ($desc) ? \file_get_contents($desc) : '', + 'description' => $this->getDescriptionContents($desc), 'demo' => \strtolower($namespace) . '/' . Template::fromCamelCaseToDash($methodObj->getMethodName()) . '.md', 'public' => $methodObj->isPublic(), ]; From 9be447aacf8b66289a09430410d30e7eeeaf961d Mon Sep 17 00:00:00 2001 From: shimon Date: Sun, 5 Apr 2026 17:20:31 +0300 Subject: [PATCH 095/118] Update enqueue timer and improve schedule function logic Reduced the ENQUEUE_TIMER constant from 60 seconds to 30 seconds. Modified the condition for currentTick to use less than or equal to (<=) instead of less than (<) for better accuracy in scheduling. Changed return statement to continue in case of missing schedule key to enhance flow control. --- src/Appwrite/Platform/Tasks/ScheduleFunctions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php index 88725a190a..69f105652c 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php @@ -61,7 +61,7 @@ class ScheduleFunctions extends ScheduleBase $nextDate = $cron->getNextRunDate(); $next = DateTime::format($nextDate); - $currentTick = $next < $timeFrame; + $currentTick = $next <= $timeFrame; if (!$currentTick) { continue; @@ -88,7 +88,7 @@ class ScheduleFunctions extends ScheduleBase $scheduleKey = $delayConfig['key']; // Ensure schedule was not deleted if (!\array_key_exists($scheduleKey, $this->schedules)) { - return; + continue; } $schedule = $this->schedules[$scheduleKey]; From 5ab28ad99acaf946db36438cdbe126ba6ebf18f7 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 5 Apr 2026 19:52:48 +0530 Subject: [PATCH 096/118] docs: add missing json migration references --- docs/references/migrations/migration-json-export.md | 1 + docs/references/migrations/migration-json-import.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 docs/references/migrations/migration-json-export.md create mode 100644 docs/references/migrations/migration-json-import.md diff --git a/docs/references/migrations/migration-json-export.md b/docs/references/migrations/migration-json-export.md new file mode 100644 index 0000000000..8a955c5990 --- /dev/null +++ b/docs/references/migrations/migration-json-export.md @@ -0,0 +1 @@ +Export documents to a JSON file from your Appwrite database. This endpoint allows you to export documents to a JSON file stored in a secure internal bucket. You'll receive an email with a download link when the export is complete. diff --git a/docs/references/migrations/migration-json-import.md b/docs/references/migrations/migration-json-import.md new file mode 100644 index 0000000000..2eeeaf5619 --- /dev/null +++ b/docs/references/migrations/migration-json-import.md @@ -0,0 +1 @@ +Import documents from a JSON file into your Appwrite database. This endpoint allows you to import documents from a JSON file uploaded to Appwrite Storage bucket. From 412d09b801eba69eff038631daa0141cacd54bd7 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 5 Apr 2026 20:06:13 +0530 Subject: [PATCH 097/118] remove unrelated changes --- app/controllers/general.php | 174 +++++++++--------- composer.json | 1 - composer.lock | 5 +- src/Appwrite/Event/Event.php | 1 + .../Http/Databases/Transactions/Update.php | 59 +++--- .../Http/VectorsDB/Collections/Create.php | 35 ---- src/Utopia/Bus/Bus.php | 2 - 7 files changed, 114 insertions(+), 163 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 61ff689a5c..72c5ce4c15 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1028,107 +1028,107 @@ Http::init() * Automatic certificate generation */ Http::init() - ->groups(['api', 'web']) - ->inject('request') - ->inject('console') - ->inject('dbForPlatform') - ->inject('queueForCertificates') - ->inject('platform') + ->groups(['api', 'web']) + ->inject('request') + ->inject('console') + ->inject('dbForPlatform') + ->inject('queueForCertificates') + ->inject('platform') ->inject('authorization') ->inject('certifiedDomains') - ->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) { - $hostname = $request->getHostname(); - $platformHostnames = $platform['hostnames'] ?? []; + ->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) { + $hostname = $request->getHostname(); + $platformHostnames = $platform['hostnames'] ?? []; - // 1. Cache hit - if ($certifiedDomains->exists(md5($hostname))) { - return; - } + // 1. Cache hit + if ($certifiedDomains->exists(md5($hostname))) { + return; + } - // 2. Domain validation - $domain = new Domain(!empty($hostname) ? $hostname : ''); - if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) { - $certifiedDomains->set(md5($domain->get()), ['value' => 0]); - return; - } + // 2. Domain validation + $domain = new Domain(!empty($hostname) ? $hostname : ''); + if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) { + $certifiedDomains->set(md5($domain->get()), ['value' => 0]); + return; + } - if (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) { - return; - } + if (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) { + return; + } - // 3. Check if domain is a main domain - if (!in_array($domain->get(), $platformHostnames)) { - return; - } + // 3. Check if domain is a main domain + if (!in_array($domain->get(), $platformHostnames)) { + return; + } - // 4. Check/create rule (requires DB access) - $authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, $certifiedDomains) { - try { - // TODO: (@Meldiron) Remove after 1.7.x migration - $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $document = $isMd5 - ? $dbForPlatform->getDocument('rules', md5($domain->get())) - : $dbForPlatform->findOne('rules', [ - Query::equal('domain', [$domain->get()]), - ]); + // 4. Check/create rule (requires DB access) + $authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, $certifiedDomains) { + try { + // TODO: (@Meldiron) Remove after 1.7.x migration + $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; + $document = $isMd5 + ? $dbForPlatform->getDocument('rules', md5($domain->get())) + : $dbForPlatform->findOne('rules', [ + Query::equal('domain', [$domain->get()]), + ]); - if (!$document->isEmpty()) { - return; - } + if (!$document->isEmpty()) { + return; + } - // 5. Create new rule - $owner = ''; + // 5. Create new rule + $owner = ''; - // Mark owner as Appwrite if its appwrite-owned domain - $appwriteDomains = []; - $appwriteDomainEnvs = [ - System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''), - System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), - System::getEnv('_APP_DOMAIN_SITES', ''), - ]; - foreach ($appwriteDomainEnvs as $appwriteDomainEnv) { - foreach (\explode(',', $appwriteDomainEnv) as $appwriteDomain) { - if (empty($appwriteDomain)) { - continue; - } - $appwriteDomains[] = $appwriteDomain; - } - } + // Mark owner as Appwrite if its appwrite-owned domain + $appwriteDomains = []; + $appwriteDomainEnvs = [ + System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''), + System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), + System::getEnv('_APP_DOMAIN_SITES', ''), + ]; + foreach ($appwriteDomainEnvs as $appwriteDomainEnv) { + foreach (\explode(',', $appwriteDomainEnv) as $appwriteDomain) { + if (empty($appwriteDomain)) { + continue; + } + $appwriteDomains[] = $appwriteDomain; + } + } - foreach ($appwriteDomains as $appwriteDomain) { - if (\str_ends_with($domain->get(), $appwriteDomain)) { - $owner = 'Appwrite'; - break; - } - } + foreach ($appwriteDomains as $appwriteDomain) { + if (\str_ends_with($domain->get(), $appwriteDomain)) { + $owner = 'Appwrite'; + break; + } + } - $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); - $document = new Document([ - '$id' => $ruleId, - 'domain' => $domain->get(), - 'type' => 'api', - 'status' => 'verifying', - 'projectId' => $console->getId(), - 'projectInternalId' => $console->getSequence(), - 'search' => implode(' ', [$ruleId, $domain->get()]), - 'owner' => $owner, - 'region' => $console->getAttribute('region') - ]); + $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); + $document = new Document([ + '$id' => $ruleId, + 'domain' => $domain->get(), + 'type' => 'api', + 'status' => 'verifying', + 'projectId' => $console->getId(), + 'projectInternalId' => $console->getSequence(), + 'search' => implode(' ', [$ruleId, $domain->get()]), + 'owner' => $owner, + 'region' => $console->getAttribute('region') + ]); - $dbForPlatform->createDocument('rules', $document); + $dbForPlatform->createDocument('rules', $document); - Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...'); - $queueForCertificates - ->setDomain($document) - ->setSkipRenewCheck(true) - ->trigger(); - } catch (Duplicate $e) { - Console::info('Certificate already exists'); - } finally { - $certifiedDomains->set(md5($domain->get()), ['value' => 1]); - } - }); - }); + Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...'); + $queueForCertificates + ->setDomain($document) + ->setSkipRenewCheck(true) + ->trigger(); + } catch (Duplicate $e) { + Console::info('Certificate already exists'); + } finally { + $certifiedDomains->set(md5($domain->get()), ['value' => 1]); + } + }); + }); Http::options() ->inject('utopia') diff --git a/composer.json b/composer.json index f2263b4b4c..d3474361e2 100644 --- a/composer.json +++ b/composer.json @@ -112,7 +112,6 @@ }, "config": { "platform": { - "php": "8.3" }, "allow-plugins": { "php-http/discovery": true, diff --git a/composer.lock b/composer.lock index 29643c7b08..123b2c88a1 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": "74c4ce8bc6eb2ee021ff2f867939cf16", + "content-hash": "9a409fa43f22650e15a20e0eaaaa4c24", "packages": [ { "name": "adhocore/jwt", @@ -8519,8 +8519,5 @@ "platform-dev": { "ext-fileinfo": "*" }, - "platform-overrides": { - "php": "8.3" - }, "plugin-api-version": "2.9.0" } diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index 5d80044527..ae75e3924f 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -519,6 +519,7 @@ class Event * @param string $pattern * @param array $params * @param ?Document $database + * @param ?Document $database * @return array * @throws \InvalidArgumentException */ diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 9f0839a14b..0c8c6a8520 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -180,35 +180,21 @@ class Update extends Action } $dbForDatabases = $getDatabasesDB($databaseDoc); - $collections = []; try { - $transaction = $authorization->skip(fn () => $dbForProject->updateDocument( - 'transactions', - $transactionId, - new Document(['status' => 'committing']) - )); + $dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $authorization) { + $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'committing', + ]))); - $operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [ - Query::equal('transactionInternalId', [$transaction->getSequence()]), - Query::orderAsc(), - Query::limit(PHP_INT_MAX), - ])); + $operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + Query::orderAsc(), + Query::limit(PHP_INT_MAX), + ])); - foreach ($operations as $operation) { - $databaseInternalId = $operation['databaseInternalId']; - $collectionInternalId = $operation['collectionInternalId']; - $collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}"; - - if (!isset($collections[$collectionId])) { - $collections[$collectionId] = $authorization->skip( - fn () => $dbForProject->getCollection($collectionId) - ); - } - } - - $dbForDatabases->withTransaction(function () use ($dbForDatabases, $transactionState, $operations, $collections, &$totalOperations, &$databaseOperations, &$currentDocumentId) { $state = []; + $collections = []; foreach ($operations as $operation) { $databaseInternalId = $operation['databaseInternalId']; @@ -224,6 +210,11 @@ class Update extends Action $data = $data->getArrayCopy(); } + if (!isset($collections[$collectionId])) { + $collections[$collectionId] = $authorization->skip( + fn () => $dbForProject->getCollection($collectionId) + ); + } $collection = $collections[$collectionId]; if (\is_array($data) && !empty($data)) { @@ -284,17 +275,17 @@ class Update extends Action break; } } + + $transaction = $authorization->skip(fn () => $dbForProject->updateDocument( + 'transactions', + $transactionId, + new Document(['status' => 'committed']) + )); + + $queueForDeletes + ->setType(DELETE_TYPE_DOCUMENT) + ->setDocument($transaction); }); - - $transaction = $authorization->skip(fn () => $dbForProject->updateDocument( - 'transactions', - $transactionId, - new Document(['status' => 'committed']) - )); - - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($transaction); } catch (NotFoundException $e) { $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php index baa31c4ef7..a7e2d68eac 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php @@ -20,7 +20,6 @@ use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; -use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\UID; @@ -117,30 +116,6 @@ class Create extends CollectionAction } /** @var Database $dbForDatabases */ $dbForDatabases = $getDatabasesDB($database); - $cleanupCollection = function () use ($authorization, $dbForProject, $database, $collection): void { - try { - $authorization->skip(fn () => $dbForProject->deleteDocument( - 'database_' . $database->getSequence(), - $collection->getId() - )); - } catch (\Throwable) { - } - - $queries = [ - Query::equal('databaseInternalId', [$database->getSequence()]), - Query::equal('collectionInternalId', [$collection->getSequence()]), - ]; - - try { - $authorization->skip(fn () => $dbForProject->deleteDocuments('attributes', $queries)); - } catch (\Throwable) { - } - - try { - $authorization->skip(fn () => $dbForProject->deleteDocuments('indexes', $queries)); - } catch (\Throwable) { - } - }; $attributes = []; $indexes = []; @@ -159,10 +134,6 @@ class Create extends CollectionAction try { $dbForDatabases->create(); } catch (DuplicateException) { - } catch (\Throwable $e) { - if (!$dbForDatabases->exists(null, Database::METADATA)) { - throw $e; - } } } $dbForDatabases->createCollection( @@ -220,17 +191,11 @@ class Create extends CollectionAction $dbForProject->createDocuments('indexes', $indexDocs); } } catch (DuplicateException) { - $cleanupCollection(); throw new Exception($this->getDuplicateException()); } catch (IndexException) { - $cleanupCollection(); throw new Exception($this->getInvalidIndexException()); } catch (LimitException) { - $cleanupCollection(); throw new Exception($this->getLimitException()); - } catch (\Throwable $e) { - $cleanupCollection(); - throw $e; } $queueForEvents diff --git a/src/Utopia/Bus/Bus.php b/src/Utopia/Bus/Bus.php index 0ff95205be..bef39f0481 100644 --- a/src/Utopia/Bus/Bus.php +++ b/src/Utopia/Bus/Bus.php @@ -2,7 +2,6 @@ namespace Utopia\Bus; -use Utopia\Console; use Utopia\Span\Span; class Bus @@ -44,7 +43,6 @@ class Bus ($listener->getCallback())($event, ...$deps); } catch (\Throwable $e) { Span::error($e); - Console::error('[Bus] Listener ' . $listener::getName() . ' failed: ' . $e->getMessage()); } finally { Span::current()?->finish(); } From b236e2546b38fe8a59db6408c10bf1cbba1c3f5e Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 5 Apr 2026 20:10:00 +0530 Subject: [PATCH 098/118] lock file --- composer.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index 123b2c88a1..fbf8937859 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": "9a409fa43f22650e15a20e0eaaaa4c24", + "content-hash": "e9c38bbebc60849e70e3640aaa4422cd", "packages": [ { "name": "adhocore/jwt", From 5d1da00138c87a7b9e6c082a5feb6413ce57ee92 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 5 Apr 2026 20:12:25 +0530 Subject: [PATCH 099/118] refactor: remove redundant desc guards --- src/Appwrite/SDK/Specification/Format/OpenAPI3.php | 3 --- src/Appwrite/SDK/Specification/Format/Swagger2.php | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 41ed386e30..88f577eac6 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -125,9 +125,6 @@ class OpenAPI3 extends Format $namespace = $sdk->getNamespace() ?? 'default'; - if ($desc === null) { - $desc = ''; - } $descContents = $this->getDescriptionContents($desc); $temp = [ diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index dc65bea215..f9c79431f0 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -126,9 +126,6 @@ class Swagger2 extends Format $sdkPlatforms = array_values(array_unique($sdkPlatforms)); $namespace = $sdk->getNamespace() ?? 'default'; - if ($desc === null) { - $desc = ''; - } $descContents = $this->getDescriptionContents($desc); $temp = [ From 452440f3c0e87618bc92656fedfbf44b5d31da37 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 5 Apr 2026 21:03:17 +0530 Subject: [PATCH 100/118] fix: use released cli container support --- app/cli.php | 58 ++++++++++--------- composer.lock | 12 ++-- .../Http/Databases/Transactions/Update.php | 58 +++++++++++-------- 3 files changed, 72 insertions(+), 56 deletions(-) diff --git a/app/cli.php b/app/cli.php index cf655143a4..06f20b9897 100644 --- a/app/cli.php +++ b/app/cli.php @@ -18,12 +18,15 @@ use Swoole\Timer; use Utopia\Cache\Adapter\Pool as CachePool; use Utopia\Cache\Adapter\Sharding; use Utopia\Cache\Cache; +use Utopia\CLI\Adapters\Generic; +use Utopia\CLI\CLI; use Utopia\Config\Config; use Utopia\Console; use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use Utopia\DI\Container; use Utopia\DSN\DSN; use Utopia\Logger\Log; use Utopia\Platform\Service; @@ -55,12 +58,15 @@ if (! isset($args[0])) { } $taskName = $args[0]; +$container = new Container(); +$cli = new CLI(new Generic(), $_SERVER['argv'] ?? [], $container); + +$platform->setCli($cli); $platform->init(Service::TYPE_TASK); -$cli = $platform->getCli(); -$cli->setResource('register', fn () => $register, []); +$container->set('register', fn () => $register, []); -$cli->setResource('cache', function ($pools) { +$container->set('cache', function ($pools) { $list = Config::getParam('pools-cache', []); $adapters = []; @@ -71,18 +77,18 @@ $cli->setResource('cache', function ($pools) { return new Cache(new Sharding($adapters)); }, ['pools']); -$cli->setResource('pools', function (Registry $register) { +$container->set('pools', function (Registry $register) { return $register->get('pools'); }, ['register']); -$cli->setResource('authorization', function () { +$container->set('authorization', function () { $authorization = new Authorization(); $authorization->disable(); return $authorization; }, []); -$cli->setResource('dbForPlatform', function ($pools, $cache, $authorization) { +$container->set('dbForPlatform', function ($pools, $cache, $authorization) { $sleep = 3; $maxAttempts = 5; $attempts = 0; @@ -125,17 +131,17 @@ $cli->setResource('dbForPlatform', function ($pools, $cache, $authorization) { return $dbForPlatform; }, ['pools', 'cache', 'authorization']); -$cli->setResource('console', function () { +$container->set('console', function () { return new Document(Config::getParam('console')); }, []); -$cli->setResource( +$container->set( 'isResourceBlocked', fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false, [] ); -$cli->setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) { +$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) { $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) { @@ -197,7 +203,7 @@ $cli->setResource('getProjectDB', function (Group $pools, Database $dbForPlatfor }; }, ['pools', 'dbForPlatform', 'cache', 'authorization']); -$cli->setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { +$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { $database = null; return function (?Document $project = null) use ($pools, $cache, &$database, $authorization) { @@ -225,41 +231,41 @@ $cli->setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizati return $database; }; }, ['pools', 'cache', 'authorization']); -$cli->setResource('publisher', function (Group $pools) { +$container->set('publisher', function (Group $pools) { return new BrokerPool(publisher: $pools->get('publisher')); }, ['pools']); -$cli->setResource('publisherDatabases', function (BrokerPool $publisher) { +$container->set('publisherDatabases', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$cli->setResource('publisherFunctions', function (BrokerPool $publisher) { +$container->set('publisherFunctions', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$cli->setResource('publisherMigrations', function (BrokerPool $publisher) { +$container->set('publisherMigrations', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$cli->setResource('publisherMessaging', function (BrokerPool $publisher) { +$container->set('publisherMessaging', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$cli->setResource('usage', function () { +$container->set('usage', function () { return new UsageContext(); }, []); -$cli->setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher( +$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher( $publisher, new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME)) ), ['publisher']); -$cli->setResource('queueForStatsResources', function (Publisher $publisher) { +$container->set('queueForStatsResources', function (Publisher $publisher) { return new StatsResources($publisher); }, ['publisher']); -$cli->setResource('queueForFunctions', function (Publisher $publisher) { +$container->set('queueForFunctions', function (Publisher $publisher) { return new Func($publisher); }, ['publisher']); -$cli->setResource('queueForDeletes', function (Publisher $publisher) { +$container->set('queueForDeletes', function (Publisher $publisher) { return new Delete($publisher); }, ['publisher']); -$cli->setResource('queueForCertificates', function (Publisher $publisher) { +$container->set('queueForCertificates', function (Publisher $publisher) { return new Certificate($publisher); }, ['publisher']); -$cli->setResource('logError', function (Registry $register) { +$container->set('logError', function (Registry $register) { return function (Throwable $error, string $namespace, string $action) use ($register) { Console::error('[Error] Timestamp: ' . date('c', time())); Console::error('[Error] Type: ' . get_class($error)); @@ -311,13 +317,13 @@ $cli->setResource('logError', function (Registry $register) { }; }, ['register']); -$cli->setResource('executor', fn () => new Executor(), []); +$container->set('executor', fn () => new Executor(), []); -$cli->setResource('bus', function (Registry $register) use ($cli) { - return $register->get('bus')->setResolver(fn (string $name) => $cli->getResource($name)); +$container->set('bus', function (Registry $register) use ($container) { + return $register->get('bus')->setResolver(fn (string $name) => $container->get($name)); }, ['register']); -$cli->setResource('telemetry', fn () => new NoTelemetry(), []); +$container->set('telemetry', fn () => new NoTelemetry(), []); $cli ->error() diff --git a/composer.lock b/composer.lock index fbf8937859..d113b99785 100644 --- a/composer.lock +++ b/composer.lock @@ -3658,16 +3658,16 @@ }, { "name": "utopia-php/cli", - "version": "0.23.0", + "version": "0.23.1", "source": { "type": "git", "url": "https://github.com/utopia-php/cli.git", - "reference": "4efef2662b27cdce0d6d09ea5c3a16a1cca2ba6c" + "reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cli/zipball/4efef2662b27cdce0d6d09ea5c3a16a1cca2ba6c", - "reference": "4efef2662b27cdce0d6d09ea5c3a16a1cca2ba6c", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/8d1955b8bc4dc631f45d7c7df689ed7b63f70621", + "reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621", "shasum": "" }, "require": { @@ -3703,9 +3703,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cli/issues", - "source": "https://github.com/utopia-php/cli/tree/0.23.0" + "source": "https://github.com/utopia-php/cli/tree/0.23.1" }, - "time": "2026-03-13T12:23:18+00:00" + "time": "2026-04-05T15:27:35+00:00" }, { "name": "utopia-php/compression", diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 0c8c6a8520..c4d51e6c64 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -182,19 +182,33 @@ class Update extends Action $dbForDatabases = $getDatabasesDB($databaseDoc); try { - $dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $authorization) { - $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'committing', - ]))); + $transaction = $authorization->skip(fn () => $dbForProject->updateDocument( + 'transactions', + $transactionId, + new Document(['status' => 'committing']) + )); - $operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [ - Query::equal('transactionInternalId', [$transaction->getSequence()]), - Query::orderAsc(), - Query::limit(PHP_INT_MAX), - ])); + $operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + Query::orderAsc(), + Query::limit(PHP_INT_MAX), + ])); + $collections = []; + foreach ($operations as $operation) { + $databaseInternalId = $operation['databaseInternalId']; + $collectionInternalId = $operation['collectionInternalId']; + $collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}"; + + if (!isset($collections[$collectionId])) { + $collections[$collectionId] = $authorization->skip( + fn () => $dbForProject->getCollection($collectionId) + ); + } + } + + $dbForDatabases->withTransaction(function () use ($dbForDatabases, $transactionState, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $collections) { $state = []; - $collections = []; foreach ($operations as $operation) { $databaseInternalId = $operation['databaseInternalId']; @@ -210,11 +224,6 @@ class Update extends Action $data = $data->getArrayCopy(); } - if (!isset($collections[$collectionId])) { - $collections[$collectionId] = $authorization->skip( - fn () => $dbForProject->getCollection($collectionId) - ); - } $collection = $collections[$collectionId]; if (\is_array($data) && !empty($data)) { @@ -276,16 +285,17 @@ class Update extends Action } } - $transaction = $authorization->skip(fn () => $dbForProject->updateDocument( - 'transactions', - $transactionId, - new Document(['status' => 'committed']) - )); - - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($transaction); }); + + $transaction = $authorization->skip(fn () => $dbForProject->updateDocument( + 'transactions', + $transactionId, + new Document(['status' => 'committed']) + )); + + $queueForDeletes + ->setType(DELETE_TYPE_DOCUMENT) + ->setDocument($transaction); } catch (NotFoundException $e) { $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', From 44f3bbae03115024ab3132d88c4ed86757aa64ac Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 6 Apr 2026 01:40:07 +0000 Subject: [PATCH 101/118] fix: add CORS headers to error responses The Http::error() handler was missing CORS headers, causing browsers to block error responses (e.g. 403 PROJECT_PAUSED) with a generic CORS error instead of showing the actual error message. This injects the cors resource into the error handler and adds CORS headers before sending the error response, matching the pattern already used in Http::init(). Co-Authored-By: Claude Opus 4.6 (1M context) --- app/controllers/general.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 3bf5f027f2..3f8adeb368 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1196,7 +1196,8 @@ Http::error() ->inject('bus') ->inject('devKey') ->inject('authorization') - ->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) { + ->inject('cors') + ->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization, Cors $cors) { $trace = $error->getTrace(); foreach (array_slice($trace, 0, 100) as $index => $traceEntry) { @@ -1493,6 +1494,10 @@ Http::error() 'type' => $type, ]; + foreach ($cors->headers($request->getOrigin()) as $name => $value) { + $response->addHeader($name, $value); + } + $response ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') ->addHeader('Expires', '0') From ba2584987136a5e4e430891e16563e313ff50d13 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 6 Apr 2026 02:59:08 +0000 Subject: [PATCH 102/118] fix: resolve cors safely in error handler to avoid cascading failures - Remove cors from inject chain; resolve via getResource() inside try-catch so DB failures don't cascade when resolving the cors resource dependency chain (cors -> allowedHostnames -> rule -> DB) - Use override:true on addHeader to prevent duplicate CORS headers when init() already set them before the exception was thrown - Degrades gracefully: if cors resolution fails, error response is sent without CORS headers (same behavior as before this PR) Co-Authored-By: Claude Opus 4.6 (1M context) --- app/controllers/general.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 3f8adeb368..3eeeef3fae 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1196,8 +1196,7 @@ Http::error() ->inject('bus') ->inject('devKey') ->inject('authorization') - ->inject('cors') - ->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization, Cors $cors) { + ->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) { $trace = $error->getTrace(); foreach (array_slice($trace, 0, 100) as $index => $traceEntry) { @@ -1494,8 +1493,17 @@ Http::error() 'type' => $type, ]; - foreach ($cors->headers($request->getOrigin()) as $name => $value) { - $response->addHeader($name, $value); + // Add CORS headers to error responses so browsers can read the error. + // Wrapped in try-catch: if the error itself is a DB failure, resolving + // the cors resource (which depends on rule -> DB) would cascade. + // Uses override:true to avoid duplicate headers if init() already set them. + try { + $cors = $utopia->getResource('cors'); + foreach ($cors->headers($request->getOrigin()) as $name => $value) { + $response->addHeader($name, $value, override: true); + } + } catch (Throwable) { + // Degrade gracefully - error response without CORS is no worse than before. } $response From cb74a5756a81d0b961756583ab348fbd2eccc894 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 10:20:18 +0530 Subject: [PATCH 103/118] Remove request and response static state --- app/controllers/general.php | 4 ++-- src/Appwrite/Utopia/Request.php | 18 ++++++++--------- src/Appwrite/Utopia/Response.php | 31 +++++++++++++++++++++++++++--- tests/unit/Utopia/RequestTest.php | 15 +++++++++++++++ tests/unit/Utopia/ResponseTest.php | 23 ++++++++++++++++++++++ 5 files changed, 77 insertions(+), 14 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 3bf5f027f2..dcc5764bdd 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -868,7 +868,7 @@ Http::init() * Request format */ $route = $utopia->getRoute(); - Request::setRoute($route); + $request->setRoute($route); if ($route === null) { $response->setStatusCode(404); @@ -1019,7 +1019,7 @@ Http::init() return; } $route = $request->getRoute(); - if ($route->getLabel('origin', false) === '*') { + if ($route?->getLabel('origin', false) === '*') { return; } if (!$originValidator->isValid($origin)) { diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index 9428ff9d88..ed602ecdd5 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -17,7 +17,7 @@ class Request extends UtopiaRequest * @var array */ private array $filters = []; - private static ?Route $route = null; + private ?Route $route = null; public function __construct(SwooleRequest $request) { @@ -34,11 +34,11 @@ class Request extends UtopiaRequest { $parameters = parent::getParams(); - if (!$this->hasFilters() || !self::hasRoute()) { + if (!$this->hasFilters() || !$this->hasRoute()) { return $parameters; } - $methods = self::getRoute()->getLabel('sdk', null); + $methods = $this->getRoute()?->getLabel('sdk', null); if (empty($methods)) { return $parameters; @@ -131,9 +131,9 @@ class Request extends UtopiaRequest * * @return void */ - public static function setRoute(?Route $route): void + public function setRoute(?Route $route): void { - self::$route = $route; + $this->route = $route; } /** @@ -141,9 +141,9 @@ class Request extends UtopiaRequest * * @return Route|null */ - public static function getRoute(): ?Route + public function getRoute(): ?Route { - return self::$route; + return $this->route; } /** @@ -151,9 +151,9 @@ class Request extends UtopiaRequest * * @return bool */ - public static function hasRoute(): bool + public function hasRoute(): bool { - return self::$route !== null; + return $this->route !== null; } /** diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index e01dc58bf6..649b0562a5 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -8,6 +8,7 @@ use Appwrite\Utopia\Response\Filter; use Appwrite\Utopia\Response\Model; use Exception; use JsonException; +use Swoole\Coroutine; use Swoole\Http\Response as SwooleHTTPResponse; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; @@ -19,6 +20,8 @@ use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; */ class Response extends SwooleResponse { + private const SHOW_SENSITIVE_CONTEXT_KEY = '__appwrite_response_show_sensitive'; + // General public const MODEL_NONE = 'none'; public const MODEL_ANY = 'any'; @@ -509,7 +512,7 @@ class Response extends SwooleResponse $isPrivilegedUser = $user->isPrivileged($roles); $isAppUser = $user->isApp($roles); - if ((!$isPrivilegedUser && !$isAppUser) && !self::$showSensitive) { + if ((!$isPrivilegedUser && !$isAppUser) && !self::isShowingSensitive()) { $data->setAttribute($key, ''); } } @@ -666,14 +669,36 @@ class Response extends SwooleResponse */ public static function showSensitive(callable $callback): array { + $previous = self::isShowingSensitive(); + try { - self::$showSensitive = true; + self::setShowSensitive(true); return $callback(); } finally { - self::$showSensitive = false; + self::setShowSensitive($previous); } } + private static function isShowingSensitive(): bool + { + if (Coroutine::getCid() !== -1) { + return (bool) (Coroutine::getContext()[self::SHOW_SENSITIVE_CONTEXT_KEY] ?? false); + } + + return self::$showSensitive; + } + + private static function setShowSensitive(bool $value): void + { + if (Coroutine::getCid() !== -1) { + Coroutine::getContext()[self::SHOW_SENSITIVE_CONTEXT_KEY] = $value; + + return; + } + + self::$showSensitive = $value; + } + private ?Authorization $authorization = null; private ?DBUser $user = null; diff --git a/tests/unit/Utopia/RequestTest.php b/tests/unit/Utopia/RequestTest.php index 78a3717c38..d5cd5d800a 100644 --- a/tests/unit/Utopia/RequestTest.php +++ b/tests/unit/Utopia/RequestTest.php @@ -147,6 +147,21 @@ class RequestTest extends TestCase $this->assertSame('unexpected', $params['extra']); } + public function testRouteIsScopedToRequestInstance(): void + { + $firstRequest = new Request(new SwooleRequest()); + $secondRequest = new Request(new SwooleRequest()); + + $firstRoute = new Route(Request::METHOD_GET, '/first'); + $secondRoute = new Route(Request::METHOD_GET, '/second'); + + $firstRequest->setRoute($firstRoute); + $secondRequest->setRoute($secondRoute); + + $this->assertSame($firstRoute, $firstRequest->getRoute()); + $this->assertSame($secondRoute, $secondRequest->getRoute()); + } + /** * Helper to attach a route with multiple SDK methods to the request. */ diff --git a/tests/unit/Utopia/ResponseTest.php b/tests/unit/Utopia/ResponseTest.php index 452119fafb..d5c3a079cd 100644 --- a/tests/unit/Utopia/ResponseTest.php +++ b/tests/unit/Utopia/ResponseTest.php @@ -5,6 +5,7 @@ namespace Tests\Unit\Utopia; use Appwrite\Utopia\Response; use Exception; use PHPUnit\Framework\TestCase; +use ReflectionMethod; use Swoole\Http\Response as SwooleResponse; use Tests\Unit\Utopia\Response\Filters\First; use Tests\Unit\Utopia\Response\Filters\Second; @@ -176,4 +177,26 @@ class ResponseTest extends TestCase $this->assertArrayHasKey('required', $single); $this->assertArrayNotHasKey('hidden', $singleFromArray); } + + public function testShowSensitiveRestoresPreviousState(): void + { + $isShowingSensitive = new ReflectionMethod(Response::class, 'isShowingSensitive'); + + $this->assertFalse($isShowingSensitive->invoke(null)); + + $payload = Response::showSensitive(function () use ($isShowingSensitive) { + return [ + 'outer' => $isShowingSensitive->invoke(null), + 'inner' => Response::showSensitive(fn () => [ + 'state' => $isShowingSensitive->invoke(null), + ]), + 'afterInner' => $isShowingSensitive->invoke(null), + ]; + }); + + $this->assertTrue($payload['outer']); + $this->assertTrue($payload['inner']['state']); + $this->assertTrue($payload['afterInner']); + $this->assertFalse($isShowingSensitive->invoke(null)); + } } From b8eb0810c2d94ac4061311fbbefead5f8729f66e Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 10:24:32 +0530 Subject: [PATCH 104/118] Make response sensitive mode instance-scoped --- app/controllers/api/account.php | 8 +++---- src/Appwrite/Utopia/Response.php | 37 ++++++------------------------ tests/unit/Utopia/ResponseTest.php | 18 +++++++-------- 3 files changed, 20 insertions(+), 43 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index d576bbce44..fb968d3972 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -3937,7 +3937,7 @@ Http::post('/v1/account/recovery') ->setParam('userId', $profile->getId()) ->setParam('tokenId', $recovery->getId()) ->setUser($profile) - ->setPayload(Response::showSensitive(fn () => $response->output($recovery, Response::MODEL_TOKEN)), sensitive: ['secret']); + ->setPayload($response->showSensitive(fn () => $response->output($recovery, Response::MODEL_TOKEN)), sensitive: ['secret']); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -4038,7 +4038,7 @@ Http::put('/v1/account/recovery') $queueForEvents ->setParam('userId', $profile->getId()) ->setParam('tokenId', $recoveryDocument->getId()) - ->setPayload(Response::showSensitive(fn () => $response->output($recoveryDocument, Response::MODEL_TOKEN)), sensitive: ['secret']); + ->setPayload($response->showSensitive(fn () => $response->output($recoveryDocument, Response::MODEL_TOKEN)), sensitive: ['secret']); $response->dynamic($recoveryDocument, Response::MODEL_TOKEN); }); @@ -4268,7 +4268,7 @@ Http::post('/v1/account/verifications/email') $queueForEvents ->setParam('userId', $user->getId()) ->setParam('tokenId', $verification->getId()) - ->setPayload(Response::showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']); + ->setPayload($response->showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -4360,7 +4360,7 @@ Http::put('/v1/account/verifications/email') $queueForEvents ->setParam('userId', $userId) ->setParam('tokenId', $verification->getId()) - ->setPayload(Response::showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']); + ->setPayload($response->showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']); $response->dynamic($verification, Response::MODEL_TOKEN); }); diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 649b0562a5..9d0e8abefa 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -8,7 +8,6 @@ use Appwrite\Utopia\Response\Filter; use Appwrite\Utopia\Response\Model; use Exception; use JsonException; -use Swoole\Coroutine; use Swoole\Http\Response as SwooleHTTPResponse; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; @@ -20,8 +19,6 @@ use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; */ class Response extends SwooleResponse { - private const SHOW_SENSITIVE_CONTEXT_KEY = '__appwrite_response_show_sensitive'; - // General public const MODEL_NONE = 'none'; public const MODEL_ANY = 'any'; @@ -302,7 +299,7 @@ class Response extends SwooleResponse /** * @var bool */ - protected static bool $showSensitive = false; + protected bool $showSensitive = false; /** * @var array @@ -512,7 +509,7 @@ class Response extends SwooleResponse $isPrivilegedUser = $user->isPrivileged($roles); $isAppUser = $user->isApp($roles); - if ((!$isPrivilegedUser && !$isAppUser) && !self::isShowingSensitive()) { + if ((!$isPrivilegedUser && !$isAppUser) && !$this->showSensitive) { $data->setAttribute($key, ''); } } @@ -662,43 +659,23 @@ class Response extends SwooleResponse } /** - * Static wrapper to show sensitive data in response + * Wrapper to show sensitive data in response * * @param callable(): array $callback The callback to show sensitive information for * @return array */ - public static function showSensitive(callable $callback): array + public function showSensitive(callable $callback): array { - $previous = self::isShowingSensitive(); + $previous = $this->showSensitive; try { - self::setShowSensitive(true); + $this->showSensitive = true; return $callback(); } finally { - self::setShowSensitive($previous); + $this->showSensitive = $previous; } } - private static function isShowingSensitive(): bool - { - if (Coroutine::getCid() !== -1) { - return (bool) (Coroutine::getContext()[self::SHOW_SENSITIVE_CONTEXT_KEY] ?? false); - } - - return self::$showSensitive; - } - - private static function setShowSensitive(bool $value): void - { - if (Coroutine::getCid() !== -1) { - Coroutine::getContext()[self::SHOW_SENSITIVE_CONTEXT_KEY] = $value; - - return; - } - - self::$showSensitive = $value; - } - private ?Authorization $authorization = null; private ?DBUser $user = null; diff --git a/tests/unit/Utopia/ResponseTest.php b/tests/unit/Utopia/ResponseTest.php index d5c3a079cd..be8cfdc216 100644 --- a/tests/unit/Utopia/ResponseTest.php +++ b/tests/unit/Utopia/ResponseTest.php @@ -5,7 +5,7 @@ namespace Tests\Unit\Utopia; use Appwrite\Utopia\Response; use Exception; use PHPUnit\Framework\TestCase; -use ReflectionMethod; +use ReflectionProperty; use Swoole\Http\Response as SwooleResponse; use Tests\Unit\Utopia\Response\Filters\First; use Tests\Unit\Utopia\Response\Filters\Second; @@ -180,23 +180,23 @@ class ResponseTest extends TestCase public function testShowSensitiveRestoresPreviousState(): void { - $isShowingSensitive = new ReflectionMethod(Response::class, 'isShowingSensitive'); + $isShowingSensitive = new ReflectionProperty(Response::class, 'showSensitive'); - $this->assertFalse($isShowingSensitive->invoke(null)); + $this->assertFalse($isShowingSensitive->getValue($this->response)); - $payload = Response::showSensitive(function () use ($isShowingSensitive) { + $payload = $this->response->showSensitive(function () use ($isShowingSensitive) { return [ - 'outer' => $isShowingSensitive->invoke(null), - 'inner' => Response::showSensitive(fn () => [ - 'state' => $isShowingSensitive->invoke(null), + 'outer' => $isShowingSensitive->getValue($this->response), + 'inner' => $this->response->showSensitive(fn () => [ + 'state' => $isShowingSensitive->getValue($this->response), ]), - 'afterInner' => $isShowingSensitive->invoke(null), + 'afterInner' => $isShowingSensitive->getValue($this->response), ]; }); $this->assertTrue($payload['outer']); $this->assertTrue($payload['inner']['state']); $this->assertTrue($payload['afterInner']); - $this->assertFalse($isShowingSensitive->invoke(null)); + $this->assertFalse($isShowingSensitive->getValue($this->response)); } } From b8ed30db55ad7e8203ad8de12d1039d0bb513429 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 12:23:50 +0530 Subject: [PATCH 105/118] Fix CORS header override for analyze --- app/controllers/general.php | 4 +++- composer.lock | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 23d0331fad..ac6b60ee17 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1497,7 +1497,9 @@ Http::error() try { $cors = $utopia->getResource('cors'); foreach ($cors->headers($request->getOrigin()) as $name => $value) { - $response->addHeader($name, $value, override: true); + $response + ->removeHeader($name) + ->addHeader($name, $value); } } catch (Throwable) { // Degrade gracefully - error response without CORS is no worse than before. diff --git a/composer.lock b/composer.lock index d113b99785..d71f78b35d 100644 --- a/composer.lock +++ b/composer.lock @@ -4271,16 +4271,16 @@ }, { "name": "utopia-php/framework", - "version": "0.34.16", + "version": "0.34.17", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "2b4021ba3f9d476264ce9fd6703d6c79de9add7f" + "reference": "d3e4143b8b06d9823d0c29a06dacefa5a1b93677" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/2b4021ba3f9d476264ce9fd6703d6c79de9add7f", - "reference": "2b4021ba3f9d476264ce9fd6703d6c79de9add7f", + "url": "https://api.github.com/repos/utopia-php/http/zipball/d3e4143b8b06d9823d0c29a06dacefa5a1b93677", + "reference": "d3e4143b8b06d9823d0c29a06dacefa5a1b93677", "shasum": "" }, "require": { @@ -4319,9 +4319,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.34.16" + "source": "https://github.com/utopia-php/http/tree/0.34.17" }, - "time": "2026-03-20T10:39:07+00:00" + "time": "2026-04-06T04:40:23+00:00" }, { "name": "utopia-php/http", From 221b52bac0a2dfbd571d01e97a6213875c5a3f17 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 12:30:25 +0530 Subject: [PATCH 106/118] Add request-scoped cookie domain resource --- app/controllers/api/account.php | 59 +++++++++++-------- app/controllers/general.php | 14 ----- app/init/resources.php | 33 +++++++++++ .../Teams/Http/Memberships/Status/Update.php | 7 ++- 4 files changed, 71 insertions(+), 42 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index fb968d3972..fa0e30ee53 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -207,7 +207,7 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr } -$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, Authorization $authorization) { +$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, ?string $cookieDomain, Authorization $authorization) { // Attempt to decode secret as a JWT (used by OAuth2 token flow to carry provider info) $oauthProvider = null; @@ -353,8 +353,8 @@ $createSession = function (string $userId, string $secret, Request $request, Res $protocol = $request->getProtocol(); $response - ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null) + ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED); $countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')); @@ -719,7 +719,8 @@ Http::delete('/v1/account/sessions') ->inject('queueForDeletes') ->inject('store') ->inject('proofForToken') - ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) { + ->inject('cookieDomain') + ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, ?string $cookieDomain) { $protocol = $request->getProtocol(); $sessions = $user->getAttribute('sessions', []); @@ -741,8 +742,8 @@ Http::delete('/v1/account/sessions') // If current session delete the cookies too $response - ->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null) + ->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite')); // Use current session for events. $currentSession = $session; @@ -849,7 +850,8 @@ Http::delete('/v1/account/sessions/:sessionId') ->inject('queueForDeletes') ->inject('store') ->inject('proofForToken') - ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) { + ->inject('cookieDomain') + ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, ?string $cookieDomain) { $protocol = $request->getProtocol(); $sessionId = ($sessionId === 'current') @@ -880,8 +882,8 @@ Http::delete('/v1/account/sessions/:sessionId') } $response - ->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null) + ->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite')); } $dbForProject->purgeCachedDocument('users', $user->getId()); @@ -1035,8 +1037,9 @@ Http::post('/v1/account/sessions/email') ->inject('store') ->inject('proofForPassword') ->inject('proofForToken') + ->inject('cookieDomain') ->inject('authorization') - ->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) { + ->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, ?string $cookieDomain, Authorization $authorization) { $email = \strtolower($email); $protocol = $request->getProtocol(); @@ -1117,8 +1120,8 @@ Http::post('/v1/account/sessions/email') $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $response - ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null) + ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED) ; @@ -1184,8 +1187,9 @@ Http::post('/v1/account/sessions/anonymous') ->inject('store') ->inject('proofForPassword') ->inject('proofForToken') + ->inject('cookieDomain') ->inject('authorization') - ->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) { + ->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, ?string $cookieDomain, Authorization $authorization) { $protocol = $request->getProtocol(); if ('console' === $project->getId()) { @@ -1283,8 +1287,8 @@ Http::post('/v1/account/sessions/anonymous') $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $response - ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null) + ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED) ; @@ -1339,7 +1343,8 @@ Http::post('/v1/account/sessions/token') ->inject('store') ->inject('proofForToken') ->inject('proofForCode') -->inject('authorization') + ->inject('cookieDomain') + ->inject('authorization') ->action($createSession); Http::get('/v1/account/sessions/oauth2/:provider') @@ -1538,8 +1543,9 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->inject('proofForPassword') ->inject('proofForToken') ->inject('plan') + ->inject('cookieDomain') ->inject('authorization') - ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, Authorization $authorization) use ($oauthDefaultSuccess) { + ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, ?string $cookieDomain, Authorization $authorization) use ($oauthDefaultSuccess) { $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; $port = $request->getPort(); $callbackBase = $protocol . '://' . $request->getHostname(); @@ -2068,14 +2074,14 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') // TODO: Remove this deprecated workaround - support only token if ($state['success']['path'] == $oauthDefaultSuccess) { $query['project'] = $project->getId(); - $query['domain'] = Config::getParam('cookieDomain'); + $query['domain'] = $cookieDomain; $query['key'] = $store->getKey(); $query['secret'] = $encoded; } $response - ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null) + ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite')); } if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) { @@ -2886,11 +2892,12 @@ Http::put('/v1/account/sessions/magic-url') ->inject('queueForMails') ->inject('store') ->inject('proofForCode') + ->inject('cookieDomain') ->inject('authorization') - ->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $authorization) use ($createSession) { + ->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $cookieDomain, $authorization) use ($createSession) { $proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL); $proofForToken->setHash(new Sha()); - $createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $authorization); + $createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $cookieDomain, $authorization); }); Http::put('/v1/account/sessions/phone') @@ -2936,6 +2943,7 @@ Http::put('/v1/account/sessions/phone') ->inject('store') ->inject('proofForToken') ->inject('proofForCode') + ->inject('cookieDomain') ->inject('authorization') ->action($createSession); @@ -3727,7 +3735,8 @@ Http::patch('/v1/account/status') ->inject('dbForProject') ->inject('queueForEvents') ->inject('store') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store) { + ->inject('cookieDomain') + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store, ?string $cookieDomain) { $user->setAttribute('status', false); @@ -3743,8 +3752,8 @@ Http::patch('/v1/account/status') $protocol = $request->getProtocol(); $response - ->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null) + ->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite')) ; $response->dynamic($user, Response::MODEL_ACCOUNT); diff --git a/app/controllers/general.php b/app/controllers/general.php index d10ad9a060..af85c4e459 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -924,20 +924,6 @@ Http::init() $isLocalHost = in_array($request->getHostname(), $localHosts); $isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false; - $isConsoleProject = $project->getAttribute('$id', '') === 'console'; - $isConsoleRootSession = System::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled'; - - Config::setParam( - 'cookieDomain', - $isLocalHost || $isIpAddress - ? null - : ( - $isConsoleProject && $isConsoleRootSession - ? '.' . $selfDomain->getRegisterable() - : '.' . $request->getHostname() - ) - ); - $warnings = []; /* diff --git a/app/init/resources.php b/app/init/resources.php index 8acecb8e3e..472c52fa4e 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -53,6 +53,7 @@ use Utopia\Database\DateTime as DatabaseDateTime; use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Domains\Domain; use Utopia\DSN\DSN; use Utopia\Http\Http; use Utopia\Locale\Locale; @@ -249,6 +250,38 @@ Http::setResource('allowedSchemes', function (array $platform, Document $project return array_unique($allowed); }, ['platform', 'project']); +/** + * Cookie domain for the current request. + */ +Http::setResource('cookieDomain', function (Request $request, Document $project) { + $localHosts = ['localhost', 'localhost:' . $request->getPort()]; + + $migrationHost = System::getEnv('_APP_MIGRATION_HOST'); + if (!empty($migrationHost)) { + $localHosts[] = $migrationHost; + $localHosts[] = $migrationHost . ':' . $request->getPort(); + } + + $hostname = $request->getHostname(); + $isLocalHost = \in_array($hostname, $localHosts, true); + $isIpAddress = \filter_var($hostname, FILTER_VALIDATE_IP) !== false; + + if ($isLocalHost || $isIpAddress) { + return; + } + + $isConsoleProject = $project->getAttribute('$id', '') === 'console'; + $isConsoleRootSession = System::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled'; + + if ($isConsoleProject && $isConsoleRootSession) { + $domain = new Domain($hostname); + + return '.' . $domain->getRegisterable(); + } + + return '.' . $hostname; +}, ['request', 'project']); + /** * Rule associated with a request origin. */ diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php index 46b6c3cacf..f1c0a5cad5 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php @@ -74,10 +74,11 @@ class Update extends Action ->inject('queueForEvents') ->inject('store') ->inject('proofForToken') + ->inject('cookieDomain') ->callback($this->action(...)); } - public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) + public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken, ?string $cookieDomain) { $protocol = $request->getProtocol(); @@ -172,7 +173,7 @@ class Update extends Action value: $encoded, expire: (new \DateTime($expire))->getTimestamp(), path: '/', - domain: Config::getParam('cookieDomain'), + domain: $cookieDomain, secure: ('https' === $protocol), httponly: true ) @@ -181,7 +182,7 @@ class Update extends Action value: $encoded, expire: (new \DateTime($expire))->getTimestamp(), path: '/', - domain: Config::getParam('cookieDomain'), + domain: $cookieDomain, secure: ('https' === $protocol), httponly: true, sameSite: Config::getParam('cookieSamesite') From d1b59ff3f3b9d5d0f60f41d3ef5eb84b45bda802 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 12:30:48 +0530 Subject: [PATCH 107/118] Remove unused cookie domain locals --- app/controllers/general.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index af85c4e459..917588bee3 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -921,9 +921,6 @@ Http::init() $localHosts[] = $migrationHost.':'.$request->getPort(); } - $isLocalHost = in_array($request->getHostname(), $localHosts); - $isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false; - $warnings = []; /* From 1f7fc4bd40a69e703f563d47b3884f0b7bd3a0ff Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 12:43:05 +0530 Subject: [PATCH 108/118] Use request-scoped domain verification --- app/controllers/api/account.php | 41 +++++++++++-------- app/controllers/general.php | 9 ---- app/init/resources.php | 12 ++++++ .../Teams/Http/Memberships/Status/Update.php | 5 ++- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index fa0e30ee53..cbdf11225a 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -207,7 +207,7 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr } -$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, ?string $cookieDomain, Authorization $authorization) { +$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) { // Attempt to decode secret as a JWT (used by OAuth2 token flow to carry provider info) $oauthProvider = null; @@ -345,7 +345,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res ->setProperty('secret', $sessionSecret) ->encode(); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } @@ -719,8 +719,9 @@ Http::delete('/v1/account/sessions') ->inject('queueForDeletes') ->inject('store') ->inject('proofForToken') + ->inject('domainVerification') ->inject('cookieDomain') - ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, ?string $cookieDomain) { + ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) { $protocol = $request->getProtocol(); $sessions = $user->getAttribute('sessions', []); @@ -729,7 +730,7 @@ Http::delete('/v1/account/sessions') foreach ($sessions as $session) {/** @var Document $session */ $dbForProject->deleteDocument('sessions', $session->getId()); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([])); } @@ -850,8 +851,9 @@ Http::delete('/v1/account/sessions/:sessionId') ->inject('queueForDeletes') ->inject('store') ->inject('proofForToken') + ->inject('domainVerification') ->inject('cookieDomain') - ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, ?string $cookieDomain) { + ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) { $protocol = $request->getProtocol(); $sessionId = ($sessionId === 'current') @@ -877,7 +879,7 @@ Http::delete('/v1/account/sessions/:sessionId') ->setAttribute('current', true) ->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'))); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([])); } @@ -1037,9 +1039,10 @@ Http::post('/v1/account/sessions/email') ->inject('store') ->inject('proofForPassword') ->inject('proofForToken') + ->inject('domainVerification') ->inject('cookieDomain') ->inject('authorization') - ->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, ?string $cookieDomain, Authorization $authorization) { + ->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) { $email = \strtolower($email); $protocol = $request->getProtocol(); @@ -1113,7 +1116,7 @@ Http::post('/v1/account/sessions/email') ->setProperty('secret', $secret) ->encode(); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } @@ -1187,9 +1190,10 @@ Http::post('/v1/account/sessions/anonymous') ->inject('store') ->inject('proofForPassword') ->inject('proofForToken') + ->inject('domainVerification') ->inject('cookieDomain') ->inject('authorization') - ->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, ?string $cookieDomain, Authorization $authorization) { + ->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) { $protocol = $request->getProtocol(); if ('console' === $project->getId()) { @@ -1280,7 +1284,7 @@ Http::post('/v1/account/sessions/anonymous') ->setProperty('secret', $secret) ->encode(); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } @@ -1343,6 +1347,7 @@ Http::post('/v1/account/sessions/token') ->inject('store') ->inject('proofForToken') ->inject('proofForCode') + ->inject('domainVerification') ->inject('cookieDomain') ->inject('authorization') ->action($createSession); @@ -1543,9 +1548,10 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->inject('proofForPassword') ->inject('proofForToken') ->inject('plan') + ->inject('domainVerification') ->inject('cookieDomain') ->inject('authorization') - ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, ?string $cookieDomain, Authorization $authorization) use ($oauthDefaultSuccess) { + ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) use ($oauthDefaultSuccess) { $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; $port = $request->getPort(); $callbackBase = $protocol . '://' . $request->getHostname(); @@ -2061,7 +2067,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->setProperty('secret', $secret) ->encode(); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } @@ -2892,12 +2898,13 @@ Http::put('/v1/account/sessions/magic-url') ->inject('queueForMails') ->inject('store') ->inject('proofForCode') + ->inject('domainVerification') ->inject('cookieDomain') ->inject('authorization') - ->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $cookieDomain, $authorization) use ($createSession) { + ->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $domainVerification, $cookieDomain, $authorization) use ($createSession) { $proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL); $proofForToken->setHash(new Sha()); - $createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $cookieDomain, $authorization); + $createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $domainVerification, $cookieDomain, $authorization); }); Http::put('/v1/account/sessions/phone') @@ -2943,6 +2950,7 @@ Http::put('/v1/account/sessions/phone') ->inject('store') ->inject('proofForToken') ->inject('proofForCode') + ->inject('domainVerification') ->inject('cookieDomain') ->inject('authorization') ->action($createSession); @@ -3735,8 +3743,9 @@ Http::patch('/v1/account/status') ->inject('dbForProject') ->inject('queueForEvents') ->inject('store') + ->inject('domainVerification') ->inject('cookieDomain') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store, ?string $cookieDomain) { + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store, bool $domainVerification, ?string $cookieDomain) { $user->setAttribute('status', false); @@ -3746,7 +3755,7 @@ Http::patch('/v1/account/status') ->setParam('userId', $user->getId()) ->setPayload($response->output($user, Response::MODEL_ACCOUNT)); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([])); } diff --git a/app/controllers/general.php b/app/controllers/general.php index 917588bee3..120f8a17d6 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -904,15 +904,6 @@ Http::init() $locale->setDefault($localeParam); } - $origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST); - $selfDomain = new Domain($request->getHostname()); - $endDomain = new Domain((string)$origin); - Config::setParam( - 'domainVerification', - ($selfDomain->getRegisterable() === $endDomain->getRegisterable()) && - $endDomain->getRegisterable() !== '' - ); - $localHosts = ['localhost','localhost:'.$request->getPort()]; $migrationHost = System::getEnv('_APP_MIGRATION_HOST'); diff --git a/app/init/resources.php b/app/init/resources.php index 472c52fa4e..67ac115b61 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -250,6 +250,18 @@ Http::setResource('allowedSchemes', function (array $platform, Document $project return array_unique($allowed); }, ['platform', 'project']); +/** + * Whether the request origin is verified against the request hostname. + */ +Http::setResource('domainVerification', function (Request $request) { + $origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST); + $selfDomain = new Domain($request->getHostname()); + $endDomain = new Domain((string) $origin); + + return ($selfDomain->getRegisterable() === $endDomain->getRegisterable()) + && $endDomain->getRegisterable() !== ''; +}, ['request']); + /** * Cookie domain for the current request. */ diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php index f1c0a5cad5..28bfa769ee 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php @@ -74,11 +74,12 @@ class Update extends Action ->inject('queueForEvents') ->inject('store') ->inject('proofForToken') + ->inject('domainVerification') ->inject('cookieDomain') ->callback($this->action(...)); } - public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken, ?string $cookieDomain) + public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken, bool $domainVerification, ?string $cookieDomain) { $protocol = $request->getProtocol(); @@ -163,7 +164,7 @@ class Update extends Action ->setProperty('secret', $secret) ->encode(); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } From e3053bb83d6bf4895944106475b72d1ace0f76e5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 12:44:48 +0530 Subject: [PATCH 109/118] Remove dead cookie config defaults --- app/controllers/general.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 120f8a17d6..00bfc6bd67 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -61,8 +61,6 @@ use Utopia\System\System; use Utopia\Validator; use Utopia\Validator\Text; -Config::setParam('domainVerification', false); -Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount) From 59a773e9a01c5b7c50deb735550e7f97a7b7dcaa Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 12:47:06 +0530 Subject: [PATCH 110/118] Document migration host local-domain handling --- app/controllers/general.php | 2 ++ app/init/resources.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/controllers/general.php b/app/controllers/general.php index 00bfc6bd67..5a5c2dd507 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -906,6 +906,8 @@ Http::init() $migrationHost = System::getEnv('_APP_MIGRATION_HOST'); if (!empty($migrationHost)) { + // Treat the migration host like localhost because internal migration and + // CI traffic may use it before a public domain is configured. $localHosts[] = $migrationHost; $localHosts[] = $migrationHost.':'.$request->getPort(); } diff --git a/app/init/resources.php b/app/init/resources.php index 67ac115b61..92164c3c95 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -270,6 +270,8 @@ Http::setResource('cookieDomain', function (Request $request, Document $project) $migrationHost = System::getEnv('_APP_MIGRATION_HOST'); if (!empty($migrationHost)) { + // Treat the migration host like localhost because internal migration and CI + // traffic may use it before a public domain is configured. $localHosts[] = $migrationHost; $localHosts[] = $migrationHost . ':' . $request->getPort(); } From 04173600ae318d2082c87c5d6c68b70135dae38a Mon Sep 17 00:00:00 2001 From: shimon Date: Mon, 6 Apr 2026 22:33:18 +0300 Subject: [PATCH 111/118] revert ScheduleFunctions.php updates --- src/Appwrite/Platform/Tasks/ScheduleFunctions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php index 69f105652c..88725a190a 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php @@ -61,7 +61,7 @@ class ScheduleFunctions extends ScheduleBase $nextDate = $cron->getNextRunDate(); $next = DateTime::format($nextDate); - $currentTick = $next <= $timeFrame; + $currentTick = $next < $timeFrame; if (!$currentTick) { continue; @@ -88,7 +88,7 @@ class ScheduleFunctions extends ScheduleBase $scheduleKey = $delayConfig['key']; // Ensure schedule was not deleted if (!\array_key_exists($scheduleKey, $this->schedules)) { - continue; + return; } $schedule = $this->schedules[$scheduleKey]; From 399c37d943a90d45847fe9d70cd76c8c6a118173 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 7 Apr 2026 14:33:43 +0530 Subject: [PATCH 112/118] fix console null route handling --- app/controllers/shared/api.php | 8 ++++++++ tests/e2e/General/HTTPTest.php | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 5166429e32..bdcbe70b83 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -401,6 +401,10 @@ Http::init() } } + if ($route === null) { + throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); + } + // Steps 7-9: Access Control - Method, Namespace and Scope Validation /** * @var ?Method $method @@ -489,6 +493,10 @@ Http::init() $request->setUser($user); $route = $utopia->getRoute(); + if ($route === null) { + throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); + } + $path = $route->getMatchedPath(); $databaseType = match (true) { str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB, diff --git a/tests/e2e/General/HTTPTest.php b/tests/e2e/General/HTTPTest.php index 450e4f2378..ab389850ce 100644 --- a/tests/e2e/General/HTTPTest.php +++ b/tests/e2e/General/HTTPTest.php @@ -122,6 +122,13 @@ class HTTPTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); } + public function testConsoleRootWithoutRouteDoesNotFatal() + { + $response = $this->client->call(Client::METHOD_GET, '/console/', $this->getHeaders()); + + $this->assertEquals(404, $response['headers']['status-code']); + } + public function testCors() { From 6c56eee0f4c41ae7d3f6b41161b7326e6e549713 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 7 Apr 2026 14:39:48 +0530 Subject: [PATCH 113/118] test console route not found error type --- tests/e2e/General/HTTPTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/General/HTTPTest.php b/tests/e2e/General/HTTPTest.php index ab389850ce..38137b1320 100644 --- a/tests/e2e/General/HTTPTest.php +++ b/tests/e2e/General/HTTPTest.php @@ -127,6 +127,7 @@ class HTTPTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/console/', $this->getHeaders()); $this->assertEquals(404, $response['headers']['status-code']); + $this->assertEquals('general_route_not_found', $response['body']['type']); } public function testCors() From 92abfb31aa1a8c092696502046370d7f9963d022 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 7 Apr 2026 14:40:18 +0530 Subject: [PATCH 114/118] fix null route guard placement --- app/controllers/shared/api.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index bdcbe70b83..8254a22ac0 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -98,6 +98,9 @@ Http::init() ->inject('authorization') ->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) { $route = $utopia->getRoute(); + if ($route === null) { + throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); + } /** * Handle user authentication and session validation. @@ -401,10 +404,6 @@ Http::init() } } - if ($route === null) { - throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); - } - // Steps 7-9: Access Control - Method, Namespace and Scope Validation /** * @var ?Method $method From e8ef4e40d78145f268a032c97c5e81b5968b2936 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 7 Apr 2026 15:05:07 +0530 Subject: [PATCH 115/118] fix post-merge e2e test regressions --- tests/e2e/General/HTTPTest.php | 8 -------- tests/e2e/General/UsageTest.php | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/e2e/General/HTTPTest.php b/tests/e2e/General/HTTPTest.php index 38137b1320..450e4f2378 100644 --- a/tests/e2e/General/HTTPTest.php +++ b/tests/e2e/General/HTTPTest.php @@ -122,14 +122,6 @@ class HTTPTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); } - public function testConsoleRootWithoutRouteDoesNotFatal() - { - $response = $this->client->call(Client::METHOD_GET, '/console/', $this->getHeaders()); - - $this->assertEquals(404, $response['headers']['status-code']); - $this->assertEquals('general_route_not_found', $response['body']['type']); - } - public function testCors() { diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index eea53d9ea8..f6eb963967 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -1324,8 +1324,8 @@ class UsageTest extends Scope $this->assertEquals($requestsTotal, $response['body']['requests'][array_key_last($response['body']['requests'])]['value']); $this->validateDates($response['body']['requests']); // vectordbTotal should reflect only VectorsDB instances, not relational databases. - $this->assertEquals($vectordbTotal, $response['body']['vectordbDatabasesTotal']); - $this->assertEquals($documentsTotal, $response['body']['vectordbDocumentsTotal']); + $this->assertEquals($vectordbTotal, $response['body']['vectorsdbDatabasesTotal']); + $this->assertEquals($documentsTotal, $response['body']['vectorsdbDocumentsTotal']); }); $response = $this->client->call( From 8f6c8f9d8d028950d19cbbe0cc86449043ec5291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 7 Apr 2026 13:21:36 +0200 Subject: [PATCH 116/118] Migrate to new endpoint --- .../Http/Project}/Labels/Update.php | 45 +++++++------------ .../Modules/Project/Services/Http.php | 3 ++ .../Modules/Projects/Services/Http.php | 2 - 3 files changed, 19 insertions(+), 31 deletions(-) rename src/Appwrite/Platform/Modules/{Projects/Http/Projects => Project/Http/Project}/Labels/Update.php (60%) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Labels/Update.php similarity index 60% rename from src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php rename to src/Appwrite/Platform/Modules/Project/Http/Project/Labels/Update.php index de11bb0091..24d1c48cf1 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Labels/Update.php @@ -1,20 +1,15 @@ setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT) - ->setHttpPath('/v1/projects/:projectId/labels') + ->setHttpPath('/v1/project/labels') + ->httpAlias('/v1/projects/:projectId/labels') ->desc('Update project labels') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') + ->groups(['api', 'project']) + ->label('scope', 'project.write') + ->label('event', 'labels.*.update') + ->label('audits.event', 'project.labels.update') + ->label('audits.resource', 'project.labels/{response.$id}') ->label('sdk', new Method( - namespace: 'projects', - group: 'projects', + namespace: 'project', + group: null, name: 'updateLabels', description: <<param('projectId', '', new UID(), 'Project unique ID.') ->param('labels', [], new ArrayList(new Text(36, allowList: [...Text::NUMBERS, ...Text::ALPHABET_UPPER, ...Text::ALPHABET_LOWER]), APP_LIMIT_ARRAY_LABELS_SIZE), 'Array of project labels. Replaces the previous labels. Maximum of ' . APP_LIMIT_ARRAY_LABELS_SIZE . ' labels are allowed, each up to 36 alphanumeric characters long.') ->inject('response') ->inject('dbForPlatform') + ->inject('project') ->callback($this->action(...)); } @@ -67,17 +60,11 @@ class Update extends Action * @param array $labels */ public function action( - string $projectId, array $labels, Response $response, - Database $dbForPlatform + Database $dbForPlatform, + Document $project ): void { - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - $labels = (array) \array_values(\array_unique($labels)); $project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document(['labels' => $labels])); diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index 949fb2bcd9..4970403032 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Project\Services; use Appwrite\Platform\Modules\Project\Http\Init; +use Appwrite\Platform\Modules\Project\Http\Project\Labels\Update as UpdateProjectLabels; use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable; use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable; use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable; @@ -25,5 +26,7 @@ class Http extends Service $this->addAction(GetVariable::getName(), new GetVariable()); $this->addAction(DeleteVariable::getName(), new DeleteVariable()); $this->addAction(UpdateVariable::getName(), new UpdateVariable()); + + $this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels()); } } diff --git a/src/Appwrite/Platform/Modules/Projects/Services/Http.php b/src/Appwrite/Platform/Modules/Projects/Services/Http.php index 8b0d6f87c8..8275e664d5 100644 --- a/src/Appwrite/Platform/Modules/Projects/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Projects/Services/Http.php @@ -8,7 +8,6 @@ use Appwrite\Platform\Modules\Projects\Http\DevKeys\Get as GetDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\Update as UpdateDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\XList as ListDevKeys; use Appwrite\Platform\Modules\Projects\Http\Projects\Create as CreateProject; -use Appwrite\Platform\Modules\Projects\Http\Projects\Labels\Update as UpdateProjectLabels; use Appwrite\Platform\Modules\Projects\Http\Projects\Team\Update as UpdateProjectTeam; use Appwrite\Platform\Modules\Projects\Http\Projects\Update as UpdateProject; use Appwrite\Platform\Modules\Projects\Http\Projects\XList as ListProjects; @@ -31,7 +30,6 @@ class Http extends Service $this->addAction(CreateProject::getName(), new CreateProject()); $this->addAction(UpdateProject::getName(), new UpdateProject()); $this->addAction(ListProjects::getName(), new ListProjects()); - $this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels()); $this->addAction(UpdateProjectTeam::getName(), new UpdateProjectTeam()); $this->addAction(CreateSchedule::getName(), new CreateSchedule()); From 9b00ce4f1d7e195f092bd33558e8965fe8de5a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 7 Apr 2026 13:28:35 +0200 Subject: [PATCH 117/118] Add new tests --- tests/e2e/Services/Project/LabelsBase.php | 152 ++++++++++++++++++ .../Project/LabelsConsoleClientTest.php | 14 ++ .../Project/LabelsCustomServerTest.php | 14 ++ 3 files changed, 180 insertions(+) create mode 100644 tests/e2e/Services/Project/LabelsBase.php create mode 100644 tests/e2e/Services/Project/LabelsConsoleClientTest.php create mode 100644 tests/e2e/Services/Project/LabelsCustomServerTest.php diff --git a/tests/e2e/Services/Project/LabelsBase.php b/tests/e2e/Services/Project/LabelsBase.php new file mode 100644 index 0000000000..2c8f4963f3 --- /dev/null +++ b/tests/e2e/Services/Project/LabelsBase.php @@ -0,0 +1,152 @@ +updateLabels(['frontend', 'backend']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertIsArray($response['body']['labels']); + $this->assertCount(2, $response['body']['labels']); + $this->assertContains('frontend', $response['body']['labels']); + $this->assertContains('backend', $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsReplace(): void + { + $response = $this->updateLabels(['alpha', 'beta']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['labels']); + $this->assertContains('alpha', $response['body']['labels']); + $this->assertContains('beta', $response['body']['labels']); + + // Replace with new labels + $response = $this->updateLabels(['gamma', 'delta', 'epsilon']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(3, $response['body']['labels']); + $this->assertContains('gamma', $response['body']['labels']); + $this->assertContains('delta', $response['body']['labels']); + $this->assertContains('epsilon', $response['body']['labels']); + $this->assertNotContains('alpha', $response['body']['labels']); + $this->assertNotContains('beta', $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsEmpty(): void + { + // Set some labels first + $response = $this->updateLabels(['toRemove']); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['labels']); + + // Clear all labels + $response = $this->updateLabels([]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertIsArray($response['body']['labels']); + $this->assertCount(0, $response['body']['labels']); + } + + public function testUpdateLabelsDeduplicated(): void + { + $response = $this->updateLabels(['duplicate', 'duplicate', 'unique']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['labels']); + $this->assertContains('duplicate', $response['body']['labels']); + $this->assertContains('unique', $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsSingleLabel(): void + { + $response = $this->updateLabels(['solo']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['labels']); + $this->assertContains('solo', $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsWithoutAuthentication(): void + { + $response = $this->updateLabels(['unauthorized'], false); + + $this->assertSame(401, $response['headers']['status-code']); + } + + public function testUpdateLabelsInvalidLabelTooLong(): void + { + $response = $this->updateLabels([str_repeat('a', 37)]); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsInvalidLabelCharacters(): void + { + $response = $this->updateLabels(['invalid-label!']); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsAlphanumericOnly(): void + { + $response = $this->updateLabels(['ABC123', 'lowercase', 'UPPERCASE', '0123456789']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(4, $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsMaxLength(): void + { + $label = str_repeat('a', 36); + $response = $this->updateLabels([$label]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['labels']); + $this->assertContains($label, $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + // Helpers + + /** + * @param array $labels + */ + protected function updateLabels(array $labels, bool $authenticated = true): mixed + { + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]; + + if ($authenticated) { + $headers = array_merge($headers, $this->getHeaders()); + } + + return $this->client->call(\Tests\E2E\Client::METHOD_PUT, '/project/labels', $headers, [ + 'labels' => $labels, + ]); + } +} diff --git a/tests/e2e/Services/Project/LabelsConsoleClientTest.php b/tests/e2e/Services/Project/LabelsConsoleClientTest.php new file mode 100644 index 0000000000..dd724338d6 --- /dev/null +++ b/tests/e2e/Services/Project/LabelsConsoleClientTest.php @@ -0,0 +1,14 @@ + Date: Tue, 7 Apr 2026 13:30:35 +0200 Subject: [PATCH 118/118] Improve tests --- tests/e2e/Services/Project/LabelsBase.php | 72 +++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/e2e/Services/Project/LabelsBase.php b/tests/e2e/Services/Project/LabelsBase.php index 2c8f4963f3..2b7074ef46 100644 --- a/tests/e2e/Services/Project/LabelsBase.php +++ b/tests/e2e/Services/Project/LabelsBase.php @@ -129,6 +129,78 @@ trait LabelsBase $this->updateLabels([]); } + public function testUpdateLabelsIdempotent(): void + { + $labels = ['stable', 'production']; + + $first = $this->updateLabels($labels); + $this->assertSame(200, $first['headers']['status-code']); + + $second = $this->updateLabels($labels); + $this->assertSame(200, $second['headers']['status-code']); + + $this->assertSame($first['body']['labels'], $second['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsDeduplicatedOrder(): void + { + $response = $this->updateLabels(['b', 'a', 'b']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['labels']); + $this->assertSame('b', $response['body']['labels'][0]); + $this->assertSame('a', $response['body']['labels'][1]); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsInvalidHyphen(): void + { + $response = $this->updateLabels(['my-label']); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsInvalidUnderscore(): void + { + $response = $this->updateLabels(['my_label']); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsInvalidSpace(): void + { + $response = $this->updateLabels(['my label']); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsInvalidEmptyString(): void + { + $response = $this->updateLabels(['']); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsResponseModel(): void + { + $response = $this->updateLabels(['test']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertArrayHasKey('$id', $response['body']); + $this->assertArrayHasKey('name', $response['body']); + $this->assertArrayHasKey('labels', $response['body']); + $this->assertIsArray($response['body']['labels']); + $this->assertContains('test', $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + // Helpers /**