From aaa2a0525f26d539b7b8bb50573e6e03842ad412 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 16 Mar 2026 23:00:36 +0530 Subject: [PATCH 01/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] fix: use released cli container support --- app/cli.php | 58 ++++++++++--------- composer.lock | 12 ++-- .../Http/Databases/Transactions/Update.php | 58 +++++++++++-------- 3 files changed, 72 insertions(+), 56 deletions(-) diff --git a/app/cli.php b/app/cli.php index cf655143a4..06f20b9897 100644 --- a/app/cli.php +++ b/app/cli.php @@ -18,12 +18,15 @@ use Swoole\Timer; use Utopia\Cache\Adapter\Pool as CachePool; use Utopia\Cache\Adapter\Sharding; use Utopia\Cache\Cache; +use Utopia\CLI\Adapters\Generic; +use Utopia\CLI\CLI; use Utopia\Config\Config; use Utopia\Console; use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use Utopia\DI\Container; use Utopia\DSN\DSN; use Utopia\Logger\Log; use Utopia\Platform\Service; @@ -55,12 +58,15 @@ if (! isset($args[0])) { } $taskName = $args[0]; +$container = new Container(); +$cli = new CLI(new Generic(), $_SERVER['argv'] ?? [], $container); + +$platform->setCli($cli); $platform->init(Service::TYPE_TASK); -$cli = $platform->getCli(); -$cli->setResource('register', fn () => $register, []); +$container->set('register', fn () => $register, []); -$cli->setResource('cache', function ($pools) { +$container->set('cache', function ($pools) { $list = Config::getParam('pools-cache', []); $adapters = []; @@ -71,18 +77,18 @@ $cli->setResource('cache', function ($pools) { return new Cache(new Sharding($adapters)); }, ['pools']); -$cli->setResource('pools', function (Registry $register) { +$container->set('pools', function (Registry $register) { return $register->get('pools'); }, ['register']); -$cli->setResource('authorization', function () { +$container->set('authorization', function () { $authorization = new Authorization(); $authorization->disable(); return $authorization; }, []); -$cli->setResource('dbForPlatform', function ($pools, $cache, $authorization) { +$container->set('dbForPlatform', function ($pools, $cache, $authorization) { $sleep = 3; $maxAttempts = 5; $attempts = 0; @@ -125,17 +131,17 @@ $cli->setResource('dbForPlatform', function ($pools, $cache, $authorization) { return $dbForPlatform; }, ['pools', 'cache', 'authorization']); -$cli->setResource('console', function () { +$container->set('console', function () { return new Document(Config::getParam('console')); }, []); -$cli->setResource( +$container->set( 'isResourceBlocked', fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false, [] ); -$cli->setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) { +$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) { $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) { @@ -197,7 +203,7 @@ $cli->setResource('getProjectDB', function (Group $pools, Database $dbForPlatfor }; }, ['pools', 'dbForPlatform', 'cache', 'authorization']); -$cli->setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { +$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { $database = null; return function (?Document $project = null) use ($pools, $cache, &$database, $authorization) { @@ -225,41 +231,41 @@ $cli->setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizati return $database; }; }, ['pools', 'cache', 'authorization']); -$cli->setResource('publisher', function (Group $pools) { +$container->set('publisher', function (Group $pools) { return new BrokerPool(publisher: $pools->get('publisher')); }, ['pools']); -$cli->setResource('publisherDatabases', function (BrokerPool $publisher) { +$container->set('publisherDatabases', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$cli->setResource('publisherFunctions', function (BrokerPool $publisher) { +$container->set('publisherFunctions', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$cli->setResource('publisherMigrations', function (BrokerPool $publisher) { +$container->set('publisherMigrations', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$cli->setResource('publisherMessaging', function (BrokerPool $publisher) { +$container->set('publisherMessaging', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$cli->setResource('usage', function () { +$container->set('usage', function () { return new UsageContext(); }, []); -$cli->setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher( +$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher( $publisher, new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME)) ), ['publisher']); -$cli->setResource('queueForStatsResources', function (Publisher $publisher) { +$container->set('queueForStatsResources', function (Publisher $publisher) { return new StatsResources($publisher); }, ['publisher']); -$cli->setResource('queueForFunctions', function (Publisher $publisher) { +$container->set('queueForFunctions', function (Publisher $publisher) { return new Func($publisher); }, ['publisher']); -$cli->setResource('queueForDeletes', function (Publisher $publisher) { +$container->set('queueForDeletes', function (Publisher $publisher) { return new Delete($publisher); }, ['publisher']); -$cli->setResource('queueForCertificates', function (Publisher $publisher) { +$container->set('queueForCertificates', function (Publisher $publisher) { return new Certificate($publisher); }, ['publisher']); -$cli->setResource('logError', function (Registry $register) { +$container->set('logError', function (Registry $register) { return function (Throwable $error, string $namespace, string $action) use ($register) { Console::error('[Error] Timestamp: ' . date('c', time())); Console::error('[Error] Type: ' . get_class($error)); @@ -311,13 +317,13 @@ $cli->setResource('logError', function (Registry $register) { }; }, ['register']); -$cli->setResource('executor', fn () => new Executor(), []); +$container->set('executor', fn () => new Executor(), []); -$cli->setResource('bus', function (Registry $register) use ($cli) { - return $register->get('bus')->setResolver(fn (string $name) => $cli->getResource($name)); +$container->set('bus', function (Registry $register) use ($container) { + return $register->get('bus')->setResolver(fn (string $name) => $container->get($name)); }, ['register']); -$cli->setResource('telemetry', fn () => new NoTelemetry(), []); +$container->set('telemetry', fn () => new NoTelemetry(), []); $cli ->error() diff --git a/composer.lock b/composer.lock index fbf8937859..d113b99785 100644 --- a/composer.lock +++ b/composer.lock @@ -3658,16 +3658,16 @@ }, { "name": "utopia-php/cli", - "version": "0.23.0", + "version": "0.23.1", "source": { "type": "git", "url": "https://github.com/utopia-php/cli.git", - "reference": "4efef2662b27cdce0d6d09ea5c3a16a1cca2ba6c" + "reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cli/zipball/4efef2662b27cdce0d6d09ea5c3a16a1cca2ba6c", - "reference": "4efef2662b27cdce0d6d09ea5c3a16a1cca2ba6c", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/8d1955b8bc4dc631f45d7c7df689ed7b63f70621", + "reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621", "shasum": "" }, "require": { @@ -3703,9 +3703,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cli/issues", - "source": "https://github.com/utopia-php/cli/tree/0.23.0" + "source": "https://github.com/utopia-php/cli/tree/0.23.1" }, - "time": "2026-03-13T12:23:18+00:00" + "time": "2026-04-05T15:27:35+00:00" }, { "name": "utopia-php/compression", diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 0c8c6a8520..c4d51e6c64 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -182,19 +182,33 @@ class Update extends Action $dbForDatabases = $getDatabasesDB($databaseDoc); try { - $dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $authorization) { - $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'committing', - ]))); + $transaction = $authorization->skip(fn () => $dbForProject->updateDocument( + 'transactions', + $transactionId, + new Document(['status' => 'committing']) + )); - $operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [ - Query::equal('transactionInternalId', [$transaction->getSequence()]), - Query::orderAsc(), - Query::limit(PHP_INT_MAX), - ])); + $operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + Query::orderAsc(), + Query::limit(PHP_INT_MAX), + ])); + $collections = []; + foreach ($operations as $operation) { + $databaseInternalId = $operation['databaseInternalId']; + $collectionInternalId = $operation['collectionInternalId']; + $collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}"; + + if (!isset($collections[$collectionId])) { + $collections[$collectionId] = $authorization->skip( + fn () => $dbForProject->getCollection($collectionId) + ); + } + } + + $dbForDatabases->withTransaction(function () use ($dbForDatabases, $transactionState, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $collections) { $state = []; - $collections = []; foreach ($operations as $operation) { $databaseInternalId = $operation['databaseInternalId']; @@ -210,11 +224,6 @@ class Update extends Action $data = $data->getArrayCopy(); } - if (!isset($collections[$collectionId])) { - $collections[$collectionId] = $authorization->skip( - fn () => $dbForProject->getCollection($collectionId) - ); - } $collection = $collections[$collectionId]; if (\is_array($data) && !empty($data)) { @@ -276,16 +285,17 @@ class Update extends Action } } - $transaction = $authorization->skip(fn () => $dbForProject->updateDocument( - 'transactions', - $transactionId, - new Document(['status' => 'committed']) - )); - - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($transaction); }); + + $transaction = $authorization->skip(fn () => $dbForProject->updateDocument( + 'transactions', + $transactionId, + new Document(['status' => 'committed']) + )); + + $queueForDeletes + ->setType(DELETE_TYPE_DOCUMENT) + ->setDocument($transaction); } catch (NotFoundException $e) { $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', From 44f3bbae03115024ab3132d88c4ed86757aa64ac Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 6 Apr 2026 01:40:07 +0000 Subject: [PATCH 73/91] fix: add CORS headers to error responses The Http::error() handler was missing CORS headers, causing browsers to block error responses (e.g. 403 PROJECT_PAUSED) with a generic CORS error instead of showing the actual error message. This injects the cors resource into the error handler and adds CORS headers before sending the error response, matching the pattern already used in Http::init(). Co-Authored-By: Claude Opus 4.6 (1M context) --- app/controllers/general.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 3bf5f027f2..3f8adeb368 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1196,7 +1196,8 @@ Http::error() ->inject('bus') ->inject('devKey') ->inject('authorization') - ->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) { + ->inject('cors') + ->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization, Cors $cors) { $trace = $error->getTrace(); foreach (array_slice($trace, 0, 100) as $index => $traceEntry) { @@ -1493,6 +1494,10 @@ Http::error() 'type' => $type, ]; + foreach ($cors->headers($request->getOrigin()) as $name => $value) { + $response->addHeader($name, $value); + } + $response ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') ->addHeader('Expires', '0') From ba2584987136a5e4e430891e16563e313ff50d13 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 6 Apr 2026 02:59:08 +0000 Subject: [PATCH 74/91] fix: resolve cors safely in error handler to avoid cascading failures - Remove cors from inject chain; resolve via getResource() inside try-catch so DB failures don't cascade when resolving the cors resource dependency chain (cors -> allowedHostnames -> rule -> DB) - Use override:true on addHeader to prevent duplicate CORS headers when init() already set them before the exception was thrown - Degrades gracefully: if cors resolution fails, error response is sent without CORS headers (same behavior as before this PR) Co-Authored-By: Claude Opus 4.6 (1M context) --- app/controllers/general.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 3f8adeb368..3eeeef3fae 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1196,8 +1196,7 @@ Http::error() ->inject('bus') ->inject('devKey') ->inject('authorization') - ->inject('cors') - ->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization, Cors $cors) { + ->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) { $trace = $error->getTrace(); foreach (array_slice($trace, 0, 100) as $index => $traceEntry) { @@ -1494,8 +1493,17 @@ Http::error() 'type' => $type, ]; - foreach ($cors->headers($request->getOrigin()) as $name => $value) { - $response->addHeader($name, $value); + // Add CORS headers to error responses so browsers can read the error. + // Wrapped in try-catch: if the error itself is a DB failure, resolving + // the cors resource (which depends on rule -> DB) would cascade. + // Uses override:true to avoid duplicate headers if init() already set them. + try { + $cors = $utopia->getResource('cors'); + foreach ($cors->headers($request->getOrigin()) as $name => $value) { + $response->addHeader($name, $value, override: true); + } + } catch (Throwable) { + // Degrade gracefully - error response without CORS is no worse than before. } $response From cb74a5756a81d0b961756583ab348fbd2eccc894 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 10:20:18 +0530 Subject: [PATCH 75/91] Remove request and response static state --- app/controllers/general.php | 4 ++-- src/Appwrite/Utopia/Request.php | 18 ++++++++--------- src/Appwrite/Utopia/Response.php | 31 +++++++++++++++++++++++++++--- tests/unit/Utopia/RequestTest.php | 15 +++++++++++++++ tests/unit/Utopia/ResponseTest.php | 23 ++++++++++++++++++++++ 5 files changed, 77 insertions(+), 14 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 3bf5f027f2..dcc5764bdd 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -868,7 +868,7 @@ Http::init() * Request format */ $route = $utopia->getRoute(); - Request::setRoute($route); + $request->setRoute($route); if ($route === null) { $response->setStatusCode(404); @@ -1019,7 +1019,7 @@ Http::init() return; } $route = $request->getRoute(); - if ($route->getLabel('origin', false) === '*') { + if ($route?->getLabel('origin', false) === '*') { return; } if (!$originValidator->isValid($origin)) { diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index 9428ff9d88..ed602ecdd5 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -17,7 +17,7 @@ class Request extends UtopiaRequest * @var array */ private array $filters = []; - private static ?Route $route = null; + private ?Route $route = null; public function __construct(SwooleRequest $request) { @@ -34,11 +34,11 @@ class Request extends UtopiaRequest { $parameters = parent::getParams(); - if (!$this->hasFilters() || !self::hasRoute()) { + if (!$this->hasFilters() || !$this->hasRoute()) { return $parameters; } - $methods = self::getRoute()->getLabel('sdk', null); + $methods = $this->getRoute()?->getLabel('sdk', null); if (empty($methods)) { return $parameters; @@ -131,9 +131,9 @@ class Request extends UtopiaRequest * * @return void */ - public static function setRoute(?Route $route): void + public function setRoute(?Route $route): void { - self::$route = $route; + $this->route = $route; } /** @@ -141,9 +141,9 @@ class Request extends UtopiaRequest * * @return Route|null */ - public static function getRoute(): ?Route + public function getRoute(): ?Route { - return self::$route; + return $this->route; } /** @@ -151,9 +151,9 @@ class Request extends UtopiaRequest * * @return bool */ - public static function hasRoute(): bool + public function hasRoute(): bool { - return self::$route !== null; + return $this->route !== null; } /** diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index e01dc58bf6..649b0562a5 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -8,6 +8,7 @@ use Appwrite\Utopia\Response\Filter; use Appwrite\Utopia\Response\Model; use Exception; use JsonException; +use Swoole\Coroutine; use Swoole\Http\Response as SwooleHTTPResponse; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; @@ -19,6 +20,8 @@ use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; */ class Response extends SwooleResponse { + private const SHOW_SENSITIVE_CONTEXT_KEY = '__appwrite_response_show_sensitive'; + // General public const MODEL_NONE = 'none'; public const MODEL_ANY = 'any'; @@ -509,7 +512,7 @@ class Response extends SwooleResponse $isPrivilegedUser = $user->isPrivileged($roles); $isAppUser = $user->isApp($roles); - if ((!$isPrivilegedUser && !$isAppUser) && !self::$showSensitive) { + if ((!$isPrivilegedUser && !$isAppUser) && !self::isShowingSensitive()) { $data->setAttribute($key, ''); } } @@ -666,14 +669,36 @@ class Response extends SwooleResponse */ public static function showSensitive(callable $callback): array { + $previous = self::isShowingSensitive(); + try { - self::$showSensitive = true; + self::setShowSensitive(true); return $callback(); } finally { - self::$showSensitive = false; + self::setShowSensitive($previous); } } + private static function isShowingSensitive(): bool + { + if (Coroutine::getCid() !== -1) { + return (bool) (Coroutine::getContext()[self::SHOW_SENSITIVE_CONTEXT_KEY] ?? false); + } + + return self::$showSensitive; + } + + private static function setShowSensitive(bool $value): void + { + if (Coroutine::getCid() !== -1) { + Coroutine::getContext()[self::SHOW_SENSITIVE_CONTEXT_KEY] = $value; + + return; + } + + self::$showSensitive = $value; + } + private ?Authorization $authorization = null; private ?DBUser $user = null; diff --git a/tests/unit/Utopia/RequestTest.php b/tests/unit/Utopia/RequestTest.php index 78a3717c38..d5cd5d800a 100644 --- a/tests/unit/Utopia/RequestTest.php +++ b/tests/unit/Utopia/RequestTest.php @@ -147,6 +147,21 @@ class RequestTest extends TestCase $this->assertSame('unexpected', $params['extra']); } + public function testRouteIsScopedToRequestInstance(): void + { + $firstRequest = new Request(new SwooleRequest()); + $secondRequest = new Request(new SwooleRequest()); + + $firstRoute = new Route(Request::METHOD_GET, '/first'); + $secondRoute = new Route(Request::METHOD_GET, '/second'); + + $firstRequest->setRoute($firstRoute); + $secondRequest->setRoute($secondRoute); + + $this->assertSame($firstRoute, $firstRequest->getRoute()); + $this->assertSame($secondRoute, $secondRequest->getRoute()); + } + /** * Helper to attach a route with multiple SDK methods to the request. */ diff --git a/tests/unit/Utopia/ResponseTest.php b/tests/unit/Utopia/ResponseTest.php index 452119fafb..d5c3a079cd 100644 --- a/tests/unit/Utopia/ResponseTest.php +++ b/tests/unit/Utopia/ResponseTest.php @@ -5,6 +5,7 @@ namespace Tests\Unit\Utopia; use Appwrite\Utopia\Response; use Exception; use PHPUnit\Framework\TestCase; +use ReflectionMethod; use Swoole\Http\Response as SwooleResponse; use Tests\Unit\Utopia\Response\Filters\First; use Tests\Unit\Utopia\Response\Filters\Second; @@ -176,4 +177,26 @@ class ResponseTest extends TestCase $this->assertArrayHasKey('required', $single); $this->assertArrayNotHasKey('hidden', $singleFromArray); } + + public function testShowSensitiveRestoresPreviousState(): void + { + $isShowingSensitive = new ReflectionMethod(Response::class, 'isShowingSensitive'); + + $this->assertFalse($isShowingSensitive->invoke(null)); + + $payload = Response::showSensitive(function () use ($isShowingSensitive) { + return [ + 'outer' => $isShowingSensitive->invoke(null), + 'inner' => Response::showSensitive(fn () => [ + 'state' => $isShowingSensitive->invoke(null), + ]), + 'afterInner' => $isShowingSensitive->invoke(null), + ]; + }); + + $this->assertTrue($payload['outer']); + $this->assertTrue($payload['inner']['state']); + $this->assertTrue($payload['afterInner']); + $this->assertFalse($isShowingSensitive->invoke(null)); + } } From b8eb0810c2d94ac4061311fbbefead5f8729f66e Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 10:24:32 +0530 Subject: [PATCH 76/91] Make response sensitive mode instance-scoped --- app/controllers/api/account.php | 8 +++---- src/Appwrite/Utopia/Response.php | 37 ++++++------------------------ tests/unit/Utopia/ResponseTest.php | 18 +++++++-------- 3 files changed, 20 insertions(+), 43 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index d576bbce44..fb968d3972 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -3937,7 +3937,7 @@ Http::post('/v1/account/recovery') ->setParam('userId', $profile->getId()) ->setParam('tokenId', $recovery->getId()) ->setUser($profile) - ->setPayload(Response::showSensitive(fn () => $response->output($recovery, Response::MODEL_TOKEN)), sensitive: ['secret']); + ->setPayload($response->showSensitive(fn () => $response->output($recovery, Response::MODEL_TOKEN)), sensitive: ['secret']); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -4038,7 +4038,7 @@ Http::put('/v1/account/recovery') $queueForEvents ->setParam('userId', $profile->getId()) ->setParam('tokenId', $recoveryDocument->getId()) - ->setPayload(Response::showSensitive(fn () => $response->output($recoveryDocument, Response::MODEL_TOKEN)), sensitive: ['secret']); + ->setPayload($response->showSensitive(fn () => $response->output($recoveryDocument, Response::MODEL_TOKEN)), sensitive: ['secret']); $response->dynamic($recoveryDocument, Response::MODEL_TOKEN); }); @@ -4268,7 +4268,7 @@ Http::post('/v1/account/verifications/email') $queueForEvents ->setParam('userId', $user->getId()) ->setParam('tokenId', $verification->getId()) - ->setPayload(Response::showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']); + ->setPayload($response->showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -4360,7 +4360,7 @@ Http::put('/v1/account/verifications/email') $queueForEvents ->setParam('userId', $userId) ->setParam('tokenId', $verification->getId()) - ->setPayload(Response::showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']); + ->setPayload($response->showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']); $response->dynamic($verification, Response::MODEL_TOKEN); }); diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 649b0562a5..9d0e8abefa 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -8,7 +8,6 @@ use Appwrite\Utopia\Response\Filter; use Appwrite\Utopia\Response\Model; use Exception; use JsonException; -use Swoole\Coroutine; use Swoole\Http\Response as SwooleHTTPResponse; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; @@ -20,8 +19,6 @@ use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; */ class Response extends SwooleResponse { - private const SHOW_SENSITIVE_CONTEXT_KEY = '__appwrite_response_show_sensitive'; - // General public const MODEL_NONE = 'none'; public const MODEL_ANY = 'any'; @@ -302,7 +299,7 @@ class Response extends SwooleResponse /** * @var bool */ - protected static bool $showSensitive = false; + protected bool $showSensitive = false; /** * @var array @@ -512,7 +509,7 @@ class Response extends SwooleResponse $isPrivilegedUser = $user->isPrivileged($roles); $isAppUser = $user->isApp($roles); - if ((!$isPrivilegedUser && !$isAppUser) && !self::isShowingSensitive()) { + if ((!$isPrivilegedUser && !$isAppUser) && !$this->showSensitive) { $data->setAttribute($key, ''); } } @@ -662,43 +659,23 @@ class Response extends SwooleResponse } /** - * Static wrapper to show sensitive data in response + * Wrapper to show sensitive data in response * * @param callable(): array $callback The callback to show sensitive information for * @return array */ - public static function showSensitive(callable $callback): array + public function showSensitive(callable $callback): array { - $previous = self::isShowingSensitive(); + $previous = $this->showSensitive; try { - self::setShowSensitive(true); + $this->showSensitive = true; return $callback(); } finally { - self::setShowSensitive($previous); + $this->showSensitive = $previous; } } - private static function isShowingSensitive(): bool - { - if (Coroutine::getCid() !== -1) { - return (bool) (Coroutine::getContext()[self::SHOW_SENSITIVE_CONTEXT_KEY] ?? false); - } - - return self::$showSensitive; - } - - private static function setShowSensitive(bool $value): void - { - if (Coroutine::getCid() !== -1) { - Coroutine::getContext()[self::SHOW_SENSITIVE_CONTEXT_KEY] = $value; - - return; - } - - self::$showSensitive = $value; - } - private ?Authorization $authorization = null; private ?DBUser $user = null; diff --git a/tests/unit/Utopia/ResponseTest.php b/tests/unit/Utopia/ResponseTest.php index d5c3a079cd..be8cfdc216 100644 --- a/tests/unit/Utopia/ResponseTest.php +++ b/tests/unit/Utopia/ResponseTest.php @@ -5,7 +5,7 @@ namespace Tests\Unit\Utopia; use Appwrite\Utopia\Response; use Exception; use PHPUnit\Framework\TestCase; -use ReflectionMethod; +use ReflectionProperty; use Swoole\Http\Response as SwooleResponse; use Tests\Unit\Utopia\Response\Filters\First; use Tests\Unit\Utopia\Response\Filters\Second; @@ -180,23 +180,23 @@ class ResponseTest extends TestCase public function testShowSensitiveRestoresPreviousState(): void { - $isShowingSensitive = new ReflectionMethod(Response::class, 'isShowingSensitive'); + $isShowingSensitive = new ReflectionProperty(Response::class, 'showSensitive'); - $this->assertFalse($isShowingSensitive->invoke(null)); + $this->assertFalse($isShowingSensitive->getValue($this->response)); - $payload = Response::showSensitive(function () use ($isShowingSensitive) { + $payload = $this->response->showSensitive(function () use ($isShowingSensitive) { return [ - 'outer' => $isShowingSensitive->invoke(null), - 'inner' => Response::showSensitive(fn () => [ - 'state' => $isShowingSensitive->invoke(null), + 'outer' => $isShowingSensitive->getValue($this->response), + 'inner' => $this->response->showSensitive(fn () => [ + 'state' => $isShowingSensitive->getValue($this->response), ]), - 'afterInner' => $isShowingSensitive->invoke(null), + 'afterInner' => $isShowingSensitive->getValue($this->response), ]; }); $this->assertTrue($payload['outer']); $this->assertTrue($payload['inner']['state']); $this->assertTrue($payload['afterInner']); - $this->assertFalse($isShowingSensitive->invoke(null)); + $this->assertFalse($isShowingSensitive->getValue($this->response)); } } From b8ed30db55ad7e8203ad8de12d1039d0bb513429 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 12:23:50 +0530 Subject: [PATCH 77/91] Fix CORS header override for analyze --- app/controllers/general.php | 4 +++- composer.lock | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 23d0331fad..ac6b60ee17 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1497,7 +1497,9 @@ Http::error() try { $cors = $utopia->getResource('cors'); foreach ($cors->headers($request->getOrigin()) as $name => $value) { - $response->addHeader($name, $value, override: true); + $response + ->removeHeader($name) + ->addHeader($name, $value); } } catch (Throwable) { // Degrade gracefully - error response without CORS is no worse than before. diff --git a/composer.lock b/composer.lock index d113b99785..d71f78b35d 100644 --- a/composer.lock +++ b/composer.lock @@ -4271,16 +4271,16 @@ }, { "name": "utopia-php/framework", - "version": "0.34.16", + "version": "0.34.17", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "2b4021ba3f9d476264ce9fd6703d6c79de9add7f" + "reference": "d3e4143b8b06d9823d0c29a06dacefa5a1b93677" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/2b4021ba3f9d476264ce9fd6703d6c79de9add7f", - "reference": "2b4021ba3f9d476264ce9fd6703d6c79de9add7f", + "url": "https://api.github.com/repos/utopia-php/http/zipball/d3e4143b8b06d9823d0c29a06dacefa5a1b93677", + "reference": "d3e4143b8b06d9823d0c29a06dacefa5a1b93677", "shasum": "" }, "require": { @@ -4319,9 +4319,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.34.16" + "source": "https://github.com/utopia-php/http/tree/0.34.17" }, - "time": "2026-03-20T10:39:07+00:00" + "time": "2026-04-06T04:40:23+00:00" }, { "name": "utopia-php/http", From 221b52bac0a2dfbd571d01e97a6213875c5a3f17 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 12:30:25 +0530 Subject: [PATCH 78/91] Add request-scoped cookie domain resource --- app/controllers/api/account.php | 59 +++++++++++-------- app/controllers/general.php | 14 ----- app/init/resources.php | 33 +++++++++++ .../Teams/Http/Memberships/Status/Update.php | 7 ++- 4 files changed, 71 insertions(+), 42 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index fb968d3972..fa0e30ee53 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -207,7 +207,7 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr } -$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, Authorization $authorization) { +$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, ?string $cookieDomain, Authorization $authorization) { // Attempt to decode secret as a JWT (used by OAuth2 token flow to carry provider info) $oauthProvider = null; @@ -353,8 +353,8 @@ $createSession = function (string $userId, string $secret, Request $request, Res $protocol = $request->getProtocol(); $response - ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null) + ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED); $countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')); @@ -719,7 +719,8 @@ Http::delete('/v1/account/sessions') ->inject('queueForDeletes') ->inject('store') ->inject('proofForToken') - ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) { + ->inject('cookieDomain') + ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, ?string $cookieDomain) { $protocol = $request->getProtocol(); $sessions = $user->getAttribute('sessions', []); @@ -741,8 +742,8 @@ Http::delete('/v1/account/sessions') // If current session delete the cookies too $response - ->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null) + ->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite')); // Use current session for events. $currentSession = $session; @@ -849,7 +850,8 @@ Http::delete('/v1/account/sessions/:sessionId') ->inject('queueForDeletes') ->inject('store') ->inject('proofForToken') - ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) { + ->inject('cookieDomain') + ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, ?string $cookieDomain) { $protocol = $request->getProtocol(); $sessionId = ($sessionId === 'current') @@ -880,8 +882,8 @@ Http::delete('/v1/account/sessions/:sessionId') } $response - ->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null) + ->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite')); } $dbForProject->purgeCachedDocument('users', $user->getId()); @@ -1035,8 +1037,9 @@ Http::post('/v1/account/sessions/email') ->inject('store') ->inject('proofForPassword') ->inject('proofForToken') + ->inject('cookieDomain') ->inject('authorization') - ->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) { + ->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, ?string $cookieDomain, Authorization $authorization) { $email = \strtolower($email); $protocol = $request->getProtocol(); @@ -1117,8 +1120,8 @@ Http::post('/v1/account/sessions/email') $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $response - ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null) + ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED) ; @@ -1184,8 +1187,9 @@ Http::post('/v1/account/sessions/anonymous') ->inject('store') ->inject('proofForPassword') ->inject('proofForToken') + ->inject('cookieDomain') ->inject('authorization') - ->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) { + ->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, ?string $cookieDomain, Authorization $authorization) { $protocol = $request->getProtocol(); if ('console' === $project->getId()) { @@ -1283,8 +1287,8 @@ Http::post('/v1/account/sessions/anonymous') $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $response - ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null) + ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED) ; @@ -1339,7 +1343,8 @@ Http::post('/v1/account/sessions/token') ->inject('store') ->inject('proofForToken') ->inject('proofForCode') -->inject('authorization') + ->inject('cookieDomain') + ->inject('authorization') ->action($createSession); Http::get('/v1/account/sessions/oauth2/:provider') @@ -1538,8 +1543,9 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->inject('proofForPassword') ->inject('proofForToken') ->inject('plan') + ->inject('cookieDomain') ->inject('authorization') - ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, Authorization $authorization) use ($oauthDefaultSuccess) { + ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, ?string $cookieDomain, Authorization $authorization) use ($oauthDefaultSuccess) { $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; $port = $request->getPort(); $callbackBase = $protocol . '://' . $request->getHostname(); @@ -2068,14 +2074,14 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') // TODO: Remove this deprecated workaround - support only token if ($state['success']['path'] == $oauthDefaultSuccess) { $query['project'] = $project->getId(); - $query['domain'] = Config::getParam('cookieDomain'); + $query['domain'] = $cookieDomain; $query['key'] = $store->getKey(); $query['secret'] = $encoded; } $response - ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null) + ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite')); } if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) { @@ -2886,11 +2892,12 @@ Http::put('/v1/account/sessions/magic-url') ->inject('queueForMails') ->inject('store') ->inject('proofForCode') + ->inject('cookieDomain') ->inject('authorization') - ->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $authorization) use ($createSession) { + ->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $cookieDomain, $authorization) use ($createSession) { $proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL); $proofForToken->setHash(new Sha()); - $createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $authorization); + $createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $cookieDomain, $authorization); }); Http::put('/v1/account/sessions/phone') @@ -2936,6 +2943,7 @@ Http::put('/v1/account/sessions/phone') ->inject('store') ->inject('proofForToken') ->inject('proofForCode') + ->inject('cookieDomain') ->inject('authorization') ->action($createSession); @@ -3727,7 +3735,8 @@ Http::patch('/v1/account/status') ->inject('dbForProject') ->inject('queueForEvents') ->inject('store') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store) { + ->inject('cookieDomain') + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store, ?string $cookieDomain) { $user->setAttribute('status', false); @@ -3743,8 +3752,8 @@ Http::patch('/v1/account/status') $protocol = $request->getProtocol(); $response - ->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null) + ->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite')) ; $response->dynamic($user, Response::MODEL_ACCOUNT); diff --git a/app/controllers/general.php b/app/controllers/general.php index d10ad9a060..af85c4e459 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -924,20 +924,6 @@ Http::init() $isLocalHost = in_array($request->getHostname(), $localHosts); $isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false; - $isConsoleProject = $project->getAttribute('$id', '') === 'console'; - $isConsoleRootSession = System::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled'; - - Config::setParam( - 'cookieDomain', - $isLocalHost || $isIpAddress - ? null - : ( - $isConsoleProject && $isConsoleRootSession - ? '.' . $selfDomain->getRegisterable() - : '.' . $request->getHostname() - ) - ); - $warnings = []; /* diff --git a/app/init/resources.php b/app/init/resources.php index 8acecb8e3e..472c52fa4e 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -53,6 +53,7 @@ use Utopia\Database\DateTime as DatabaseDateTime; use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Domains\Domain; use Utopia\DSN\DSN; use Utopia\Http\Http; use Utopia\Locale\Locale; @@ -249,6 +250,38 @@ Http::setResource('allowedSchemes', function (array $platform, Document $project return array_unique($allowed); }, ['platform', 'project']); +/** + * Cookie domain for the current request. + */ +Http::setResource('cookieDomain', function (Request $request, Document $project) { + $localHosts = ['localhost', 'localhost:' . $request->getPort()]; + + $migrationHost = System::getEnv('_APP_MIGRATION_HOST'); + if (!empty($migrationHost)) { + $localHosts[] = $migrationHost; + $localHosts[] = $migrationHost . ':' . $request->getPort(); + } + + $hostname = $request->getHostname(); + $isLocalHost = \in_array($hostname, $localHosts, true); + $isIpAddress = \filter_var($hostname, FILTER_VALIDATE_IP) !== false; + + if ($isLocalHost || $isIpAddress) { + return; + } + + $isConsoleProject = $project->getAttribute('$id', '') === 'console'; + $isConsoleRootSession = System::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled'; + + if ($isConsoleProject && $isConsoleRootSession) { + $domain = new Domain($hostname); + + return '.' . $domain->getRegisterable(); + } + + return '.' . $hostname; +}, ['request', 'project']); + /** * Rule associated with a request origin. */ diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php index 46b6c3cacf..f1c0a5cad5 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php @@ -74,10 +74,11 @@ class Update extends Action ->inject('queueForEvents') ->inject('store') ->inject('proofForToken') + ->inject('cookieDomain') ->callback($this->action(...)); } - public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) + public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken, ?string $cookieDomain) { $protocol = $request->getProtocol(); @@ -172,7 +173,7 @@ class Update extends Action value: $encoded, expire: (new \DateTime($expire))->getTimestamp(), path: '/', - domain: Config::getParam('cookieDomain'), + domain: $cookieDomain, secure: ('https' === $protocol), httponly: true ) @@ -181,7 +182,7 @@ class Update extends Action value: $encoded, expire: (new \DateTime($expire))->getTimestamp(), path: '/', - domain: Config::getParam('cookieDomain'), + domain: $cookieDomain, secure: ('https' === $protocol), httponly: true, sameSite: Config::getParam('cookieSamesite') From d1b59ff3f3b9d5d0f60f41d3ef5eb84b45bda802 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 12:30:48 +0530 Subject: [PATCH 79/91] Remove unused cookie domain locals --- app/controllers/general.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index af85c4e459..917588bee3 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -921,9 +921,6 @@ Http::init() $localHosts[] = $migrationHost.':'.$request->getPort(); } - $isLocalHost = in_array($request->getHostname(), $localHosts); - $isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false; - $warnings = []; /* From 1f7fc4bd40a69e703f563d47b3884f0b7bd3a0ff Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 12:43:05 +0530 Subject: [PATCH 80/91] Use request-scoped domain verification --- app/controllers/api/account.php | 41 +++++++++++-------- app/controllers/general.php | 9 ---- app/init/resources.php | 12 ++++++ .../Teams/Http/Memberships/Status/Update.php | 5 ++- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index fa0e30ee53..cbdf11225a 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -207,7 +207,7 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr } -$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, ?string $cookieDomain, Authorization $authorization) { +$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) { // Attempt to decode secret as a JWT (used by OAuth2 token flow to carry provider info) $oauthProvider = null; @@ -345,7 +345,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res ->setProperty('secret', $sessionSecret) ->encode(); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } @@ -719,8 +719,9 @@ Http::delete('/v1/account/sessions') ->inject('queueForDeletes') ->inject('store') ->inject('proofForToken') + ->inject('domainVerification') ->inject('cookieDomain') - ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, ?string $cookieDomain) { + ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) { $protocol = $request->getProtocol(); $sessions = $user->getAttribute('sessions', []); @@ -729,7 +730,7 @@ Http::delete('/v1/account/sessions') foreach ($sessions as $session) {/** @var Document $session */ $dbForProject->deleteDocument('sessions', $session->getId()); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([])); } @@ -850,8 +851,9 @@ Http::delete('/v1/account/sessions/:sessionId') ->inject('queueForDeletes') ->inject('store') ->inject('proofForToken') + ->inject('domainVerification') ->inject('cookieDomain') - ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, ?string $cookieDomain) { + ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) { $protocol = $request->getProtocol(); $sessionId = ($sessionId === 'current') @@ -877,7 +879,7 @@ Http::delete('/v1/account/sessions/:sessionId') ->setAttribute('current', true) ->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'))); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([])); } @@ -1037,9 +1039,10 @@ Http::post('/v1/account/sessions/email') ->inject('store') ->inject('proofForPassword') ->inject('proofForToken') + ->inject('domainVerification') ->inject('cookieDomain') ->inject('authorization') - ->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, ?string $cookieDomain, Authorization $authorization) { + ->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) { $email = \strtolower($email); $protocol = $request->getProtocol(); @@ -1113,7 +1116,7 @@ Http::post('/v1/account/sessions/email') ->setProperty('secret', $secret) ->encode(); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } @@ -1187,9 +1190,10 @@ Http::post('/v1/account/sessions/anonymous') ->inject('store') ->inject('proofForPassword') ->inject('proofForToken') + ->inject('domainVerification') ->inject('cookieDomain') ->inject('authorization') - ->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, ?string $cookieDomain, Authorization $authorization) { + ->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) { $protocol = $request->getProtocol(); if ('console' === $project->getId()) { @@ -1280,7 +1284,7 @@ Http::post('/v1/account/sessions/anonymous') ->setProperty('secret', $secret) ->encode(); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } @@ -1343,6 +1347,7 @@ Http::post('/v1/account/sessions/token') ->inject('store') ->inject('proofForToken') ->inject('proofForCode') + ->inject('domainVerification') ->inject('cookieDomain') ->inject('authorization') ->action($createSession); @@ -1543,9 +1548,10 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->inject('proofForPassword') ->inject('proofForToken') ->inject('plan') + ->inject('domainVerification') ->inject('cookieDomain') ->inject('authorization') - ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, ?string $cookieDomain, Authorization $authorization) use ($oauthDefaultSuccess) { + ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) use ($oauthDefaultSuccess) { $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; $port = $request->getPort(); $callbackBase = $protocol . '://' . $request->getHostname(); @@ -2061,7 +2067,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->setProperty('secret', $secret) ->encode(); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } @@ -2892,12 +2898,13 @@ Http::put('/v1/account/sessions/magic-url') ->inject('queueForMails') ->inject('store') ->inject('proofForCode') + ->inject('domainVerification') ->inject('cookieDomain') ->inject('authorization') - ->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $cookieDomain, $authorization) use ($createSession) { + ->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $domainVerification, $cookieDomain, $authorization) use ($createSession) { $proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL); $proofForToken->setHash(new Sha()); - $createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $cookieDomain, $authorization); + $createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $domainVerification, $cookieDomain, $authorization); }); Http::put('/v1/account/sessions/phone') @@ -2943,6 +2950,7 @@ Http::put('/v1/account/sessions/phone') ->inject('store') ->inject('proofForToken') ->inject('proofForCode') + ->inject('domainVerification') ->inject('cookieDomain') ->inject('authorization') ->action($createSession); @@ -3735,8 +3743,9 @@ Http::patch('/v1/account/status') ->inject('dbForProject') ->inject('queueForEvents') ->inject('store') + ->inject('domainVerification') ->inject('cookieDomain') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store, ?string $cookieDomain) { + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store, bool $domainVerification, ?string $cookieDomain) { $user->setAttribute('status', false); @@ -3746,7 +3755,7 @@ Http::patch('/v1/account/status') ->setParam('userId', $user->getId()) ->setPayload($response->output($user, Response::MODEL_ACCOUNT)); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([])); } diff --git a/app/controllers/general.php b/app/controllers/general.php index 917588bee3..120f8a17d6 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -904,15 +904,6 @@ Http::init() $locale->setDefault($localeParam); } - $origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST); - $selfDomain = new Domain($request->getHostname()); - $endDomain = new Domain((string)$origin); - Config::setParam( - 'domainVerification', - ($selfDomain->getRegisterable() === $endDomain->getRegisterable()) && - $endDomain->getRegisterable() !== '' - ); - $localHosts = ['localhost','localhost:'.$request->getPort()]; $migrationHost = System::getEnv('_APP_MIGRATION_HOST'); diff --git a/app/init/resources.php b/app/init/resources.php index 472c52fa4e..67ac115b61 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -250,6 +250,18 @@ Http::setResource('allowedSchemes', function (array $platform, Document $project return array_unique($allowed); }, ['platform', 'project']); +/** + * Whether the request origin is verified against the request hostname. + */ +Http::setResource('domainVerification', function (Request $request) { + $origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST); + $selfDomain = new Domain($request->getHostname()); + $endDomain = new Domain((string) $origin); + + return ($selfDomain->getRegisterable() === $endDomain->getRegisterable()) + && $endDomain->getRegisterable() !== ''; +}, ['request']); + /** * Cookie domain for the current request. */ diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php index f1c0a5cad5..28bfa769ee 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php @@ -74,11 +74,12 @@ class Update extends Action ->inject('queueForEvents') ->inject('store') ->inject('proofForToken') + ->inject('domainVerification') ->inject('cookieDomain') ->callback($this->action(...)); } - public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken, ?string $cookieDomain) + public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken, bool $domainVerification, ?string $cookieDomain) { $protocol = $request->getProtocol(); @@ -163,7 +164,7 @@ class Update extends Action ->setProperty('secret', $secret) ->encode(); - if (!Config::getParam('domainVerification')) { + if (!$domainVerification) { $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } From e3053bb83d6bf4895944106475b72d1ace0f76e5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 12:44:48 +0530 Subject: [PATCH 81/91] Remove dead cookie config defaults --- app/controllers/general.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 120f8a17d6..00bfc6bd67 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -61,8 +61,6 @@ use Utopia\System\System; use Utopia\Validator; use Utopia\Validator\Text; -Config::setParam('domainVerification', false); -Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount) From 59a773e9a01c5b7c50deb735550e7f97a7b7dcaa Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 6 Apr 2026 12:47:06 +0530 Subject: [PATCH 82/91] Document migration host local-domain handling --- app/controllers/general.php | 2 ++ app/init/resources.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/controllers/general.php b/app/controllers/general.php index 00bfc6bd67..5a5c2dd507 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -906,6 +906,8 @@ Http::init() $migrationHost = System::getEnv('_APP_MIGRATION_HOST'); if (!empty($migrationHost)) { + // Treat the migration host like localhost because internal migration and + // CI traffic may use it before a public domain is configured. $localHosts[] = $migrationHost; $localHosts[] = $migrationHost.':'.$request->getPort(); } diff --git a/app/init/resources.php b/app/init/resources.php index 67ac115b61..92164c3c95 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -270,6 +270,8 @@ Http::setResource('cookieDomain', function (Request $request, Document $project) $migrationHost = System::getEnv('_APP_MIGRATION_HOST'); if (!empty($migrationHost)) { + // Treat the migration host like localhost because internal migration and CI + // traffic may use it before a public domain is configured. $localHosts[] = $migrationHost; $localHosts[] = $migrationHost . ':' . $request->getPort(); } From 04173600ae318d2082c87c5d6c68b70135dae38a Mon Sep 17 00:00:00 2001 From: shimon Date: Mon, 6 Apr 2026 22:33:18 +0300 Subject: [PATCH 83/91] revert ScheduleFunctions.php updates --- src/Appwrite/Platform/Tasks/ScheduleFunctions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php index 69f105652c..88725a190a 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php @@ -61,7 +61,7 @@ class ScheduleFunctions extends ScheduleBase $nextDate = $cron->getNextRunDate(); $next = DateTime::format($nextDate); - $currentTick = $next <= $timeFrame; + $currentTick = $next < $timeFrame; if (!$currentTick) { continue; @@ -88,7 +88,7 @@ class ScheduleFunctions extends ScheduleBase $scheduleKey = $delayConfig['key']; // Ensure schedule was not deleted if (!\array_key_exists($scheduleKey, $this->schedules)) { - continue; + return; } $schedule = $this->schedules[$scheduleKey]; From 399c37d943a90d45847fe9d70cd76c8c6a118173 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 7 Apr 2026 14:33:43 +0530 Subject: [PATCH 84/91] 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 85/91] 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 86/91] 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 87/91] 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 88/91] 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 89/91] 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 90/91] 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 91/91] 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",