From aaa2a0525f26d539b7b8bb50573e6e03842ad412 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 16 Mar 2026 23:00:36 +0530 Subject: [PATCH 01/89] 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 02/89] 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 03/89] 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 04/89] 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 05/89] 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 06/89] 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 07/89] 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 08/89] 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 09/89] 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 10/89] 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 11/89] 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 12/89] 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 13/89] 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 14/89] 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 15/89] 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 16/89] 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 17/89] 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 18/89] 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 19/89] 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 20/89] 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 21/89] 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 22/89] 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 23/89] 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 24/89] 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 25/89] 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 26/89] 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 27/89] 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 28/89] 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 29/89] 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 30/89] 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 31/89] 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 32/89] 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 33/89] 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 34/89] 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 35/89] 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 36/89] 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 37/89] 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 38/89] 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 39/89] 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 40/89] 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 41/89] 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 42/89] 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 43/89] 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 44/89] 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 45/89] 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 46/89] 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 47/89] 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 48/89] 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 49/89] 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 50/89] 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 51/89] 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 52/89] 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 53/89] 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 54/89] 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 55/89] 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 56/89] 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 57/89] 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 58/89] 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 59/89] 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 fb26da5df15ef9799fe1babe6ecdff7b319ef457 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 15:15:48 +0530 Subject: [PATCH 60/89] 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 61/89] 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 62/89] 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 63/89] fix: preserve multi-value headers like Set-Cookie instead of comma-joining addHeader() already accumulates multiple values for the same key into an array internally, so calling it once per value is the correct approach. Comma-joining violates RFC 6265 for Set-Cookie headers. --- app/controllers/general.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 5fd6eb2a57..6fdeabb3a8 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -748,7 +748,9 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S } if (\is_array($values)) { - $response->addHeader($name, \implode(', ', $values)); + foreach ($values as $value) { + $response->addHeader($name, $value); + } } else { $response->addHeader($name, $values); } From 15b2ab321e03a1f9ff79c691cd8a23354f2565da Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 16:34:58 +0530 Subject: [PATCH 64/89] 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 d13644a47a1b4267074436b4bd545970031a64d1 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 11:38:19 +0530 Subject: [PATCH 65/89] 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 66/89] 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 67/89] 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 68/89] 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 3018b478ba281f998cac8bf3f701de1cdc5723da Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 17:25:13 +0530 Subject: [PATCH 69/89] 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 412d09b801eba69eff038631daa0141cacd54bd7 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 5 Apr 2026 20:06:13 +0530 Subject: [PATCH 70/89] 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 71/89] 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 452440f3c0e87618bc92656fedfbf44b5d31da37 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 5 Apr 2026 21:03:17 +0530 Subject: [PATCH 72/89] 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 a06ba57384ce58a9f4812ad982b4899abc7387f0 Mon Sep 17 00:00:00 2001 From: Aditya Date: Sun, 5 Apr 2026 23:39:13 +0530 Subject: [PATCH 73/89] fix appwrite auth broken link in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed83252e2f..88d527f060 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Table of Contents: ## 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 Auth](https://appwrite.io/docs/products/auth)** - 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. From b8ed30db55ad7e8203ad8de12d1039d0bb513429 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 12:23:50 +0530 Subject: [PATCH 74/89] 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 f11bd7ce0e6b0ef0045ff5342e338a8eabdea7fa Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 7 Apr 2026 12:45:10 +0530 Subject: [PATCH 75/89] fix: reset SDK dev branch to base branch before pushing The dev branch was being reset to origin/dev, which retains stale commits after squash merges. This caused recurring merge conflicts and inflated PR diffs. Using `checkout -B dev ` ensures dev always starts fresh from the default branch. --- src/Appwrite/Platform/Tasks/SDKs.php | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index e8a69afddb..02ee97fc54 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -639,29 +639,15 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } catch (\Throwable) { } - // Checkout dev branch (or create if it doesn't exist) + // Create or checkout dev branch from the base branch + // This ensures dev always starts from the latest base branch, + // avoiding history divergence caused by squash merges. try { - $repo->execute('checkout', '-f', $gitBranch); + $repo->execute('checkout', '-B', $gitBranch, $repoBranch); } catch (\Throwable) { $repo->execute('checkout', '-b', $gitBranch); } - // Fetch dev branch, or push to create it on remote - try { - $repo->execute('fetch', 'origin', $gitBranch, '--quiet', '--no-tags', '--depth', '1'); - } catch (\Throwable) { - try { - $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); - } catch (\Throwable) { - } - } - - // Sync with remote dev branch - try { - $repo->execute('reset', '--hard', "origin/{$gitBranch}"); - } catch (\Throwable) { - } - // Backup .github before cleaning working tree $githubDir = $target . '/.github'; $githubBackup = \sys_get_temp_dir() . '/.github-backup-' . \getmypid(); From 7864a5b9d10c85720be68a86c3618552ba149430 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 7 Apr 2026 12:52:09 +0530 Subject: [PATCH 76/89] fix: use --force-with-lease on SDK dev branch push After resetting dev to the base branch, the remote dev may have diverged history from squash merges. A plain push would be rejected as non-fast-forward. Using --force-with-lease safely overwrites the remote since we just fetched. --- src/Appwrite/Platform/Tasks/SDKs.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 02ee97fc54..4725f4095f 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -685,7 +685,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND return true; } - $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); + $repo->execute('push', '--force-with-lease', '-u', 'origin', $gitBranch, '--quiet'); } catch (\Throwable $e) { Console::warning(" Git push failed: " . $e->getMessage()); return false; From 399c37d943a90d45847fe9d70cd76c8c6a118173 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 7 Apr 2026 14:33:43 +0530 Subject: [PATCH 77/89] fix console null route handling --- app/controllers/shared/api.php | 8 ++++++++ tests/e2e/General/HTTPTest.php | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 5166429e32..bdcbe70b83 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -401,6 +401,10 @@ Http::init() } } + if ($route === null) { + throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); + } + // Steps 7-9: Access Control - Method, Namespace and Scope Validation /** * @var ?Method $method @@ -489,6 +493,10 @@ Http::init() $request->setUser($user); $route = $utopia->getRoute(); + if ($route === null) { + throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); + } + $path = $route->getMatchedPath(); $databaseType = match (true) { str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB, diff --git a/tests/e2e/General/HTTPTest.php b/tests/e2e/General/HTTPTest.php index 450e4f2378..ab389850ce 100644 --- a/tests/e2e/General/HTTPTest.php +++ b/tests/e2e/General/HTTPTest.php @@ -122,6 +122,13 @@ class HTTPTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); } + public function testConsoleRootWithoutRouteDoesNotFatal() + { + $response = $this->client->call(Client::METHOD_GET, '/console/', $this->getHeaders()); + + $this->assertEquals(404, $response['headers']['status-code']); + } + public function testCors() { From 6c56eee0f4c41ae7d3f6b41161b7326e6e549713 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 7 Apr 2026 14:39:48 +0530 Subject: [PATCH 78/89] test console route not found error type --- tests/e2e/General/HTTPTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/General/HTTPTest.php b/tests/e2e/General/HTTPTest.php index ab389850ce..38137b1320 100644 --- a/tests/e2e/General/HTTPTest.php +++ b/tests/e2e/General/HTTPTest.php @@ -127,6 +127,7 @@ class HTTPTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/console/', $this->getHeaders()); $this->assertEquals(404, $response['headers']['status-code']); + $this->assertEquals('general_route_not_found', $response['body']['type']); } public function testCors() From 92abfb31aa1a8c092696502046370d7f9963d022 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 7 Apr 2026 14:40:18 +0530 Subject: [PATCH 79/89] fix null route guard placement --- app/controllers/shared/api.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index bdcbe70b83..8254a22ac0 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -98,6 +98,9 @@ Http::init() ->inject('authorization') ->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) { $route = $utopia->getRoute(); + if ($route === null) { + throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); + } /** * Handle user authentication and session validation. @@ -401,10 +404,6 @@ Http::init() } } - if ($route === null) { - throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); - } - // Steps 7-9: Access Control - Method, Namespace and Scope Validation /** * @var ?Method $method From e8ef4e40d78145f268a032c97c5e81b5968b2936 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 7 Apr 2026 15:05:07 +0530 Subject: [PATCH 80/89] fix post-merge e2e test regressions --- tests/e2e/General/HTTPTest.php | 8 -------- tests/e2e/General/UsageTest.php | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/e2e/General/HTTPTest.php b/tests/e2e/General/HTTPTest.php index 38137b1320..450e4f2378 100644 --- a/tests/e2e/General/HTTPTest.php +++ b/tests/e2e/General/HTTPTest.php @@ -122,14 +122,6 @@ class HTTPTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); } - public function testConsoleRootWithoutRouteDoesNotFatal() - { - $response = $this->client->call(Client::METHOD_GET, '/console/', $this->getHeaders()); - - $this->assertEquals(404, $response['headers']['status-code']); - $this->assertEquals('general_route_not_found', $response['body']['type']); - } - public function testCors() { diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index eea53d9ea8..f6eb963967 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -1324,8 +1324,8 @@ class UsageTest extends Scope $this->assertEquals($requestsTotal, $response['body']['requests'][array_key_last($response['body']['requests'])]['value']); $this->validateDates($response['body']['requests']); // vectordbTotal should reflect only VectorsDB instances, not relational databases. - $this->assertEquals($vectordbTotal, $response['body']['vectordbDatabasesTotal']); - $this->assertEquals($documentsTotal, $response['body']['vectordbDocumentsTotal']); + $this->assertEquals($vectordbTotal, $response['body']['vectorsdbDatabasesTotal']); + $this->assertEquals($documentsTotal, $response['body']['vectorsdbDocumentsTotal']); }); $response = $this->client->call( From 8f6c8f9d8d028950d19cbbe0cc86449043ec5291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 7 Apr 2026 13:21:36 +0200 Subject: [PATCH 81/89] Migrate to new endpoint --- .../Http/Project}/Labels/Update.php | 45 +++++++------------ .../Modules/Project/Services/Http.php | 3 ++ .../Modules/Projects/Services/Http.php | 2 - 3 files changed, 19 insertions(+), 31 deletions(-) rename src/Appwrite/Platform/Modules/{Projects/Http/Projects => Project/Http/Project}/Labels/Update.php (60%) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Labels/Update.php similarity index 60% rename from src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php rename to src/Appwrite/Platform/Modules/Project/Http/Project/Labels/Update.php index de11bb0091..24d1c48cf1 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Labels/Update.php @@ -1,20 +1,15 @@ setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT) - ->setHttpPath('/v1/projects/:projectId/labels') + ->setHttpPath('/v1/project/labels') + ->httpAlias('/v1/projects/:projectId/labels') ->desc('Update project labels') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') + ->groups(['api', 'project']) + ->label('scope', 'project.write') + ->label('event', 'labels.*.update') + ->label('audits.event', 'project.labels.update') + ->label('audits.resource', 'project.labels/{response.$id}') ->label('sdk', new Method( - namespace: 'projects', - group: 'projects', + namespace: 'project', + group: null, name: 'updateLabels', description: <<param('projectId', '', new UID(), 'Project unique ID.') ->param('labels', [], new ArrayList(new Text(36, allowList: [...Text::NUMBERS, ...Text::ALPHABET_UPPER, ...Text::ALPHABET_LOWER]), APP_LIMIT_ARRAY_LABELS_SIZE), 'Array of project labels. Replaces the previous labels. Maximum of ' . APP_LIMIT_ARRAY_LABELS_SIZE . ' labels are allowed, each up to 36 alphanumeric characters long.') ->inject('response') ->inject('dbForPlatform') + ->inject('project') ->callback($this->action(...)); } @@ -67,17 +60,11 @@ class Update extends Action * @param array $labels */ public function action( - string $projectId, array $labels, Response $response, - Database $dbForPlatform + Database $dbForPlatform, + Document $project ): void { - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - $labels = (array) \array_values(\array_unique($labels)); $project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document(['labels' => $labels])); diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index 949fb2bcd9..4970403032 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Project\Services; use Appwrite\Platform\Modules\Project\Http\Init; +use Appwrite\Platform\Modules\Project\Http\Project\Labels\Update as UpdateProjectLabels; use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable; use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable; use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable; @@ -25,5 +26,7 @@ class Http extends Service $this->addAction(GetVariable::getName(), new GetVariable()); $this->addAction(DeleteVariable::getName(), new DeleteVariable()); $this->addAction(UpdateVariable::getName(), new UpdateVariable()); + + $this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels()); } } diff --git a/src/Appwrite/Platform/Modules/Projects/Services/Http.php b/src/Appwrite/Platform/Modules/Projects/Services/Http.php index 8b0d6f87c8..8275e664d5 100644 --- a/src/Appwrite/Platform/Modules/Projects/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Projects/Services/Http.php @@ -8,7 +8,6 @@ use Appwrite\Platform\Modules\Projects\Http\DevKeys\Get as GetDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\Update as UpdateDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\XList as ListDevKeys; use Appwrite\Platform\Modules\Projects\Http\Projects\Create as CreateProject; -use Appwrite\Platform\Modules\Projects\Http\Projects\Labels\Update as UpdateProjectLabels; use Appwrite\Platform\Modules\Projects\Http\Projects\Team\Update as UpdateProjectTeam; use Appwrite\Platform\Modules\Projects\Http\Projects\Update as UpdateProject; use Appwrite\Platform\Modules\Projects\Http\Projects\XList as ListProjects; @@ -31,7 +30,6 @@ class Http extends Service $this->addAction(CreateProject::getName(), new CreateProject()); $this->addAction(UpdateProject::getName(), new UpdateProject()); $this->addAction(ListProjects::getName(), new ListProjects()); - $this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels()); $this->addAction(UpdateProjectTeam::getName(), new UpdateProjectTeam()); $this->addAction(CreateSchedule::getName(), new CreateSchedule()); From 9b00ce4f1d7e195f092bd33558e8965fe8de5a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 7 Apr 2026 13:28:35 +0200 Subject: [PATCH 82/89] Add new tests --- tests/e2e/Services/Project/LabelsBase.php | 152 ++++++++++++++++++ .../Project/LabelsConsoleClientTest.php | 14 ++ .../Project/LabelsCustomServerTest.php | 14 ++ 3 files changed, 180 insertions(+) create mode 100644 tests/e2e/Services/Project/LabelsBase.php create mode 100644 tests/e2e/Services/Project/LabelsConsoleClientTest.php create mode 100644 tests/e2e/Services/Project/LabelsCustomServerTest.php diff --git a/tests/e2e/Services/Project/LabelsBase.php b/tests/e2e/Services/Project/LabelsBase.php new file mode 100644 index 0000000000..2c8f4963f3 --- /dev/null +++ b/tests/e2e/Services/Project/LabelsBase.php @@ -0,0 +1,152 @@ +updateLabels(['frontend', 'backend']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertIsArray($response['body']['labels']); + $this->assertCount(2, $response['body']['labels']); + $this->assertContains('frontend', $response['body']['labels']); + $this->assertContains('backend', $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsReplace(): void + { + $response = $this->updateLabels(['alpha', 'beta']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['labels']); + $this->assertContains('alpha', $response['body']['labels']); + $this->assertContains('beta', $response['body']['labels']); + + // Replace with new labels + $response = $this->updateLabels(['gamma', 'delta', 'epsilon']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(3, $response['body']['labels']); + $this->assertContains('gamma', $response['body']['labels']); + $this->assertContains('delta', $response['body']['labels']); + $this->assertContains('epsilon', $response['body']['labels']); + $this->assertNotContains('alpha', $response['body']['labels']); + $this->assertNotContains('beta', $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsEmpty(): void + { + // Set some labels first + $response = $this->updateLabels(['toRemove']); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['labels']); + + // Clear all labels + $response = $this->updateLabels([]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertIsArray($response['body']['labels']); + $this->assertCount(0, $response['body']['labels']); + } + + public function testUpdateLabelsDeduplicated(): void + { + $response = $this->updateLabels(['duplicate', 'duplicate', 'unique']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['labels']); + $this->assertContains('duplicate', $response['body']['labels']); + $this->assertContains('unique', $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsSingleLabel(): void + { + $response = $this->updateLabels(['solo']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['labels']); + $this->assertContains('solo', $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsWithoutAuthentication(): void + { + $response = $this->updateLabels(['unauthorized'], false); + + $this->assertSame(401, $response['headers']['status-code']); + } + + public function testUpdateLabelsInvalidLabelTooLong(): void + { + $response = $this->updateLabels([str_repeat('a', 37)]); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsInvalidLabelCharacters(): void + { + $response = $this->updateLabels(['invalid-label!']); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsAlphanumericOnly(): void + { + $response = $this->updateLabels(['ABC123', 'lowercase', 'UPPERCASE', '0123456789']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(4, $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsMaxLength(): void + { + $label = str_repeat('a', 36); + $response = $this->updateLabels([$label]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['labels']); + $this->assertContains($label, $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + // Helpers + + /** + * @param array $labels + */ + protected function updateLabels(array $labels, bool $authenticated = true): mixed + { + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]; + + if ($authenticated) { + $headers = array_merge($headers, $this->getHeaders()); + } + + return $this->client->call(\Tests\E2E\Client::METHOD_PUT, '/project/labels', $headers, [ + 'labels' => $labels, + ]); + } +} diff --git a/tests/e2e/Services/Project/LabelsConsoleClientTest.php b/tests/e2e/Services/Project/LabelsConsoleClientTest.php new file mode 100644 index 0000000000..dd724338d6 --- /dev/null +++ b/tests/e2e/Services/Project/LabelsConsoleClientTest.php @@ -0,0 +1,14 @@ + Date: Tue, 7 Apr 2026 13:30:35 +0200 Subject: [PATCH 83/89] Improve tests --- tests/e2e/Services/Project/LabelsBase.php | 72 +++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/e2e/Services/Project/LabelsBase.php b/tests/e2e/Services/Project/LabelsBase.php index 2c8f4963f3..2b7074ef46 100644 --- a/tests/e2e/Services/Project/LabelsBase.php +++ b/tests/e2e/Services/Project/LabelsBase.php @@ -129,6 +129,78 @@ trait LabelsBase $this->updateLabels([]); } + public function testUpdateLabelsIdempotent(): void + { + $labels = ['stable', 'production']; + + $first = $this->updateLabels($labels); + $this->assertSame(200, $first['headers']['status-code']); + + $second = $this->updateLabels($labels); + $this->assertSame(200, $second['headers']['status-code']); + + $this->assertSame($first['body']['labels'], $second['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsDeduplicatedOrder(): void + { + $response = $this->updateLabels(['b', 'a', 'b']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['labels']); + $this->assertSame('b', $response['body']['labels'][0]); + $this->assertSame('a', $response['body']['labels'][1]); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsInvalidHyphen(): void + { + $response = $this->updateLabels(['my-label']); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsInvalidUnderscore(): void + { + $response = $this->updateLabels(['my_label']); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsInvalidSpace(): void + { + $response = $this->updateLabels(['my label']); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsInvalidEmptyString(): void + { + $response = $this->updateLabels(['']); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsResponseModel(): void + { + $response = $this->updateLabels(['test']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertArrayHasKey('$id', $response['body']); + $this->assertArrayHasKey('name', $response['body']); + $this->assertArrayHasKey('labels', $response['body']); + $this->assertIsArray($response['body']['labels']); + $this->assertContains('test', $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + // Helpers /** From 9403e4d65de2c950922a8cef74722e58b70c297d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 7 Apr 2026 17:35:32 +0530 Subject: [PATCH 84/89] Bump utopia-php/framework to 0.34.18 --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index d71f78b35d..de0628e76c 100644 --- a/composer.lock +++ b/composer.lock @@ -4271,16 +4271,16 @@ }, { "name": "utopia-php/framework", - "version": "0.34.17", + "version": "0.34.18", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "d3e4143b8b06d9823d0c29a06dacefa5a1b93677" + "reference": "c8e7e8fc9b9b68aa874e365c83010fefe8ae8ccc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/d3e4143b8b06d9823d0c29a06dacefa5a1b93677", - "reference": "d3e4143b8b06d9823d0c29a06dacefa5a1b93677", + "url": "https://api.github.com/repos/utopia-php/http/zipball/c8e7e8fc9b9b68aa874e365c83010fefe8ae8ccc", + "reference": "c8e7e8fc9b9b68aa874e365c83010fefe8ae8ccc", "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.17" + "source": "https://github.com/utopia-php/http/tree/0.34.18" }, - "time": "2026-04-06T04:40:23+00:00" + "time": "2026-04-07T08:06:39+00:00" }, { "name": "utopia-php/http", From 7781d377ae4a2238ee82236f859eedfaa67fc619 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:44:24 +0100 Subject: [PATCH 85/89] fix: persist session before purging user cache in email/password login Swap the order of createDocument('sessions') and purgeCachedDocument('users') in the email/password session creation flow. Previously, the cache was purged before the session was written, opening a race window in Swoole's async environment where a concurrent account.get() could re-cache the user with no sessions, causing sessionVerify to fail with a 401. This matches the correct ordering already used by the token-based flows (magic URL, OTP, phone). Co-Authored-By: Claude Sonnet 4.6 --- app/controllers/api/account.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index cbdf11225a..8eb49ea27b 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1103,14 +1103,14 @@ Http::post('/v1/account/sessions/email') ])); } - $dbForProject->purgeCachedDocument('users', $user->getId()); - $session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [ Permission::read(Role::user($user->getId())), Permission::update(Role::user($user->getId())), Permission::delete(Role::user($user->getId())), ])); + $dbForProject->purgeCachedDocument('users', $user->getId()); + $encoded = $store ->setProperty('id', $user->getId()) ->setProperty('secret', $secret) From 6dba407aedf15db79861cb2038adbf918433e21c Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 8 Apr 2026 10:10:16 +0530 Subject: [PATCH 86/89] test: add E2E test for email/password session cache race condition Adds testEmailPasswordSessionNotCorruptedByConcurrentRequests which reproduces the cross-worker Redis cache race that caused 401s after login. The test fires a login request, waits for it to reach the cache purge point, then injects concurrent GET /v1/account requests that re-cache a stale user document. Verifies the new session is immediately usable. Fails against the old ordering (purge before create), passes with the fix (create before purge). --- .../Account/AccountCustomClientTest.php | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 107dceaa5e..ee1bb31ede 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -4150,4 +4150,178 @@ class AccountCustomClientTest extends Scope $this->assertEquals(401, $verification3['headers']['status-code']); } + + /** + * Test that a new email/password session is immediately usable even when + * a concurrent request re-populates the user cache between the cache purge + * and session creation. + * + * Regression test for: purging the user cache BEFORE persisting the session + * allows a concurrent request (from a different Swoole worker) to re-cache + * a stale user document that lacks the new session, causing sessionVerify + * to fail with 401 on subsequent requests using the new session. + */ + public function testEmailPasswordSessionNotCorruptedByConcurrentRequests(): void + { + $projectId = $this->getProject()['$id']; + $endpoint = $this->client->getEndpoint(); + + $email = uniqid('race_', true) . getmypid() . '@localhost.test'; + $password = 'password123!'; + + // Create user + $response = $this->client->call(Client::METHOD_POST, '/account', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => $password, + 'name' => 'Race Test User', + ]); + $this->assertEquals(201, $response['headers']['status-code']); + + // Login to get session A + $responseA = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], [ + 'email' => $email, + 'password' => $password, + ]); + $this->assertEquals(201, $responseA['headers']['status-code']); + $sessionA = $responseA['cookies']['a_session_' . $projectId]; + + // Verify session A works + $verifyA = $this->client->call(Client::METHOD_GET, '/account', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'cookie' => 'a_session_' . $projectId . '=' . $sessionA, + ]); + $this->assertEquals(200, $verifyA['headers']['status-code']); + + /** + * Race condition scenario: + * 1. Start login B via curl_multi (non-blocking) + * 2. Drive the transfer for ~150ms so login B reaches purgeCachedDocument + * (findOne ~15ms + Argon2 hash verify ~60ms + middleware overhead) + * 3. THEN add GET requests to curl_multi - these hit different workers and + * re-cache a stale user document (without session B) during the window + * between purgeCachedDocument and createDocument + * 4. After all complete, verify session B is usable + */ + for ($attempt = 0; $attempt < 5; $attempt++) { + $loginCookies = []; + + $multi = curl_multi_init(); + + // Start login B first (alone) + $loginHandle = curl_init("{$endpoint}/account/sessions/email"); + curl_setopt_array($loginHandle, [ + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + 'origin: http://localhost', + 'content-type: application/json', + "x-appwrite-project: {$projectId}", + ], + CURLOPT_POSTFIELDS => \json_encode([ + 'email' => $email, + 'password' => $password, + ]), + CURLOPT_HEADERFUNCTION => function ($curl, $header) use (&$loginCookies) { + if (\stripos($header, 'set-cookie:') === 0) { + $cookiePart = \trim(\substr($header, 11)); + $eqPos = \strpos($cookiePart, '='); + if ($eqPos !== false) { + $name = \substr($cookiePart, 0, $eqPos); + $rest = \substr($cookiePart, $eqPos + 1); + $semiPos = \strpos($rest, ';'); + $loginCookies[$name] = $semiPos !== false + ? \substr($rest, 0, $semiPos) + : $rest; + } + } + return \strlen($header); + }, + ]); + curl_multi_add_handle($multi, $loginHandle); + + // Drive the login transfer forward and wait for the server to start + // processing the login (past hash verification + cache purge). + $deadline = \microtime(true) + 0.15; // 150ms + do { + curl_multi_exec($multi, $active); + curl_multi_select($multi, 0.005); + } while (\microtime(true) < $deadline && $active); + + // NOW add GET requests - they arrive after the cache purge + // but before session creation (which is delayed by the usleep or I/O). + $getHandles = []; + for ($i = 0; $i < 10; $i++) { + $gh = curl_init("{$endpoint}/account"); + curl_setopt_array($gh, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + 'origin: http://localhost', + 'content-type: application/json', + "x-appwrite-project: {$projectId}", + "cookie: a_session_{$projectId}={$sessionA}", + ], + ]); + curl_multi_add_handle($multi, $gh); + $getHandles[] = $gh; + } + + // Drive all to completion + do { + $status = curl_multi_exec($multi, $active); + if ($active) { + curl_multi_select($multi, 0.05); + } + } while ($active && $status === CURLM_OK); + + $loginStatus = curl_getinfo($loginHandle, CURLINFO_HTTP_CODE); + + curl_multi_remove_handle($multi, $loginHandle); + curl_close($loginHandle); + foreach ($getHandles as $gh) { + curl_multi_remove_handle($multi, $gh); + curl_close($gh); + } + curl_multi_close($multi); + + $this->assertEquals(201, $loginStatus, 'Login for session B should succeed'); + + $sessionBCookie = $loginCookies["a_session_{$projectId}"] ?? null; + $this->assertNotNull($sessionBCookie, 'Session B cookie should be set'); + + // THE CRITICAL CHECK: verify session B is usable immediately + $verifyB = $this->client->call(Client::METHOD_GET, '/account', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'cookie' => "a_session_{$projectId}={$sessionBCookie}", + ]); + + $this->assertEquals( + 200, + $verifyB['headers']['status-code'], + 'Session B must be immediately usable after login. ' + . 'A 401 here means a stale user cache (without the new session) was served. ' + . 'The fix is to create the session document BEFORE purging the user cache.' + ); + + // Clean up session B for next iteration + $this->client->call(Client::METHOD_DELETE, '/account/sessions/current', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'cookie' => "a_session_{$projectId}={$sessionBCookie}", + ]); + } + } } From dd4a43b78c083137ddc7c2488efeb9e39253bd62 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 8 Apr 2026 10:41:43 +0530 Subject: [PATCH 87/89] fix: throw RuntimeException for unresolved response models in spec generation Spec generation silently produced a fatal error when a response model string could not be resolved to a registered model object. Now throws a clear RuntimeException in both Swagger2 and OpenAPI3 formats, for both single and array model responses. Also adds a CI job to run spec generation on every PR so unresolved models are caught before merge. --- .github/workflows/ci.yml | 20 +++++++++++++++++++ src/Appwrite/Platform/Tasks/Specs.php | 7 ++++++- .../SDK/Specification/Format/OpenAPI3.php | 12 +++++++++++ .../SDK/Specification/Format/Swagger2.php | 12 +++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f44d5eedf3..19a36f0380 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,6 +161,26 @@ jobs: - name: Run PHPStan run: composer analyze -- --no-progress + specs: + name: Checks / Specs + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: composer:v2 + coverage: none + + - name: Install dependencies + run: composer install --prefer-dist --no-progress --ignore-platform-reqs + + - name: Generate specs + run: _APP_STORAGE_LIMIT=5368709120 php app/cli.php specs --version=latest --git=no + locale: name: Checks / Locale runs-on: ubuntu-latest diff --git a/src/Appwrite/Platform/Tasks/Specs.php b/src/Appwrite/Platform/Tasks/Specs.php index ebc4f6731a..0953610a69 100644 --- a/src/Appwrite/Platform/Tasks/Specs.php +++ b/src/Appwrite/Platform/Tasks/Specs.php @@ -482,7 +482,12 @@ class Specs extends Action ? $specsDir . '/' . $format . '-mocks-' . $platform . '.json' : $specsDir . '/' . $format . '-' . $version . '-' . $platform . '.json'; - $parsedSpecs = $specs->parse(); + try { + $parsedSpecs = $specs->parse(); + } catch (\RuntimeException $e) { + throw new \RuntimeException("Spec generation failed for {$platform} ({$format}): " . $e->getMessage(), 0, $e); + } + $encodedSpecs = \json_encode($parsedSpecs, JSON_PRETTY_PRINT); unset($parsedSpecs); diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 88f577eac6..4284cdd18d 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -278,6 +278,18 @@ class OpenAPI3 extends Format } } + if (\is_string($model)) { + throw new \RuntimeException("Unresolved response model '{$model}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered."); + } + + if (\is_array($model)) { + foreach ($model as $m) { + if (\is_string($m)) { + throw new \RuntimeException("Unresolved response model '{$m}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered."); + } + } + } + if (!(\is_array($model)) && $model->isNone()) { $temp['responses'][(string)$response->getCode() ?? '500'] = [ 'description' => in_array($produces, [ diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index f9c79431f0..792cafb159 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -285,6 +285,18 @@ class Swagger2 extends Format } } + if (\is_string($model)) { + throw new \RuntimeException("Unresolved response model '{$model}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered."); + } + + if (\is_array($model)) { + foreach ($model as $m) { + if (\is_string($m)) { + throw new \RuntimeException("Unresolved response model '{$m}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered."); + } + } + } + if (!(\is_array($model)) && $model->isNone()) { $temp['responses'][(string)$response->getCode() ?? '500'] = [ 'description' => in_array($produces, [ From f5ab593261cf43664b50f28fa836649440e28c21 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 8 Apr 2026 10:47:37 +0530 Subject: [PATCH 88/89] fix: make Project model public for server SDK spec generation The project.updateLabels route uses AuthType::KEY which makes it available on the server platform, but the Project model had public=false causing it to be filtered out during spec generation. --- src/Appwrite/Utopia/Response/Model/Project.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index 5902902e9e..9378ebad86 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -12,7 +12,7 @@ class Project extends Model /** * @var bool */ - protected bool $public = false; + protected bool $public = true; public function __construct() { From 62b6ef06e6d8ba7596c32ade25b33139f7578669 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 8 Apr 2026 10:49:50 +0530 Subject: [PATCH 89/89] fix: add swoole extension to specs CI job --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19a36f0380..6cc5ecdcf1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,6 +172,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: '8.3' + extensions: swoole tools: composer:v2 coverage: none