diff --git a/app/controllers/general.php b/app/controllers/general.php index 3890406808..6feab6e2f7 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -370,12 +370,28 @@ Http::init() ->inject('locale') ->inject('localeCodes') ->inject('clients') - ->inject('geodb') - ->inject('queueForUsage') - ->inject('queueForEvents') + // ->inject('geodb') + // ->inject('queueForUsage') + // ->inject('queueForEvents') ->inject('queueForCertificates') ->inject('authorization') - ->action(function (Request $request, Response $response, Route $route, Document $console, Document $project, Database $dbForConsole, Locale $locale, array $localeCodes, array $clients, Reader $geodb, Usage $queueForUsage, Event $queueForEvents, Certificate $queueForCertificates, Authorization $authorization) { + ->action(function ( + Request $request, + Response $response, + Route $route, + Document $console, + Document $project, + Database $dbForConsole, + Locale $locale, array $localeCodes, + array $clients, + /** + * @disregard P1009 Undefined type + */ + // Reader $geodb, + // Usage $queueForUsage, + // Event $queueForEvents, + Certificate $queueForCertificates, + Authorization $authorization) { /* * Appwrite Router */ diff --git a/app/http.php b/app/http.php index d6f3322680..2fe80777f3 100644 --- a/app/http.php +++ b/app/http.php @@ -56,9 +56,14 @@ $http = new Http($server, $container, 'UTC'); $http->setRequestClass(Request::class); $http->setResponseClass(Response::class); -require_once __DIR__ . '/init.php'; +//require_once __DIR__ . '/init.php'; +require_once __DIR__ . '/init/constants.php'; +require_once __DIR__ . '/init/config.php'; +require_once __DIR__ . '/init/locale.php'; +require_once __DIR__ . '/init/database/filters.php'; +require_once __DIR__ . '/init/database/formats.php'; require_once __DIR__ . '/init2.php'; -include __DIR__ . '/controllers/general.php'; +require_once __DIR__ . '/controllers/general.php'; global $global; diff --git a/app/init/config.php b/app/init/config.php new file mode 100644 index 0000000000..5ae3c82dbc --- /dev/null +++ b/app/init/config.php @@ -0,0 +1,35 @@ + $value], JSON_PRESERVE_ZERO_FRACTION); + }, + function (mixed $value) { + if (is_null($value)) { + return; + } + + return json_decode($value, true)['value']; + } +); + +Database::addFilter( + 'enum', + function (mixed $value, Document $attribute) { + if ($attribute->isSet('elements')) { + $attribute->removeAttribute('elements'); + } + + return $value; + }, + function (mixed $value, Document $attribute) { + $formatOptions = \json_decode($attribute->getAttribute('formatOptions', '[]'), true); + if (isset($formatOptions['elements'])) { + $attribute->setAttribute('elements', $formatOptions['elements']); + } + + return $value; + } +); + +Database::addFilter( + 'range', + function (mixed $value, Document $attribute) { + if ($attribute->isSet('min')) { + $attribute->removeAttribute('min'); + } + if ($attribute->isSet('max')) { + $attribute->removeAttribute('max'); + } + + return $value; + }, + function (mixed $value, Document $attribute) { + $formatOptions = json_decode($attribute->getAttribute('formatOptions', '[]'), true); + if (isset($formatOptions['min']) || isset($formatOptions['max'])) { + $attribute + ->setAttribute('min', $formatOptions['min']) + ->setAttribute('max', $formatOptions['max']) + ; + } + + return $value; + } +); + +Database::addFilter( + 'subQueryAttributes', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + $attributes = $database->find('attributes', [ + Query::equal('collectionInternalId', [$document->getInternalId()]), + Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), + Query::limit($database->getLimitForAttributes()), + ]); + + foreach ($attributes as $attribute) { + if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) { + $options = $attribute->getAttribute('options'); + foreach ($options as $key => $value) { + $attribute->setAttribute($key, $value); + } + $attribute->removeAttribute('options'); + } + } + + return $attributes; + } +); + +Database::addFilter( + 'subQueryIndexes', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('indexes', [ + Query::equal('collectionInternalId', [$document->getInternalId()]), + Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), + Query::limit($database->getLimitForIndexes()), + ]); + } +); + +Database::addFilter( + 'subQueryPlatforms', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('platforms', [ + Query::equal('projectInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ]); + } +); + +Database::addFilter( + 'subQueryKeys', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('keys', [ + Query::equal('projectInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ]); + } +); + +Database::addFilter( + 'subQueryWebhooks', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('webhooks', [ + Query::equal('projectInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ]); + } +); + +Database::addFilter( + 'subQuerySessions', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database->getAuthorization()->skip(fn () => $database->find('sessions', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); + +Database::addFilter( + 'subQueryTokens', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database->getAuthorization()->skip(fn () => $database + ->find('tokens', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); + +Database::addFilter( + 'subQueryChallenges', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database->getAuthorization()->skip(fn () => $database + ->find('challenges', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); + +Database::addFilter( + 'subQueryAuthenticators', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database->getAuthorization()->skip(fn () => $database + ->find('authenticators', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); + +Database::addFilter( + 'subQueryMemberships', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database->getAuthorization()->skip(fn () => $database + ->find('memberships', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); + +Database::addFilter( + 'subQueryVariables', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('variables', [ + Query::equal('resourceInternalId', [$document->getInternalId()]), + Query::equal('resourceType', ['function']), + Query::limit(APP_LIMIT_SUBQUERY), + ]); + } +); + +Database::addFilter( + 'encrypt', + function (mixed $value) { + $key = System::getEnv('_APP_OPENSSL_KEY_V1'); + $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); + $tag = null; + + return json_encode([ + 'data' => OpenSSL::encrypt($value, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), + 'method' => OpenSSL::CIPHER_AES_128_GCM, + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag ?? ''), + 'version' => '1', + ]); + }, + function (mixed $value) { + if (is_null($value)) { + return; + } + $value = json_decode($value, true); + $key = System::getEnv('_APP_OPENSSL_KEY_V' . $value['version']); + + return OpenSSL::decrypt($value['data'], $value['method'], $key, 0, hex2bin($value['iv']), hex2bin($value['tag'])); + } +); + +Database::addFilter( + 'subQueryProjectVariables', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('variables', [ + Query::equal('resourceType', ['project']), + Query::limit(APP_LIMIT_SUBQUERY) + ]); + } +); + +Database::addFilter( + 'userSearch', + function (mixed $value, Document $user) { + $searchValues = [ + $user->getId(), + $user->getAttribute('email', ''), + $user->getAttribute('name', ''), + $user->getAttribute('phone', '') + ]; + + foreach ($user->getAttribute('labels', []) as $label) { + $searchValues[] = 'label:' . $label; + } + + $search = implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + +Database::addFilter( + 'subQueryTargets', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database->getAuthorization()->skip(fn () => $database + ->find('targets', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY) + ])); + } +); + +Database::addFilter( + 'subQueryTopicTargets', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + $targetIds = $database->getAuthorization()->skip(fn () => \array_map( + fn ($document) => $document->getAttribute('targetInternalId'), + $database->find('subscribers', [ + Query::equal('topicInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBSCRIBERS_SUBQUERY) + ]) + )); + if (\count($targetIds) > 0) { + return $database->find('targets', [ + Query::equal('$internalId', $targetIds) + ]); + } + return []; + } +); + +Database::addFilter( + 'providerSearch', + function (mixed $value, Document $provider) { + $searchValues = [ + $provider->getId(), + $provider->getAttribute('name', ''), + $provider->getAttribute('provider', ''), + $provider->getAttribute('type', '') + ]; + + $search = \implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + +Database::addFilter( + 'topicSearch', + function (mixed $value, Document $topic) { + $searchValues = [ + $topic->getId(), + $topic->getAttribute('name', ''), + $topic->getAttribute('description', ''), + ]; + + $search = \implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + +Database::addFilter( + 'messageSearch', + function (mixed $value, Document $message) { + $searchValues = [ + $message->getId(), + $message->getAttribute('description', ''), + $message->getAttribute('status', ''), + ]; + + $data = \json_decode($message->getAttribute('data', []), true); + $providerType = $message->getAttribute('providerType', ''); + + if ($providerType === MESSAGE_TYPE_EMAIL) { + $searchValues = \array_merge($searchValues, [$data['subject'], MESSAGE_TYPE_EMAIL]); + } elseif ($providerType === MESSAGE_TYPE_SMS) { + $searchValues = \array_merge($searchValues, [$data['content'], MESSAGE_TYPE_SMS]); + } else { + $searchValues = \array_merge($searchValues, [$data['title'], MESSAGE_TYPE_PUSH]); + } + + $search = \implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); \ No newline at end of file diff --git a/app/init/database/formats.php b/app/init/database/formats.php new file mode 100644 index 0000000000..f1bf54a456 --- /dev/null +++ b/app/init/database/formats.php @@ -0,0 +1,43 @@ + new MariaDB($resource), + 'mysql' => new MySQL($resource), + default => null + }; + + $adapter->setDatabase($scheme); + break; + case 'pubsub': + $adapter = $resource(); + break; + case 'queue': + $adapter = match ($scheme) { + //'redis' => new Queue\Connection\Redis($dsn->getHost(), $dsn->getPort()), + default => null + }; + break; + case 'cache': + $adapter = match ($scheme) { + 'redis' => new RedisCache($resource), + default => null + }; + break; + + default: + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Server error: Missing adapter implementation."); + } + + return $adapter; +} + +$global = new Registry(); + +$global->set('logger', function () { + // Register error logger + $providerName = System::getEnv('_APP_LOGGING_PROVIDER', ''); + $providerConfig = System::getEnv('_APP_LOGGING_CONFIG', ''); + + if (empty($providerName) || empty($providerConfig)) { + return; + } + + if (!Logger::hasProvider($providerName)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Logging provider not supported. Logging is disabled"); + } + + $classname = '\\Utopia\\Logger\\Adapter\\' . \ucfirst($providerName); + $adapter = new $classname($providerConfig); + return new Logger($adapter); +}); + +$global->set('geodb', function () { + /** + * @disregard P1009 Undefined type + */ + return new Reader(__DIR__ . '/assets/dbip/dbip-country-lite-2024-02.mmdb'); +}); + +$global->set('pools', (function () { + $fallbackForDB = 'db_main=' . URL::unparse([ + 'scheme' => 'mariadb', + 'host' => System::getEnv('_APP_DB_HOST', 'mariadb'), + 'port' => System::getEnv('_APP_DB_PORT', '3306'), + 'user' => System::getEnv('_APP_DB_USER', ''), + 'pass' => System::getEnv('_APP_DB_PASS', ''), + 'path' => System::getEnv('_APP_DB_SCHEMA', ''), + ]); + $fallbackForRedis = 'redis_main=' . URL::unparse([ + 'scheme' => 'redis', + 'host' => System::getEnv('_APP_REDIS_HOST', 'redis'), + 'port' => System::getEnv('_APP_REDIS_PORT', '6379'), + 'user' => System::getEnv('_APP_REDIS_USER', ''), + 'pass' => System::getEnv('_APP_REDIS_PASS', ''), + ]); + + $connections = [ + 'console' => [ + 'type' => 'database', + 'dsns' => System::getEnv('_APP_CONNECTIONS_DB_CONSOLE', $fallbackForDB), + 'multiple' => false, + 'schemes' => ['mariadb', 'mysql'], + ], + 'database' => [ + 'type' => 'database', + 'dsns' => System::getEnv('_APP_CONNECTIONS_DB_PROJECT', $fallbackForDB), + 'multiple' => true, + 'schemes' => ['mariadb', 'mysql'], + ], + 'queue' => [ + 'type' => 'queue', + 'dsns' => System::getEnv('_APP_CONNECTIONS_QUEUE', $fallbackForRedis), + 'multiple' => false, + 'schemes' => ['redis'], + ], + 'pubsub' => [ + 'type' => 'pubsub', + 'dsns' => System::getEnv('_APP_CONNECTIONS_PUBSUB', $fallbackForRedis), + 'multiple' => false, + 'schemes' => ['redis'], + ], + 'cache' => [ + 'type' => 'cache', + 'dsns' => System::getEnv('_APP_CONNECTIONS_CACHE', $fallbackForRedis), + 'multiple' => true, + 'schemes' => ['redis'], + ], + ]; + + $pools = []; + $poolSize = (int)System::getEnv('_APP_POOL_CLIENTS', 9000); + $poolSize = 9000; + + foreach ($connections as $key => $connection) { + $dsns = $connection['dsns'] ?? ''; + $multipe = $connection['multiple'] ?? false; + $schemes = $connection['schemes'] ?? []; + $dsns = explode(',', $connection['dsns'] ?? ''); + foreach ($dsns as &$dsn) { + $dsn = explode('=', $dsn); + $name = ($multipe) ? $dsn[0] : 'main'; + $dsn = $dsn[1] ?? ''; + + if (empty($dsn)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Missing value for DSN connection in {$key}"); + } + + $dsn = new DSN($dsn); + $dsnHost = $dsn->getHost(); + $dsnPort = $dsn->getPort(); + $dsnUser = $dsn->getUser(); + $dsnPass = $dsn->getPassword(); + $dsnScheme = $dsn->getScheme(); + $dsnDatabase = $dsn->getPath(); + + if (!in_array($dsnScheme, $schemes)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid console database scheme"); + } + + /** + * Get Resource + * + * Creation could be reused accross connection types like database, cache, queue, etc. + * + * Resource assignment to an adapter will happen below. + */ + switch ($dsnScheme) { + case 'mysql': + case 'mariadb': + $pool = new PDOPool((new PDOConfig) + ->withHost($dsnHost) + ->withPort($dsnPort) + ->withDbName($dsnDatabase) + ->withCharset('utf8mb4') + ->withUsername($dsnUser) + ->withPassword($dsnPass) + ->withOptions([ + // No need to set PDO::ATTR_ERRMODE it is overwitten in PDOProxy + // PDO::ATTR_TIMEOUT => 3, // Seconds + // PDO::ATTR_PERSISTENT => true, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => true, + PDO::ATTR_STRINGIFY_FETCHES => true, + PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, + + ]), + $poolSize + ); + break; + case 'redis': + $pool = new RedisPool((new RedisConfig) + ->withHost($dsnHost) + ->withPort((int)$dsnPort) + ->withAuth($dsnPass) + , $poolSize); + break; + + default: + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid scheme"); + } + + $pools['pools-' . $key . '-' . $name] = [ + 'pool' => $pool, + 'dsn' => $dsn, + ]; + } + } + + return function () use ($pools): array { + return $pools; + }; +})()); + +$mode = new Dependency(); +$mode + ->setName('mode') + ->inject('request') + ->setCallback(function (Request $request) { + /** + * Defines the mode for the request: + * - 'default' => Requests for Client and Server Side + * - 'admin' => Request from the Console on non-console projects + */ + return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); + }); +$container->set($mode); + +$user = new Dependency(); +$user + ->setName('user') + ->inject('mode') + ->inject('project') + ->inject('console') + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('dbForConsole') + ->inject('authorization') + ->setCallback(function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization) { + $authorization->setDefaultStatus(true); + + Auth::setCookieName('a_session_' . $project->getId()); + + if (APP_MODE_ADMIN === $mode) { + Auth::setCookieName('a_session_' . $console->getId()); + } + + $session = Auth::decodeSession( + $request->getCookie( + Auth::$cookieName, // Get sessions + $request->getCookie(Auth::$cookieName . '_legacy', '') + ) + ); + + // Get session from header for SSR clients + if (empty($session['id']) && empty($session['secret'])) { + $sessionHeader = $request->getHeader('x-appwrite-session', ''); + + if (!empty($sessionHeader)) { + $session = Auth::decodeSession($sessionHeader); + } + } + + // Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies + if ($response) { + $response->addHeader('X-Debug-Fallback', 'false'); + } + + if (empty($session['id']) && empty($session['secret'])) { + if ($response) { + $response->addHeader('X-Debug-Fallback', 'true'); + } + $fallback = $request->getHeader('x-fallback-cookies', ''); + $fallback = \json_decode($fallback, true); + $session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : '')); + } + + Auth::$unique = $session['id'] ?? ''; + Auth::$secret = $session['secret'] ?? ''; + + if (APP_MODE_ADMIN !== $mode) { + if ($project->isEmpty()) { + $user = new Document([]); + } else { + if ($project->getId() === 'console') { + $user = $dbForConsole->getDocument('users', Auth::$unique); + } else { + $user = $dbForProject->getDocument('users', Auth::$unique); + } + } + } else { + $user = $dbForConsole->getDocument('users', Auth::$unique); + } + + if ( + $user->isEmpty() // Check a document has been found in the DB + || !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) + ) { // Validate user has valid login token + $user = new Document([]); + } + + if (APP_MODE_ADMIN === $mode) { + if ($user->find('teamId', $project->getAttribute('teamId'), 'memberships')) { + $authorization->setDefaultStatus(false); // Cancel security segmentation for admin users. + } else { + $user = new Document([]); + } + } + + $authJWT = $request->getHeader('x-appwrite-jwt', ''); + + if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway. + + try { + $payload = $jwt->decode($authJWT); + } catch (JWTException $error) { + throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage()); + } + + $jwtUserId = $payload['userId'] ?? ''; + $jwtSessionId = $payload['sessionId'] ?? ''; + + if ($jwtUserId && $jwtSessionId) { + $user = $dbForProject->getDocument('users', $jwtUserId); + } + + if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token + $user = new Document([]); + } + } + + // Adds logs to database queries + $dbForProject->setMetadata('user', $user->getId()); + $dbForConsole->setMetadata('user', $user->getId()); + + return $user; + }); +$container->set($user); + +$console = new Dependency(); +$console + ->setName('console') + ->setCallback(function () { + return new Document([ + '$id' => ID::custom('console'), + '$internalId' => ID::custom('console'), + 'name' => 'Appwrite', + '$collection' => ID::custom('projects'), + 'description' => 'Appwrite core engine', + 'logo' => '', + 'teamId' => -1, + 'webhooks' => [], + 'keys' => [], + 'platforms' => [ + [ + '$collection' => ID::custom('platforms'), + 'name' => 'Localhost', + 'type' => Origin::CLIENT_TYPE_WEB, + 'hostname' => 'localhost', + ], // Current host is added on app init + ], + 'legalName' => '', + 'legalCountry' => '', + 'legalState' => '', + 'legalCity' => '', + 'legalAddress' => '', + 'legalTaxId' => '', + 'auths' => [ + 'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled', + 'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user + 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds + ], + 'authWhitelistEmails' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [], + 'authWhitelistIPs' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [], + 'oAuthProviders' => [ + 'githubEnabled' => true, + 'githubSecret' => System::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''), + 'githubAppid' => System::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '') + ], + ]); + }); +$container->set($console); + +$project = new Dependency(); +$project + ->setName('project') + ->inject('dbForConsole') + ->inject('request') + ->inject('console') + ->inject('authorization') + ->setCallback(function (Database $dbForConsole, Request $request, Document $console, Authorization $authorization) { + $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', '')); + + if (empty($projectId) || $projectId === 'console') { + return $console; + } + + $project = $authorization->skip(fn () => $dbForConsole->getDocument('projects', $projectId)); + + return $project; + }); +$container->set($project); + +$pools = new Dependency(); +$pools + ->setName('pools') + ->inject('registry') + ->setCallback(function (Registry $registry) { + return $registry->get('pools'); + }); +$container->set($pools); + +$dbForProject = new Dependency(); +$dbForProject + ->setName('dbForProject') + ->inject('pools') + ->inject('project') + ->inject('cache') + ->inject('dbForConsole') + ->inject('connections') + ->inject('authorization') + ->setCallback(function(array $pools, Document $project, Cache $cache, Database $dbForConsole, Connections $connections, Authorization $authorization) { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForConsole; + } + + $pool = $pools['pools-database-'.$project->getAttribute('database')]['pool']; + $dsn = $pools['pools-database-'.$project->getAttribute('database')]['dsn']; + + $connection = $pool->get(); + $connections->add($connection, $pool); + $adapter = match ($dsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + + $database = new Database($adapter, $cache); + $database->setAuthorization($authorization); + + $database + ->setNamespace('_' . $project->getInternalId()) + ->setMetadata('host', \gethostname()) + ->setMetadata('project', $project->getId()) + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); + + return $database; + }); +$container->set($dbForProject); + +$dbForConsole = new Dependency(); +$dbForConsole + ->setName('dbForConsole') + ->inject('pools') + ->inject('cache') + ->inject('authorization') + ->inject('connections') + ->setCallback(function(array $pools, Cache $cache, Authorization $authorization, Connections $connections): Database { + $pool = $pools['pools-console-main']['pool']; + $dsn = $pools['pools-console-main']['dsn']; + $connection = $pool->get(); + $connections->add($connection, $pool); + + $adapter = match ($dsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + + $adapter->setDatabase('appwrite'); + + $database = new Database($adapter, $cache); + $database->setAuthorization($authorization); + + $database + ->setNamespace('_console') + ->setMetadata('host', \gethostname()) + ->setMetadata('project', 'console') + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); + + return $database; + }); +$container->set($dbForConsole); + +$cache = new Dependency(); +$cache + ->setName('cache') + ->setCallback(function (): Cache { + return new Cache(new None()); + }); +$container->set($cache); + +$authorization = new Dependency(); +$authorization + ->setName('authorization') + ->setCallback(function (): Authorization { + return new Authorization(); + }); +$container->set($authorization); + +$registry = new Dependency(); +$registry + ->setName('registry') + ->setCallback(function () use (&$global): Registry { + return $global; + }); +$container->set($registry); + +$pools = new Dependency(); +$pools + ->setName('pools') + ->inject('registry') + ->setCallback(function (Registry $registry) { + return $registry->get('pools'); + }); +$container->set($pools); + +$logger = new Dependency(); +$logger + ->setName('logger') + ->inject('registry') + ->setCallback(function (Registry $registry) { + return $registry->get('logger'); + }); +$container->set($logger); + +$log = new Dependency(); +$log + ->setName('log') + ->setCallback(function () { + return new Log(); + }); +$container->set($log); + +$connections = new Dependency(); +$connections + ->setName('connections') + ->setCallback(function () { + return new Connections(); + }); +$container->set($connections); + +$locale = new Dependency(); +$locale + ->setName('locale') + ->setCallback(fn () => new Locale(System::getEnv('_APP_LOCALE', 'en'))); +$container->set($locale); + +$localeCodes = new Dependency(); +$localeCodes + ->setName('localeCodes') + ->setCallback(fn () => array_map(fn ($locale) => $locale['code'], Config::getParam('locale-codes', []))); +$container->set($localeCodes); + +$queue = new Dependency(); +$queue + ->setName('queue') + ->inject('pools') + ->inject('connections') + ->setCallback(function (array $pools, Connections $connections) { + $pool = $pools['pools-queue-main']['pool']; + $dsn = $pools['pools-queue-main']['dsn']; + $connection = $pool->get(); + $connections->add($connection, $pool); + + return new Queue\Connection\Redis($dsn->getHost(), $dsn->getPort()); + }); +$container->set($queue); + +$queueForMessaging = new Dependency(); +$queueForMessaging + ->setName('queueForMessaging') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Messaging($queue); + }); +$container->set($queueForMessaging); + +$queueForMails = new Dependency(); +$queueForMails + ->setName('queueForMails') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Mail($queue); + }); +$container->set($queueForMails); + +$queueForBuilds = new Dependency(); +$queueForBuilds + ->setName('queueForBuilds') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Build($queue); + }); +$container->set($queueForBuilds); + +$queueForDatabase = new Dependency(); +$queueForDatabase + ->setName('queueForDatabase') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new EventDatabase($queue); + }); +$container->set($queueForDatabase); + +$queueForDeletes = new Dependency(); +$queueForDeletes + ->setName('queueForDeletes') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Delete($queue); + }); +$container->set($queueForDeletes); + +$queueForEvents = new Dependency(); +$queueForEvents + ->setName('queueForEvents') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Event($queue); + }); +$container->set($queueForEvents); + +$queueForAudits = new Dependency(); +$queueForAudits + ->setName('queueForAudits') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Audit($queue); + }); +$container->set($queueForAudits); + +$queueForFunctions = new Dependency(); +$queueForFunctions + ->setName('queueForFunctions') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Func($queue); + }); +$container->set($queueForFunctions); + +$queueForUsage = new Dependency(); +$queueForUsage + ->setName('queueForUsage') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Usage($queue); + }); +$container->set($queueForUsage); + +$queueForCertificates = new Dependency(); +$queueForCertificates + ->setName('queueForCertificates') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Certificate($queue); + }); +$container->set($queueForCertificates); + +$queueForMigrations = new Dependency(); +$queueForMigrations + ->setName('queueForMigrations') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Migration($queue); + }); +$container->set($queueForMigrations); + +$clients = new Dependency(); +$clients + ->setName('clients') + ->inject('request') + ->inject('console') + ->inject('project') + ->setCallback(function (Request $request, Document $console, Document $project) { + $console->setAttribute('platforms', [ // Always allow current host + '$collection' => ID::custom('platforms'), + 'name' => 'Current Host', + 'type' => Origin::CLIENT_TYPE_WEB, + 'hostname' => $request->getHostname(), + ], Document::SET_TYPE_APPEND); + + $hostnames = explode(',', System::getEnv('_APP_CONSOLE_HOSTNAMES', '')); + $validator = new Hostname(); + foreach ($hostnames as $hostname) { + $hostname = trim($hostname); + if (!$validator->isValid($hostname)) { + continue; + } + $console->setAttribute('platforms', [ + '$collection' => ID::custom('platforms'), + 'type' => Origin::CLIENT_TYPE_WEB, + 'name' => $hostname, + 'hostname' => $hostname, + ], Document::SET_TYPE_APPEND); + } + + /** + * Get All verified client URLs for both console and current projects + * + Filter for duplicated entries + */ + $clientsConsole = \array_map( + fn ($node) => $node['hostname'], + \array_filter( + $console->getAttribute('platforms', []), + fn ($node) => (isset($node['type']) && ($node['type'] === Origin::CLIENT_TYPE_WEB) && isset($node['hostname']) && !empty($node['hostname'])) + ) + ); + + $clients = \array_unique( + \array_merge( + $clientsConsole, + \array_map( + fn ($node) => $node['hostname'], + \array_filter( + $project->getAttribute('platforms', []), + fn ($node) => (isset($node['type']) && ($node['type'] === Origin::CLIENT_TYPE_WEB || $node['type'] === Origin::CLIENT_TYPE_FLUTTER_WEB) && isset($node['hostname']) && !empty($node['hostname'])) + ) + ) + ) + ); + + return $clients; + }); +$container->set($clients); + +$geodb = new Dependency(); +$geodb + ->setName('geodb') + ->inject('registry') + ->setCallback(function (Registry $register) { + return $register->get('geodb'); + }); +$container->set($geodb); \ No newline at end of file