diff --git a/app/cli.php b/app/cli.php index b636707f1c..458df2d642 100644 --- a/app/cli.php +++ b/app/cli.php @@ -18,13 +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\Dependency; +use Utopia\DI\Container; use Utopia\DSN\DSN; use Utopia\Logger\Log; use Utopia\Platform\Service; @@ -47,7 +49,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])) { @@ -56,21 +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(); -$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); -}; +$container->set('register', fn () => $register, []); -$setResource('register', fn () => $register, []); - -$setResource('cache', function ($pools) { +$container->set('cache', function ($pools) { $list = Config::getParam('pools-cache', []); $adapters = []; @@ -81,18 +77,18 @@ $setResource('cache', function ($pools) { return new Cache(new Sharding($adapters)); }, ['pools']); -$setResource('pools', function (Registry $register) { +$container->set('pools', function (Registry $register) { return $register->get('pools'); }, ['register']); -$setResource('authorization', function () { +$container->set('authorization', function () { $authorization = new Authorization(); $authorization->disable(); return $authorization; }, []); -$setResource('dbForPlatform', function ($pools, $cache, $authorization) { +$container->set('dbForPlatform', function ($pools, $cache, $authorization) { $sleep = 3; $maxAttempts = 5; $attempts = 0; @@ -135,17 +131,17 @@ $setResource('dbForPlatform', function ($pools, $cache, $authorization) { return $dbForPlatform; }, ['pools', 'cache', 'authorization']); -$setResource('console', function () { +$container->set('console', function () { return new Document(Config::getParam('console')); }, []); -$setResource( +$container->set( 'isResourceBlocked', fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false, [] ); -$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) { @@ -207,10 +203,10 @@ $setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $c }; }, ['pools', 'dbForPlatform', 'cache', 'authorization']); -$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) { + 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; @@ -235,41 +231,41 @@ $setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $a return $database; }; }, ['pools', 'cache', 'authorization']); -$setResource('publisher', function (Group $pools) { +$container->set('publisher', function (Group $pools) { return new BrokerPool(publisher: $pools->get('publisher')); }, ['pools']); -$setResource('publisherDatabases', function (BrokerPool $publisher) { +$container->set('publisherDatabases', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$setResource('publisherFunctions', function (BrokerPool $publisher) { +$container->set('publisherFunctions', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$setResource('publisherMigrations', function (BrokerPool $publisher) { +$container->set('publisherMigrations', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$setResource('publisherMessaging', function (BrokerPool $publisher) { +$container->set('publisherMessaging', function (BrokerPool $publisher) { return $publisher; }, ['publisher']); -$setResource('usage', function () { +$container->set('usage', function () { return new UsageContext(); }, []); -$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']); -$setResource('queueForStatsResources', function (Publisher $publisher) { +$container->set('queueForStatsResources', function (Publisher $publisher) { return new StatsResources($publisher); }, ['publisher']); -$setResource('queueForFunctions', function (Publisher $publisher) { +$container->set('queueForFunctions', function (Publisher $publisher) { return new Func($publisher); }, ['publisher']); -$setResource('queueForDeletes', function (Publisher $publisher) { +$container->set('queueForDeletes', function (Publisher $publisher) { return new Delete($publisher); }, ['publisher']); -$setResource('queueForCertificates', function (Publisher $publisher) { +$container->set('queueForCertificates', function (Publisher $publisher) { return new Certificate($publisher); }, ['publisher']); -$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)); @@ -321,13 +317,13 @@ $setResource('logError', function (Registry $register) { }; }, ['register']); -$setResource('executor', fn () => new Executor(), []); +$container->set('executor', fn () => new Executor(), []); -$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']); -$setResource('telemetry', fn () => new NoTelemetry(), []); +$container->set('telemetry', fn () => new NoTelemetry(), []); $exitCode = 0; diff --git a/app/controllers/general.php b/app/controllers/general.php index 5a5c2dd507..c6e2eacb33 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -752,11 +752,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); @@ -1474,7 +1471,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/app/http.php b/app/http.php index 4532839727..67da67376d 100644 --- a/app/http.php +++ b/app/http.php @@ -1,16 +1,13 @@ 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); +$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)); +$swooleAdapter = new Server( + 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, +); + +$container->set('container', fn () => fn () => $swooleAdapter->getContainer()); + +$http = $swooleAdapter->getServer(); + /** * Assigns HTTP requests to worker threads by analyzing its payload/content. * @@ -69,16 +84,16 @@ $totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intva * 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 @@ -161,18 +176,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) { }); @@ -189,16 +192,14 @@ $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'; 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; @@ -289,13 +290,13 @@ 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 ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) { + $app = new Http($swooleAdapter, 'UTC'); - go(function () use ($register, $app) { - $pools = $register->get('pools'); - /** @var \Utopia\Pools\Group $pools */ - Http::setResource('pools', fn () => $pools); + /** @var \Utopia\Pools\Group $pools */ + $pools = $app->getResource('pools'); + + go(function () use ($app, $pools) { /** @var array $collections */ $collections = Config::getParam('collections', []); @@ -511,14 +512,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 ($files, $swooleAdapter, $registerRequestResources) { 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()); @@ -534,13 +532,18 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool return; } - $app = new Http('UTC'); + $requestContainer = $swooleAdapter->getContainer(); + $requestContainer->set('request', fn () => $request); + $requestContainer->set('response', fn () => $response); + + $app = new Http($swooleAdapter, 'UTC'); + $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 - $pools = $register->get('pools'); - Http::setResource('pools', fn () => $pools); - try { $authorization = $app->getResource('authorization'); @@ -624,6 +627,7 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool } } + $swooleResponse = $utopiaResponse->getSwooleResponse(); $swooleResponse->setStatusCode(500); $output = ((Http::isDevelopment())) ? [ @@ -647,11 +651,10 @@ $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 ($swooleAdapter) { $lastSyncUpdate = null; - $pools = $register->get('pools'); - Http::setResource('pools', fn () => $pools); - $app = new Http('UTC'); + + $app = new Http($swooleAdapter, 'UTC'); /** @var Utopia\Database\Database $dbForPlatform */ $dbForPlatform = $app->getResource('dbForPlatform'); @@ -726,4 +729,4 @@ $http->on(Constant::EVENT_TASK, function () use ($register) { }); }); -$http->start(); +$swooleAdapter->start(); diff --git a/app/init/registers.php b/app/init/registers.php index 68ccbd2097..c07bc9da8b 100644 --- a/app/init/registers.php +++ b/app/init/registers.php @@ -245,19 +245,8 @@ $register->set('pools', function () { $maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151); $instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14); - $multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled'; - - 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/app/init/resources.php b/app/init/resources.php index 92164c3c95..fdca88c30e 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -1,47 +1,10 @@ new Log()); -Http::setResource('logger', function ($register) { +global $register; +global $container; +$container = new Container(); + +$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 () { - $locale = new Locale(System::getEnv('_APP_LOCALE', 'en')); - $locale->setFallback(System::getEnv('_APP_LOCALE', 'en')); +$container->set('register', fn () => $register); - 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) { +// Queues - shared infrastructure (stateless pool wrappers) +$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) { - return new Messaging($publisher); -}, ['publisher']); -Http::setResource('queueForMails', function (Publisher $publisher) { - return new Mail($publisher); -}, ['publisher']); -Http::setResource('queueForBuilds', function (Publisher $publisher) { - return new Build($publisher); -}, ['publisher']); -Http::setResource('queueForScreenshots', function (Publisher $publisher) { - return new Screenshot($publisher); -}, ['publisher']); -Http::setResource('queueForDatabase', function (Publisher $publisher) { - return new EventDatabase($publisher); -}, ['publisher']); -Http::setResource('queueForDeletes', function (Publisher $publisher) { - return new Delete($publisher); -}, ['publisher']); -Http::setResource('queueForEvents', function (Publisher $publisher) { - return new Event($publisher); -}, ['publisher']); -Http::setResource('queueForWebhooks', function (Publisher $publisher) { - return new Webhook($publisher); -}, ['publisher']); -Http::setResource('queueForRealtime', function () { - return new Realtime(); -}, []); -Http::setResource('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) { - return new AuditEvent($publisher); -}, ['publisher']); -Http::setResource('queueForFunctions', function (Publisher $publisher) { - return new Func($publisher); -}, ['publisher']); -Http::setResource('eventProcessor', function () { - return new EventProcessor(); -}, []); -Http::setResource('queueForCertificates', function (Publisher $publisher) { - return new Certificate($publisher); -}, ['publisher']); -Http::setResource('queueForMigrations', function (Publisher $publisher) { - return new Migration($publisher); -}, ['publisher']); -Http::setResource('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) { - $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']); - -/** - * 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. - */ -Http::setResource('cookieDomain', function (Request $request, Document $project) { - $localHosts = ['localhost', 'localhost:' . $request->getPort()]; - - $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(); - } - - $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. - */ -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; -}); - -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, Request $request) { - 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); - }; - - /** - * Prefix metrics with database type when applicable. - * Avoids prefixing for legacy and tablesdb types to preserve historical metrics. - */ - $getDatabaseTypePrefixedMetric = function (string $databaseType, string $metric): string { - if ( - $databaseType === '' || - $databaseType === DATABASE_TYPE_LEGACY || - $databaseType === DATABASE_TYPE_TABLESDB - ) { - return $metric; - } - - return $databaseType . '.' . $metric; - }; - - // Determine database type from request path, similar to api.php - $path = $request->getURI(); - $databaseType = match (true) { - str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB, - str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB, - default => '', - }; - - $usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) use ($getDatabaseTypePrefixedMetric, $databaseType) { - $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 - $metric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASES); - $usage->addMetric($metric, $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; - $collectionMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_COLLECTIONS); - $databaseIdCollectionMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_COLLECTIONS); - $usage - ->addMetric($collectionMetric, $value) // per project - ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdCollectionMetric), $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; - $documentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DOCUMENTS); - $databaseIdDocumentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_DOCUMENTS); - $databaseIdCollectionIdDocumentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS); - $usage - ->addMetric($documentsMetric, $value) // per project - ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database - ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $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', 'request']); - -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); @@ -934,200 +117,7 @@ Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authori return $database; }, ['pools', 'cache', 'authorization']); -Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) { - - return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database { - $databaseDSN = $database->getAttribute('database', $project->getAttribute('database', '')); - $databaseType = $database->getAttribute('type', ''); - - try { - $databaseDSN = new DSN($databaseDSN); - } catch (\InvalidArgumentException) { - // for old databases migrated through patch script - // databaseDSN determines the adapter - $databaseDSN = new DSN('mysql://'.$databaseDSN); - } - 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')); - } - - $pool = $pools->get($databaseDSN->getHost()); - - $adapter = new DatabasePool($pool); - $database = new Database($adapter, $cache); - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - - $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); - // inside pools authorization needs to be set first - $database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB); - if (\in_array($dsn->getHost(), $sharedTables)) { - $database - ->setSharedTables(true) - ->setTenant((int)$project->getSequence()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getSequence()); - } - $timeout = \intval($request->getHeader('x-appwrite-timeout')); - if (!empty($timeout) && Http::isDevelopment()) { - $database->setTimeout($timeout); - } - - // Register database event listeners for usage stats collection - $documentsMetric = METRIC_DOCUMENTS; - $databaseIdDocumentsMetric = METRIC_DATABASE_ID_DOCUMENTS; - $databaseIdCollectionIdDocumentsMetric = METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS; - if ($databaseType !== DATABASE_TYPE_LEGACY && $databaseType !== DATABASE_TYPE_TABLESDB) { - $documentsMetric = $databaseType. '.' .$documentsMetric; - $databaseIdDocumentsMetric = $databaseType. '.' .$databaseIdDocumentsMetric; - $databaseIdCollectionIdDocumentsMetric = $databaseType . '.' .$databaseIdCollectionIdDocumentsMetric; - } - $database - ->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) { - $value = 1; - - if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) { - $parts = explode('_', $document->getCollection()); - $databaseInternalId = $parts[1] ?? 0; - $collectionInternalId = $parts[3] ?? 0; - $usage - ->addMetric($documentsMetric, $value) // per project - ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database - ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection - } - }) - ->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) { - $value = -1; - - if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) { - $parts = explode('_', $document->getCollection()); - $databaseInternalId = $parts[1] ?? 0; - $collectionInternalId = $parts[3] ?? 0; - $usage - ->addMetric($documentsMetric, $value) // per project - ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database - ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection - } - }) - ->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) { - $value = $document->getAttribute('modified', 0); - - if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) { - $parts = explode('_', $document->getCollection()); - $databaseInternalId = $parts[1] ?? 0; - $collectionInternalId = $parts[3] ?? 0; - $usage - ->addMetric($documentsMetric, $value) // per project - ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database - ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection - } - }) - ->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) { - $value = -1 * $document->getAttribute('modified', 0); - - if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) { - $parts = explode('_', $document->getCollection()); - $databaseInternalId = $parts[1] ?? 0; - $collectionInternalId = $parts[3] ?? 0; - $usage - ->addMetric($documentsMetric, $value) // per project - ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database - ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection - } - }) - ->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) { - $value = $document->getAttribute('created', 0); - - if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) { - $parts = explode('_', $document->getCollection()); - $databaseInternalId = $parts[1] ?? 0; - $collectionInternalId = $parts[3] ?? 0; - $usage - ->addMetric($documentsMetric, $value) // per project - ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database - ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection - } - }); - - return $database; - }; - -}, ['pools','cache','project','request','usage','authorization']); - -Http::setResource('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']); - -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) { @@ -1156,15 +146,9 @@ Http::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizati }; }, ['pools', 'cache', 'authorization']); -Http::setResource('audit', function ($dbForProject) { - $adapter = new AdapterDatabase($dbForProject); +$container->set('telemetry', fn () => new NoTelemetry()); - return new Audit($adapter); -}, ['dbForProject']); - -Http::setResource('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 = []; @@ -1178,7 +162,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', ''); @@ -1193,31 +177,15 @@ 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) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); -Http::setResource('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) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); -Http::setResource('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) { - 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', ''); @@ -1325,33 +293,17 @@ function getDevice(string $root, string $connection = ''): Device } } -Http::setResource('mode', function (Request $request, Document $project) { - /** - * Defines the mode for the request: - * - 'default' => Requests for Client and Server Side - * - 'admin' => Request from the Console on non-console projects - */ - $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']); - -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]; @@ -1362,358 +314,25 @@ 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) { - - $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']); - -Http::setResource('gitHub', function (Cache $cache) { +$container->set('gitHub', function (Cache $cache) { return new VcsGitHub($cache); }, ['cache']); -Http::setResource('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']); - -Http::setResource('plan', function (array $plan = []) { +$container->set('plan', function () { return []; }); -Http::setResource('smsRates', function () { +$container->set('smsRates', function () { return []; }); -Http::setResource('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']); - -Http::setResource('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']); - -Http::setResource( +$container->set( 'isResourceBlocked', fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false ); -Http::setResource('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']); - -Http::setResource('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']); - -Http::setResource('executor', fn () => new Executor()); - -Http::setResource('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']); - -Http::setResource('transactionState', function (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) { - return new TransactionState($dbForProject, $authorization, $getDatabasesDB); -}, ['dbForProject', 'authorization', 'getDatabasesDB']); - -Http::setResource('executionsRetentionCount', function (Document $project, array $plan) { - if ($project->getId() === 'console' || empty($plan)) { - return 0; - } - - return (int) ($plan['executionsRetentionCount'] ?? 100); -}, ['project', 'plan']); - -Http::setResource('embeddingAgent', function ($register) { - $adapter = new Ollama(); - $adapter->setEndpoint(System::getEnv('_APP_EMBEDDING_ENDPOINT', 'http://ollama:11434/api/embed')); - $adapter->setTimeout((int) System::getEnv('_APP_EMBEDDING_TIMEOUT', '30000')); - return new Agent($adapter); -}, ['register']); +$container->set('executor', fn () => new Executor()); diff --git a/app/init/resources/request.php b/app/init/resources/request.php new file mode 100644 index 0000000000..2bec412882 --- /dev/null +++ b/app/init/resources/request.php @@ -0,0 +1,1455 @@ +set('utopia:graphql', function ($utopia) { + return $utopia; + }, ['utopia']); + + $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. + */ + $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']); + + /** + * Whether the request origin is verified against the request hostname. + */ + $container->set('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. + */ + $container->set('cookieDomain', function (Request $request, Document $project) { + $localHosts = ['localhost', 'localhost:' . $request->getPort()]; + + $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(); + } + + $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. + */ + $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; + } + } + } + + // 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']); + + $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 */ + $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']); + + $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; + } + } + + 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) { + 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); + }; + + /** + * Prefix metrics with database type when applicable. + * Avoids prefixing for legacy and tablesdb types to preserve historical metrics. + */ + $getDatabaseTypePrefixedMetric = function (string $databaseType, string $metric): string { + if ( + $databaseType === '' || + $databaseType === DATABASE_TYPE_LEGACY || + $databaseType === DATABASE_TYPE_TABLESDB + ) { + return $metric; + } + + return $databaseType . '.' . $metric; + }; + + // Determine database type from request path, similar to api.php + $path = $request->getURI(); + $databaseType = match (true) { + str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB, + str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB, + default => '', + }; + + $usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) use ($getDatabaseTypePrefixedMetric, $databaseType) { + $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 + $metric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASES); + $usage->addMetric($metric, $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; + $collectionMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_COLLECTIONS); + $databaseIdCollectionMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_COLLECTIONS); + $usage + ->addMetric($collectionMetric, $value) // per project + ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdCollectionMetric), $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; + $documentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DOCUMENTS); + $databaseIdDocumentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_DOCUMENTS); + $databaseIdCollectionIdDocumentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS); + $usage + ->addMetric($documentsMetric, $value) // per project + ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database + ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $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', 'request']); + + $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, Document $project) { + /** @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 + */ + $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 + $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('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) { + + return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database { + $databaseDSN = $database->getAttribute('database', $project->getAttribute('database', '')); + $databaseType = $database->getAttribute('type', ''); + + try { + $databaseDSN = new DSN($databaseDSN); + } catch (\InvalidArgumentException) { + // for old databases migrated through patch script + // databaseDSN determines the adapter + $databaseDSN = new DSN('mysql://' . $databaseDSN); + } + 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')); + } + + $pool = $pools->get($databaseDSN->getHost()); + + $adapter = new DatabasePool($pool); + $database = new Database($adapter, $cache); + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + + $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); + // inside pools authorization needs to be set first + $database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB); + if (\in_array($dsn->getHost(), $sharedTables)) { + $database + ->setSharedTables(true) + ->setTenant((int) $project->getSequence()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getSequence()); + } + $timeout = \intval($request->getHeader('x-appwrite-timeout')); + if (!empty($timeout) && Http::isDevelopment()) { + $database->setTimeout($timeout); + } + + // Register database event listeners for usage stats collection + $documentsMetric = METRIC_DOCUMENTS; + $databaseIdDocumentsMetric = METRIC_DATABASE_ID_DOCUMENTS; + $databaseIdCollectionIdDocumentsMetric = METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS; + if ($databaseType !== DATABASE_TYPE_LEGACY && $databaseType !== DATABASE_TYPE_TABLESDB) { + $documentsMetric = $databaseType . '.' . $documentsMetric; + $databaseIdDocumentsMetric = $databaseType . '.' . $databaseIdDocumentsMetric; + $databaseIdCollectionIdDocumentsMetric = $databaseType . '.' . $databaseIdCollectionIdDocumentsMetric; + } + $database + ->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) { + $value = 1; + + if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) { + $parts = explode('_', $document->getCollection()); + $databaseInternalId = $parts[1] ?? 0; + $collectionInternalId = $parts[3] ?? 0; + $usage + ->addMetric($documentsMetric, $value) // per project + ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database + ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection + } + }) + ->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) { + $value = -1; + + if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) { + $parts = explode('_', $document->getCollection()); + $databaseInternalId = $parts[1] ?? 0; + $collectionInternalId = $parts[3] ?? 0; + $usage + ->addMetric($documentsMetric, $value) // per project + ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database + ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection + } + }) + ->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) { + $value = $document->getAttribute('modified', 0); + + if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) { + $parts = explode('_', $document->getCollection()); + $databaseInternalId = $parts[1] ?? 0; + $collectionInternalId = $parts[3] ?? 0; + $usage + ->addMetric($documentsMetric, $value) // per project + ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database + ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection + } + }) + ->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) { + $value = -1 * $document->getAttribute('modified', 0); + + if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) { + $parts = explode('_', $document->getCollection()); + $databaseInternalId = $parts[1] ?? 0; + $collectionInternalId = $parts[3] ?? 0; + $usage + ->addMetric($documentsMetric, $value) // per project + ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database + ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection + } + }) + ->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) { + $value = $document->getAttribute('created', 0); + + if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) { + $parts = explode('_', $document->getCollection()); + $databaseInternalId = $parts[1] ?? 0; + $collectionInternalId = $parts[3] ?? 0; + $usage + ->addMetric($documentsMetric, $value) // per project + ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database + ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection + } + }); + + return $database; + }; + + }, ['pools', 'cache', 'project', 'request', 'usage', 'authorization']); + + $container->set('transactionState', function (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) { + return new TransactionState($dbForProject, $authorization, $getDatabasesDB); + }, ['dbForProject', 'authorization', 'getDatabasesDB']); + + $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('embeddingAgent', function ($register) { + $adapter = new Ollama(); + $adapter->setEndpoint(System::getEnv('_APP_EMBEDDING_ENDPOINT', 'http://ollama:11434/api/embed')); + $adapter->setTimeout((int) System::getEnv('_APP_EMBEDDING_TIMEOUT', '30000')); + return new Agent($adapter); + }, ['register']); +}; diff --git a/app/init/worker/message.php b/app/init/worker/message.php new file mode 100644 index 0000000000..a444ada91d --- /dev/null +++ b/app/init/worker/message.php @@ -0,0 +1,435 @@ +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('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) { + return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database { + $projectDocument ??= $project; + $databaseDSN = $database->getAttribute('database', $project->getAttribute('database', '')); + $databaseType = $database->getAttribute('type', ''); + + // Backwards-compatibility: older or seeded legacy databases may not have a DSN stored + // in the "database" attribute. In that case, fall back to the project's database DSN. + if ($databaseDSN === '') { + $databaseDSN = $projectDocument->getAttribute('database', ''); + } + + try { + $databaseDSN = new DSN($databaseDSN); + } catch (\InvalidArgumentException) { + $databaseDSN = new DSN('mysql://' . $databaseDSN); + } + + try { + $dsn = new DSN($projectDocument->getAttribute('database')); + } catch (\InvalidArgumentException) { + // Temporary fallback until all projects use shared tables + $dsn = new DSN('mysql://' . $projectDocument->getAttribute('database')); + } + + $pools = $register->get('pools'); + $pool = $pools->get($databaseDSN->getHost()); + + $adapter = new DatabasePool($pool); + $database = new Database($adapter, $cache); + $database + ->setDatabase(APP_DATABASE) + ->setAuthorization($authorization); + $database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB); + + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + + if (\in_array($dsn->getHost(), $sharedTables, true)) { + $database + ->setSharedTables(true) + ->setTenant((int) $projectDocument->getSequence()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $projectDocument->getSequence()); + } + + $database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER); + return $database; + }; + }, ['cache', 'register', 'project', '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 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months + } + + 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 * (int) 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/realtime.php b/app/realtime.php index 67cfb19e2a..97ea7e32d5 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -33,7 +33,9 @@ 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\Adapter\FPM\Server as HttpServer; use Utopia\Http\Http; use Utopia\Logger\Log; use Utopia\Pools\Group; @@ -48,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 @@ -240,6 +244,11 @@ if (!function_exists('triggerStats')) { } } +global $container; +$container->set('pools', function ($register) { + return $register->get('pools'); +}, ['register']); + $realtime = getRealtime(); /** @@ -614,16 +623,22 @@ $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) { - $app = new Http('UTC'); +$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $registerRequestResources) { + 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); + $connectionContainer = new Container($container); + + $adapter = new HttpServer($connectionContainer); + $app = new Http($adapter, 'UTC'); + $connectionContainer->set('utopia', fn () => $app); + $connectionContainer->set('request', fn () => $request); + $connectionContainer->set('response', fn () => $response); + + $registerRequestResources($connectionContainer); $project = null; $logUser = null; diff --git a/app/worker.php b/app/worker.php index d573a60231..e55abb587c 100644 --- a/app/worker.php +++ b/app/worker.php @@ -1,564 +1,69 @@ $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('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) { - return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database { - $projectDocument ??= $project; - $databaseDSN = $database->getAttribute('database', $project->getAttribute('database', '')); - $databaseType = $database->getAttribute('type', ''); - - // Backwards‑compatibility: older or seeded legacy databases may not have a DSN stored - // in the "database" attribute. In that case, fall back to the project's database DSN. - if ($databaseDSN === '') { - $databaseDSN = $projectDocument->getAttribute('database', ''); - } - - try { - $databaseDSN = new DSN($databaseDSN); - } catch (\InvalidArgumentException) { - $databaseDSN = new DSN('mysql://'.$databaseDSN); - } - - try { - $dsn = new DSN($projectDocument->getAttribute('database')); - } catch (\InvalidArgumentException) { - // Temporary fallback until all projects use shared tables - $dsn = new DSN('mysql://' . $projectDocument->getAttribute('database')); - } - - $pools = $register->get('pools'); - $pool = $pools->get($databaseDSN->getHost()); - - $adapter = new DatabasePool($pool); - $database = new Database($adapter, $cache); - $database - ->setDatabase(APP_DATABASE) - ->setAuthorization($authorization); - $database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB); - - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - - if (\in_array($dsn->getHost(), $sharedTables, true)) { - $database - ->setSharedTables(true) - ->setTenant((int) $projectDocument->getSequence()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $projectDocument->getSequence()); - } - - $database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER); - return $database; - }; -}, ['cache', 'register', 'project', '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 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months - } - - return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days -}, ['project']); - -Server::setResource('executionRetention', function () { - return DateTime::addSeconds(new \DateTime(), -1 * (int) 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'); +$args = $_SERVER['argv'] ?? []; if (! isset($args[1])) { Console::error('Missing worker name'); @@ -574,38 +79,45 @@ if (\str_starts_with($workerName, 'databases')) { $queueName = System::getEnv('_APP_QUEUE_NAME', 'v1-' . strtolower($workerName)); } +/** @var \Utopia\Pools\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 { - /** - * 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 - */ + $worker->init()->action(function () use ($worker, $registerWorkerMessageResources) { + $registerWorkerMessageResources($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 0f4dcfb8db..d3474361e2 100644 --- a/composer.json +++ b/composer.json @@ -52,34 +52,34 @@ "appwrite/php-runtimes": "0.19.*", "appwrite/php-clamav": "2.0.*", "utopia-php/abuse": "1.2.*", + "utopia-php/agents": "1.2.*", "utopia-php/analytics": "0.15.*", "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/agents": "1.*", "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": "0.34.*", "utopia-php/fetch": "0.5.*", "utopia-php/image": "0.8.*", "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.22.*", "utopia-php/migration": "1.9.*", - "utopia-php/platform": "0.7.*", + "utopia-php/platform": "0.12.*", "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.17.*", + "utopia-php/servers": "0.3.*", "utopia-php/registry": "0.5.*", "utopia-php/storage": "1.0.*", "utopia-php/system": "0.10.*", @@ -94,8 +94,7 @@ "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.*" }, "require-dev": { "ext-fileinfo": "*", diff --git a/composer.lock b/composer.lock index 420dddc9a5..d71f78b35d 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": "4fe91e67f343fbe6deac1fdc7eda949f", + "content-hash": "e9c38bbebc60849e70e3640aaa4422cd", "packages": [ { "name": "adhocore/jwt", @@ -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", @@ -3658,21 +3658,21 @@ }, { "name": "utopia-php/cli", - "version": "0.22.0", + "version": "0.23.1", "source": { "type": "git", "url": "https://github.com/utopia-php/cli.git", - "reference": "a7ac387ee626fd27075a87e836fb72c5be38add4" + "reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621" }, "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/8d1955b8bc4dc631f45d7c7df689ed7b63f70621", + "reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621", "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.1" }, - "time": "2025-10-21T10:42:45+00:00" + "time": "2026-04-05T15:27:35+00:00" }, { "name": "utopia-php/compression", @@ -3954,25 +3954,26 @@ }, { "name": "utopia-php/di", - "version": "0.1.0", + "version": "0.3.2", "source": { "type": "git", "url": "https://github.com/utopia-php/di.git", - "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31" + "reference": "07025d721ed5d9be27932e8e640acf1467fc4b9d" }, "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/07025d721ed5d9be27932e8e640acf1467fc4b9d", + "reference": "07025d721ed5d9be27932e8e640acf1467fc4b9d", "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" }, @@ -3989,16 +3990,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.2" }, - "time": "2024-08-08T14:35:19+00:00" + "time": "2026-03-21T07:42:10+00:00" }, { "name": "utopia-php/dns", @@ -4268,30 +4271,34 @@ }, { "name": "utopia-php/framework", - "version": "0.33.41", + "version": "0.34.17", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "0f3bf2377c867e547c929c3733b8224afee6ef06" + "reference": "d3e4143b8b06d9823d0c29a06dacefa5a1b93677" }, "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/d3e4143b8b06d9823d0c29a06dacefa5a1b93677", + "reference": "d3e4143b8b06d9823d0c29a06dacefa5a1b93677", "shasum": "" }, "require": { - "php": ">=8.3", + "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.*", + "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": { @@ -4303,17 +4310,72 @@ "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/0.34.17" }, - "time": "2026-02-24T12:01:28+00:00" + "time": "2026-04-06T04:40:23+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", @@ -4634,30 +4696,30 @@ }, { "name": "utopia-php/platform", - "version": "0.7.16", + "version": "0.12.0", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "34e67e4b80b5741c380071fe765fbc12a132de4f" + "reference": "068ee46228f0c3972e6b569f2c86b6c80fe583d8" }, "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/068ee46228f0c3972e6b569f2c86b6c80fe583d8", + "reference": "068ee46228f0c3972e6b569f2c86b6c80fe583d8", "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.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": { @@ -4679,9 +4741,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.12.0" }, - "time": "2026-02-11T06:36:48+00:00" + "time": "2026-03-31T14:44:23+00:00" }, { "name": "utopia-php/pools", @@ -4791,32 +4853,33 @@ }, { "name": "utopia-php/queue", - "version": "0.15.6", + "version": "0.17.0", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "08e361d69610f371382b344c369eef355ca414b4" + "reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f" }, "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/0fbc7d7312f5cf76ec112513fb93317000901f5f", + "reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f", "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.2.*", + "utopia-php/servers": "0.3.*", "utopia-php/telemetry": "0.2.*", "utopia-php/validators": "0.2.*" }, "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" }, @@ -4851,9 +4914,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.17.0" }, - "time": "2026-02-23T13:03:51+00:00" + "time": "2026-03-23T16:21:31+00:00" }, { "name": "utopia-php/registry", @@ -4909,21 +4972,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": { @@ -4957,9 +5020,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", diff --git a/src/Appwrite/GraphQL/Resolvers.php b/src/Appwrite/GraphQL/Resolvers.php index e422bcbf96..689724d9f1 100644 --- a/src/Appwrite/GraphQL/Resolvers.php +++ b/src/Appwrite/GraphQL/Resolvers.php @@ -26,9 +26,9 @@ class Resolvers ): callable { return static fn ($type, $args, $context, $info) => new Swoole( function (callable $resolve, callable $reject) use ($utopia, $route, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); $path = $route->getPath(); foreach ($args as $key => $value) { @@ -93,9 +93,9 @@ class Resolvers ): callable { return static fn ($type, $args, $context, $info) => new Swoole( function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); $request->setMethod('GET'); $request->setURI($url($databaseId, $collectionId, $args)); @@ -124,9 +124,9 @@ class Resolvers ): callable { return static fn ($type, $args, $context, $info) => new Swoole( function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); $request->setMethod('GET'); $request->setURI($url($databaseId, $collectionId, $args)); @@ -160,9 +160,9 @@ class Resolvers ): callable { return static fn ($type, $args, $context, $info) => new Swoole( function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); $request->setMethod('POST'); $request->setURI($url($databaseId, $collectionId, $args)); @@ -192,9 +192,9 @@ class Resolvers ): callable { return static fn ($type, $args, $context, $info) => new Swoole( function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); $request->setMethod('PATCH'); $request->setURI($url($databaseId, $collectionId, $args)); @@ -222,9 +222,9 @@ class Resolvers ): callable { return static fn ($type, $args, $context, $info) => new Swoole( function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); $request->setMethod('DELETE'); $request->setURI($url($databaseId, $collectionId, $args)); @@ -266,7 +266,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/GraphQL/Schema.php b/src/Appwrite/GraphQL/Schema.php index aa68fd28a1..4ff96fb635 100644 --- a/src/Appwrite/GraphQL/Schema.php +++ b/src/Appwrite/GraphQL/Schema.php @@ -32,10 +32,6 @@ class Schema array $urls, array $params, ): GQLSchema { - Http::setResource('utopia:graphql', static function () use ($utopia) { - return $utopia; - }); - if (!empty(self::$schema)) { return self::$schema; } diff --git a/src/Appwrite/Platform/Installer/Server.php b/src/Appwrite/Platform/Installer/Server.php index 6d9cd5412f..99ec9e65d2 100644 --- a/src/Appwrite/Platform/Installer/Server.php +++ b/src/Appwrite/Platform/Installer/Server.php @@ -145,9 +145,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(); @@ -160,17 +171,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()); @@ -180,7 +180,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)) { @@ -190,7 +190,7 @@ class Server return; } - $app = new Http('UTC'); + $app = new Http($adapter, 'UTC'); $app->run($request, $response); }); 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', diff --git a/src/Appwrite/Platform/Tasks/Migrate.php b/src/Appwrite/Platform/Tasks/Migrate.php index b952808998..6f9f92435a 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('authorization') + ->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)) { @@ -125,8 +126,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 a6a5284fb0..ebc4f6731a 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,9 @@ 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; use Utopia\Http\Request as UtopiaRequest; use Utopia\Http\Response as UtopiaResponse; @@ -336,11 +340,17 @@ 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()))); + // Mock dependencies needed by param validator injections in route definitions + $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()))); + $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(); @@ -438,7 +448,7 @@ class Specs extends Action } $arguments = [ - new Http('UTC'), + new Http(new FPMServer($specsContainer), 'UTC'), $services, $routes, $models, 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) { diff --git a/tests/unit/Event/MockPublisher.php b/tests/unit/Event/MockPublisher.php index a7118d3c09..8363ed4e85 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] = [];