From ff06920e2479610bec304e289ad42f4aaad9b67f Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 13 Oct 2025 23:43:54 +0100 Subject: [PATCH 001/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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/131] 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 b9aaecba254bb75b4777cdc62f81602433dd85a3 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 16:25:33 +0530 Subject: [PATCH 078/131] Replace PHPMailer with utopia-php/messaging SMTP adapter Use Utopia\Messaging\Adapter\Email\SMTP instead of raw PHPMailer for the smtp register, Mails worker, and Doctor task. This enables swapping email adapters (e.g. Resend) via DI override in downstream repos by type-hinting against the EmailAdapter base class. --- app/init/registers.php | 44 +++----- composer.json | 2 +- composer.lock | 18 +-- src/Appwrite/Platform/Tasks/Doctor.php | 20 ++-- src/Appwrite/Platform/Workers/Mails.php | 142 +++++++++++------------- 5 files changed, 103 insertions(+), 123 deletions(-) diff --git a/app/init/registers.php b/app/init/registers.php index 7c2f822fdd..4fac60b877 100644 --- a/app/init/registers.php +++ b/app/init/registers.php @@ -6,7 +6,6 @@ use Appwrite\Hooks\Hooks; use Appwrite\PubSub\Adapter\Redis as PubSub; use Appwrite\URL\URL as AppwriteURL; use MaxMind\Db\Reader; -use PHPMailer\PHPMailer\PHPMailer; use Swoole\Database\PDOProxy; use Utopia\Cache\Adapter\Redis as RedisCache; use Utopia\Config\Config; @@ -25,6 +24,7 @@ use Utopia\Logger\Adapter\LogOwl; use Utopia\Logger\Adapter\Raygun; use Utopia\Logger\Adapter\Sentry; use Utopia\Logger\Logger; +use Utopia\Messaging\Adapter\Email\SMTP; use Utopia\Mongo\Client as MongoClient; use Utopia\Pools\Adapter\Stack as StackPool; use Utopia\Pools\Adapter\Swoole as SwoolePool; @@ -433,35 +433,21 @@ $register->set('db', function () { }); $register->set('smtp', function () { - $mail = new PHPMailer(true); + $username = System::getEnv('_APP_SMTP_USERNAME', ''); + $password = System::getEnv('_APP_SMTP_PASSWORD', ''); - $mail->isSMTP(); - - $username = System::getEnv('_APP_SMTP_USERNAME'); - $password = System::getEnv('_APP_SMTP_PASSWORD'); - - $mail->XMailer = 'Appwrite Mailer'; - $mail->Host = System::getEnv('_APP_SMTP_HOST', 'smtp'); - $mail->Port = System::getEnv('_APP_SMTP_PORT', 25); - $mail->SMTPAuth = !empty($username) && !empty($password); - $mail->Username = $username; - $mail->Password = $password; - $mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', ''); - $mail->SMTPAutoTLS = false; - $mail->SMTPKeepAlive = true; - $mail->CharSet = 'UTF-8'; - $mail->Timeout = 10; /* Connection timeout */ - $mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */ - - $from = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')); - $email = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); - - $mail->setFrom($email, $from); - $mail->addReplyTo($email, $from); - - $mail->isHTML(true); - - return $mail; + return new SMTP( + host: System::getEnv('_APP_SMTP_HOST', 'smtp'), + port: (int) System::getEnv('_APP_SMTP_PORT', 25), + username: $username, + password: $password, + smtpSecure: System::getEnv('_APP_SMTP_SECURE', ''), + smtpAutoTLS: false, + xMailer: 'Appwrite Mailer', + timeout: 10, + keepAlive: true, + timelimit: 30, + ); }); $register->set('geodb', function () { return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2025-12.mmdb'); diff --git a/composer.json b/composer.json index 1a530bfc5b..3d7e1b0e55 100644 --- a/composer.json +++ b/composer.json @@ -72,7 +72,7 @@ "utopia-php/image": "0.8.*", "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", - "utopia-php/messaging": "0.20.*", + "utopia-php/messaging": "dev-feat-smtp-enhancements", "utopia-php/migration": "1.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", diff --git a/composer.lock b/composer.lock index 0a79c5bee3..5d92374109 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": "b5261855586680e467168f527e0634ae", + "content-hash": "505d09ff5da7c5199622679ae9e11bc6", "packages": [ { "name": "adhocore/jwt", @@ -4467,16 +4467,16 @@ }, { "name": "utopia-php/messaging", - "version": "0.20.1", + "version": "dev-feat-smtp-enhancements", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0" + "reference": "6988c227c08095ad88aad030bf3648f4ea6c88f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/fcb4c3c46a48008a677957690bd45ec934dd33b0", - "reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/6988c227c08095ad88aad030bf3648f4ea6c88f0", + "reference": "6988c227c08095ad88aad030bf3648f4ea6c88f0", "shasum": "" }, "require": { @@ -4512,9 +4512,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.20.1" + "source": "https://github.com/utopia-php/messaging/tree/feat-smtp-enhancements" }, - "time": "2026-02-06T09:56:06+00:00" + "time": "2026-04-01T10:41:57+00:00" }, { "name": "utopia-php/migration", @@ -8435,7 +8435,9 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": { + "utopia-php/messaging": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/src/Appwrite/Platform/Tasks/Doctor.php b/src/Appwrite/Platform/Tasks/Doctor.php index 9a9c2fdf73..aa916a559c 100644 --- a/src/Appwrite/Platform/Tasks/Doctor.php +++ b/src/Appwrite/Platform/Tasks/Doctor.php @@ -4,7 +4,6 @@ namespace Appwrite\Platform\Tasks; use Appwrite\ClamAV\Network; use Appwrite\PubSub\Adapter\Pool as PubSubPool; -use PHPMailer\PHPMailer\PHPMailer; use Utopia\Cache\Adapter\Pool as CachePool; use Utopia\Config\Config; use Utopia\Console; @@ -13,6 +12,8 @@ use Utopia\Domains\Domain; use Utopia\DSN\DSN; use Utopia\Http\Http; use Utopia\Logger\Logger; +use Utopia\Messaging\Adapter\Email as EmailAdapter; +use Utopia\Messaging\Messages\Email as EmailMessage; use Utopia\Platform\Action; use Utopia\Pools\Group; use Utopia\Queue\Broker\Pool as BrokerPool; @@ -212,15 +213,18 @@ class Doctor extends Action } try { - /* @var PHPMailer $mail */ - $mail = $register->get('smtp'); + /** @var EmailAdapter $smtp */ + $smtp = $register->get('smtp'); - $mail->addAddress('demo@example.com', 'Example.com'); - $mail->Subject = 'Test SMTP Connection'; - $mail->Body = 'Hello World'; - $mail->AltBody = 'Hello World'; + $emailMessage = new EmailMessage( + to: ['demo@example.com'], + subject: 'Test SMTP Connection', + content: 'Hello World', + fromName: \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')), + fromEmail: System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), + ); - $mail->send(); + $smtp->send($emailMessage); Console::success('🟢 ' . str_pad("SMTP", 50, '.') . 'connected'); } catch (\Throwable) { Console::error('🔴 ' . str_pad("SMTP", 47, '.') . 'disconnected'); diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php index f144c58e1b..83ddb082ab 100644 --- a/src/Appwrite/Platform/Workers/Mails.php +++ b/src/Appwrite/Platform/Workers/Mails.php @@ -4,10 +4,13 @@ namespace Appwrite\Platform\Workers; use Appwrite\Template\Template; use Exception; -use PHPMailer\PHPMailer\PHPMailer; use Swoole\Runtime; use Utopia\Database\Document; use Utopia\Logger\Log; +use Utopia\Messaging\Adapter\Email as EmailAdapter; +use Utopia\Messaging\Adapter\Email\SMTP; +use Utopia\Messaging\Messages\Email as EmailMessage; +use Utopia\Messaging\Messages\Email\Attachment; use Utopia\Platform\Action; use Utopia\Queue\Message; use Utopia\Registry\Registry; @@ -49,9 +52,9 @@ class Mails extends Action /** * @param Message $message + * @param Document $project * @param Registry $register * @param Log $log - * @throws \PHPMailer\PHPMailer\Exception * @return void * @throws Exception */ @@ -132,58 +135,77 @@ class Mails extends Action // render() will return the subject in

tags, so use strip_tags() to remove them $subject = \strip_tags($subjectTemplate->render()); - /** @var PHPMailer $mail */ - $mail = empty($smtp) + /** @var EmailAdapter $adapter */ + $adapter = empty($smtp) ? $register->get('smtp') - : $this->getMailer($smtp); + : new SMTP( + host: $smtp['host'], + port: (int) $smtp['port'], + username: $smtp['username'] ?? '', + password: $smtp['password'] ?? '', + smtpSecure: $smtp['secure'] ?? '', + smtpAutoTLS: false, + xMailer: 'Appwrite Mailer', + timeout: 10, + keepAlive: true, + timelimit: 30, + ); - $mail->clearAddresses(); - $mail->clearAllRecipients(); - $mail->clearReplyTos(); - $mail->clearAttachments(); - $mail->clearBCCs(); - $mail->clearCCs(); - $mail->addAddress($recipient, $name); - $mail->Subject = $subject; - $mail->Body = $body; + // Resolve from/replyTo using fallback hierarchy: Custom options > SMTP config > Defaults + $defaultFromEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); + $defaultFromName = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')); - $mail->AltBody = $body; - $mail->AltBody = preg_replace('/]*>(.*?)<\/style>/is', '', $mail->AltBody); - $mail->AltBody = \strip_tags($mail->AltBody); - $mail->AltBody = \trim($mail->AltBody); + $fromEmail = !empty($smtp) ? ($smtp['senderEmail'] ?? $defaultFromEmail) : $defaultFromEmail; + $fromName = !empty($smtp) ? ($smtp['senderName'] ?? $defaultFromName) : $defaultFromName; + $replyTo = $defaultFromEmail; + $replyToName = $defaultFromName; - $replyTo = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); - $replyToName = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')); - - $customMailOptions = $payload['customMailOptions'] ?? []; - - // fallback hierarchy: Custom options > SMTP config > Defaults. - if (!empty($customMailOptions['senderEmail']) || !empty($customMailOptions['senderName'])) { - $fromEmail = $customMailOptions['senderEmail'] ?? $mail->From; - $fromName = $customMailOptions['senderName'] ?? $mail->FromName; - $mail->setFrom($fromEmail, $fromName); - } - - if (!empty($customMailOptions['replyToEmail']) || !empty($customMailOptions['replyToName'])) { - $replyTo = $customMailOptions['replyToEmail'] ?? $replyTo; - $replyToName = $customMailOptions['replyToName'] ?? $replyToName; - } elseif (!empty($smtp)) { + if (!empty($smtp)) { $replyTo = !empty($smtp['replyTo']) ? $smtp['replyTo'] : ($smtp['senderEmail'] ?? $replyTo); $replyToName = $smtp['senderName'] ?? $replyToName; } - $mail->addReplyTo($replyTo, $replyToName); - if (!empty($attachment['content'] ?? '')) { - $mail->AddStringAttachment( - base64_decode($attachment['content']), - $attachment['filename'] ?? 'unknown.file', - $attachment['encoding'] ?? PHPMailer::ENCODING_BASE64, - $attachment['type'] ?? 'plain/text' - ); + $customMailOptions = $payload['customMailOptions'] ?? []; + + if (!empty($customMailOptions['senderEmail'])) { + $fromEmail = $customMailOptions['senderEmail']; + } + if (!empty($customMailOptions['senderName'])) { + $fromName = $customMailOptions['senderName']; + } + if (!empty($customMailOptions['replyToEmail'])) { + $replyTo = $customMailOptions['replyToEmail']; + } + if (!empty($customMailOptions['replyToName'])) { + $replyToName = $customMailOptions['replyToName']; } + $attachments = null; + if (!empty($attachment['content'] ?? '')) { + $attachments = [ + new Attachment( + name: $attachment['filename'] ?? 'unknown.file', + path: '', + type: $attachment['type'] ?? 'plain/text', + content: \base64_decode($attachment['content']), + ), + ]; + } + + $emailMessage = new EmailMessage( + to: [$recipient], + subject: $subject, + content: $body, + fromName: $fromName, + fromEmail: $fromEmail, + replyToName: $replyToName, + replyToEmail: $replyTo, + attachments: $attachments, + html: true, + ); + try { - $mail->send(); + $adapter->send($emailMessage); } catch (\Throwable $error) { if ($type === 'smtp') { throw new Exception('Error sending mail: ' . $error->getMessage(), 401); @@ -191,38 +213,4 @@ class Mails extends Action throw new Exception('Error sending mail: ' . $error->getMessage(), 500); } } - - /** - * @param array $smtp - * @return PHPMailer - * @throws \PHPMailer\PHPMailer\Exception - */ - protected function getMailer(array $smtp): PHPMailer - { - $mail = new PHPMailer(true); - - $mail->isSMTP(); - - $username = $smtp['username']; - $password = $smtp['password']; - - $mail->XMailer = 'Appwrite Mailer'; - $mail->Host = $smtp['host']; - $mail->Port = $smtp['port']; - $mail->SMTPAuth = (!empty($username) && !empty($password)); - $mail->Username = $username; - $mail->Password = $password; - $mail->SMTPSecure = $smtp['secure']; - $mail->SMTPAutoTLS = false; - $mail->SMTPKeepAlive = true; - $mail->CharSet = 'UTF-8'; - $mail->Timeout = 10; /* Connection timeout */ - $mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */ - - $mail->setFrom($smtp['senderEmail'], $smtp['senderName']); - - $mail->isHTML(); - - return $mail; - } } From ce63b00cf445c2ca1866d83211d2b86121418f87 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 16:29:06 +0530 Subject: [PATCH 079/131] lock file --- composer.json | 2 +- composer.lock | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 3d7e1b0e55..02b5730c91 100644 --- a/composer.json +++ b/composer.json @@ -72,7 +72,7 @@ "utopia-php/image": "0.8.*", "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", - "utopia-php/messaging": "dev-feat-smtp-enhancements", + "utopia-php/messaging": "0.21.*", "utopia-php/migration": "1.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", diff --git a/composer.lock b/composer.lock index 5d92374109..c0b2b9aa30 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": "505d09ff5da7c5199622679ae9e11bc6", + "content-hash": "012f943cdfac560cb95ce5703d7588e1", "packages": [ { "name": "adhocore/jwt", @@ -4467,16 +4467,16 @@ }, { "name": "utopia-php/messaging", - "version": "dev-feat-smtp-enhancements", + "version": "0.21.0", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "6988c227c08095ad88aad030bf3648f4ea6c88f0" + "reference": "b2358fde423e27ff0cfe7846eb9a78b5b295552a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/6988c227c08095ad88aad030bf3648f4ea6c88f0", - "reference": "6988c227c08095ad88aad030bf3648f4ea6c88f0", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/b2358fde423e27ff0cfe7846eb9a78b5b295552a", + "reference": "b2358fde423e27ff0cfe7846eb9a78b5b295552a", "shasum": "" }, "require": { @@ -4512,9 +4512,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/feat-smtp-enhancements" + "source": "https://github.com/utopia-php/messaging/tree/0.21.0" }, - "time": "2026-04-01T10:41:57+00:00" + "time": "2026-04-01T10:58:13+00:00" }, { "name": "utopia-php/migration", @@ -8435,9 +8435,7 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "utopia-php/messaging": 20 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { From 15b2ab321e03a1f9ff79c691cd8a23354f2565da Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 16:34:58 +0530 Subject: [PATCH 080/131] 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 f1dc468e50d99d4c2ef3f4d90f04b693031f36cb Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 16:37:08 +0530 Subject: [PATCH 081/131] Restore replyTo elseif logic to preserve old behavior customMailOptions replyTo and smtp replyTo are mutually exclusive, matching the original PHPMailer implementation. --- src/Appwrite/Platform/Workers/Mails.php | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php index 83ddb082ab..01fe4ced9e 100644 --- a/src/Appwrite/Platform/Workers/Mails.php +++ b/src/Appwrite/Platform/Workers/Mails.php @@ -160,11 +160,6 @@ class Mails extends Action $replyTo = $defaultFromEmail; $replyToName = $defaultFromName; - if (!empty($smtp)) { - $replyTo = !empty($smtp['replyTo']) ? $smtp['replyTo'] : ($smtp['senderEmail'] ?? $replyTo); - $replyToName = $smtp['senderName'] ?? $replyToName; - } - $customMailOptions = $payload['customMailOptions'] ?? []; if (!empty($customMailOptions['senderEmail'])) { @@ -173,11 +168,13 @@ class Mails extends Action if (!empty($customMailOptions['senderName'])) { $fromName = $customMailOptions['senderName']; } - if (!empty($customMailOptions['replyToEmail'])) { - $replyTo = $customMailOptions['replyToEmail']; - } - if (!empty($customMailOptions['replyToName'])) { - $replyToName = $customMailOptions['replyToName']; + + if (!empty($customMailOptions['replyToEmail']) || !empty($customMailOptions['replyToName'])) { + $replyTo = $customMailOptions['replyToEmail'] ?? $replyTo; + $replyToName = $customMailOptions['replyToName'] ?? $replyToName; + } elseif (!empty($smtp)) { + $replyTo = !empty($smtp['replyTo']) ? $smtp['replyTo'] : ($smtp['senderEmail'] ?? $replyTo); + $replyToName = $smtp['senderName'] ?? $replyToName; } $attachments = null; From 53bc12601512b3bc797a8b6528edff79ebf0f296 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 17:01:15 +0530 Subject: [PATCH 082/131] Remove stale PHPMailer baseline entry from PHPStan The PHPMailer $Port type error no longer occurs since registers.php now uses the SMTP adapter instead of PHPMailer directly. --- phpstan-baseline.neon | 6 ------ 1 file changed, 6 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8e9d8a5a38..b0fbbe25ae 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -138,12 +138,6 @@ parameters: count: 1 path: app/init/registers.php - - - message: '#^Property PHPMailer\\PHPMailer\\PHPMailer\:\:\$Port \(int\) does not accept string\.$#' - identifier: assign.propertyType - count: 1 - path: app/init/registers.php - - message: '#^Variable \$providerConfig in empty\(\) always exists and is not falsy\.$#' identifier: empty.variable From 7044601603562dc7a61eede84436638111e06b51 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 17:05:14 +0530 Subject: [PATCH 083/131] lock file --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index c0b2b9aa30..fcb69195c5 100644 --- a/composer.lock +++ b/composer.lock @@ -4467,16 +4467,16 @@ }, { "name": "utopia-php/messaging", - "version": "0.21.0", + "version": "0.21.1", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "b2358fde423e27ff0cfe7846eb9a78b5b295552a" + "reference": "de068654476a673921ea9df03692d2940b7941ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/b2358fde423e27ff0cfe7846eb9a78b5b295552a", - "reference": "b2358fde423e27ff0cfe7846eb9a78b5b295552a", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/de068654476a673921ea9df03692d2940b7941ee", + "reference": "de068654476a673921ea9df03692d2940b7941ee", "shasum": "" }, "require": { @@ -4512,9 +4512,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.21.0" + "source": "https://github.com/utopia-php/messaging/tree/0.21.1" }, - "time": "2026-04-01T10:58:13+00:00" + "time": "2026-04-01T11:34:20+00:00" }, { "name": "utopia-php/migration", From d531c29fc8c1d9ecbf8ce04f57be6faecdcfa539 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 17:20:34 +0530 Subject: [PATCH 084/131] Remove final from Usage class to allow cloud override --- src/Appwrite/Event/Message/Usage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Event/Message/Usage.php b/src/Appwrite/Event/Message/Usage.php index ec8484e45e..776188d5b5 100644 --- a/src/Appwrite/Event/Message/Usage.php +++ b/src/Appwrite/Event/Message/Usage.php @@ -4,7 +4,7 @@ namespace Appwrite\Event\Message; use Utopia\Database\Document; -final class Usage extends Base +class Usage extends Base { /** * @param Document $project 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 085/131] 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 fb96aecbefa147a2b5468381c0af5bea496f1e39 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 19:52:16 +0530 Subject: [PATCH 086/131] Use new static() in Usage::fromArray() for late static binding Required since the class is no longer final and the return type is static, so subclasses get the correct type. --- src/Appwrite/Event/Message/Usage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Event/Message/Usage.php b/src/Appwrite/Event/Message/Usage.php index 776188d5b5..c97b96a855 100644 --- a/src/Appwrite/Event/Message/Usage.php +++ b/src/Appwrite/Event/Message/Usage.php @@ -40,7 +40,7 @@ class Usage extends Base */ public static function fromArray(array $data): static { - return new self( + return new static( project: new Document($data['project'] ?? []), metrics: $data['metrics'] ?? [], reduce: array_map(fn (array $doc) => new Document($doc), $data['reduce'] ?? []), From 7d428ffe83c1083fffc7479c28d657d33150fe92 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 21:03:07 +0530 Subject: [PATCH 087/131] trigger ci From 92cc382a6bf56e11467b75c7b9f1154e07b64dbd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 21:25:22 +0530 Subject: [PATCH 088/131] Change Usage::fromArray() return type from static to self The cloud subclass has a different constructor signature so new static() is unsafe. Use self since subclasses that need deserialization should override fromArray() themselves. --- src/Appwrite/Event/Message/Usage.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Event/Message/Usage.php b/src/Appwrite/Event/Message/Usage.php index c97b96a855..22330ddb52 100644 --- a/src/Appwrite/Event/Message/Usage.php +++ b/src/Appwrite/Event/Message/Usage.php @@ -36,11 +36,11 @@ class Usage extends Base /** * @param array $data - * @return static + * @return self */ - public static function fromArray(array $data): static + public static function fromArray(array $data): self { - return new static( + return new self( project: new Document($data['project'] ?? []), metrics: $data['metrics'] ?? [], reduce: array_map(fn (array $doc) => new Document($doc), $data['reduce'] ?? []), From 30e0ca81bd366964f9f3887a4a8b22747c1547ea Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 21:30:36 +0530 Subject: [PATCH 089/131] Fix Usage::fromArray() to use static return type with new static() Keep covariant return type with parent Base::fromArray(). The new static() is safe here because the cloud subclass constructor is backwards-compatible via optional params. --- src/Appwrite/Event/Message/Usage.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Event/Message/Usage.php b/src/Appwrite/Event/Message/Usage.php index 22330ddb52..c72bc8ae2a 100644 --- a/src/Appwrite/Event/Message/Usage.php +++ b/src/Appwrite/Event/Message/Usage.php @@ -36,11 +36,12 @@ class Usage extends Base /** * @param array $data - * @return self + * @return static */ - public static function fromArray(array $data): self + public static function fromArray(array $data): static { - return new self( + /** @phpstan-ignore new.static (subclass constructors are backwards-compatible via optional params) */ + return new static( project: new Document($data['project'] ?? []), metrics: $data['metrics'] ?? [], reduce: array_map(fn (array $doc) => new Document($doc), $data['reduce'] ?? []), From 2c1060f57aedd21a5114b4d151d0a445d9445dfd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 21:31:08 +0530 Subject: [PATCH 090/131] Fix null array key in HeadersTest Cast null to string to avoid invalid array key type error while preserving the test's intent of validating empty keys. --- tests/unit/Functions/Validator/HeadersTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/Functions/Validator/HeadersTest.php b/tests/unit/Functions/Validator/HeadersTest.php index 4a45f57427..b9cc7c6ea4 100644 --- a/tests/unit/Functions/Validator/HeadersTest.php +++ b/tests/unit/Functions/Validator/HeadersTest.php @@ -77,7 +77,7 @@ class HeadersTest extends TestCase $this->assertFalse($this->object->isValid($headers)); $headers = [ - null => 'value', + (string) null => 'value', ]; $this->assertFalse($this->object->isValid($headers)); From 33f8e35b62feecd88d642fce2e11242dad633dfe Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 23:01:11 +0530 Subject: [PATCH 091/131] chore: remove phpstan baseline --- app/config/templates/function.php | 3 - app/config/templates/site.php | 9 +- app/controllers/api/account.php | 19 +- app/controllers/api/locale.php | 1 + app/controllers/api/messaging.php | 2 +- app/controllers/api/projects.php | 2 +- app/controllers/api/users.php | 12 +- app/controllers/general.php | 27 +- app/controllers/mock.php | 58 +- app/http.php | 6 +- app/init/registers.php | 14 +- app/init/resources.php | 2 +- app/realtime.php | 13 +- app/worker.php | 6 +- phpstan-baseline.neon | 721 ------------------ phpstan.neon | 4 - src/Appwrite/Event/Event.php | 8 +- src/Appwrite/GraphQL/Resolvers.php | 16 +- src/Appwrite/Network/Cors.php | 2 +- src/Appwrite/OpenSSL/OpenSSL.php | 2 +- .../Platform/Modules/Avatars/Http/Action.php | 1 - .../Modules/Avatars/Http/Favicon/Get.php | 1 - .../Modules/Avatars/Http/Image/Get.php | 1 - .../Modules/Avatars/Http/Initials/Get.php | 2 +- .../Databases/Http/Databases/Action.php | 5 +- .../Collections/Documents/Action.php | 7 +- .../Collections/Documents/Create.php | 2 +- .../Collections/Documents/Update.php | 1 - .../Databases/Collections/Documents/XList.php | 2 +- .../Http/Databases/Collections/Update.php | 2 - .../Databases/Http/Databases/Logs/XList.php | 9 +- .../Http/Databases/Transactions/Action.php | 5 +- .../Transactions/Operations/Create.php | 2 +- .../Http/Databases/Transactions/Update.php | 5 +- .../Databases/Http/TablesDB/Logs/XList.php | 9 +- .../Functions/Http/Executions/Create.php | 59 +- .../Functions/Http/Functions/Create.php | 2 +- .../Modules/Functions/Workers/Builds.php | 10 +- .../Proxy/Http/Rules/Verification/Update.php | 3 +- .../Storage/Http/Buckets/Files/Create.php | 2 + .../Modules/Teams/Http/Memberships/Create.php | 6 +- src/Appwrite/Platform/Tasks/Doctor.php | 2 +- src/Appwrite/Platform/Tasks/Install.php | 2 +- src/Appwrite/Platform/Tasks/SDKs.php | 2 + .../Platform/Tasks/StatsResources.php | 2 +- src/Appwrite/Platform/Workers/Audits.php | 4 +- src/Appwrite/Platform/Workers/Deletes.php | 21 +- src/Appwrite/Platform/Workers/Functions.php | 15 +- src/Appwrite/Platform/Workers/Messaging.php | 4 +- src/Appwrite/Platform/Workers/Migrations.php | 2 + .../Platform/Workers/StatsResources.php | 2 +- src/Appwrite/Platform/Workers/StatsUsage.php | 8 +- src/Appwrite/Platform/Workers/Webhooks.php | 2 +- src/Appwrite/Promises/Promise.php | 1 + src/Appwrite/SDK/Method.php | 2 +- .../Utopia/Database/Documents/User.php | 2 - .../e2e/Services/GraphQL/Legacy/AbuseTest.php | 2 +- .../Services/GraphQL/TablesDB/AbuseTest.php | 2 +- tests/unit/Event/EventTest.php | 2 +- .../unit/Functions/Validator/HeadersTest.php | 5 - 60 files changed, 207 insertions(+), 938 deletions(-) delete mode 100644 phpstan-baseline.neon diff --git a/app/config/templates/function.php b/app/config/templates/function.php index 6bdfb5ab80..df3a569705 100644 --- a/app/config/templates/function.php +++ b/app/config/templates/function.php @@ -31,9 +31,6 @@ class FunctionUseCases public const DEV_TOOLS = 'dev-tools'; public const AUTH = 'auth'; - /** - * @var array - */ public static function getAll(): array { return [ diff --git a/app/config/templates/site.php b/app/config/templates/site.php index 50a9fb8d5d..26f8e39817 100644 --- a/app/config/templates/site.php +++ b/app/config/templates/site.php @@ -25,9 +25,6 @@ class SiteUseCases public const FORMS = 'forms'; public const DASHBOARD = 'dashboard'; - /** - * @var array - */ public static function getAll(): array { return [ @@ -252,7 +249,7 @@ return [ 'frameworks' => [ getFramework('VITE', [ 'providerRootDirectory' => './vite/vitepress', - 'outputDirectory' => '404.html', + 'fallbackFile' => '404.html', 'installCommand' => 'npm i vitepress && npm install', 'buildCommand' => 'npm run docs:build', 'outputDirectory' => './.vitepress/dist', @@ -275,7 +272,7 @@ return [ 'frameworks' => [ getFramework('VUE', [ 'providerRootDirectory' => './vue/vuepress', - 'outputDirectory' => '404.html', + 'fallbackFile' => '404.html', 'installCommand' => 'npm install', 'buildCommand' => 'npm run build', 'outputDirectory' => './src/.vuepress/dist', @@ -298,7 +295,7 @@ return [ 'frameworks' => [ getFramework('REACT', [ 'providerRootDirectory' => './react/docusaurus', - 'outputDirectory' => '404.html', + 'fallbackFile' => '404.html', 'installCommand' => 'npm install', 'buildCommand' => 'npm run build', 'outputDirectory' => './build', diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 3d7db8f457..4cfc4476b6 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -695,6 +695,7 @@ Http::delete('/v1/account/sessions') $protocol = $request->getProtocol(); $sessions = $user->getAttribute('sessions', []); + $currentSession = null; foreach ($sessions as $session) {/** @var Document $session */ $dbForProject->deleteDocument('sessions', $session->getId()); @@ -716,6 +717,7 @@ Http::delete('/v1/account/sessions') ->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); // Use current session for events. + $currentSession = $session; $queueForEvents ->setPayload($response->output($session, Response::MODEL_SESSION)); @@ -728,9 +730,11 @@ Http::delete('/v1/account/sessions') $dbForProject->purgeCachedDocument('users', $user->getId()); - $queueForEvents - ->setParam('userId', $user->getId()) - ->setParam('sessionId', $session->getId()); + if ($currentSession instanceof Document) { + $queueForEvents + ->setParam('userId', $user->getId()) + ->setParam('sessionId', $currentSession->getId()); + } $response->noContent(); }); @@ -776,7 +780,8 @@ Http::get('/v1/account/sessions/:sessionId') ->setAttribute('secret', $session->getAttribute('secret', '')) ; - return $response->dynamic($session, Response::MODEL_SESSION); + $response->dynamic($session, Response::MODEL_SESSION); + return; } } @@ -956,7 +961,7 @@ Http::patch('/v1/account/sessions/:sessionId') ->setPayload($response->output($session, Response::MODEL_SESSION)) ; - return $response->dynamic($session, Response::MODEL_SESSION); + $response->dynamic($session, Response::MODEL_SESSION); }); Http::post('/v1/account/sessions/email') @@ -1988,7 +1993,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); } - if (isset($sessionUpgrade) && $sessionUpgrade) { + if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) { foreach ($user->getAttribute('targets', []) as $target) { if ($target->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) { continue; @@ -4715,5 +4720,5 @@ Http::delete('/v1/account/identities/:identityId') ->setParam('identityId', $identity->getId()) ->setPayload($response->output($identity, Response::MODEL_IDENTITY)); - return $response->noContent(); + $response->noContent(); }); diff --git a/app/controllers/api/locale.php b/app/controllers/api/locale.php index d6b4bb814d..c70102a6f1 100644 --- a/app/controllers/api/locale.php +++ b/app/controllers/api/locale.php @@ -231,6 +231,7 @@ Http::get('/v1/locale/continents') ->inject('locale') ->action(function (Response $response, Locale $locale) { $list = array_keys(Config::getParam('locale-continents')); + $output = []; foreach ($list as $value) { $output[] = new Document([ diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 1ba5eb1119..95a8afc963 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -3566,7 +3566,7 @@ Http::post('/v1/messaging/messages/push') $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; $endpoint = "$protocol://{$platform['apiHostname']}/v1"; - $scheduleTime = $currentScheduledAt ?? $scheduledAt; + $scheduleTime = $scheduledAt; if (!\is_null($scheduleTime)) { $expiry = (new \DateTime($scheduleTime))->add(new \DateInterval('P15D'))->format('U'); } else { diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 2fc20ba83f..4eb537d923 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1570,7 +1570,7 @@ Http::post('/v1/projects/:projectId/smtp/tests') ->trigger(); } - return $response->noContent(); + $response->noContent(); }); Http::get('/v1/projects/:projectId/templates/sms/:type/:locale') diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 3b21d4797d..e4900e6564 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -2337,9 +2337,8 @@ Http::post('/v1/users/:userId/sessions') ->setParam('sessionId', $session->getId()) ->setPayload($response->output($session, Response::MODEL_SESSION)); - return $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($session, Response::MODEL_SESSION); + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($session, Response::MODEL_SESSION); }); Http::post('/v1/users/:userId/tokens') @@ -2402,9 +2401,8 @@ Http::post('/v1/users/:userId/tokens') ->setParam('tokenId', $token->getId()) ->setPayload($response->output($token, Response::MODEL_TOKEN)); - return $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($token, Response::MODEL_TOKEN); + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($token, Response::MODEL_TOKEN); }); Http::delete('/v1/users/:userId/sessions/:sessionId') @@ -2658,7 +2656,7 @@ Http::delete('/v1/users/identities/:identityId') ->setParam('identityId', $identity->getId()) ->setPayload($response->output($identity, Response::MODEL_IDENTITY)); - return $response->noContent(); + $response->noContent(); }); Http::post('/v1/users/:userId/jwts') diff --git a/app/controllers/general.php b/app/controllers/general.php index 79929816d9..3bf5f027f2 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -166,14 +166,14 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S if ($request->getMethod() !== Request::METHOD_GET) { throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.', view: $errorView); } - return $response->redirect('https://' . $request->getHostname() . $request->getURI()); + $response->redirect('https://' . $request->getHostname() . $request->getURI()); + return false; } } /** @var Database $dbForProject */ $dbForProject = $getProjectDB($project); - /** @var Document $deployment */ if (!empty($rule->getAttribute('deploymentId', ''))) { $deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId'))); } else { @@ -244,6 +244,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S if ($isPreview && $requirePreview) { $cookie = $request->getCookie(COOKIE_NAME_PREVIEW, ''); $authorized = false; + $user = new Document(); // Security checks to mark authorized true if (!empty($cookie)) { @@ -273,7 +274,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S $membershipExists = false; $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); - if (!$project->isEmpty() && isset($user)) { + if (!$project->isEmpty() && !$user->isEmpty()) { $teamId = $project->getAttribute('teamId', ''); $membership = $user->find('teamId', $teamId, 'memberships'); if (!empty($membership)) { @@ -379,7 +380,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S $executionId = ID::unique(); $headers = \array_merge([], $requestHeaders); - $headers['x-appwrite-execution-id'] = $executionId ?? ''; + $headers['x-appwrite-execution-id'] = $executionId; $headers['x-appwrite-user-id'] = ''; $headers['x-appwrite-country-code'] = ''; $headers['x-appwrite-continent-code'] = ''; @@ -459,7 +460,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S if ($version === 'v2') { $vars = \array_merge($vars, [ 'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '', - 'APPWRITE_FUNCTION_DATA' => $body ?? '', + 'APPWRITE_FUNCTION_DATA' => $body, 'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '', 'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? '' ]); @@ -529,6 +530,11 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S } /** Execute function */ + $executionResponse = [ + 'headers' => [], + 'body' => '', + ]; + try { $version = match ($type) { 'function' => $resource->getAttribute('version', 'v2'), @@ -734,7 +740,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S $execution->setAttribute('responseBody', $executionResponse['body'] ?? ''); $execution->setAttribute('responseHeaders', $headers); - $body = $execution['responseBody'] ?? ''; + $body = $execution['responseBody']; $contentType = 'text/plain'; foreach ($executionResponse['headers'] as $name => $values) { @@ -865,9 +871,9 @@ Http::init() Request::setRoute($route); if ($route === null) { - return $response - ->setStatusCode(404) - ->send('Not Found'); + $response->setStatusCode(404); + $response->send('Not Found'); + return; } $requestFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', '')); @@ -973,7 +979,8 @@ Http::init() throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.'); } - return $response->redirect('https://' . $request->getHostname() . $request->getURI()); + $response->redirect('https://' . $request->getHostname() . $request->getURI()); + return; } } }); diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 712d4b7742..99713af430 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -244,36 +244,38 @@ Http::get('/v1/mock/github/callback') throw new Exception(Exception::PROJECT_NOT_FOUND, $error); } - if (!empty($providerInstallationId)) { - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - $owner = $github->getOwnerName($providerInstallationId) ?? ''; - - $projectInternalId = $project->getSequence(); - - $teamId = $project->getAttribute('teamId', ''); - - $installation = new Document([ - '$id' => ID::unique(), - '$permissions' => [ - Permission::read(Role::team(ID::custom($teamId))), - Permission::update(Role::team(ID::custom($teamId), 'owner')), - Permission::update(Role::team(ID::custom($teamId), 'developer')), - Permission::delete(Role::team(ID::custom($teamId), 'owner')), - Permission::delete(Role::team(ID::custom($teamId), 'developer')), - ], - 'providerInstallationId' => $providerInstallationId, - 'projectId' => $projectId, - 'projectInternalId' => $projectInternalId, - 'provider' => 'github', - 'organization' => $owner, - 'personal' => false - ]); - - $installation = $dbForPlatform->createDocument('installations', $installation); + if (empty($providerInstallationId)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Missing provider installation ID'); } + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + $owner = $github->getOwnerName($providerInstallationId) ?? ''; + + $projectInternalId = $project->getSequence(); + + $teamId = $project->getAttribute('teamId', ''); + + $installation = new Document([ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::team(ID::custom($teamId))), + Permission::update(Role::team(ID::custom($teamId), 'owner')), + Permission::update(Role::team(ID::custom($teamId), 'developer')), + Permission::delete(Role::team(ID::custom($teamId), 'owner')), + Permission::delete(Role::team(ID::custom($teamId), 'developer')), + ], + 'providerInstallationId' => $providerInstallationId, + 'projectId' => $projectId, + 'projectInternalId' => $projectInternalId, + 'provider' => 'github', + 'organization' => $owner, + 'personal' => false + ]); + + $installation = $dbForPlatform->createDocument('installations', $installation); + $response->json([ 'installationId' => $installation->getId(), ]); diff --git a/app/http.php b/app/http.php index 1742bc7cdd..3eb8284ddf 100644 --- a/app/http.php +++ b/app/http.php @@ -3,6 +3,8 @@ require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/init/span.php'; +global $register; + use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Swoole\Constant; @@ -293,7 +295,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot go(function () use ($register, $app) { $pools = $register->get('pools'); - /** @var Group $pools */ + /** @var \Utopia\Pools\Group $pools */ Http::setResource('pools', fn () => $pools); /** @var array $collections */ @@ -655,7 +657,7 @@ $http->on(Constant::EVENT_TASK, function () use ($register) { /** @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/init/registers.php b/app/init/registers.php index 7c2f822fdd..88160aae79 100644 --- a/app/init/registers.php +++ b/app/init/registers.php @@ -56,7 +56,7 @@ $register->set('logger', function () { } try { - $loggingProvider = new DSN($providerConfig ?? ''); + $loggingProvider = new DSN($providerConfig); $providerName = $loggingProvider->getScheme(); $providerConfig = match ($providerName) { @@ -76,7 +76,7 @@ $register->set('logger', function () { }; } - if (empty($providerName) || empty($providerConfig)) { + if (empty($providerName)) { return; } @@ -121,7 +121,7 @@ $register->set('realtimeLogger', function () { default => ['key' => $loggingProvider->getHost()], }; - if (empty($providerName) || empty($providerConfig)) { + if (empty($providerName)) { return; } @@ -242,8 +242,8 @@ $register->set('pools', function () { ], ]; - $maxConnections = System::getEnv('_APP_CONNECTIONS_MAX', 151); - $instanceConnections = $maxConnections / System::getEnv('_APP_POOL_CLIENTS', 14); + $maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151); + $instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14); $multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled'; @@ -308,7 +308,7 @@ $register->set('pools', function () { ]); }); }, - 'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase, $dsn) { + 'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) { try { $mongo = new MongoClient($dsnDatabase, $dsnHost, (int)$dsnPort, $dsnUser, $dsnPass, false); @$mongo->connect(); @@ -442,7 +442,7 @@ $register->set('smtp', function () { $mail->XMailer = 'Appwrite Mailer'; $mail->Host = System::getEnv('_APP_SMTP_HOST', 'smtp'); - $mail->Port = System::getEnv('_APP_SMTP_PORT', 25); + $mail->Port = (int) System::getEnv('_APP_SMTP_PORT', 25); $mail->SMTPAuth = !empty($username) && !empty($password); $mail->Username = $username; $mail->Password = $password; diff --git a/app/init/resources.php b/app/init/resources.php index 3481e73e0b..8acecb8e3e 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -696,7 +696,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor $cacheKey = \sprintf( '%s-cache-%s:%s:%s:project:%s:functions:events', $dbForProject->getCacheName(), - $hostname ?? '', + $hostname, $dbForProject->getNamespace(), $dbForProject->getTenant(), $project->getId() diff --git a/app/realtime.php b/app/realtime.php index 1c453ce05b..67cfb19e2a 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -237,7 +237,6 @@ if (!function_exists('getTelemetry')) { if (!function_exists('triggerStats')) { function triggerStats(array $event, string $projectId): void { - return; } } @@ -320,14 +319,14 @@ if (!function_exists('logError')) { $server->error(logError(...)); -$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument) { +$server->onStart(function () use ($stats, $containerId, &$statsDocument) { sleep(5); // wait for the initial database schema to be ready Console::success('Server started successfully'); /** * Create document for this worker to share stats across Containers. */ - go(function () use ($register, $containerId, &$statsDocument) { + go(function () use ($containerId, &$statsDocument) { $attempts = 0; $database = getConsoleDB(); @@ -357,7 +356,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume */ // TODO: Remove this if check once it doesn't cause issues for cloud if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') { - Timer::tick(5000, function () use ($register, $stats, &$statsDocument) { + Timer::tick(5000, function () use ($stats, &$statsDocument) { $payload = []; foreach ($stats as $projectId => $value) { $payload[$projectId] = $stats->get($projectId, 'connectionsTotal'); @@ -396,7 +395,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $attempts = 0; $start = time(); - Timer::tick(5000, function () use ($server, $register, $realtime, $stats) { + Timer::tick(5000, function () use ($server, $realtime, $stats) { /** * Sending current connections to project channels on the console project every 5 seconds. */ @@ -798,7 +797,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, } }); -$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) { +$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId) { $project = null; $authorization = null; @@ -810,7 +809,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re // Get authorization from connection (stored during onOpen) $authorization = $realtime->connections[$connection]['authorization'] ?? null; if ($authorization === null) { - $authorization = new Authorization(''); + $authorization = new Authorization(); } $database = getConsoleDB(); diff --git a/app/worker.php b/app/worker.php index 71446ee94f..4a0a4870e4 100644 --- a/app/worker.php +++ b/app/worker.php @@ -280,14 +280,14 @@ Server::setResource('abuseRetention', function () { 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 * (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']); Server::setResource('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 }); Server::setResource('cache', function (Registry $register) { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon deleted file mode 100644 index 8e9d8a5a38..0000000000 --- a/phpstan-baseline.neon +++ /dev/null @@ -1,721 +0,0 @@ -parameters: - ignoreErrors: - - - message: '#^PHPDoc tag @var above a method has no effect\.$#' - identifier: varTag.misplaced - count: 1 - path: app/config/templates/function.php - - - - message: '#^Array has 2 duplicate keys with value ''outputDirectory'' \(''outputDirectory'', ''outputDirectory''\)\.$#' - identifier: array.duplicateKey - count: 3 - path: app/config/templates/site.php - - - - message: '#^PHPDoc tag @var above a method has no effect\.$#' - identifier: varTag.misplaced - count: 1 - path: app/config/templates/site.php - - - - message: '#^Result of method Appwrite\\Utopia\\Response\:\:dynamic\(\) \(void\) is used\.$#' - identifier: method.void - count: 2 - path: app/controllers/api/account.php - - - - message: '#^Result of method Utopia\\Http\\Response\:\:noContent\(\) \(void\) is used\.$#' - identifier: method.void - count: 1 - path: app/controllers/api/account.php - - - - message: '#^Variable \$session might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: app/controllers/api/account.php - - - - message: '#^Variable \$output might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: app/controllers/api/locale.php - - - - message: '#^Variable \$currentScheduledAt on left side of \?\? is never defined\.$#' - identifier: nullCoalesce.variable - count: 1 - path: app/controllers/api/messaging.php - - - - message: '#^Result of method Utopia\\Http\\Response\:\:noContent\(\) \(void\) is used\.$#' - identifier: method.void - count: 1 - path: app/controllers/api/projects.php - - - - message: '#^Result of method Appwrite\\Utopia\\Response\:\:dynamic\(\) \(void\) is used\.$#' - identifier: method.void - count: 2 - path: app/controllers/api/users.php - - - - message: '#^Result of method Utopia\\Http\\Response\:\:noContent\(\) \(void\) is used\.$#' - identifier: method.void - count: 1 - path: app/controllers/api/users.php - - - - message: '#^Result of method Utopia\\Http\\Response\:\:redirect\(\) \(void\) is used\.$#' - identifier: method.void - count: 2 - path: app/controllers/general.php - - - - message: '#^Result of method Utopia\\Http\\Response\:\:send\(\) \(void\) is used\.$#' - identifier: method.void - count: 1 - path: app/controllers/general.php - - - - message: '#^Variable \$body on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: app/controllers/general.php - - - - message: '#^Variable \$deployment in PHPDoc tag @var does not exist\.$#' - identifier: varTag.variableNotFound - count: 1 - path: app/controllers/general.php - - - - message: '#^Variable \$executionId on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: app/controllers/general.php - - - - message: '#^Variable \$executionResponse might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: app/controllers/general.php - - - - message: '#^Variable \$user might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: app/controllers/general.php - - - - message: '#^Variable \$installation might not be defined\.$#' - identifier: variable.undefined - 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 - count: 1 - path: app/init/database/filters.php - - - - message: '#^Anonymous function has an unused use \$dsn\.$#' - identifier: closure.unusedUse - count: 1 - path: app/init/registers.php - - - - message: '#^Binary operation "/" between string and string results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: app/init/registers.php - - - - message: '#^Property PHPMailer\\PHPMailer\\PHPMailer\:\:\$Port \(int\) does not accept string\.$#' - identifier: assign.propertyType - count: 1 - path: app/init/registers.php - - - - message: '#^Variable \$providerConfig in empty\(\) always exists and is not falsy\.$#' - identifier: empty.variable - count: 2 - path: app/init/registers.php - - - - message: '#^Variable \$providerConfig on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - 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 - count: 4 - path: app/realtime.php - - - - message: '#^Class Utopia\\Database\\Validator\\Authorization does not have a constructor and must be instantiated without any parameters\.$#' - identifier: new.noConstructor - count: 1 - path: app/realtime.php - - - - message: '#^Function triggerStats\(\) returns void but does not have any side effects\.$#' - identifier: void.pure - 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 - count: 1 - path: src/Appwrite/GraphQL/Resolvers.php - - - - message: '#^Anonymous function has an unused use \$info\.$#' - identifier: closure.unusedUse - count: 1 - path: src/Appwrite/GraphQL/Resolvers.php - - - - message: '#^Anonymous function has an unused use \$type\.$#' - identifier: closure.unusedUse - count: 5 - path: src/Appwrite/GraphQL/Resolvers.php - - - - message: '#^Variable \$request in PHPDoc tag @var does not exist\.$#' - identifier: varTag.variableNotFound - count: 1 - path: src/Appwrite/GraphQL/Resolvers.php - - - - message: '#^Variable \$response in PHPDoc tag @var does not exist\.$#' - identifier: varTag.variableNotFound - count: 1 - path: src/Appwrite/GraphQL/Resolvers.php - - - - message: '#^Method Appwrite\\Network\\Cors\:\:headers\(\) should return array\ but returns array\\.$#' - identifier: return.type - count: 5 - path: src/Appwrite/Network/Cors.php - - - - message: '#^Parameter &\$tag by\-ref type of method Appwrite\\OpenSSL\\OpenSSL\:\:encrypt\(\) expects null, string\|null given\.$#' - identifier: parameterByRef.type - count: 1 - path: src/Appwrite/OpenSSL/OpenSSL.php - - - - message: '#^Variable \$output in empty\(\) always exists and is not falsy\.$#' - identifier: empty.variable - count: 1 - path: src/Appwrite/Platform/Modules/Avatars/Http/Action.php - - - - message: '#^Variable \$output in empty\(\) always exists and is not falsy\.$#' - identifier: empty.variable - count: 1 - path: src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php - - - - message: '#^Variable \$output in empty\(\) always exists and is not falsy\.$#' - identifier: empty.variable - count: 1 - path: src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php - - - - message: '#^Binary operation "%%" between string and 5 results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php - - - - message: '#^Method Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Action\:\:setHttpPath\(\) should return Appwrite\\Platform\\Action but returns Utopia\\Platform\\Action\.$#' - identifier: return.type - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php - - - - message: '#^Method Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Collections\\Documents\\Action\:\:setHttpPath\(\) should return Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Action but returns Appwrite\\Platform\\Action\.$#' - identifier: return.type - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php - - - - message: '#^Variable \$relations in empty\(\) always exists and is not falsy\.$#' - identifier: empty.variable - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php - - - - message: '#^Anonymous function has an unused use \$dbForProject\.$#' - identifier: closure.unusedUse - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php - - - - message: '#^Variable \$document in PHPDoc tag @var does not match assigned variable \$collectionTableId\.$#' - identifier: varTag.differentVariable - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php - - - - message: '#^Variable \$hostname on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php - - - - message: '#^Variable \$enabled on left side of \?\?\= always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php - - - - message: '#^Offset ''deviceBrand'' does not exist on int\.$#' - identifier: offsetAccess.notFound - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php - - - - message: '#^Offset ''deviceModel'' does not exist on int\.$#' - identifier: offsetAccess.notFound - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php - - - - message: '#^Offset ''deviceName'' does not exist on int\.$#' - identifier: offsetAccess.notFound - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php - - - - message: '#^Method Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Transactions\\Action\:\:setHttpPath\(\) should return Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Action but returns Appwrite\\Platform\\Action\.$#' - identifier: return.type - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php - - - - message: '#^Anonymous function has an unused use \$existing\.$#' - identifier: closure.unusedUse - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php - - - - message: '#^Anonymous function has an unused use \$queueForEvents\.$#' - identifier: closure.unusedUse - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php - - - - message: '#^Anonymous function has an unused use \$queueForFunctions\.$#' - identifier: closure.unusedUse - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php - - - - message: '#^Anonymous function has an unused use \$queueForRealtime\.$#' - identifier: closure.unusedUse - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php - - - - message: '#^Anonymous function has an unused use \$queueForWebhooks\.$#' - identifier: closure.unusedUse - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php - - - - message: '#^Anonymous function has an unused use \$usage\.$#' - identifier: closure.unusedUse - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php - - - - message: '#^PHPDoc tag @throws with type Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Transactions\\Structure\|Throwable\|Utopia\\Database\\Validator\\Authorization is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php - - - - message: '#^Offset ''deviceBrand'' does not exist on int\.$#' - identifier: offsetAccess.notFound - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php - - - - message: '#^Offset ''deviceModel'' does not exist on int\.$#' - identifier: offsetAccess.notFound - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php - - - - message: '#^Offset ''deviceName'' does not exist on int\.$#' - identifier: offsetAccess.notFound - count: 1 - path: src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php - - - - message: '#^Call to method getAttribute\(\) on an unknown class Appwrite\\Platform\\Modules\\Functions\\Http\\Executions\\Utopia\\Database\\Document\.$#' - identifier: class.notFound - count: 1 - path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php - - - - message: '#^Call to method getId\(\) on an unknown class Appwrite\\Platform\\Modules\\Functions\\Http\\Executions\\Utopia\\Database\\Document\.$#' - identifier: class.notFound - count: 1 - path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php - - - - message: '#^Call to method isEmpty\(\) on an unknown class Appwrite\\Platform\\Modules\\Functions\\Http\\Executions\\Utopia\\Database\\Document\.$#' - identifier: class.notFound - count: 1 - path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php - - - - message: '#^Method Appwrite\\Platform\\Modules\\Functions\\Http\\Executions\\Create\:\:enqueueDeletes\(\) returns void but does not have any side effects\.$#' - identifier: void.pure - count: 1 - path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php - - - - message: '#^PHPDoc tag @var for variable \$session contains unknown class Appwrite\\Platform\\Modules\\Functions\\Http\\Executions\\Utopia\\Database\\Document\.$#' - identifier: class.notFound - count: 1 - path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php - - - - message: '#^Result of method Appwrite\\Utopia\\Response\:\:dynamic\(\) \(void\) is used\.$#' - identifier: method.void - count: 1 - path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php - - - - message: '#^Variable \$body on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php - - - - message: '#^Variable \$executionId on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php - - - - message: '#^Variable \$jwt on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php - - - - message: '#^Call to an undefined method Appwrite\\Event\\Event\:\:setSubscribers\(\)\.$#' - identifier: method.notFound - 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 - count: 4 - path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php - - - - message: '#^Variable \$logsAfter on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php - - - - message: '#^Variable \$logsBefore on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php - - - - message: '#^Variable \$providerCommitHash on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php - - - - message: '#^Variable \$rule in empty\(\) always exists and is not falsy\.$#' - identifier: empty.variable - count: 1 - path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php - - - - message: '#^Result of method Appwrite\\Utopia\\Response\:\:dynamic\(\) \(void\) is used\.$#' - identifier: method.void - count: 1 - path: src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php - - - - message: '#^Variable \$iv might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php - - - - message: '#^Variable \$tag might not be defined\.$#' - identifier: variable.undefined - 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 - count: 14 - path: src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php - - - - message: '#^Variable \$providerConfig on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: src/Appwrite/Platform/Tasks/Doctor.php - - - - message: '#^Variable \$compose in isset\(\) always exists and is not nullable\.$#' - identifier: isset.variable - count: 1 - path: src/Appwrite/Platform/Tasks/Install.php - - - - message: '#^Variable \$prUrls might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/Appwrite/Platform/Tasks/SDKs.php - - - - message: '#^Anonymous function has an unused use \$dbForPlatform\.$#' - identifier: closure.unusedUse - count: 1 - path: src/Appwrite/Platform/Tasks/StatsResources.php - - - - message: '#^PHPDoc tag @param references unknown parameter\: \$getProjectDB$#' - identifier: parameter.notFound - count: 1 - path: src/Appwrite/Platform/Workers/Audits.php - - - - message: '#^Anonymous function has an unused use \$certificates\.$#' - identifier: closure.unusedUse - count: 2 - path: src/Appwrite/Platform/Workers/Deletes.php - - - - message: '#^Anonymous function has an unused use \$dbForPlatform\.$#' - identifier: closure.unusedUse - count: 1 - path: src/Appwrite/Platform/Workers/Deletes.php - - - - message: '#^Anonymous function has an unused use \$dbForProject\.$#' - identifier: closure.unusedUse - count: 1 - path: src/Appwrite/Platform/Workers/Deletes.php - - - - message: '#^Anonymous function has an unused use \$project\.$#' - identifier: closure.unusedUse - count: 6 - path: src/Appwrite/Platform/Workers/Deletes.php - - - - message: '#^Anonymous function has an unused use \$resourceType\.$#' - identifier: closure.unusedUse - count: 1 - path: src/Appwrite/Platform/Workers/Deletes.php - - - - message: '#^PHPDoc tag @param references unknown parameter\: \$build$#' - identifier: parameter.notFound - count: 1 - path: src/Appwrite/Platform/Workers/Deletes.php - - - - message: '#^PHPDoc tag @param references unknown parameter\: \$dbForPlatform$#' - identifier: parameter.notFound - count: 1 - path: src/Appwrite/Platform/Workers/Deletes.php - - - - message: '#^PHPDoc tag @param references unknown parameter\: \$target$#' - identifier: parameter.notFound - count: 1 - path: src/Appwrite/Platform/Workers/Deletes.php - - - - message: '#^PHPDoc tag @throws with type Appwrite\\Extend\\Exception\|Utopia\\Database\\Exception\|Utopia\\Database\\Validator\\Authorization is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 2 - path: src/Appwrite/Platform/Workers/Deletes.php - - - - message: '#^PHPDoc tag @throws with type Appwrite\\Extend\\Exception\|Utopia\\Database\\Validator\\Authorization is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 1 - path: src/Appwrite/Platform/Workers/Deletes.php - - - - message: '#^PHPDoc tag @throws with type Appwrite\\Platform\\Workers\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 2 - path: src/Appwrite/Platform/Workers/Functions.php - - - - message: '#^Variable \$body on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 2 - path: src/Appwrite/Platform/Workers/Functions.php - - - - message: '#^Variable \$errorCode might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/Appwrite/Platform/Workers/Functions.php - - - - message: '#^Variable \$executionId on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: src/Appwrite/Platform/Workers/Functions.php - - - - message: '#^Binary operation "\+\=" between 0 and array\\|int\|string\>\|int\|string results in an error\.$#' - identifier: assignOp.invalid - count: 1 - path: src/Appwrite/Platform/Workers/Messaging.php - - - - message: '#^Variable \$provider might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/Appwrite/Platform/Workers/Messaging.php - - - - message: '#^Variable \$aggregatedResources might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/Appwrite/Platform/Workers/Migrations.php - - - - message: '#^Anonymous function has an unused use \$dbForLogs\.$#' - identifier: closure.unusedUse - count: 1 - path: src/Appwrite/Platform/Workers/StatsResources.php - - - - message: '#^Anonymous function has an unused use \$sequence\.$#' - identifier: closure.unusedUse - count: 1 - path: src/Appwrite/Platform/Workers/StatsUsage.php - - - - message: '#^Callable callable\(\)\: Utopia\\Database\\Database invoked with 1 parameter, 0 required\.$#' - identifier: arguments.count - count: 2 - path: src/Appwrite/Platform/Workers/StatsUsage.php - - - - message: '#^Variable \$curlError on left side of \?\? is never defined\.$#' - identifier: nullCoalesce.variable - count: 1 - path: src/Appwrite/Platform/Workers/Webhooks.php - - - - message: '#^Unsafe usage of new static\(\)\.$#' - identifier: new.static - count: 3 - path: src/Appwrite/Promises/Promise.php - - - - message: '#^Property Appwrite\\SDK\\Method\:\:\$hide on left side of \?\? is not nullable nor uninitialized\.$#' - identifier: nullCoalesce.initializedProperty - count: 1 - path: src/Appwrite/SDK/Method.php - - - - message: '#^PHPDoc tag @param has invalid value \(Document \$this\)\: Unexpected token "\$this", expected variable at offset 69 on line 4$#' - identifier: phpDoc.parseError - count: 1 - path: src/Appwrite/Utopia/Database/Documents/User.php - - - - message: '#^PHPDoc tag @param references unknown parameter\: \$sessions$#' - identifier: parameter.notFound - count: 1 - path: src/Appwrite/Utopia/Database/Documents/User.php - - - - message: '#^Binary operation "\+" between string and 1 results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: tests/e2e/Services/GraphQL/Legacy/AbuseTest.php - - - - message: '#^Binary operation "\+" between string and 1 results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: tests/e2e/Services/GraphQL/TablesDB/AbuseTest.php - - - - message: '#^Call to an undefined method Utopia\\Queue\\Publisher\:\:getEvents\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/unit/Event/EventTest.php diff --git a/phpstan.neon b/phpstan.neon index 25fe377ecf..85d18fd44d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,3 @@ -includes: - - phpstan-baseline.neon - parameters: level: 3 tmpDir: .phpstan-cache @@ -15,4 +12,3 @@ parameters: - vendor/swoole/ide-helper excludePaths: - tests/resources - diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index bf6339f8a0..ae75e3924f 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -285,7 +285,7 @@ class Event * * @param string $key * @param Document $context - * @return self + * @return static */ public function setContext(string $key, Document $context): self { @@ -309,7 +309,7 @@ class Event /** * Set class used for this event. * @param string $class - * @return self + * @return static */ public function setClass(string $class): self { @@ -648,10 +648,8 @@ class Event * * @param Event $event * - * @return self - * */ - public function from(Event $event): self + public function from(Event $event): static { $this->project = $event->getProject(); $this->user = $event->getUser(); diff --git a/src/Appwrite/GraphQL/Resolvers.php b/src/Appwrite/GraphQL/Resolvers.php index 484cafb0ab..e422bcbf96 100644 --- a/src/Appwrite/GraphQL/Resolvers.php +++ b/src/Appwrite/GraphQL/Resolvers.php @@ -25,11 +25,7 @@ class Resolvers ?Route $route, ): callable { return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $route, $args, $context, $info) { - /** @var Http $utopia */ - /** @var Response $response */ - /** @var Request $request */ - + function (callable $resolve, callable $reject) use ($utopia, $route, $args) { $utopia = $utopia->getResource('utopia:graphql', true); $request = $utopia->getResource('request', true); $response = $utopia->getResource('response', true); @@ -96,7 +92,7 @@ class Resolvers callable $url, ): callable { return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) { + function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) { $utopia = $utopia->getResource('utopia:graphql', true); $request = $utopia->getResource('request', true); $response = $utopia->getResource('response', true); @@ -127,7 +123,7 @@ class Resolvers callable $params, ): callable { return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) { + function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) { $utopia = $utopia->getResource('utopia:graphql', true); $request = $utopia->getResource('request', true); $response = $utopia->getResource('response', true); @@ -163,7 +159,7 @@ class Resolvers callable $params, ): callable { return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) { + function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) { $utopia = $utopia->getResource('utopia:graphql', true); $request = $utopia->getResource('request', true); $response = $utopia->getResource('response', true); @@ -195,7 +191,7 @@ class Resolvers callable $params, ): callable { return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) { + function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) { $utopia = $utopia->getResource('utopia:graphql', true); $request = $utopia->getResource('request', true); $response = $utopia->getResource('response', true); @@ -225,7 +221,7 @@ class Resolvers callable $url, ): callable { return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) { + function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) { $utopia = $utopia->getResource('utopia:graphql', true); $request = $utopia->getResource('request', true); $response = $utopia->getResource('response', true); diff --git a/src/Appwrite/Network/Cors.php b/src/Appwrite/Network/Cors.php index 9fc47c5808..e21c660a32 100644 --- a/src/Appwrite/Network/Cors.php +++ b/src/Appwrite/Network/Cors.php @@ -57,7 +57,7 @@ final class Cors self::HEADER_ALLOW_HEADERS => implode(', ', $this->allowedHeaders), self::HEADER_EXPOSE_HEADERS => implode(', ', $this->exposedHeaders), self::HEADER_ALLOW_CREDENTIALS => $this->allowCredentials ? 'true' : 'false', - self::HEADER_MAX_AGE => $this->maxAge, + self::HEADER_MAX_AGE => (string) $this->maxAge, ]; // Wildcard allow-all diff --git a/src/Appwrite/OpenSSL/OpenSSL.php b/src/Appwrite/OpenSSL/OpenSSL.php index 1965a3c858..787feb0904 100644 --- a/src/Appwrite/OpenSSL/OpenSSL.php +++ b/src/Appwrite/OpenSSL/OpenSSL.php @@ -18,7 +18,7 @@ class OpenSSL * * @return string */ - public static function encrypt($data, $method, $key, $options = 0, $iv = '', &$tag = null, $aad = '', $tag_length = 16) + public static function encrypt($data, $method, $key, $options = 0, $iv = '', ?string &$tag = null, $aad = '', $tag_length = 16) { return \openssl_encrypt($data, $method, $key, $options, $iv, $tag, $aad, $tag_length); } diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Action.php b/src/Appwrite/Platform/Modules/Avatars/Http/Action.php index bf7d01764f..d6b58b33dd 100644 --- a/src/Appwrite/Platform/Modules/Avatars/Http/Action.php +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Action.php @@ -49,7 +49,6 @@ class Action extends PlatformAction $image = new Image(\file_get_contents($path)); $image->crop((int) $width, (int) $height); - $output = (empty($output)) ? $type : $output; $data = $image->output($output, $quality); $response ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php index 0a4d652d0e..b6cc408dde 100644 --- a/src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php @@ -204,7 +204,6 @@ class Get extends Action $image = new Image($data); $image->crop((int) $width, (int) $height); - $output = (empty($output)) ? $type : $output; $data = $image->output($output, $quality); $response diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php index eb56ddf0b2..77ff9dd8ce 100644 --- a/src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php @@ -95,7 +95,6 @@ class Get extends Action } $image->crop((int) $width, (int) $height); - $output = (empty($output)) ? $type : $output; $data = $image->output($output, $quality); $response diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php index 8278a43ea3..c73d20fb0c 100644 --- a/src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php @@ -90,7 +90,7 @@ class Get extends Action } } - $rand = \substr($code, -1); + $rand = (int) \substr((string) $code, -1); $rand = ($rand > \count($themes) - 1) ? $rand % \count($themes) : $rand; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php index b2417871ed..60449aeab6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php @@ -17,7 +17,7 @@ class Action extends AppwriteAction return $this->context; } - public function setHttpPath(string $path): AppwriteAction + public function setHttpPath(string $path): self { if (\str_contains($path, '/tablesdb')) { $this->context = DATABASE_TYPE_TABLESDB; @@ -28,7 +28,8 @@ class Action extends AppwriteAction if (\str_contains($path, '/vectorsdb')) { $this->context = DATABASE_TYPE_VECTORSDB; } - return parent::setHttpPath($path); + parent::setHttpPath($path); + return $this; } /** diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index 0bd4a2e080..91dd9c603c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -24,7 +24,7 @@ abstract class Action extends DatabasesAction */ abstract protected function getResponseModel(): string; - public function setHttpPath(string $path): DatabasesAction + public function setHttpPath(string $path): self { if (str_contains($path, '/tablesdb/')) { $this->context = ROWS; @@ -47,7 +47,8 @@ abstract class Action extends DatabasesAction ], ]; - return parent::setHttpPath($path); + parent::setHttpPath($path); + return $this; } protected function getDatabasesOperationReadMetric(): string @@ -406,8 +407,6 @@ abstract class Action extends DatabasesAction if (\is_array($related)) { $document->setAttribute($relationship->getAttribute('key'), \array_values($relations)); - } elseif (empty($relations)) { - $document->setAttribute($relationship->getAttribute('key'), null); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 7f2e895228..38c84c4ae1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -209,7 +209,7 @@ class Create extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSDKNamespace() . ' with relationship ' . $this->getStructureContext()); } - $setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $dbForProject, $authorization) { + $setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $authorization) { $allowedPermissions = [ Database::PERMISSION_READ, Database::PERMISSION_UPDATE, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php index 27ccaafc71..b86d934ffb 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php @@ -122,7 +122,6 @@ class Update extends Action $dbForDatabases = $getDatabasesDB($database); // Read permission should not be required for update - /** @var Document $document */ $collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(); if ($transactionId !== null) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php index 744a4fd922..b3046fe22d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php @@ -147,7 +147,7 @@ class XList extends Action $cacheKeyBase = \sprintf( '%s-cache-%s:%s:%s:collection:%s:%s:user:%s:%s', $dbForProject->getCacheName(), - $hostname ?? '', + $hostname, $dbForProject->getNamespace(), $dbForProject->getTenant(), $collectionId, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php index 5d9d425d71..1142f38aa9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php @@ -99,8 +99,6 @@ class Update extends Action // Map aggregate permissions into the multiple permissions they represent. $permissions = Permission::aggregate($permissions); - $enabled ??= $collection->getAttribute('enabled', true); - $collection = $dbForProject->updateDocument( 'database_' . $database->getSequence(), $collectionId, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php index 0c0f5f1273..4763a1611d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php @@ -103,6 +103,9 @@ class XList extends Action $os = $detector->getOS(); $client = $detector->getClient(); $device = $detector->getDevice(); + $deviceName = \is_array($device) ? ($device['deviceName'] ?? '') : ''; + $deviceBrand = \is_array($device) ? ($device['deviceBrand'] ?? '') : ''; + $deviceModel = \is_array($device) ? ($device['deviceModel'] ?? '') : ''; $output[$i] = new Document([ 'event' => $log['event'], @@ -121,9 +124,9 @@ class XList extends Action 'clientVersion' => $client['clientVersion'], 'clientEngine' => $client['clientEngine'], 'clientEngineVersion' => $client['clientEngineVersion'], - 'deviceName' => $device['deviceName'], - 'deviceBrand' => $device['deviceBrand'], - 'deviceModel' => $device['deviceModel'], + 'deviceName' => $deviceName, + 'deviceBrand' => $deviceBrand, + 'deviceModel' => $deviceModel, ]); $record = $geodb->get($log['ip']); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php index f3edf010d4..91bc1a3ccf 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php @@ -33,7 +33,7 @@ abstract class Action extends DatabasesAction return $this->databaseType.'.'.METRIC_DATABASE_ID_OPERATIONS_WRITES; } - public function setHttpPath(string $path): DatabasesAction + public function setHttpPath(string $path): self { switch (true) { case str_contains($path, '/tablesdb'): @@ -50,7 +50,8 @@ abstract class Action extends DatabasesAction $this->databaseType = VECTORSDB; break; } - return parent::setHttpPath($path); + parent::setHttpPath($path); + return $this; } /** diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php index 30c9b7cb30..f06feccdee 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php @@ -239,7 +239,7 @@ class Create extends Action } } - $transaction = $authorization->skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) { + $transaction = $authorization->skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $operations) { $dbForProject->createDocuments('transactionLogs', $staged); return $dbForProject->increaseDocumentAttribute( 'transactions', 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 a5d96d5768..0c8c6a8520 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -105,8 +105,7 @@ class Update extends Action * @throws Exception * @throws \Throwable * @throws \Utopia\Database\Exception - * @throws Authorization - * @throws Structure + * @throws StructureException * @throws \Utopia\Http\Exception */ public function action(string $transactionId, bool $commit, bool $rollback, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void @@ -183,7 +182,7 @@ class Update extends Action $dbForDatabases = $getDatabasesDB($databaseDoc); try { - $dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $usage, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) { + $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', ]))); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php index 6754179425..23541db146 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php @@ -97,6 +97,9 @@ class XList extends Action $os = $detector->getOS(); $client = $detector->getClient(); $device = $detector->getDevice(); + $deviceName = \is_array($device) ? ($device['deviceName'] ?? '') : ''; + $deviceBrand = \is_array($device) ? ($device['deviceBrand'] ?? '') : ''; + $deviceModel = \is_array($device) ? ($device['deviceModel'] ?? '') : ''; $output[$i] = new Document([ 'event' => $log['event'], @@ -115,9 +118,9 @@ class XList extends Action 'clientVersion' => $client['clientVersion'], 'clientEngine' => $client['clientEngine'], 'clientEngineVersion' => $client['clientEngineVersion'], - 'deviceName' => $device['deviceName'], - 'deviceBrand' => $device['deviceBrand'], - 'deviceModel' => $device['deviceModel'], + 'deviceName' => $deviceName, + 'deviceBrand' => $deviceBrand, + 'deviceModel' => $deviceModel, ]); $record = $geodb->get($log['ip']); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index c6d25a15fc..37292ce984 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -213,7 +213,10 @@ class Create extends Base $current = new Document(); foreach ($sessions as $session) { - /** @var Utopia\Database\Document $session */ + if (!$session instanceof Document) { + continue; + } + if ($proofForToken->verify($store->getProperty('secret', ''), $session->getAttribute('secret'))) { // Find most recent active session for user ID and JWT headers $current = $session; } @@ -237,11 +240,11 @@ class Create extends Base ]); $executionId = ID::unique(); - $headers['x-appwrite-execution-id'] = $executionId ?? ''; + $headers['x-appwrite-execution-id'] = $executionId; $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey; $headers['x-appwrite-trigger'] = 'http'; - $headers['x-appwrite-user-id'] = $user->getId() ?? ''; - $headers['x-appwrite-user-jwt'] = $jwt ?? ''; + $headers['x-appwrite-user-id'] = $user->getId(); + $headers['x-appwrite-user-jwt'] = $jwt; $headers['x-appwrite-country-code'] = ''; $headers['x-appwrite-continent-code'] = ''; $headers['x-appwrite-continent-eu'] = 'false'; @@ -350,16 +353,18 @@ class Create extends Base } } - $this->enqueueDeletes( - $project, - $function->getSequence(), - $executionsRetentionCount, + if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) { $queueForDeletes - ); + ->setProject($project) + ->setResource($function->getSequence()) + ->setResourceType(RESOURCE_TYPE_FUNCTIONS) + ->setType(DELETE_TYPE_EXECUTIONS_LIMIT) + ->trigger(); + } - return $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($execution, Response::MODEL_EXECUTION); + $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); + $response->dynamic($execution, Response::MODEL_EXECUTION); + return; } $durationStart = \microtime(true); @@ -370,7 +375,7 @@ class Create extends Base if ($version === 'v2') { $vars = \array_merge($vars, [ 'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '', - 'APPWRITE_FUNCTION_DATA' => $body ?? '', + 'APPWRITE_FUNCTION_DATA' => $body, 'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '', 'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? '' ]); @@ -537,32 +542,18 @@ class Create extends Base } } - $this->enqueueDeletes( - $project, - $function->getSequence(), - $executionsRetentionCount, + if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) { $queueForDeletes - ); + ->setProject($project) + ->setResource($function->getSequence()) + ->setResourceType(RESOURCE_TYPE_FUNCTIONS) + ->setType(DELETE_TYPE_EXECUTIONS_LIMIT) + ->trigger(); + } $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($execution, Response::MODEL_EXECUTION); } - private function enqueueDeletes( - Document $project, - string $resourceId, - int $executionsRetentionCount, - DeleteEvent $queueForDeletes - ): void { - /* cleanup */ - if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) { - $queueForDeletes - ->setProject($project) - ->setResource($resourceId) - ->setResourceType(RESOURCE_TYPE_FUNCTIONS) - ->setType(DELETE_TYPE_EXECUTIONS_LIMIT) - ->trigger(); - } - } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index d281c64414..8d4ad5d403 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -424,8 +424,8 @@ class Create extends Base /** Trigger Realtime Events */ $queueForRealtime - ->from($ruleCreate) ->setSubscribers(['console', $project->getId()]) + ->from($ruleCreate) ->trigger(); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index bcedeee764..c6c4a0b38c 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -450,7 +450,7 @@ class Builds extends Action $providerCommitHash = \trim($stdout); - $deployment->setAttribute('providerCommitHash', $providerCommitHash ?? ''); + $deployment->setAttribute('providerCommitHash', $providerCommitHash); $deployment->setAttribute('providerCommitAuthorUrl', APP_VCS_GITHUB_URL); $deployment->setAttribute('providerCommitAuthor', APP_VCS_GITHUB_USERNAME); $deployment->setAttribute('providerCommitMessage', "Create '" . $resource->getAttribute('name', '') . "' function"); @@ -862,7 +862,7 @@ class Builds extends Action if (\str_contains($logs, '{APPWRITE_DETECTION_SEPARATOR_START}')) { [$logsBefore, $detectionLogsStart] = \explode('{APPWRITE_DETECTION_SEPARATOR_START}', $logs, 2); [$detectionLogs, $logsAfter] = \explode('{APPWRITE_DETECTION_SEPARATOR_END}', $detectionLogsStart, 2); - $logs = ($logsBefore ?? '') . ($logsAfter ?? ''); + $logs = $logsBefore . $logsAfter; } $deployment->setAttribute('buildLogs', $logs); @@ -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 = (int) ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT); + $memory = (int) ($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT); switch ($deployment->getAttribute('status')) { case 'ready': @@ -1364,6 +1366,8 @@ class Builds extends Action Realtime $queueForRealtime, array $platform ): void { + $deployment = new Document(); + try { if ($resource->getAttribute('providerSilentMode', false) === true) { return; @@ -1444,7 +1448,7 @@ class Builds extends Action $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; $previewUrl = match ($resource->getCollection()) { 'functions' => '', - 'sites' => ! empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '', + 'sites' => !$rule->isEmpty() ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '', default => throw new \Exception('Invalid resource type') }; diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php index fb83cb34c5..8a0d341132 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php @@ -83,7 +83,8 @@ class Update extends Action // If rule is already verified or in certificate generation state, don't queue for verification again if ($rule->getAttribute('status') === RULE_STATUS_VERIFIED || $rule->getAttribute('status') === RULE_STATUS_CERTIFICATE_GENERATING) { - return $response->dynamic($rule, Response::MODEL_PROXY_RULE); + $response->dynamic($rule, Response::MODEL_PROXY_RULE); + return; } try { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php index c67601dae9..c5f4f3dccd 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php @@ -286,6 +286,8 @@ class Create extends Action $mimeType = $deviceForFiles->getFileMimeType($path); // Get mime-type before compression and encryption $fileHash = $deviceForFiles->getFileHash($path); // Get file hash before compression and encryption $data = ''; + $iv = ''; + $tag = null; // Compression $algorithm = $bucket->getAttribute('compression', Compression::NONE); if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != Compression::NONE) { diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php index 777184f2f2..4a3dbee5c9 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php @@ -37,6 +37,7 @@ use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\ArrayList; use Utopia\Validator\Text; +use Throwable; class Create extends Action { @@ -102,6 +103,8 @@ class Create extends Action { $isAppUser = $user->isApp($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); + $invitee = new Document(); + $hash = ''; if (empty($url)) { if (! $isAppUser && ! $isPrivilegedUser) { @@ -145,9 +148,6 @@ class Create extends Action } } elseif (! empty($phone)) { $invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]); - if (! $invitee->isEmpty() && ! empty($email) && $invitee->getAttribute('email', '') !== $email) { - throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given phone and email doesn\'t match', 409); - } } if ($invitee->isEmpty()) { // Create new user if no user with same email found diff --git a/src/Appwrite/Platform/Tasks/Doctor.php b/src/Appwrite/Platform/Tasks/Doctor.php index 9a9c2fdf73..67b6eb4462 100644 --- a/src/Appwrite/Platform/Tasks/Doctor.php +++ b/src/Appwrite/Platform/Tasks/Doctor.php @@ -124,7 +124,7 @@ class Doctor extends Action $providerConfig = System::getEnv('_APP_LOGGING_CONFIG', ''); try { - $loggingProvider = new DSN($providerConfig ?? ''); + $loggingProvider = new DSN($providerConfig); $providerName = $loggingProvider->getScheme(); diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php index 79a052b34a..dd7bed0137 100644 --- a/src/Appwrite/Platform/Tasks/Install.php +++ b/src/Appwrite/Platform/Tasks/Install.php @@ -227,7 +227,7 @@ class Install extends Action // Fall back to CLI mode $enableAssistant = false; $assistantExistsInOldCompose = false; - if ($existingInstallation && isset($compose)) { + if ($existingInstallation) { try { $assistantService = $compose->getService('appwrite-assistant'); $assistantExistsInOldCompose = $assistantService !== null; diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index a36959af33..e8a69afddb 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -566,6 +566,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $repoBranch = $language['repoBranch'] ?? 'main'; if ($git && !empty($gitUrl)) { + $prUrls = []; + // Generate commit message: use provided message, AI changelog, or fallback if (! empty($message)) { $commitMessage = $message; diff --git a/src/Appwrite/Platform/Tasks/StatsResources.php b/src/Appwrite/Platform/Tasks/StatsResources.php index 6c0e8da48b..220e377619 100644 --- a/src/Appwrite/Platform/Tasks/StatsResources.php +++ b/src/Appwrite/Platform/Tasks/StatsResources.php @@ -60,7 +60,7 @@ class StatsResources extends Action $interval = (int) System::getEnv('_APP_STATS_RESOURCES_INTERVAL', '3600'); - Console::loop(function () use ($queueForStatsResources, $dbForPlatform) { + Console::loop(function () use ($queueForStatsResources) { $last24Hours = (new \DateTime())->sub(\DateInterval::createFromDateString('24 hours')); /** diff --git a/src/Appwrite/Platform/Workers/Audits.php b/src/Appwrite/Platform/Workers/Audits.php index 55ec39026b..9588c1a9d3 100644 --- a/src/Appwrite/Platform/Workers/Audits.php +++ b/src/Appwrite/Platform/Workers/Audits.php @@ -51,13 +51,11 @@ class Audits extends Action /** * @param Message $message - * @param callable $getProjectDB * @param Document $project - * @param callable $getAudit + * @param callable(Document): \Utopia\Audit\Audit $getAudit * @return Commit|NoCommit * @throws Throwable * @throws \Utopia\Database\Exception - * @throws Authorization * @throws Structure */ public function action(Message $message, Document $project, callable $getAudit): Commit|NoCommit diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 716969e67a..a4faa64462 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -363,7 +363,6 @@ class Deletes extends Action /** * @param Document $project * @param callable $getProjectDB - * @param Document $target * @return void * @throws Exception */ @@ -438,7 +437,6 @@ class Deletes extends Action * @param string $resource * @param string|null $resourceType * @return void - * @throws Authorization * @throws Exception */ private function deleteCacheByResource(Document $project, callable $getProjectDB, string $resource, ?string $resourceType = null): void @@ -518,7 +516,6 @@ class Deletes extends Action } /** - * @param Database $dbForPlatform * @param callable $getProjectDB * @param string $hourlyUsageRetentionDatetime * @return void @@ -586,7 +583,6 @@ class Deletes extends Action * @param Database $dbForPlatform * @param Document $document * @return void - * @throws Authorization * @throws DatabaseException * @throws Conflict * @throws Restricted @@ -623,7 +619,6 @@ class Deletes extends Action * @param Document $document * @return void * @throws Exception - * @throws Authorization * @throws DatabaseException */ protected function deleteProject(Database $dbForPlatform, callable $getProjectDB, callable $getDatabasesDB, Device $deviceForFiles, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, Document $document): void @@ -952,7 +947,7 @@ class Deletes extends Action // fast path, no need to list anything! $delete($dbForProject, $resourceInternalId, $resourceType); } else { - $processResource = function (string $type) use ($dbForProject, $delete, $resourceType) { + $processResource = function (string $type) use ($dbForProject, $delete) { $this->listByGroup( collection: $type, queries: [Query::select(['$id', '$sequence'])], @@ -1109,7 +1104,7 @@ class Deletes extends Action Query::equal('resourceInternalId', [$siteInternalId]), Query::equal('resourceType', ['sites']), Query::orderAsc() - ], $dbForProject, function (Document $document) use ($project, $certificates, $deviceForSites, $deviceForBuilds, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) { + ], $dbForProject, function (Document $document) use ($deviceForSites, $deviceForBuilds, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds, &$deploymentIds) { $deploymentInternalIds[] = $document->getSequence(); $deploymentIds[] = $document->getId(); $this->deleteBuildFiles($deviceForBuilds, $document); @@ -1172,7 +1167,7 @@ class Deletes extends Action Query::equal('deploymentResourceInternalId', [$functionInternalId]), Query::equal('projectInternalId', [$project->getSequence()]), Query::orderAsc() - ], $dbForPlatform, function (Document $document) use ($project, $dbForPlatform, $certificates) { + ], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) { $this->deleteRule($dbForPlatform, $document, $certificates); }); @@ -1196,7 +1191,7 @@ class Deletes extends Action Query::equal('resourceInternalId', [$functionInternalId]), Query::equal('resourceType', ['functions']), Query::orderAsc() - ], $dbForProject, function (Document $document) use ($dbForPlatform, $project, $certificates, $deviceForFunctions, $deviceForBuilds, &$deploymentInternalIds) { + ], $dbForProject, function (Document $document) use ($deviceForFunctions, $deviceForBuilds, &$deploymentInternalIds) { $deploymentInternalIds[] = $document->getSequence(); $this->deleteDeploymentFiles($deviceForFunctions, $document); $this->deleteBuildFiles($deviceForBuilds, $document); @@ -1321,7 +1316,7 @@ class Deletes extends Action /** * @param Device $device - * @param Document $build + * @param Document $deployment * @return void */ private function deleteBuildFiles(Device $device, Document $deployment): void @@ -1631,9 +1626,9 @@ class Deletes extends Action try { $dbForProject->deleteDocuments('transactions', [ Query::lessThan('expiresAt', DateTime::format(new \DateTime())), - ], onNext: function (Document $transaction) use ($dbForProject, $project, &$transactionInternalIds) { + ], onNext: function (Document $transaction) use (&$transactionInternalIds) { $transactionInternalIds[] = $transaction->getSequence(); - }, onError: function (Throwable $th) use ($project) { + }, onError: function (Throwable $th) { // Swallow errors to avoid breaking the cleanup process }); } catch (Throwable $th) { @@ -1646,7 +1641,7 @@ class Deletes extends Action $dbForProject->deleteDocuments('transactionLogs', [ Query::equal('transactionInternalId', $transactionInternalIds), - ], onError: function (Throwable $th) use ($project) { + ], onError: function (Throwable $th) { // Swallow errors to avoid breaking the cleanup process }); } diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index 3f19abdf22..bed28dad1c 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -33,7 +33,7 @@ class Functions extends Action } /** - * @throws Exception + * @throws \Exception */ public function __construct() { @@ -256,7 +256,7 @@ class Functions extends Action * @param Document $user * @param string|null $jwt * @param string|null $event - * @throws Exception + * @throws \Exception */ private function fail( string $message, @@ -271,10 +271,10 @@ class Functions extends Action ?string $event = null, ): void { $executionId = ID::unique(); - $headers['x-appwrite-execution-id'] = $executionId ?? ''; + $headers['x-appwrite-execution-id'] = $executionId; $headers['x-appwrite-trigger'] = $trigger; $headers['x-appwrite-event'] = $event ?? ''; - $headers['x-appwrite-user-id'] = $user->getId() ?? ''; + $headers['x-appwrite-user-id'] = $user->getId(); $headers['x-appwrite-user-jwt'] = $jwt ?? ''; $headersFiltered = []; @@ -458,8 +458,8 @@ class Functions extends Action if ($version === 'v2') { $vars = \array_merge($vars, [ 'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '', - 'APPWRITE_FUNCTION_DATA' => $body ?? '', - 'APPWRITE_FUNCTION_EVENT_DATA' => $body ?? '', + 'APPWRITE_FUNCTION_DATA' => $body, + 'APPWRITE_FUNCTION_EVENT_DATA' => $body, 'APPWRITE_FUNCTION_EVENT' => $headers['x-appwrite-event'] ?? '', 'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '', 'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? '' @@ -508,6 +508,9 @@ class Functions extends Action ]); /** Execute function */ + $error = null; + $errorCode = 0; + try { $version = $function->getAttribute('version', 'v2'); $command = $runtime['startCommand']; diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index af7d2027e3..ff5eb2417a 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -285,7 +285,7 @@ class Messaging extends Action try { $response = $adapter->send($data); - $deliveredTotal += $response['deliveredTo']; + $deliveredTotal += (int) $response['deliveredTo']; foreach ($response['results'] as $result) { if ($result['status'] === 'failure') { $deliveryErrors[] = "Failed sending to target {$result['recipient']} with error: {$result['error']}"; @@ -380,7 +380,7 @@ class Messaging extends Action ])); // Delete any attachments that were downloaded to local storage - if ($provider->getAttribute('type') === MESSAGE_TYPE_EMAIL) { + if ($providerType === MESSAGE_TYPE_EMAIL) { if ($deviceForFiles->getType() === Storage::DEVICE_LOCAL) { return; } diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 1080ff066f..6a7f52cb37 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -550,6 +550,8 @@ class Migrations extends Action $destination?->error(); } + $aggregatedResources = []; + if ($migration->getAttribute('status', '') === 'completed') { foreach ($aggregatedResources as $resource) { $this->processMigrationResourceStats( diff --git a/src/Appwrite/Platform/Workers/StatsResources.php b/src/Appwrite/Platform/Workers/StatsResources.php index 0e7a9bb0a7..b2823d3722 100644 --- a/src/Appwrite/Platform/Workers/StatsResources.php +++ b/src/Appwrite/Platform/Workers/StatsResources.php @@ -208,7 +208,7 @@ class StatsResources extends Action { $totalFiles = 0; $totalStorage = 0; - $this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $dbForLogs, $region, &$totalFiles, &$totalStorage) { + $this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $region, &$totalFiles, &$totalStorage) { try { $files = $dbForProject->count('bucket_' . $bucket->getSequence()); } catch (Throwable $th) { diff --git a/src/Appwrite/Platform/Workers/StatsUsage.php b/src/Appwrite/Platform/Workers/StatsUsage.php index 1e0a2eabba..144c429629 100644 --- a/src/Appwrite/Platform/Workers/StatsUsage.php +++ b/src/Appwrite/Platform/Workers/StatsUsage.php @@ -140,7 +140,7 @@ class StatsUsage extends Action /** * @param Message $message - * @param callable(): Database $getProjectDB + * @param callable(Document): Database $getProjectDB * @param callable(): Database $getLogsDB * @param Registry $register * @return void @@ -212,7 +212,7 @@ class StatsUsage extends Action * @param Document $project * @param Document $document * @param array $metrics - * @param callable(): Database $getProjectDB + * @param callable(Document): Database $getProjectDB * @param string $databaseType Database type from context * @return void */ @@ -394,7 +394,7 @@ class StatsUsage extends Action /** * Commit stats to DB - * @param callable(): Database $getProjectDB + * @param callable(Document): Database $getProjectDB * @return void */ public function commitToDb(callable $getProjectDB): void @@ -459,7 +459,7 @@ class StatsUsage extends Action /** * Sort by unique index key reduce locks/deadlocks */ - usort($projectStats['stats'], function ($a, $b) use ($sequence) { + usort($projectStats['stats'], function ($a, $b) { // Metric DESC $cmp = strcmp($b['metric'], $a['metric']); if ($cmp !== 0) { diff --git a/src/Appwrite/Platform/Workers/Webhooks.php b/src/Appwrite/Platform/Workers/Webhooks.php index fce3c7b149..509f0a6313 100644 --- a/src/Appwrite/Platform/Workers/Webhooks.php +++ b/src/Appwrite/Platform/Workers/Webhooks.php @@ -233,7 +233,7 @@ class Webhooks extends Action $template->setParam('{{webhook}}', $webhook->getAttribute('name')); $template->setParam('{{project}}', $project->getAttribute('name')); $template->setParam('{{url}}', $webhook->getAttribute('url')); - $template->setParam('{{error}}', $curlError ?? 'The server returned ' . $statusCode . ' status code'); + $template->setParam('{{error}}', 'The server returned ' . $statusCode . ' status code'); $template->setParam('{{path}}', "/console/project-$region-$projectId/settings/webhooks/$webhookId"); $template->setParam('{{attempts}}', $attempts); diff --git a/src/Appwrite/Promises/Promise.php b/src/Appwrite/Promises/Promise.php index a6b1aa79d5..a58c7c29a8 100644 --- a/src/Appwrite/Promises/Promise.php +++ b/src/Appwrite/Promises/Promise.php @@ -2,6 +2,7 @@ namespace Appwrite\Promises; +/** @phpstan-consistent-constructor */ abstract class Promise { protected const STATE_PENDING = 1; diff --git a/src/Appwrite/SDK/Method.php b/src/Appwrite/SDK/Method.php index 3cf96b5413..c74406b2a6 100644 --- a/src/Appwrite/SDK/Method.php +++ b/src/Appwrite/SDK/Method.php @@ -197,7 +197,7 @@ class Method public function isHidden(): bool|array { - return $this->hide ?? false; + return $this->hide; } public function isPackaging(): bool diff --git a/src/Appwrite/Utopia/Database/Documents/User.php b/src/Appwrite/Utopia/Database/Documents/User.php index 50e66dac38..211c6449dc 100644 --- a/src/Appwrite/Utopia/Database/Documents/User.php +++ b/src/Appwrite/Utopia/Database/Documents/User.php @@ -86,7 +86,6 @@ class User extends Document /** * Check if user is anonymous. * - * @param Document $this * @return bool */ public function isAnonymous(): bool @@ -153,7 +152,6 @@ class User extends Document /** * Verify session and check that its not expired. * - * @param array $sessions * @param string $secret * * @return bool|string diff --git a/tests/e2e/Services/GraphQL/Legacy/AbuseTest.php b/tests/e2e/Services/GraphQL/Legacy/AbuseTest.php index dfec046f55..39939fdc0e 100644 --- a/tests/e2e/Services/GraphQL/Legacy/AbuseTest.php +++ b/tests/e2e/Services/GraphQL/Legacy/AbuseTest.php @@ -102,7 +102,7 @@ class AbuseTest extends Scope $maxQueries = System::getEnv('_APP_GRAPHQL_MAX_QUERIES', 10); $query = []; - for ($i = 0; $i <= $maxQueries + 1; $i++) { + for ($i = 0; $i <= ((int) $maxQueries) + 1; $i++) { $query[] = ['query' => $this->getQuery(self::LIST_COUNTRIES)]; } diff --git a/tests/e2e/Services/GraphQL/TablesDB/AbuseTest.php b/tests/e2e/Services/GraphQL/TablesDB/AbuseTest.php index 05185149fd..c358882b8b 100644 --- a/tests/e2e/Services/GraphQL/TablesDB/AbuseTest.php +++ b/tests/e2e/Services/GraphQL/TablesDB/AbuseTest.php @@ -102,7 +102,7 @@ class AbuseTest extends Scope $maxQueries = System::getEnv('_APP_GRAPHQL_MAX_QUERIES', 10); $query = []; - for ($i = 0; $i <= $maxQueries + 1; $i++) { + for ($i = 0; $i <= ((int) $maxQueries) + 1; $i++) { $query[] = ['query' => $this->getQuery(self::LIST_COUNTRIES)]; } diff --git a/tests/unit/Event/EventTest.php b/tests/unit/Event/EventTest.php index d5936e4b8f..fcaf1f734d 100644 --- a/tests/unit/Event/EventTest.php +++ b/tests/unit/Event/EventTest.php @@ -13,7 +13,7 @@ class EventTest extends TestCase { protected ?Event $object = null; protected string $queue = ''; - protected Publisher $publisher; + protected MockPublisher $publisher; public function setUp(): void { diff --git a/tests/unit/Functions/Validator/HeadersTest.php b/tests/unit/Functions/Validator/HeadersTest.php index 4a45f57427..563131d480 100644 --- a/tests/unit/Functions/Validator/HeadersTest.php +++ b/tests/unit/Functions/Validator/HeadersTest.php @@ -76,11 +76,6 @@ class HeadersTest extends TestCase ]; $this->assertFalse($this->object->isValid($headers)); - $headers = [ - null => 'value', - ]; - $this->assertFalse($this->object->isValid($headers)); - $headers = [ 'X-Header' => null, ]; From 24bd4c3d7b25fbbe03f2ed68c634f6913164f659 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 08:11:57 +0530 Subject: [PATCH 092/131] fix: preserve migration resource stats --- src/Appwrite/Platform/Workers/Migrations.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 6a7f52cb37..2534899f67 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -408,6 +408,7 @@ class Migrations extends Action $tempAPIKey = $this->generateAPIKey($project); $transfer = $source = $destination = null; + $aggregatedResources = []; $host = System::getEnv('_APP_MIGRATION_HOST'); if (empty($host)) { @@ -444,7 +445,6 @@ class Migrations extends Action $destination ); - $aggregatedResources = []; /** Start Transfer */ if (empty($source->getErrors())) { $migration->setAttribute('stage', 'migrating'); @@ -550,8 +550,6 @@ class Migrations extends Action $destination?->error(); } - $aggregatedResources = []; - if ($migration->getAttribute('status', '') === 'completed') { foreach ($aggregatedResources as $resource) { $this->processMigrationResourceStats( From 77b4f8b7a0eb8e13badd399d8dbb08e00efa09a5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 08:23:51 +0530 Subject: [PATCH 093/131] style: apply formatter --- app/http.php | 1 - src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php | 2 +- src/Appwrite/Platform/Workers/Audits.php | 1 - src/Appwrite/Platform/Workers/Deletes.php | 1 - tests/unit/Event/EventTest.php | 1 - 5 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/http.php b/app/http.php index 3eb8284ddf..4532839727 100644 --- a/app/http.php +++ b/app/http.php @@ -33,7 +33,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; diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php index 4a3dbee5c9..f863729263 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php @@ -17,6 +17,7 @@ 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; @@ -37,7 +38,6 @@ use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\ArrayList; use Utopia\Validator\Text; -use Throwable; class Create extends Action { diff --git a/src/Appwrite/Platform/Workers/Audits.php b/src/Appwrite/Platform/Workers/Audits.php index 9588c1a9d3..6bcc85bc36 100644 --- a/src/Appwrite/Platform/Workers/Audits.php +++ b/src/Appwrite/Platform/Workers/Audits.php @@ -6,7 +6,6 @@ use Exception; use Throwable; use Utopia\Console; use Utopia\Database\Document; -use Utopia\Database\Exception\Authorization; use Utopia\Database\Exception\Structure; use Utopia\Platform\Action; use Utopia\Queue\Message; diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index a4faa64462..c420444112 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -24,7 +24,6 @@ use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization; use Utopia\DSN\DSN; use Utopia\Logger\Log; use Utopia\Platform\Action; diff --git a/tests/unit/Event/EventTest.php b/tests/unit/Event/EventTest.php index fcaf1f734d..d050ce5f64 100644 --- a/tests/unit/Event/EventTest.php +++ b/tests/unit/Event/EventTest.php @@ -5,7 +5,6 @@ namespace Tests\Unit\Event; use Appwrite\Event\Event; use InvalidArgumentException; use PHPUnit\Framework\TestCase; -use Utopia\Queue\Publisher; require_once __DIR__ . '/../../../app/init.php'; From eb366cf94bcbcb6e1b684a04ba3cb2b8dbc8b294 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 08:25:59 +0530 Subject: [PATCH 094/131] fix: preserve cors max age type --- src/Appwrite/Network/Cors.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Network/Cors.php b/src/Appwrite/Network/Cors.php index e21c660a32..593c82ab24 100644 --- a/src/Appwrite/Network/Cors.php +++ b/src/Appwrite/Network/Cors.php @@ -48,7 +48,7 @@ final class Cors /** * Build CORS headers for a given request origin. * - * @return array + * @return array */ public function headers(string $origin): array { @@ -57,7 +57,7 @@ final class Cors self::HEADER_ALLOW_HEADERS => implode(', ', $this->allowedHeaders), self::HEADER_EXPOSE_HEADERS => implode(', ', $this->exposedHeaders), self::HEADER_ALLOW_CREDENTIALS => $this->allowCredentials ? 'true' : 'false', - self::HEADER_MAX_AGE => (string) $this->maxAge, + self::HEADER_MAX_AGE => $this->maxAge, ]; // Wildcard allow-all From 04943b6313fbc0cd0b551a4746c2a67380254b7e Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 09:29:57 +0530 Subject: [PATCH 095/131] Pass recipient display name in EmailMessage to field Use associative array format ['email' => ..., 'name' => ...] for the to field so the recipient display name appears in the To header. --- src/Appwrite/Platform/Workers/Mails.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php index 01fe4ced9e..32de1e50d6 100644 --- a/src/Appwrite/Platform/Workers/Mails.php +++ b/src/Appwrite/Platform/Workers/Mails.php @@ -190,7 +190,7 @@ class Mails extends Action } $emailMessage = new EmailMessage( - to: [$recipient], + to: [['email' => $recipient, 'name' => $name]], subject: $subject, content: $body, fromName: $fromName, From 2dc20ef0eb67c849db34ec4c4a447c69b25f8fe8 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 09:31:48 +0530 Subject: [PATCH 096/131] Update utopia-php/messaging lock to latest --- composer.json | 2 +- composer.lock | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 02b5730c91..33b9f79de6 100644 --- a/composer.json +++ b/composer.json @@ -72,7 +72,7 @@ "utopia-php/image": "0.8.*", "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", - "utopia-php/messaging": "0.21.*", + "utopia-php/messaging": "dev-feat-recipient-value-object", "utopia-php/migration": "1.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", diff --git a/composer.lock b/composer.lock index fcb69195c5..64a22f38a0 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": "012f943cdfac560cb95ce5703d7588e1", + "content-hash": "8d41008e18681373142296c17131c860", "packages": [ { "name": "adhocore/jwt", @@ -4467,16 +4467,16 @@ }, { "name": "utopia-php/messaging", - "version": "0.21.1", + "version": "dev-feat-recipient-value-object", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "de068654476a673921ea9df03692d2940b7941ee" + "reference": "92aaa2772cf4a67b64590a10634f24126cb7f608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/de068654476a673921ea9df03692d2940b7941ee", - "reference": "de068654476a673921ea9df03692d2940b7941ee", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/92aaa2772cf4a67b64590a10634f24126cb7f608", + "reference": "92aaa2772cf4a67b64590a10634f24126cb7f608", "shasum": "" }, "require": { @@ -4512,9 +4512,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.21.1" + "source": "https://github.com/utopia-php/messaging/tree/feat-recipient-value-object" }, - "time": "2026-04-01T11:34:20+00:00" + "time": "2026-04-02T03:35:05+00:00" }, { "name": "utopia-php/migration", @@ -8435,7 +8435,9 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": { + "utopia-php/messaging": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { From 24fcf4247c06c6ae2c576f26b3d8b353940c6638 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 09:41:20 +0530 Subject: [PATCH 097/131] Fix flaky vectordb migration test --- .../Modules/Databases/Http/VectorsDB/Collections/Create.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 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 b85a8b30b4..a7e2d68eac 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php @@ -130,9 +130,11 @@ class Create extends CollectionAction $indexes[] = new Document($index); } try { - // passing null in creates only creates the metadata collection if (!$dbForDatabases->exists(null, Database::METADATA)) { - $dbForDatabases->create(); + try { + $dbForDatabases->create(); + } catch (DuplicateException) { + } } $dbForDatabases->createCollection( id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), From c2dd8acedae746921721d122616f4d578edd55dd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 09:42:59 +0530 Subject: [PATCH 098/131] use stable --- composer.json | 2 +- composer.lock | 20 +++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index 33b9f79de6..0f4dcfb8db 100644 --- a/composer.json +++ b/composer.json @@ -72,7 +72,7 @@ "utopia-php/image": "0.8.*", "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", - "utopia-php/messaging": "dev-feat-recipient-value-object", + "utopia-php/messaging": "0.22.*", "utopia-php/migration": "1.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", diff --git a/composer.lock b/composer.lock index 64a22f38a0..420dddc9a5 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": "8d41008e18681373142296c17131c860", + "content-hash": "4fe91e67f343fbe6deac1fdc7eda949f", "packages": [ { "name": "adhocore/jwt", @@ -4467,23 +4467,23 @@ }, { "name": "utopia-php/messaging", - "version": "dev-feat-recipient-value-object", + "version": "0.22.0", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "92aaa2772cf4a67b64590a10634f24126cb7f608" + "reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/92aaa2772cf4a67b64590a10634f24126cb7f608", - "reference": "92aaa2772cf4a67b64590a10634f24126cb7f608", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030", + "reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030", "shasum": "" }, "require": { "ext-curl": "*", "ext-openssl": "*", "giggsey/libphonenumber-for-php-lite": "9.0.23", - "php": ">=8.0.0", + "php": ">=8.1.0", "phpmailer/phpmailer": "6.9.1" }, "require-dev": { @@ -4512,9 +4512,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/feat-recipient-value-object" + "source": "https://github.com/utopia-php/messaging/tree/0.22.0" }, - "time": "2026-04-02T03:35:05+00:00" + "time": "2026-04-02T04:09:19+00:00" }, { "name": "utopia-php/migration", @@ -8435,9 +8435,7 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "utopia-php/messaging": 20 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { From b6e95f4502d3cec63def0f90dc1fcf4d8a63673d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 11:05:03 +0530 Subject: [PATCH 099/131] test: remove redundant headers empty-key case --- tests/unit/Functions/Validator/HeadersTest.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/unit/Functions/Validator/HeadersTest.php b/tests/unit/Functions/Validator/HeadersTest.php index b9cc7c6ea4..563131d480 100644 --- a/tests/unit/Functions/Validator/HeadersTest.php +++ b/tests/unit/Functions/Validator/HeadersTest.php @@ -76,11 +76,6 @@ class HeadersTest extends TestCase ]; $this->assertFalse($this->object->isValid($headers)); - $headers = [ - (string) null => 'value', - ]; - $this->assertFalse($this->object->isValid($headers)); - $headers = [ 'X-Header' => null, ]; From d13644a47a1b4267074436b4bd545970031a64d1 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 11:38:19 +0530 Subject: [PATCH 100/131] 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 101/131] 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 102/131] 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 103/131] 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 104/131] 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 105/131] 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 106/131] 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 107/131] 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 108/131] 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 109/131] 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 110/131] 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 111/131] 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 112/131] 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 113/131] 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 114/131] 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 115/131] 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 116/131] 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 117/131] 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 118/131] 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 119/131] 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 120/131] 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 121/131] 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 122/131] 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 123/131] 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 124/131] 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 125/131] 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 126/131] 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 127/131] 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 128/131] 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 129/131] 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 130/131] 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 131/131] 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];