From d14edf4207f9721c64a1caae78ae8492629acf03 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:30:47 -0400 Subject: [PATCH 01/13] reverting: to set coroutines as head --- .github/workflows/codeql-phpstan.yml | 16 + app/cli.php | 256 +-- app/config/variables.php | 2 +- app/controllers/api/account.php | 581 +++--- app/controllers/api/avatars.php | 102 +- app/controllers/api/console.php | 12 +- app/controllers/api/databases.php | 427 ++-- app/controllers/api/functions.php | 211 +- app/controllers/api/graphql.php | 29 +- app/controllers/api/health.php | 132 +- app/controllers/api/locale.php | 20 +- app/controllers/api/messaging.php | 181 +- app/controllers/api/migrations.php | 54 +- app/controllers/api/project.php | 35 +- app/controllers/api/projects.php | 274 +-- app/controllers/api/proxy.php | 20 +- app/controllers/api/storage.php | 211 +- app/controllers/api/teams.php | 106 +- app/controllers/api/users.php | 111 +- app/controllers/api/vcs.php | 89 +- app/controllers/general.php | 263 ++- app/controllers/mock.php | 34 +- app/controllers/shared/api.php | 118 +- app/controllers/shared/api/auth.php | 22 +- app/controllers/web/console.php | 6 +- app/controllers/web/home.php | 4 +- app/http.php | 515 ++--- app/init.php | 1832 ++--------------- app/init/config.php | 37 + app/init/constants.php | 194 ++ app/init/database/filters.php | 397 ++++ app/init/database/formats.php | 43 + app/init/locale.php | 23 + app/init/resources.php | 1046 ++++++++++ app/realtime.php | 277 +-- app/views/install/compose.phtml | 5 +- app/worker.php | 363 +--- composer.json | 45 +- composer.lock | 622 +++--- docker-compose.yml | 6 +- docs/tutorials/add-route.md | 30 +- phpstan.neon | 11 + src/Appwrite/Auth/Auth.php | 36 +- src/Appwrite/Auth/Authentication.php | 57 + src/Appwrite/Auth/Validator/MockNumber.php | 5 +- src/Appwrite/Auth/Validator/Password.php | 2 +- src/Appwrite/Auth/Validator/Phone.php | 2 +- src/Appwrite/Event/Func.php | 4 +- src/Appwrite/Event/Validator/Event.php | 2 +- src/Appwrite/Functions/Validator/Headers.php | 2 +- src/Appwrite/Functions/Validator/Payload.php | 2 +- .../Validator/RuntimeSpecification.php | 2 +- src/Appwrite/GraphQL/Resolvers.php | 170 +- src/Appwrite/GraphQL/Schema.php | 119 +- src/Appwrite/GraphQL/Types/Mapper.php | 64 +- src/Appwrite/Migration/Migration.php | 36 +- src/Appwrite/Network/Validator/CNAME.php | 2 +- src/Appwrite/Network/Validator/Email.php | 4 +- src/Appwrite/Network/Validator/Origin.php | 4 +- src/Appwrite/Platform/Tasks/Doctor.php | 80 +- src/Appwrite/Platform/Tasks/Install.php | 11 +- src/Appwrite/Platform/Tasks/Migrate.php | 22 +- src/Appwrite/Platform/Tasks/QueueCount.php | 4 +- src/Appwrite/Platform/Tasks/QueueRetry.php | 4 +- src/Appwrite/Platform/Tasks/SSL.php | 4 +- src/Appwrite/Platform/Tasks/ScheduleBase.php | 123 +- .../Platform/Tasks/ScheduleExecutions.php | 30 +- .../Platform/Tasks/ScheduleFunctions.php | 17 +- .../Platform/Tasks/ScheduleMessages.php | 24 +- src/Appwrite/Platform/Tasks/Specs.php | 50 +- src/Appwrite/Platform/Tasks/Upgrade.php | 4 +- src/Appwrite/Platform/Workers/Audits.php | 6 +- src/Appwrite/Platform/Workers/Builds.php | 66 +- .../Platform/Workers/Certificates.php | 30 +- src/Appwrite/Platform/Workers/Deletes.php | 14 +- src/Appwrite/Platform/Workers/Mails.php | 6 +- src/Appwrite/Promises/Promise.php | 2 +- src/Appwrite/Promises/Swoole.php | 17 +- src/Appwrite/Specification/Format.php | 10 +- .../Specification/Format/OpenAPI3.php | 53 +- .../Specification/Format/Swagger2.php | 44 +- src/Appwrite/Task/Validator/Cron.php | 2 +- .../Utopia/Database/Validator/CompoundUID.php | 2 +- .../Utopia/Database/Validator/ProjectId.php | 2 +- src/Appwrite/Utopia/Queue/Connections.php | 36 + src/Appwrite/Utopia/Request.php | 36 +- src/Appwrite/Utopia/Response.php | 318 +-- src/Appwrite/Utopia/Response/Models.php | 304 +++ src/Appwrite/Utopia/View.php | 2 +- .../e2e/Services/Databases/DatabasesBase.php | 4 +- .../Databases/DatabasesCustomClientTest.php | 4 +- .../DatabasesPermissionsGuestTest.php | 20 +- .../e2e/Services/Functions/FunctionsBase.php | 5 +- .../Functions/FunctionsCustomServerTest.php | 6 +- tests/e2e/Services/GraphQL/Base.php | 2 +- .../Services/GraphQL/StorageClientTest.php | 1 - .../Services/GraphQL/StorageServerTest.php | 1 - .../Projects/ProjectsConsoleClientTest.php | 2 +- .../Realtime/RealtimeCustomClientTest.php | 5 +- .../Storage/StorageCustomClientTest.php | 4 +- .../Webhooks/WebhooksCustomServerTest.php | 5 +- tests/resources/docker/docker-compose.yml | 2 +- tests/unit/Auth/AuthTest.php | 35 +- tests/unit/Event/EventTest.php | 10 +- tests/unit/GraphQL/BuilderTest.php | 9 +- .../unit/Messaging/MessagingChannelsTest.php | 9 +- tests/unit/Migration/MigrationTest.php | 2 +- tests/unit/Utopia/RequestTest.php | 7 +- tests/unit/Utopia/ResponseTest.php | 10 +- 109 files changed, 5668 insertions(+), 5073 deletions(-) create mode 100644 .github/workflows/codeql-phpstan.yml create mode 100644 app/init/config.php create mode 100644 app/init/constants.php create mode 100644 app/init/database/filters.php create mode 100644 app/init/database/formats.php create mode 100644 app/init/locale.php create mode 100644 app/init/resources.php create mode 100644 phpstan.neon create mode 100644 src/Appwrite/Auth/Authentication.php create mode 100644 src/Appwrite/Utopia/Queue/Connections.php create mode 100644 src/Appwrite/Utopia/Response/Models.php diff --git a/.github/workflows/codeql-phpstan.yml b/.github/workflows/codeql-phpstan.yml new file mode 100644 index 0000000000..3253e2c38b --- /dev/null +++ b/.github/workflows/codeql-phpstan.yml @@ -0,0 +1,16 @@ +name: "CodeQL" + +on: [pull_request] +jobs: + lint: + name: CodeQL + runs-on: ubuntu-latest + + steps: + - name: Check out the repo + uses: actions/checkout@v2 + + - name: Run CodeQL + run: | + docker run --rm -v $PWD:/app composer sh -c \ + "composer install --profile --ignore-platform-reqs && composer check" \ No newline at end of file diff --git a/app/cli.php b/app/cli.php index 23502ec402..51a8e86137 100644 --- a/app/cli.php +++ b/app/cli.php @@ -7,195 +7,101 @@ use Appwrite\Event\Delete; use Appwrite\Event\Func; use Appwrite\Platform\Appwrite; use Appwrite\Runtimes\Runtimes; -use Utopia\Cache\Adapter\Sharding; -use Utopia\Cache\Cache; -use Utopia\CLI\CLI; +use Swoole\Runtime; +use Utopia\CLI\Adapters\Swoole as SwooleCLI; use Utopia\CLI\Console; use Utopia\Config\Config; -use Utopia\Database\Database; -use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; -use Utopia\DSN\DSN; +use Utopia\DI\Dependency; use Utopia\Logger\Log; use Utopia\Platform\Service; -use Utopia\Pools\Group; use Utopia\Queue\Connection; use Utopia\Registry\Registry; use Utopia\System\System; +global $registry, $container; + +Runtime::enableCoroutine(SWOOLE_HOOK_ALL); + // overwriting runtimes to be architectur agnostic for CLI Config::setParam('runtimes', (new Runtimes('v4'))->getAll(supported: false)); // require controllers after overwriting runtimes require_once __DIR__ . '/controllers/general.php'; -Authorization::disable(); +/** + * @var Registry $registry + * @var Container $container + */ +$context = new Dependency(); +$register = new Dependency(); +$logError = new Dependency(); +$queueForDeletes = new Dependency(); +$queueForFunctions = new Dependency(); +$queueForCertificates = new Dependency(); -CLI::setResource('register', fn () => $register); +$context + ->setName('context') + ->setCallback(fn () => $container); -CLI::setResource('cache', function ($pools) { - $list = Config::getParam('pools-cache', []); - $adapters = []; +$register + ->setName('register') + ->setCallback(function () use (&$registry): Registry { + return $registry; + }); - foreach ($list as $value) { - $adapters[] = $pools - ->get($value) - ->pop() - ->getResource() - ; - } +$queueForFunctions + ->setName('queueForFunctions') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Func($queue); + }); - return new Cache(new Sharding($adapters)); -}, ['pools']); -CLI::setResource('pools', function (Registry $register) { - return $register->get('pools'); -}, ['register']); +$queueForDeletes + ->setName('queueForDeletes') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Delete($queue); + }); -CLI::setResource('dbForConsole', function ($pools, $cache) { - $sleep = 3; - $maxAttempts = 5; - $attempts = 0; - $ready = false; +$queueForCertificates + ->setName('queueForCertificates') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Certificate($queue); + }); - do { - $attempts++; - try { - // Prepare database connection - $dbAdapter = $pools - ->get('console') - ->pop() - ->getResource(); +$logError + ->setName('logError') + ->inject('register') + ->setCallback(function (Registry $register) { + return function (Throwable $error, string $namespace, string $action) use ($register) { + $logger = $register->get('logger'); - $dbForConsole = new Database($dbAdapter, $cache); + if ($logger) { + $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); - $dbForConsole - ->setNamespace('_console') - ->setMetadata('host', \gethostname()) - ->setMetadata('project', 'console'); + $log = new Log(); + $log->setNamespace($namespace); + $log->setServer(\gethostname()); + $log->setVersion($version); + $log->setType(Log::TYPE_ERROR); + $log->setMessage($error->getMessage()); - // Ensure tables exist - $collections = Config::getParam('collections', [])['console']; - $last = \array_key_last($collections); + $log->addTag('code', $error->getCode()); + $log->addTag('verboseType', get_class($error)); - if (!($dbForConsole->exists($dbForConsole->getDatabase(), $last))) { /** TODO cache ready variable using registry */ - throw new Exception('Tables not ready yet.'); - } + $log->addExtra('file', $error->getFile()); + $log->addExtra('line', $error->getLine()); + $log->addExtra('trace', $error->getTraceAsString()); + $log->addExtra('trace', $error->getTraceAsString()); - $ready = true; - } catch (\Throwable $err) { - Console::warning($err->getMessage()); - $pools->get('console')->reclaim(); - sleep($sleep); - } - } while ($attempts < $maxAttempts && !$ready); + $log->setAction($action); - if (!$ready) { - throw new Exception("Console is not ready yet. Please try again later."); - } + $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; - return $dbForConsole; -}, ['pools', 'cache']); - -CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) { - $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools - - return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForConsole; - } - - 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()]; - - if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } - - return $database; - } - - $dbAdapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - - $database = new Database($dbAdapter, $cache); - - $databases[$dsn->getHost()] = $database; - - if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } - - $database - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()); - - return $database; - }; -}, ['pools', 'dbForConsole', 'cache']); - -CLI::setResource('queue', function (Group $pools) { - return $pools->get('queue')->pop()->getResource(); -}, ['pools']); -CLI::setResource('queueForFunctions', function (Connection $queue) { - return new Func($queue); -}, ['queue']); -CLI::setResource('queueForDeletes', function (Connection $queue) { - return new Delete($queue); -}, ['queue']); -CLI::setResource('queueForCertificates', function (Connection $queue) { - return new Certificate($queue); -}, ['queue']); -CLI::setResource('logError', function (Registry $register) { - return function (Throwable $error, string $namespace, string $action) use ($register) { - $logger = $register->get('logger'); - - if ($logger) { - $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); - - $log = new Log(); - $log->setNamespace($namespace); - $log->setServer(\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->addExtra('file', $error->getFile()); - $log->addExtra('line', $error->getLine()); - $log->addExtra('trace', $error->getTraceAsString()); - - $log->setAction($action); - - $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; - $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); + $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); try { $responseCode = $logger->addLog($log); @@ -203,18 +109,32 @@ CLI::setResource('logError', function (Registry $register) { } catch (Throwable $th) { Console::error('Error pushing log: ' . $th->getMessage()); } - } + } - Console::warning("Failed: {$error->getMessage()}"); - Console::warning($error->getTraceAsString()); - }; -}, ['register']); + Console::warning("Failed: {$error->getMessage()}"); + Console::warning($error->getTraceAsString()); + }; + }); + +$container->set($context); +$container->set($logError); +$container->set($register); +$container->set($queueForDeletes); +$container->set($queueForFunctions); +$container->set($queueForCertificates); $platform = new Appwrite(); -$platform->init(Service::TYPE_TASK); +$platform->init(Service::TYPE_TASK, ['adapter' => new SwooleCLI(1)]); $cli = $platform->getCli(); +$cli + ->init() + ->inject('authorization') + ->action(function (Authorization $authorization) { + $authorization->disable(); + }); + $cli ->error() ->inject('error') @@ -222,4 +142,6 @@ $cli Console::error($error->getMessage()); }); -$cli->run(); +$cli + ->setContainer($container) + ->run(); diff --git a/app/config/variables.php b/app/config/variables.php index 113fbae335..161951e3de 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -254,7 +254,7 @@ return [ 'name' => '_APP_WORKER_PER_CORE', 'description' => 'Internal Worker per core for the API, Realtime and Executor containers. Can be configured to optimize performance.', 'introduction' => '0.13.0', - 'default' => 6, + 'default' => 2, 'required' => false, 'question' => '', 'filter' => '' diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 64441fee5c..a76b9dbea5 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2,6 +2,7 @@ use Ahc\Jwt\JWT; use Appwrite\Auth\Auth; +use Appwrite\Auth\Authentication; use Appwrite\Auth\MFA\Challenge; use Appwrite\Auth\MFA\Type; use Appwrite\Auth\MFA\Type\TOTP; @@ -28,7 +29,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Identities; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; -use Utopia\App; use Utopia\Audit\Audit as EventAudit; use Utopia\Config\Config; use Utopia\Database\Database; @@ -45,15 +45,16 @@ use Utopia\Database\Validator\Queries; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Assoc; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Host; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\URL; +use Utopia\Http\Validator\WhiteList; use Utopia\Locale\Locale; use Utopia\System\System; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Assoc; -use Utopia\Validator\Boolean; -use Utopia\Validator\Host; -use Utopia\Validator\Text; -use Utopia\Validator\URL; -use Utopia\Validator\WhiteList; $oauthDefaultSuccess = '/console/auth/oauth2/success'; $oauthDefaultFailure = '/console/auth/oauth2/failure'; @@ -144,14 +145,13 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, Doc ->trigger(); }; - -$createSession = function (string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails) { - $roles = Authorization::getRoles(); +$createSession = function (string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Authorization $authorization, Authentication $authentication) { + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); /** @var Utopia\Database\Document $user */ - $userFromRequest = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId)); + $userFromRequest = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId)); if ($userFromRequest->isEmpty()) { throw new Exception(Exception::USER_INVALID_TOKEN); @@ -197,7 +197,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res $detector->getDevice() )); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $session = $dbForProject->createDocument('sessions', $session ->setAttribute('$permissions', [ @@ -206,7 +206,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res Permission::delete(Role::user($user->getId())), ])); - Authorization::skip(fn () => $dbForProject->deleteDocument('tokens', $verifiedToken->getId())); + $authorization->skip(fn () => $dbForProject->deleteDocument('tokens', $verifiedToken->getId())); $dbForProject->purgeCachedDocument('users', $user->getId()); // Magic URL + Email OTP @@ -247,15 +247,15 @@ $createSession = function (string $userId, string $secret, Request $request, Res ->setParam('sessionId', $session->getId()); if (!Config::getParam('domainVerification')) { - $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $sessionSecret)])); + $response->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $sessionSecret)])); } $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $protocol = $request->getProtocol(); $response - ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED); $countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')); @@ -270,7 +270,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res $response->dynamic($session, Response::MODEL_SESSION); }; -App::post('/v1/account') +Http::post('/v1/account') ->desc('Create account') ->groups(['api', 'account', 'auth']) ->label('event', 'users.[userId].create') @@ -298,7 +298,8 @@ App::post('/v1/account') ->inject('dbForProject') ->inject('queueForEvents') ->inject('hooks') - ->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { + ->inject('authorization') + ->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks, Authorization $authorization) { $email = \strtolower($email); if ('console' === $project->getId()) { @@ -376,9 +377,9 @@ App::post('/v1/account') 'accessedAt' => DateTime::now(), ]); $user->removeAttribute('$internalId'); - $user = Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); try { - $target = Authorization::skip(fn () => $dbForProject->createDocument('targets', new Document([ + $target = $authorization->skip(fn () => $dbForProject->createDocument('targets', new Document([ '$permissions' => [ Permission::read(Role::user($user->getId())), Permission::update(Role::user($user->getId())), @@ -404,9 +405,9 @@ App::post('/v1/account') throw new Exception(Exception::USER_ALREADY_EXISTS); } - Authorization::unsetRole(Role::guests()->toString()); - Authorization::setRole(Role::user($user->getId())->toString()); - Authorization::setRole(Role::users()->toString()); + $authorization->removeRole(Role::guests()->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::users()->toString()); $queueForEvents->setParam('userId', $user->getId()); @@ -415,7 +416,7 @@ App::post('/v1/account') ->dynamic($user, Response::MODEL_ACCOUNT); }); -App::get('/v1/account') +Http::get('/v1/account') ->desc('Get account') ->groups(['api', 'account']) ->label('scope', 'account') @@ -438,7 +439,7 @@ App::get('/v1/account') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::delete('/v1/account') +Http::delete('/v1/account') ->desc('Delete account') ->groups(['api', 'account']) ->label('event', 'users.[userId].delete') @@ -486,7 +487,7 @@ App::delete('/v1/account') $response->noContent(); }); -App::get('/v1/account/sessions') +Http::get('/v1/account/sessions') ->desc('List sessions') ->groups(['api', 'account']) ->label('scope', 'account') @@ -501,15 +502,16 @@ App::get('/v1/account/sessions') ->inject('response') ->inject('user') ->inject('locale') - ->inject('project') - ->action(function (Response $response, Document $user, Locale $locale, Document $project) { + ->inject('authorization') + ->inject('authentication') + ->action(function (Response $response, Document $user, Locale $locale, Authorization $authorization, Authentication $authentication) { - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); $sessions = $user->getAttribute('sessions', []); - $current = Auth::sessionVerify($sessions, Auth::$secret); + $current = Auth::sessionVerify($sessions, $authentication->getSecret()); foreach ($sessions as $key => $session) {/** @var Document $session */ $countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')); @@ -527,7 +529,7 @@ App::get('/v1/account/sessions') ]), Response::MODEL_SESSION_LIST); }); -App::delete('/v1/account/sessions') +Http::delete('/v1/account/sessions') ->desc('Delete sessions') ->groups(['api', 'account']) ->label('scope', 'account') @@ -548,7 +550,8 @@ App::delete('/v1/account/sessions') ->inject('locale') ->inject('queueForEvents') ->inject('queueForDeletes') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes) { + ->inject('authentication') + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Authentication $authentication) { $protocol = $request->getProtocol(); $sessions = $user->getAttribute('sessions', []); @@ -564,13 +567,13 @@ App::delete('/v1/account/sessions') ->setAttribute('current', false) ->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'))); - if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { + if ($session->getAttribute('secret') == Auth::hash($authentication->getSecret())) { $session->setAttribute('current', true); // If current session delete the cookies too $response - ->addCookie(Auth::$cookieName . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie($authentication->getCookieName() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); // Use current session for events. $queueForEvents @@ -592,7 +595,7 @@ App::delete('/v1/account/sessions') $response->noContent(); }); -App::get('/v1/account/sessions/:sessionId') +Http::get('/v1/account/sessions/:sessionId') ->desc('Get session') ->groups(['api', 'account']) ->label('scope', 'account') @@ -609,16 +612,17 @@ App::get('/v1/account/sessions/:sessionId') ->inject('response') ->inject('user') ->inject('locale') - ->inject('project') - ->action(function (?string $sessionId, Response $response, Document $user, Locale $locale, Document $project) { + ->inject('authorization') + ->inject('authentication') + ->action(function (?string $sessionId, Response $response, Document $user, Locale $locale, Authorization $authorization, Authentication $authentication) { - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); $sessions = $user->getAttribute('sessions', []); $sessionId = ($sessionId === 'current') - ? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret) + ? Auth::sessionVerify($user->getAttribute('sessions'), $authentication->getSecret()) : $sessionId; foreach ($sessions as $session) {/** @var Document $session */ @@ -626,7 +630,7 @@ App::get('/v1/account/sessions/:sessionId') $countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')); $session - ->setAttribute('current', ($session->getAttribute('secret') == Auth::hash(Auth::$secret))) + ->setAttribute('current', ($session->getAttribute('secret') == Auth::hash($authentication->getSecret()))) ->setAttribute('countryName', $countryName) ->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $session->getAttribute('secret', '') : '') ; @@ -638,7 +642,7 @@ App::get('/v1/account/sessions/:sessionId') throw new Exception(Exception::USER_SESSION_NOT_FOUND); }); -App::delete('/v1/account/sessions/:sessionId') +Http::delete('/v1/account/sessions/:sessionId') ->desc('Delete session') ->groups(['api', 'account', 'mfa']) ->label('scope', 'account') @@ -661,12 +665,12 @@ App::delete('/v1/account/sessions/:sessionId') ->inject('locale') ->inject('queueForEvents') ->inject('queueForDeletes') - ->inject('project') - ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Document $project) { + ->inject('authentication') + ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Authentication $authentication) { $protocol = $request->getProtocol(); $sessionId = ($sessionId === 'current') - ? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret) + ? Auth::sessionVerify($user->getAttribute('sessions'), $authentication->getSecret()) : $sessionId; $sessions = $user->getAttribute('sessions', []); @@ -685,7 +689,7 @@ App::delete('/v1/account/sessions/:sessionId') $session->setAttribute('current', false); - if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + if ($session->getAttribute('secret') == Auth::hash($authentication->getSecret())) { // If current session delete the cookies too $session ->setAttribute('current', true) ->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'))); @@ -695,8 +699,8 @@ App::delete('/v1/account/sessions/:sessionId') } $response - ->addCookie(Auth::$cookieName . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie($authentication->getCookieName() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); } $dbForProject->purgeCachedDocument('users', $user->getId()); @@ -718,7 +722,7 @@ App::delete('/v1/account/sessions/:sessionId') throw new Exception(Exception::USER_SESSION_NOT_FOUND); }); -App::patch('/v1/account/sessions/:sessionId') +Http::patch('/v1/account/sessions/:sessionId') ->desc('Update session') ->groups(['api', 'account']) ->label('scope', 'account') @@ -740,10 +744,11 @@ App::patch('/v1/account/sessions/:sessionId') ->inject('dbForProject') ->inject('project') ->inject('queueForEvents') - ->action(function (?string $sessionId, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents) { + ->inject('authentication') + ->action(function (?string $sessionId, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents, Authentication $authentication) { $sessionId = ($sessionId === 'current') - ? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret) + ? Auth::sessionVerify($user->getAttribute('sessions'), $authentication->getSecret()) : $sessionId; $sessions = $user->getAttribute('sessions', []); @@ -794,7 +799,7 @@ App::patch('/v1/account/sessions/:sessionId') return $response->dynamic($session, Response::MODEL_SESSION); }); -App::post('/v1/account/sessions/email') +Http::post('/v1/account/sessions/email') ->alias('/v1/account/sessions') ->desc('Create email password session') ->groups(['api', 'account', 'auth', 'session']) @@ -825,7 +830,9 @@ App::post('/v1/account/sessions/email') ->inject('queueForEvents') ->inject('queueForMails') ->inject('hooks') - ->action(function (string $email, string $password, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks) { + ->inject('authorization') + ->inject('authentication') + ->action(function (string $email, string $password, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Authorization $authorization, Authentication $authentication) { $email = \strtolower($email); $protocol = $request->getProtocol(); @@ -841,7 +848,7 @@ App::post('/v1/account/sessions/email') throw new Exception(Exception::USER_BLOCKED); // User is in status blocked } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -872,7 +879,7 @@ App::post('/v1/account/sessions/email') $detector->getDevice() )); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); // Re-hash if not using recommended algo if ($user->getAttribute('hash') !== Auth::DEFAULT_ALGO) { @@ -893,15 +900,15 @@ App::post('/v1/account/sessions/email') if (!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) + ->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $secret)])) ; } $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $response - ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED) ; @@ -929,7 +936,7 @@ App::post('/v1/account/sessions/email') $response->dynamic($session, Response::MODEL_SESSION); }); -App::post('/v1/account/sessions/anonymous') +Http::post('/v1/account/sessions/anonymous') ->desc('Create anonymous session') ->groups(['api', 'account', 'auth', 'session']) ->label('event', 'users.[userId].sessions.[sessionId].create') @@ -955,9 +962,11 @@ App::post('/v1/account/sessions/anonymous') ->inject('dbForProject') ->inject('geodb') ->inject('queueForEvents') - ->action(function (Request $request, Response $response, Locale $locale, Document $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents) { + ->inject('authorization') + ->inject('authentication') + ->action(function (Request $request, Response $response, Locale $locale, Document $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Authorization $authorization, Authentication $authentication) { $protocol = $request->getProtocol(); - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -1003,7 +1012,7 @@ App::post('/v1/account/sessions/anonymous') 'accessedAt' => DateTime::now(), ]); $user->removeAttribute('$internalId'); - Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); // Create session token $duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; @@ -1029,7 +1038,7 @@ App::post('/v1/account/sessions/anonymous') $detector->getDevice() )); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $session = $dbForProject->createDocument('sessions', $session-> setAttribute('$permissions', [ Permission::read(Role::user($user->getId())), @@ -1045,14 +1054,14 @@ App::post('/v1/account/sessions/anonymous') ; if (!Config::getParam('domainVerification')) { - $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])); + $response->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $secret)])); } $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $response - ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED) ; @@ -1067,7 +1076,7 @@ App::post('/v1/account/sessions/anonymous') $response->dynamic($session, Response::MODEL_SESSION); }); -App::post('/v1/account/sessions/token') +Http::post('/v1/account/sessions/token') ->desc('Create session') ->label('event', 'users.[userId].sessions.[sessionId].create') ->groups(['api', 'account', 'session']) @@ -1095,9 +1104,11 @@ App::post('/v1/account/sessions/token') ->inject('geodb') ->inject('queueForEvents') ->inject('queueForMails') + ->inject('authorization') + ->inject('authentication') ->action($createSession); -App::get('/v1/account/sessions/oauth2/:provider') +Http::get('/v1/account/sessions/oauth2/:provider') ->desc('Create OAuth2 session') ->groups(['api', 'account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -1167,7 +1178,7 @@ App::get('/v1/account/sessions/oauth2/:provider') ->redirect($oauth2->getLoginURL()); }); -App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId') +Http::get('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->desc('OAuth2 callback') ->groups(['account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -1197,7 +1208,7 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId') . \http_build_query($params)); }); -App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') +Http::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->desc('OAuth2 callback') ->groups(['account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -1228,7 +1239,7 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') . \http_build_query($params)); }); -App::get('/v1/account/sessions/oauth2/:provider/redirect') +Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->desc('OAuth2 redirect') ->groups(['api', 'account', 'session']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -1252,7 +1263,9 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->inject('dbForProject') ->inject('geodb') ->inject('queueForEvents') - ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $queueForEvents) use ($oauthDefaultSuccess) { + ->inject('authorization') + ->inject('authentication') + ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Authorization $authorization, Authentication $authentication) use ($oauthDefaultSuccess) { $protocol = $request->getProtocol(); $callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId(); $defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => '']; @@ -1389,7 +1402,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') } $sessions = $user->getAttribute('sessions', []); - $current = Auth::sessionVerify($sessions, Auth::$secret); + $current = Auth::sessionVerify($sessions, $authentication->getSecret()); if ($current) { // Delete current session of new one. $currentDocument = $dbForProject->getDocument('sessions', $current); @@ -1486,15 +1499,16 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'accessedAt' => DateTime::now(), ]); $user->removeAttribute('$internalId'); - $userDoc = Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); + $dbForProject->createDocument('targets', new Document([ '$permissions' => [ Permission::read(Role::user($user->getId())), Permission::update(Role::user($user->getId())), Permission::delete(Role::user($user->getId())), ], - 'userId' => $userDoc->getId(), - 'userInternalId' => $userDoc->getInternalId(), + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), 'providerType' => MESSAGE_TYPE_EMAIL, 'identifier' => $email, ])); @@ -1504,8 +1518,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') } } - Authorization::setRole(Role::user($user->getId())->toString()); - Authorization::setRole(Role::users()->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::users()->toString()); if (false === $user->getAttribute('status')) { // Account is blocked $failureRedirect(Exception::USER_BLOCKED); // User is in status blocked @@ -1564,7 +1578,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $dbForProject->updateDocument('users', $user->getId(), $user); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $state['success'] = URLParser::parse($state['success']); $query = URLParser::parseQuery($state['success']['query']); @@ -1586,7 +1600,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'ip' => $request->getIP(), ]); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $token = $dbForProject->createDocument('tokens', $token ->setAttribute('$permissions', [ @@ -1636,7 +1650,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $session->setAttribute('expire', $expire); if (!Config::getParam('domainVerification')) { - $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])); + $response->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $secret)])); } $queueForEvents @@ -1649,13 +1663,13 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') if ($state['success']['path'] == $oauthDefaultSuccess) { $query['project'] = $project->getId(); $query['domain'] = Config::getParam('cookieDomain'); - $query['key'] = Auth::$cookieName; + $query['key'] = $authentication->getCookieName(); $query['secret'] = Auth::encodeSession($user->getId(), $secret); } $response - ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); } if (isset($sessionUpgrade) && $sessionUpgrade) { @@ -1686,7 +1700,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ; }); -App::get('/v1/account/tokens/oauth2/:provider') +Http::get('/v1/account/tokens/oauth2/:provider') ->desc('Create OAuth2 token') ->groups(['api', 'account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -1755,7 +1769,7 @@ App::get('/v1/account/tokens/oauth2/:provider') ->redirect($oauth2->getLoginURL()); }); -App::post('/v1/account/tokens/magic-url') +Http::post('/v1/account/tokens/magic-url') ->alias('/v1/account/sessions/magic-url') ->desc('Create magic URL token') ->groups(['api', 'account', 'auth']) @@ -1785,7 +1799,8 @@ App::post('/v1/account/tokens/magic-url') ->inject('locale') ->inject('queueForEvents') ->inject('queueForMails') - ->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) { + ->inject('authorization') + ->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, Authorization $authorization) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); } @@ -1795,7 +1810,7 @@ App::post('/v1/account/tokens/magic-url') $phrase = Phrase::generate(); } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -1850,7 +1865,7 @@ App::post('/v1/account/tokens/magic-url') ]); $user->removeAttribute('$internalId'); - Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); } $tokenSecret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_MAGIC_URL); @@ -1867,7 +1882,7 @@ App::post('/v1/account/tokens/magic-url') 'ip' => $request->getIP(), ]); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $token = $dbForProject->createDocument('tokens', $token ->setAttribute('$permissions', [ @@ -1999,7 +2014,7 @@ App::post('/v1/account/tokens/magic-url') ->dynamic($token, Response::MODEL_TOKEN); }); -App::post('/v1/account/tokens/email') +Http::post('/v1/account/tokens/email') ->desc('Create email token (OTP)') ->groups(['api', 'account', 'auth']) ->label('scope', 'sessions.write') @@ -2027,7 +2042,8 @@ App::post('/v1/account/tokens/email') ->inject('locale') ->inject('queueForEvents') ->inject('queueForMails') - ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) { + ->inject('authorization') + ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, Authorization $authorization) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); } @@ -2036,7 +2052,7 @@ App::post('/v1/account/tokens/email') $phrase = Phrase::generate(); } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -2089,7 +2105,7 @@ App::post('/v1/account/tokens/email') ]); $user->removeAttribute('$internalId'); - Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); } $tokenSecret = Auth::codeGenerator(6); @@ -2106,7 +2122,7 @@ App::post('/v1/account/tokens/email') 'ip' => $request->getIP(), ]); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $token = $dbForProject->createDocument('tokens', $token ->setAttribute('$permissions', [ @@ -2228,7 +2244,7 @@ App::post('/v1/account/tokens/email') ->dynamic($token, Response::MODEL_TOKEN); }); -App::put('/v1/account/sessions/magic-url') +Http::put('/v1/account/sessions/magic-url') ->desc('Update magic URL session') ->label('event', 'users.[userId].sessions.[sessionId].create') ->groups(['api', 'account', 'session']) @@ -2257,9 +2273,11 @@ App::put('/v1/account/sessions/magic-url') ->inject('geodb') ->inject('queueForEvents') ->inject('queueForMails') + ->inject('authorization') + ->inject('authentication') ->action($createSession); -App::put('/v1/account/sessions/phone') +Http::put('/v1/account/sessions/phone') ->desc('Update phone session') ->label('event', 'users.[userId].sessions.[sessionId].create') ->groups(['api', 'account', 'session']) @@ -2288,9 +2306,11 @@ App::put('/v1/account/sessions/phone') ->inject('geodb') ->inject('queueForEvents') ->inject('queueForMails') + ->inject('authorization') + ->inject('authentication') ->action($createSession); -App::post('/v1/account/tokens/phone') +Http::post('/v1/account/tokens/phone') ->alias('/v1/account/sessions/phone') ->desc('Create phone token') ->groups(['api', 'account']) @@ -2318,12 +2338,13 @@ App::post('/v1/account/tokens/phone') ->inject('queueForEvents') ->inject('queueForMessaging') ->inject('locale') - ->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale) { + ->inject('authorization') + ->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, Authorization $authorization) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -2367,9 +2388,9 @@ App::post('/v1/account/tokens/phone') ]); $user->removeAttribute('$internalId'); - Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); try { - $target = Authorization::skip(fn () => $dbForProject->createDocument('targets', new Document([ + $target = $authorization->skip(fn () => $dbForProject->createDocument('targets', new Document([ '$permissions' => [ Permission::read(Role::user($user->getId())), Permission::update(Role::user($user->getId())), @@ -2415,7 +2436,7 @@ App::post('/v1/account/tokens/phone') 'ip' => $request->getIP(), ]); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $token = $dbForProject->createDocument('tokens', $token ->setAttribute('$permissions', [ @@ -2471,7 +2492,7 @@ App::post('/v1/account/tokens/phone') ->dynamic($token, Response::MODEL_TOKEN); }); -App::post('/v1/account/jwts') +Http::post('/v1/account/jwts') ->alias('/v1/account/jwt') ->desc('Create JWT') ->groups(['api', 'account', 'auth']) @@ -2489,14 +2510,15 @@ App::post('/v1/account/jwts') ->inject('response') ->inject('user') ->inject('dbForProject') - ->action(function (Response $response, Document $user, Database $dbForProject) { + ->inject('authentication') + ->action(function (Response $response, Document $user, Database $dbForProject, Authentication $authentication) { $sessions = $user->getAttribute('sessions', []); $current = new Document(); foreach ($sessions as $session) { /** @var Utopia\Database\Document $session */ - if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + if ($session->getAttribute('secret') == Auth::hash($authentication->getSecret())) { // If current session delete the cookies too $current = $session; } } @@ -2515,7 +2537,7 @@ App::post('/v1/account/jwts') ])]), Response::MODEL_JWT); }); -App::get('/v1/account/prefs') +Http::get('/v1/account/prefs') ->desc('Get account preferences') ->groups(['api', 'account']) ->label('scope', 'account') @@ -2537,7 +2559,7 @@ App::get('/v1/account/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -App::get('/v1/account/logs') +Http::get('/v1/account/logs') ->desc('List logs') ->groups(['api', 'account']) ->label('scope', 'account') @@ -2554,7 +2576,8 @@ App::get('/v1/account/logs') ->inject('locale') ->inject('geodb') ->inject('dbForProject') - ->action(function (array $queries, Response $response, Document $user, Locale $locale, Reader $geodb, Database $dbForProject) { + ->inject('authorization') + ->action(function (array $queries, Response $response, Document $user, Locale $locale, Reader $geodb, Database $dbForProject, Authorization $authorization) { try { $queries = Query::parseQueries($queries); @@ -2566,7 +2589,7 @@ App::get('/v1/account/logs') $limit = $grouped['limit'] ?? APP_LIMIT_COUNT; $offset = $grouped['offset'] ?? 0; - $audit = new EventAudit($dbForProject); + $audit = new EventAudit($dbForProject, $authorization); $logs = $audit->getLogsByUser($user->getInternalId(), $limit, $offset); @@ -2602,7 +2625,101 @@ App::get('/v1/account/logs') ]), Response::MODEL_LOG_LIST); }); -App::patch('/v1/account/name') +Http::patch('/v1/account/email') + ->desc('Update email') + ->groups(['api', 'account']) + ->label('event', 'users.[userId].update.email') + ->label('scope', 'account') + ->label('audits.event', 'user.update') + ->label('audits.resource', 'user/{response.$id}') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'updateEmail') + ->label('sdk.description', '/docs/references/account/update-email.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) + ->label('sdk.offline.model', '/account') + ->label('sdk.offline.key', 'current') + ->param('email', '', new Email(), 'User email.') + ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') + ->inject('requestTimestamp') + ->inject('response') + ->inject('user') + ->inject('dbForProject') + ->inject('queueForEvents') + ->inject('project') + ->inject('hooks') + ->inject('authorization') + ->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, Authorization $authorization) { + // passwordUpdate will be empty if the user has never set a password + $passwordUpdate = $user->getAttribute('passwordUpdate'); + + if ( + !empty($passwordUpdate) && + !Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions')) + ) { // Double check user password + throw new Exception(Exception::USER_INVALID_CREDENTIALS); + } + + $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]); + + $oldEmail = $user->getAttribute('email'); + + $email = \strtolower($email); + + // Makes sure this email is not already used in another identity + $identityWithMatchingEmail = $dbForProject->findOne('identities', [ + Query::equal('providerEmail', [$email]), + Query::notEqual('userInternalId', $user->getInternalId()), + ]); + if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ + } + + $user + ->setAttribute('email', $email) + ->setAttribute('emailVerification', false) // After this user needs to confirm mail again + ; + + if (empty($passwordUpdate)) { + $user + ->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS)) + ->setAttribute('hash', Auth::DEFAULT_ALGO) + ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS) + ->setAttribute('passwordUpdate', DateTime::now()); + } + + $target = $authorization->skip(fn () => $dbForProject->findOne('targets', [ + Query::equal('identifier', [$email]), + ])); + + if ($target instanceof Document && !$target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + + try { + $user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user)); + /** + * @var Document $oldTarget + */ + $oldTarget = $user->find('identifier', $oldEmail, 'targets'); + + if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { + $authorization->skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email))); + } + $dbForProject->purgeCachedDocument('users', $user->getId()); + } catch (Duplicate) { + throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ + } + + $queueForEvents->setParam('userId', $user->getId()); + + $response->dynamic($user, Response::MODEL_ACCOUNT); + }); + + +Http::patch('/v1/account/name') ->desc('Update name') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.name') @@ -2635,7 +2752,7 @@ App::patch('/v1/account/name') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::patch('/v1/account/password') +Http::patch('/v1/account/password') ->desc('Update password') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.password') @@ -2705,99 +2822,7 @@ App::patch('/v1/account/password') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::patch('/v1/account/email') - ->desc('Update email') - ->groups(['api', 'account']) - ->label('event', 'users.[userId].update.email') - ->label('scope', 'account') - ->label('audits.event', 'user.update') - ->label('audits.resource', 'user/{response.$id}') - ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) - ->label('sdk.namespace', 'account') - ->label('sdk.method', 'updateEmail') - ->label('sdk.description', '/docs/references/account/update-email.md') - ->label('sdk.response.code', Response::STATUS_CODE_OK) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_USER) - ->label('sdk.offline.model', '/account') - ->label('sdk.offline.key', 'current') - ->param('email', '', new Email(), 'User email.') - ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') - ->inject('requestTimestamp') - ->inject('response') - ->inject('user') - ->inject('dbForProject') - ->inject('queueForEvents') - ->inject('project') - ->inject('hooks') - ->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks) { - // passwordUpdate will be empty if the user has never set a password - $passwordUpdate = $user->getAttribute('passwordUpdate'); - - if ( - !empty($passwordUpdate) && - !Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions')) - ) { // Double check user password - throw new Exception(Exception::USER_INVALID_CREDENTIALS); - } - - $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]); - - $oldEmail = $user->getAttribute('email'); - - $email = \strtolower($email); - - // Makes sure this email is not already used in another identity - $identityWithMatchingEmail = $dbForProject->findOne('identities', [ - Query::equal('providerEmail', [$email]), - Query::notEqual('userInternalId', $user->getInternalId()), - ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { - throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ - } - - $user - ->setAttribute('email', $email) - ->setAttribute('emailVerification', false) // After this user needs to confirm mail again - ; - - if (empty($passwordUpdate)) { - $user - ->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS)) - ->setAttribute('hash', Auth::DEFAULT_ALGO) - ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS) - ->setAttribute('passwordUpdate', DateTime::now()); - } - - $target = Authorization::skip(fn () => $dbForProject->findOne('targets', [ - Query::equal('identifier', [$email]), - ])); - - if ($target instanceof Document && !$target->isEmpty()) { - throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); - } - - try { - $user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user)); - /** - * @var Document $oldTarget - */ - $oldTarget = $user->find('identifier', $oldEmail, 'targets'); - - if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { - Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email))); - } - $dbForProject->purgeCachedDocument('users', $user->getId()); - } catch (Duplicate) { - throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ - } - - $queueForEvents->setParam('userId', $user->getId()); - - $response->dynamic($user, Response::MODEL_ACCOUNT); - }); - -App::patch('/v1/account/phone') +Http::patch('/v1/account/phone') ->desc('Update phone') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.phone') @@ -2822,7 +2847,8 @@ App::patch('/v1/account/phone') ->inject('queueForEvents') ->inject('project') ->inject('hooks') - ->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks) { + ->inject('authorization') + ->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, Authorization $authorization) { // passwordUpdate will be empty if the user has never set a password $passwordUpdate = $user->getAttribute('passwordUpdate'); @@ -2835,7 +2861,7 @@ App::patch('/v1/account/phone') $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]); - $target = Authorization::skip(fn () => $dbForProject->findOne('targets', [ + $target = $authorization->skip(fn () => $dbForProject->findOne('targets', [ Query::equal('identifier', [$phone]), ])); @@ -2866,7 +2892,7 @@ App::patch('/v1/account/phone') $oldTarget = $user->find('identifier', $oldPhone, 'targets'); if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { - Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone))); + $authorization->skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone))); } $dbForProject->purgeCachedDocument('users', $user->getId()); } catch (Duplicate $th) { @@ -2878,7 +2904,7 @@ App::patch('/v1/account/phone') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::patch('/v1/account/prefs') +Http::patch('/v1/account/prefs') ->desc('Update preferences') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.prefs') @@ -2911,7 +2937,7 @@ App::patch('/v1/account/prefs') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::patch('/v1/account/status') +Http::patch('/v1/account/status') ->desc('Update status') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.status') @@ -2931,7 +2957,8 @@ App::patch('/v1/account/status') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { + ->inject('authentication') + ->action(function (?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authentication $authentication) { $user->setAttribute('status', false); @@ -2947,14 +2974,14 @@ App::patch('/v1/account/status') $protocol = $request->getProtocol(); $response - ->addCookie(Auth::$cookieName . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($authentication->getCookieName() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ; $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::post('/v1/account/recovery') +Http::post('/v1/account/recovery') ->desc('Create password recovery') ->groups(['api', 'account']) ->label('scope', 'sessions.write') @@ -2981,14 +3008,15 @@ App::post('/v1/account/recovery') ->inject('locale') ->inject('queueForMails') ->inject('queueForEvents') - ->action(function (string $email, string $url, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Mail $queueForMails, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $email, string $url, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Mail $queueForMails, Event $queueForEvents, Authorization $authorization) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); } $url = htmlentities($url); - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -3022,7 +3050,7 @@ App::post('/v1/account/recovery') 'ip' => $request->getIP(), ]); - Authorization::setRole(Role::user($profile->getId())->toString()); + $authorization->addRole(Role::user($profile->getId())->toString()); $recovery = $dbForProject->createDocument('tokens', $recovery ->setAttribute('$permissions', [ @@ -3044,7 +3072,7 @@ App::post('/v1/account/recovery') $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); $message - ->setParam('{{body}}', $body, escapeHtml: false) + ->setParam('{{body}}', $body, escape: false) ->setParam('{{hello}}', $locale->getText("emails.recovery.hello")) ->setParam('{{footer}}', $locale->getText("emails.recovery.footer")) ->setParam('{{thanks}}', $locale->getText("emails.recovery.thanks")) @@ -3134,7 +3162,7 @@ App::post('/v1/account/recovery') ->dynamic($recovery, Response::MODEL_TOKEN); }); -App::put('/v1/account/recovery') +Http::put('/v1/account/recovery') ->desc('Create password recovery (confirmation)') ->groups(['api', 'account']) ->label('scope', 'sessions.write') @@ -3160,7 +3188,8 @@ App::put('/v1/account/recovery') ->inject('project') ->inject('queueForEvents') ->inject('hooks') - ->action(function (string $userId, string $secret, string $password, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks) { + ->inject('authorization') + ->action(function (string $userId, string $secret, string $password, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks, Authorization $authorization) { $profile = $dbForProject->getDocument('users', $userId); if ($profile->isEmpty()) { @@ -3174,7 +3203,7 @@ App::put('/v1/account/recovery') throw new Exception(Exception::USER_INVALID_TOKEN); } - Authorization::setRole(Role::user($profile->getId())->toString()); + $authorization->addRole(Role::user($profile->getId())->toString()); $newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS); @@ -3219,7 +3248,7 @@ App::put('/v1/account/recovery') $response->dynamic($recoveryDocument, Response::MODEL_TOKEN); }); -App::post('/v1/account/verification') +Http::post('/v1/account/verification') ->desc('Create email verification') ->groups(['api', 'account']) ->label('scope', 'account') @@ -3244,7 +3273,8 @@ App::post('/v1/account/verification') ->inject('locale') ->inject('queueForEvents') ->inject('queueForMails') - ->action(function (string $url, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) { + ->inject('authorization') + ->action(function (string $url, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, Authorization $authorization) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); @@ -3255,7 +3285,7 @@ App::post('/v1/account/verification') throw new Exception(Exception::USER_EMAIL_ALREADY_VERIFIED); } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); $verificationSecret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_VERIFICATION); @@ -3272,7 +3302,7 @@ App::post('/v1/account/verification') 'ip' => $request->getIP(), ]); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $verification = $dbForProject->createDocument('tokens', $verification ->setAttribute('$permissions', [ @@ -3294,7 +3324,7 @@ App::post('/v1/account/verification') $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); $message - ->setParam('{{body}}', $body, escapeHtml: false) + ->setParam('{{body}}', $body, escape: false) ->setParam('{{hello}}', $locale->getText("emails.verification.hello")) ->setParam('{{footer}}', $locale->getText("emails.verification.footer")) ->setParam('{{thanks}}', $locale->getText("emails.verification.thanks")) @@ -3384,7 +3414,7 @@ App::post('/v1/account/verification') ->dynamic($verification, Response::MODEL_TOKEN); }); -App::put('/v1/account/verification') +Http::put('/v1/account/verification') ->desc('Create email verification (confirmation)') ->groups(['api', 'account']) ->label('scope', 'public') @@ -3406,9 +3436,10 @@ App::put('/v1/account/verification') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { - $profile = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId)); + $profile = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId)); if ($profile->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); @@ -3421,7 +3452,7 @@ App::put('/v1/account/verification') throw new Exception(Exception::USER_INVALID_TOKEN); } - Authorization::setRole(Role::user($profile->getId())->toString()); + $authorization->addRole(Role::user($profile->getId())->toString()); $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true)); @@ -3443,7 +3474,7 @@ App::put('/v1/account/verification') $response->dynamic($verification, Response::MODEL_TOKEN); }); -App::post('/v1/account/verification/phone') +Http::post('/v1/account/verification/phone') ->desc('Create phone verification') ->groups(['api', 'account', 'auth']) ->label('scope', 'account') @@ -3468,7 +3499,8 @@ App::post('/v1/account/verification/phone') ->inject('queueForMessaging') ->inject('project') ->inject('locale') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale) { + ->inject('authorization') + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, Authorization $authorization) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } @@ -3482,7 +3514,7 @@ App::post('/v1/account/verification/phone') throw new Exception(Exception::USER_PHONE_ALREADY_VERIFIED); } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -3511,7 +3543,7 @@ App::post('/v1/account/verification/phone') 'ip' => $request->getIP(), ]); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $verification = $dbForProject->createDocument('tokens', $verification ->setAttribute('$permissions', [ @@ -3571,7 +3603,7 @@ App::post('/v1/account/verification/phone') ->dynamic($verification, Response::MODEL_TOKEN); }); -App::put('/v1/account/verification/phone') +Http::put('/v1/account/verification/phone') ->desc('Update phone verification (confirmation)') ->groups(['api', 'account']) ->label('scope', 'public') @@ -3593,9 +3625,10 @@ App::put('/v1/account/verification/phone') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { - $profile = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId)); + $profile = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId)); if ($profile->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); @@ -3607,7 +3640,7 @@ App::put('/v1/account/verification/phone') throw new Exception(Exception::USER_INVALID_TOKEN); } - Authorization::setRole(Role::user($profile->getId())->toString()); + $authorization->addRole(Role::user($profile->getId())->toString()); $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true)); @@ -3629,7 +3662,7 @@ App::put('/v1/account/verification/phone') $response->dynamic($verificationDocument, Response::MODEL_TOKEN); }); -App::patch('/v1/account/mfa') +Http::patch('/v1/account/mfa') ->desc('Update MFA') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.mfa') @@ -3682,8 +3715,8 @@ App::patch('/v1/account/mfa') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::get('/v1/account/mfa/factors') - ->desc('List factors') +Http::get('/v1/account/mfa/factors') + ->desc('List Factors') ->groups(['api', 'account', 'mfa']) ->label('scope', 'account') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) @@ -3714,8 +3747,8 @@ App::get('/v1/account/mfa/factors') $response->dynamic($factors, Response::MODEL_MFA_FACTORS); }); -App::post('/v1/account/mfa/authenticators/:type') - ->desc('Create authenticator') +Http::post('/v1/account/mfa/authenticators/:type') + ->desc('Create Authenticator') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.mfa') ->label('scope', 'account') @@ -3786,8 +3819,8 @@ App::post('/v1/account/mfa/authenticators/:type') $response->dynamic($model, Response::MODEL_MFA_TYPE); }); -App::put('/v1/account/mfa/authenticators/:type') - ->desc('Verify authenticator') +Http::put('/v1/account/mfa/authenticators/:type') + ->desc('Verify Authenticator') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.mfa') ->label('scope', 'account') @@ -3851,8 +3884,8 @@ App::put('/v1/account/mfa/authenticators/:type') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::post('/v1/account/mfa/recovery-codes') - ->desc('Create MFA recovery codes') +Http::post('/v1/account/mfa/recovery-codes') + ->desc('Create MFA Recovery Codes') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.mfa') ->label('scope', 'account') @@ -3893,8 +3926,8 @@ App::post('/v1/account/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -App::patch('/v1/account/mfa/recovery-codes') - ->desc('Regenerate MFA recovery codes') +Http::patch('/v1/account/mfa/recovery-codes') + ->desc('Regenerate MFA Recovery Codes') ->groups(['api', 'account', 'mfaProtected']) ->label('event', 'users.[userId].update.mfa') ->label('scope', 'account') @@ -3934,8 +3967,8 @@ App::patch('/v1/account/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -App::get('/v1/account/mfa/recovery-codes') - ->desc('Get MFA recovery codes') +Http::get('/v1/account/mfa/recovery-codes') + ->desc('Get MFA Recovery Codes') ->groups(['api', 'account', 'mfaProtected']) ->label('scope', 'account') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) @@ -3964,8 +3997,8 @@ App::get('/v1/account/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -App::delete('/v1/account/mfa/authenticators/:type') - ->desc('Delete authenticator') +Http::delete('/v1/account/mfa/authenticators/:type') + ->desc('Delete Authenticator') ->groups(['api', 'account', 'mfaProtected']) ->label('event', 'users.[userId].delete.mfa') ->label('scope', 'account') @@ -4002,8 +4035,8 @@ App::delete('/v1/account/mfa/authenticators/:type') $response->noContent(); }); -App::post('/v1/account/mfa/challenge') - ->desc('Create MFA challenge') +Http::post('/v1/account/mfa/challenge') + ->desc('Create MFA Challenge') ->groups(['api', 'account', 'mfa']) ->label('scope', 'account') ->label('event', 'users.[userId].challenges.[challengeId].create') @@ -4190,8 +4223,8 @@ App::post('/v1/account/mfa/challenge') $response->dynamic($challenge, Response::MODEL_MFA_CHALLENGE); }); -App::put('/v1/account/mfa/challenge') - ->desc('Create MFA challenge (confirmation)') +Http::put('/v1/account/mfa/challenge') + ->desc('Create MFA Challenge (confirmation)') ->groups(['api', 'account', 'mfa']) ->label('scope', 'account') ->label('event', 'users.[userId].sessions.[sessionId].create') @@ -4277,7 +4310,7 @@ App::put('/v1/account/mfa/challenge') $response->dynamic($session, Response::MODEL_SESSION); }); -App::post('/v1/account/targets/push') +Http::post('/v1/account/targets/push') ->desc('Create push target') ->groups(['api', 'account']) ->label('scope', 'targets.write') @@ -4298,12 +4331,14 @@ App::post('/v1/account/targets/push') ->inject('request') ->inject('response') ->inject('dbForProject') - ->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) { + ->inject('authorization') + ->inject('authentication') + ->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization, Authentication $authentication) { $targetId = $targetId == 'unique()' ? ID::unique() : $targetId; - $provider = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId)); + $provider = $authorization->skip(fn () => $dbForProject->getDocument('providers', $providerId)); - $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); + $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId)); if (!$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); @@ -4314,7 +4349,7 @@ App::post('/v1/account/targets/push') $device = $detector->getDevice(); - $sessionId = Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret); + $sessionId = Auth::sessionVerify($user->getAttribute('sessions'), $authentication->getSecret()); $session = $dbForProject->getDocument('sessions', $sessionId); try { @@ -4350,7 +4385,7 @@ App::post('/v1/account/targets/push') ->dynamic($target, Response::MODEL_TARGET); }); -App::put('/v1/account/targets/:targetId/push') +Http::put('/v1/account/targets/:targetId/push') ->desc('Update push target') ->groups(['api', 'account']) ->label('scope', 'targets.write') @@ -4370,9 +4405,10 @@ App::put('/v1/account/targets/:targetId/push') ->inject('request') ->inject('response') ->inject('dbForProject') - ->action(function (string $targetId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $targetId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) { - $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); + $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId)); if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); @@ -4405,7 +4441,7 @@ App::put('/v1/account/targets/:targetId/push') ->dynamic($target, Response::MODEL_TARGET); }); -App::delete('/v1/account/targets/:targetId/push') +Http::delete('/v1/account/targets/:targetId/push') ->desc('Delete push target') ->groups(['api', 'account']) ->label('scope', 'targets.write') @@ -4425,8 +4461,9 @@ App::delete('/v1/account/targets/:targetId/push') ->inject('request') ->inject('response') ->inject('dbForProject') - ->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject) { - $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); + ->inject('authorization') + ->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) { + $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId)); if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); @@ -4451,8 +4488,8 @@ App::delete('/v1/account/targets/:targetId/push') $response->noContent(); }); -App::get('/v1/account/identities') - ->desc('List identities') +Http::get('/v1/account/identities') + ->desc('List Identities') ->groups(['api', 'account']) ->label('scope', 'account') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) @@ -4507,7 +4544,7 @@ App::get('/v1/account/identities') ]), Response::MODEL_IDENTITY_LIST); }); -App::delete('/v1/account/identities/:identityId') +Http::delete('/v1/account/identities/:identityId') ->desc('Delete identity') ->groups(['api', 'account']) ->label('scope', 'account') diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index fcff3e4179..54ba096c5d 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -5,7 +5,7 @@ use Appwrite\URL\URL as URLParse; use Appwrite\Utopia\Response; use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QROptions; -use Utopia\App; +use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -14,15 +14,17 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Domains\Domain; use Utopia\Fetch\Client; +use Utopia\Http\Http; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\HexColor; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\URL; +use Utopia\Http\Validator\WhiteList; use Utopia\Image\Image; +use Utopia\Logger\Log; use Utopia\Logger\Logger; use Utopia\System\System; -use Utopia\Validator\Boolean; -use Utopia\Validator\HexColor; -use Utopia\Validator\Range; -use Utopia\Validator\Text; -use Utopia\Validator\URL; -use Utopia\Validator\WhiteList; $avatarCallback = function (string $type, string $code, int $width, int $height, int $quality, Response $response) { @@ -61,9 +63,9 @@ $avatarCallback = function (string $type, string $code, int $width, int $height, unset($image); }; -$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForConsole, ?Logger $logger) { +$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForConsole, ?Logger $logger, Authorization $auth) { try { - $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); + $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); $sessions = $user->getAttribute('sessions', []); @@ -114,7 +116,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro ->setAttribute('providerRefreshToken', $refreshToken) ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry(''))); - Authorization::skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession)); + $auth->skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession)); $dbForProject->purgeCachedDocument('users', $user->getId()); } catch (Throwable $err) { @@ -122,7 +124,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro do { $previousAccessToken = $gitHubSession->getAttribute('providerAccessToken'); - $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); + $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); $sessions = $user->getAttribute('sessions', []); $gitHubSession = new Document(); @@ -154,11 +156,42 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro 'id' => $githubId ]; } catch (Exception $error) { + if ($logger) { + $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); + + $log = new Log(); + $log->setNamespace('console'); + $log->setServer(\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->addExtra('file', $error->getFile()); + $log->addExtra('line', $error->getLine()); + $log->addExtra('trace', $error->getTraceAsString()); + $log->addExtra('detailedTrace', $error->getTrace()); + + $log->setAction('avatarsGetGitHub'); + + $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; + + $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); + + $responseCode = $logger->addLog($log); + Console::info('GitHub error log pushed with status code: ' . $responseCode); + } + + Console::warning("Failed: {$error->getMessage()}"); + Console::warning($error->getTraceAsString()); + return []; } }; -App::get('/v1/avatars/credit-cards/:code') +Http::get('/v1/avatars/credit-cards/:code') ->desc('Get credit card icon') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -178,7 +211,7 @@ App::get('/v1/avatars/credit-cards/:code') ->inject('response') ->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('credit-cards', $code, $width, $height, $quality, $response)); -App::get('/v1/avatars/browsers/:code') +Http::get('/v1/avatars/browsers/:code') ->desc('Get browser icon') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -198,7 +231,7 @@ App::get('/v1/avatars/browsers/:code') ->inject('response') ->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('browsers', $code, $width, $height, $quality, $response)); -App::get('/v1/avatars/flags/:code') +Http::get('/v1/avatars/flags/:code') ->desc('Get country flag') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -218,7 +251,7 @@ App::get('/v1/avatars/flags/:code') ->inject('response') ->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('flags', $code, $width, $height, $quality, $response)); -App::get('/v1/avatars/image') +Http::get('/v1/avatars/image') ->desc('Get image from URL') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -281,7 +314,7 @@ App::get('/v1/avatars/image') unset($image); }); -App::get('/v1/avatars/favicon') +Http::get('/v1/avatars/favicon') ->desc('Get favicon') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -426,7 +459,7 @@ App::get('/v1/avatars/favicon') unset($image); }); -App::get('/v1/avatars/qr') +Http::get('/v1/avatars/qr') ->desc('Get QR code') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -466,7 +499,7 @@ App::get('/v1/avatars/qr') ->send($image->output('png', 9)); }); -App::get('/v1/avatars/initials') +Http::get('/v1/avatars/initials') ->desc('Get user initials') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -549,8 +582,8 @@ App::get('/v1/avatars/initials') ->file($image->getImageBlob()); }); -App::get('/v1/cards/cloud') - ->desc('Get front Of Cloud Card') +Http::get('/v1/cards/cloud') + ->desc('Get Front Of Cloud Card') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') ->label('cache', true) @@ -571,8 +604,9 @@ App::get('/v1/cards/cloud') ->inject('contributors') ->inject('employees') ->inject('logger') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { - $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); + ->inject('auth') + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $auth) use ($getUserGitHub) { + $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); if ($user->isEmpty() && empty($mock)) { throw new Exception(Exception::USER_NOT_FOUND); @@ -583,7 +617,7 @@ App::get('/v1/cards/cloud') $email = $user->getAttribute('email', ''); $createdAt = new \DateTime($user->getCreatedAt()); - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger); + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $auth); $githubName = $gitHub['name'] ?? ''; $githubId = $gitHub['id'] ?? ''; @@ -756,8 +790,8 @@ App::get('/v1/cards/cloud') ->file($baseImage->getImageBlob()); }); -App::get('/v1/cards/cloud-back') - ->desc('Get back Of Cloud Card') +Http::get('/v1/cards/cloud-back') + ->desc('Get Back Of Cloud Card') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') ->label('cache', true) @@ -778,8 +812,9 @@ App::get('/v1/cards/cloud-back') ->inject('contributors') ->inject('employees') ->inject('logger') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { - $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); + ->inject('auth') + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $auth) use ($getUserGitHub) { + $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); if ($user->isEmpty() && empty($mock)) { throw new Exception(Exception::USER_NOT_FOUND); @@ -789,7 +824,7 @@ App::get('/v1/cards/cloud-back') $userId = $user->getId(); $email = $user->getAttribute('email', ''); - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger); + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $auth); $githubId = $gitHub['id'] ?? ''; $isHero = \array_key_exists($email, $heroes); @@ -834,8 +869,8 @@ App::get('/v1/cards/cloud-back') ->file($baseImage->getImageBlob()); }); -App::get('/v1/cards/cloud-og') - ->desc('Get OG image From Cloud Card') +Http::get('/v1/cards/cloud-og') + ->desc('Get OG Image From Cloud Card') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') ->label('cache', true) @@ -856,8 +891,9 @@ App::get('/v1/cards/cloud-og') ->inject('contributors') ->inject('employees') ->inject('logger') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { - $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); + ->inject('auth') + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $auth) use ($getUserGitHub) { + $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); if ($user->isEmpty() && empty($mock)) { throw new Exception(Exception::USER_NOT_FOUND); @@ -872,7 +908,7 @@ App::get('/v1/cards/cloud-og') $email = $user->getAttribute('email', ''); $createdAt = new \DateTime($user->getCreatedAt()); - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger); + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $auth); $githubName = $gitHub['name'] ?? ''; $githubId = $gitHub['id'] ?? ''; diff --git a/app/controllers/api/console.php b/app/controllers/api/console.php index eeb823a3d3..2a393497ae 100644 --- a/app/controllers/api/console.php +++ b/app/controllers/api/console.php @@ -2,12 +2,12 @@ use Appwrite\Extend\Exception; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Database\Document; +use Utopia\Http\Http; +use Utopia\Http\Validator\Text; use Utopia\System\System; -use Utopia\Validator\Text; -App::init() +Http::init() ->groups(['console']) ->inject('project') ->action(function (Document $project) { @@ -17,7 +17,7 @@ App::init() }); -App::get('/v1/console/variables') +Http::get('/v1/console/variables') ->desc('Get variables') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -56,8 +56,8 @@ App::get('/v1/console/variables') $response->dynamic($variables, Response::MODEL_CONSOLE_VARIABLES); }); -App::post('/v1/console/assistant') - ->desc('Ask query') +Http::post('/v1/console/assistant') + ->desc('Ask Query') ->groups(['api', 'assistant']) ->label('scope', 'assistant.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index b76548b725..4bc074fb8f 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -16,7 +16,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Indexes; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; -use Utopia\App; use Utopia\Audit\Audit; use Utopia\Config\Config; use Utopia\Database\Database; @@ -35,6 +34,7 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Index as IndexValidator; use Utopia\Database\Validator\Key; @@ -44,18 +44,19 @@ use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\Structure; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\FloatValidator; +use Utopia\Http\Validator\Integer; +use Utopia\Http\Validator\IP; +use Utopia\Http\Validator\JSON; +use Utopia\Http\Validator\Nullable; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\URL; +use Utopia\Http\Validator\WhiteList; use Utopia\Locale\Locale; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Boolean; -use Utopia\Validator\FloatValidator; -use Utopia\Validator\Integer; -use Utopia\Validator\IP; -use Utopia\Validator\JSON; -use Utopia\Validator\Nullable; -use Utopia\Validator\Range; -use Utopia\Validator\Text; -use Utopia\Validator\URL; -use Utopia\Validator\WhiteList; /** * * Create attribute of varying type @@ -77,7 +78,7 @@ use Utopia\Validator\WhiteList; * @throws ConflictException * @throws Exception */ -function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): Document +function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): Document { $key = $attribute->getAttribute('key'); $type = $attribute->getAttribute('type', ''); @@ -91,7 +92,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att $default = $attribute->getAttribute('default'); $options = $attribute->getAttribute('options', []); - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -226,6 +227,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att } function updateAttribute( + Authorization $authorization, string $databaseId, string $collectionId, string $key, @@ -239,10 +241,10 @@ function updateAttribute( int|float $min = null, int|float $max = null, array $elements = null, - array $options = [], string $newKey = null, + array $options = [], ): Document { - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -421,19 +423,19 @@ function updateAttribute( return $attribute; } -App::init() +Http::init() ->groups(['api', 'database']) ->inject('request') ->inject('dbForProject') ->action(function (Request $request, Database $dbForProject) { $timeout = \intval($request->getHeader('x-appwrite-timeout')); - if (!empty($timeout) && App::isDevelopment()) { + if (!empty($timeout) && Http::isDevelopment()) { $dbForProject->setTimeout($timeout); } }); -App::post('/v1/databases') +Http::post('/v1/databases') ->desc('Create database') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].create') @@ -511,7 +513,7 @@ App::post('/v1/databases') ->dynamic($database, Response::MODEL_DATABASE); }); -App::get('/v1/databases') +Http::get('/v1/databases') ->desc('List databases') ->groups(['api', 'database']) ->label('scope', 'databases.read') @@ -564,7 +566,7 @@ App::get('/v1/databases') ]), Response::MODEL_DATABASE_LIST); }); -App::get('/v1/databases/:databaseId') +Http::get('/v1/databases/:databaseId') ->desc('Get database') ->groups(['api', 'database']) ->label('scope', 'databases.read') @@ -589,7 +591,7 @@ App::get('/v1/databases/:databaseId') $response->dynamic($database, Response::MODEL_DATABASE); }); -App::get('/v1/databases/:databaseId/logs') +Http::get('/v1/databases/:databaseId/logs') ->desc('List database logs') ->groups(['api', 'database']) ->label('scope', 'databases.read') @@ -606,7 +608,8 @@ App::get('/v1/databases/:databaseId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $databaseId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $databaseId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { $database = $dbForProject->getDocument('databases', $databaseId); @@ -680,7 +683,7 @@ App::get('/v1/databases/:databaseId/logs') }); -App::put('/v1/databases/:databaseId') +Http::put('/v1/databases/:databaseId') ->desc('Update database') ->groups(['api', 'database', 'schema']) ->label('scope', 'databases.write') @@ -718,7 +721,7 @@ App::put('/v1/databases/:databaseId') $response->dynamic($database, Response::MODEL_DATABASE); }); -App::delete('/v1/databases/:databaseId') +Http::delete('/v1/databases/:databaseId') ->desc('Delete database') ->groups(['api', 'database', 'schema']) ->label('scope', 'databases.write') @@ -766,7 +769,7 @@ App::delete('/v1/databases/:databaseId') $response->noContent(); }); -App::post('/v1/databases/:databaseId/collections') +Http::post('/v1/databases/:databaseId/collections') ->desc('Create collection') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].create') @@ -790,9 +793,10 @@ App::post('/v1/databases/:databaseId/collections') ->inject('dbForProject') ->inject('mode') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -833,7 +837,7 @@ App::post('/v1/databases/:databaseId/collections') ->dynamic($collection, Response::MODEL_COLLECTION); }); -App::get('/v1/databases/:databaseId/collections') +Http::get('/v1/databases/:databaseId/collections') ->alias('/v1/database/collections', ['databaseId' => 'default']) ->desc('List collections') ->groups(['api', 'database']) @@ -851,9 +855,10 @@ App::get('/v1/databases/:databaseId/collections') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $databaseId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { + ->inject('authorization') + ->action(function (string $databaseId, array $queries, string $search, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -896,7 +901,7 @@ App::get('/v1/databases/:databaseId/collections') ]), Response::MODEL_COLLECTION_LIST); }); -App::get('/v1/databases/:databaseId/collections/:collectionId') +Http::get('/v1/databases/:databaseId/collections/:collectionId') ->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default']) ->desc('Get collection') ->groups(['api', 'database']) @@ -913,9 +918,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, string $mode) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -930,7 +936,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId') $response->dynamic($collection, Response::MODEL_COLLECTION); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/logs') +Http::get('/v1/databases/:databaseId/collections/:collectionId/logs') ->alias('/v1/database/collections/:collectionId/logs', ['databaseId' => 'default']) ->desc('List collection logs') ->groups(['api', 'database']) @@ -949,9 +955,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1030,7 +1037,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs') }); -App::put('/v1/databases/:databaseId/collections/:collectionId') +Http::put('/v1/databases/:databaseId/collections/:collectionId') ->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default']) ->desc('Update collection') ->groups(['api', 'database', 'schema']) @@ -1055,9 +1062,10 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') ->inject('dbForProject') ->inject('mode') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1093,7 +1101,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') $response->dynamic($collection, Response::MODEL_COLLECTION); }); -App::delete('/v1/databases/:databaseId/collections/:collectionId') +Http::delete('/v1/databases/:databaseId/collections/:collectionId') ->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default']) ->desc('Delete collection') ->groups(['api', 'database', 'schema']) @@ -1114,9 +1122,10 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId') ->inject('queueForDatabase') ->inject('queueForEvents') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, string $mode) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, string $mode, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1148,7 +1157,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId') $response->noContent(); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string') ->alias('/v1/database/collections/:collectionId/attributes/string', ['databaseId' => 'default']) ->desc('Create string attribute') ->groups(['api', 'database', 'schema']) @@ -1175,7 +1184,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?int $size, ?bool $required, ?string $default, bool $array, bool $encrypt, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?int $size, ?bool $required, ?string $default, bool $array, bool $encrypt, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { // Ensure attribute default is within required size $validator = new Text($size, 0); @@ -1197,7 +1207,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string 'default' => $default, 'array' => $array, 'filters' => $filters, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response @@ -1205,7 +1215,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string ->dynamic($attribute, Response::MODEL_ATTRIBUTE_STRING); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email') ->alias('/v1/database/collections/:collectionId/attributes/email', ['databaseId' => 'default']) ->desc('Create email attribute') ->groups(['api', 'database', 'schema']) @@ -1230,7 +1240,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email' ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -1240,14 +1251,14 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email' 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_EMAIL, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_EMAIL); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') ->alias('/v1/database/collections/:collectionId/attributes/enum', ['databaseId' => 'default']) ->desc('Create enum attribute') ->groups(['api', 'database', 'schema']) @@ -1273,7 +1284,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { if (!is_null($default) && !in_array($default, $elements)) { throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Default value not found in elements'); } @@ -1287,14 +1299,14 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_ENUM, 'formatOptions' => ['elements' => $elements], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_ENUM); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') ->alias('/v1/database/collections/:collectionId/attributes/ip', ['databaseId' => 'default']) ->desc('Create IP address attribute') ->groups(['api', 'database', 'schema']) @@ -1319,7 +1331,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -1329,14 +1342,14 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_IP, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_IP); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->alias('/v1/database/collections/:collectionId/attributes/url', ['databaseId' => 'default']) ->desc('Create URL attribute') ->groups(['api', 'database', 'schema']) @@ -1361,7 +1374,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -1371,14 +1385,14 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_URL, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_URL); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/integer') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/integer') ->alias('/v1/database/collections/:collectionId/attributes/integer', ['databaseId' => 'default']) ->desc('Create integer attribute') ->groups(['api', 'database', 'schema']) @@ -1405,7 +1419,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { // Ensure attribute default is within range $min = (is_null($min)) ? PHP_INT_MIN : \intval($min); @@ -1435,7 +1450,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege 'min' => $min, 'max' => $max, ], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $formatOptions = $attribute->getAttribute('formatOptions', []); @@ -1449,7 +1464,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege ->dynamic($attribute, Response::MODEL_ATTRIBUTE_INTEGER); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float') ->alias('/v1/database/collections/:collectionId/attributes/float', ['databaseId' => 'default']) ->desc('Create float attribute') ->groups(['api', 'database', 'schema']) @@ -1476,7 +1491,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float' ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { // Ensure attribute default is within range $min = (is_null($min)) ? -PHP_FLOAT_MAX : \floatval($min); @@ -1509,7 +1525,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float' 'min' => $min, 'max' => $max, ], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $formatOptions = $attribute->getAttribute('formatOptions', []); @@ -1523,7 +1539,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float' ->dynamic($attribute, Response::MODEL_ATTRIBUTE_FLOAT); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean') ->alias('/v1/database/collections/:collectionId/attributes/boolean', ['databaseId' => 'default']) ->desc('Create boolean attribute') ->groups(['api', 'database', 'schema']) @@ -1548,7 +1564,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -1557,14 +1574,14 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea 'required' => $required, 'default' => $default, 'array' => $array, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_BOOLEAN); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime') ->alias('/v1/database/collections/:collectionId/attributes/datetime', ['databaseId' => 'default']) ->desc('Create datetime attribute') ->groups(['api', 'database']) @@ -1589,7 +1606,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { $filters[] = 'datetime'; @@ -1601,14 +1619,14 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti 'default' => $default, 'array' => $array, 'filters' => $filters, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_DATETIME); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relationship') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relationship') ->alias('/v1/database/collections/:collectionId/attributes/relationship', ['databaseId' => 'default']) ->desc('Create relationship attribute') ->groups(['api', 'database']) @@ -1635,6 +1653,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relati ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->action(function ( string $databaseId, string $collectionId, @@ -1647,12 +1666,13 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relati Response $response, Database $dbForProject, EventDatabase $queueForDatabase, - Event $queueForEvents + Event $queueForEvents, + Authorization $authorization ) { $key ??= $relatedCollectionId; $twoWayKey ??= $collectionId; - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1722,7 +1742,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relati $response, $dbForProject, $queueForDatabase, - $queueForEvents + $queueForEvents, + $authorization ); $options = $attribute->getAttribute('options', []); @@ -1736,7 +1757,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relati ->dynamic($attribute, Response::MODEL_ATTRIBUTE_RELATIONSHIP); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') +Http::get('/v1/databases/:databaseId/collections/:collectionId/attributes') ->alias('/v1/database/collections/:collectionId/attributes', ['databaseId' => 'default']) ->desc('List attributes') ->groups(['api', 'database']) @@ -1753,9 +1774,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') ->param('queries', [], new Attributes(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Attributes::ALLOWED_ATTRIBUTES), true) ->inject('response') ->inject('dbForProject') - ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Authorization $authorization) { /** @var Document $database */ - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1789,7 +1811,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') if ($cursor) { $attributeId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->find('attributes', [ + $cursorDocument = $authorization->skip(fn () => $dbForProject->find('attributes', [ Query::equal('collectionInternalId', [$collection->getInternalId()]), Query::equal('databaseInternalId', [$database->getInternalId()]), Query::equal('key', [$attributeId]), @@ -1814,7 +1836,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') ]), Response::MODEL_ATTRIBUTE_LIST); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') +Http::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') ->alias('/v1/database/collections/:collectionId/attributes/:key', ['databaseId' => 'default']) ->desc('Get attribute') ->groups(['api', 'database']) @@ -1841,9 +1863,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') ->param('key', '', new Key(), 'Attribute Key.') ->inject('response') ->inject('dbForProject') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1889,7 +1912,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') $response->dynamic($attribute, $model); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/string/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/string/:key') ->desc('Update string attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -1912,9 +1935,11 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/strin ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?int $size, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?int $size, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -1932,7 +1957,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/strin ->dynamic($attribute, Response::MODEL_ATTRIBUTE_STRING); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/email/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/email/:key') ->desc('Update email attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -1954,8 +1979,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/email ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -1973,7 +2000,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/email ->dynamic($attribute, Response::MODEL_ATTRIBUTE_EMAIL); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/enum/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/enum/:key') ->desc('Update enum attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -1996,8 +2023,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/enum/ ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?array $elements, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?array $elements, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2016,7 +2045,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/enum/ ->dynamic($attribute, Response::MODEL_ATTRIBUTE_ENUM); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/ip/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/ip/:key') ->desc('Update IP address attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2038,8 +2067,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/ip/:k ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2057,7 +2088,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/ip/:k ->dynamic($attribute, Response::MODEL_ATTRIBUTE_IP); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/url/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/url/:key') ->desc('Update URL attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2079,8 +2110,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/url/: ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2098,7 +2131,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/url/: ->dynamic($attribute, Response::MODEL_ATTRIBUTE_URL); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/integer/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/integer/:key') ->desc('Update integer attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2122,8 +2155,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/integ ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2149,7 +2184,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/integ ->dynamic($attribute, Response::MODEL_ATTRIBUTE_INTEGER); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/float/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/float/:key') ->desc('Update float attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2173,8 +2208,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/float ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2200,7 +2237,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/float ->dynamic($attribute, Response::MODEL_ATTRIBUTE_FLOAT); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean/:key') ->desc('Update boolean attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2222,8 +2259,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/boole ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2240,7 +2279,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/boole ->dynamic($attribute, Response::MODEL_ATTRIBUTE_BOOLEAN); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime/:key') ->desc('Update dateTime attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2262,8 +2301,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/datet ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2280,7 +2321,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/datet ->dynamic($attribute, Response::MODEL_ATTRIBUTE_DATETIME); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/relationship') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/relationship') ->desc('Update relationship attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2301,6 +2342,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/ ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->action(function ( string $databaseId, string $collectionId, @@ -2309,9 +2351,11 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/ ?string $newKey, Response $response, Database $dbForProject, - Event $queueForEvents + Event $queueForEvents, + Authorization $authorization ) { $attribute = updateAttribute( + $authorization, $databaseId, $collectionId, $key, @@ -2336,7 +2380,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/ ->dynamic($attribute, Response::MODEL_ATTRIBUTE_RELATIONSHIP); }); -App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') +Http::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') ->alias('/v1/database/collections/:collectionId/attributes/:key', ['databaseId' => 'default']) ->desc('Delete attribute') ->groups(['api', 'database', 'schema']) @@ -2358,9 +2402,10 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key ->inject('queueForDatabase') ->inject('queueForEvents') ->inject('queueForUsage') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Usage $queueForUsage) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Usage $queueForUsage, Authorization $authorization) { - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2449,7 +2494,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key $response->noContent(); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') +Http::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->alias('/v1/database/collections/:collectionId/indexes', ['databaseId' => 'default']) ->desc('Create index') ->groups(['api', 'database']) @@ -2474,9 +2519,10 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2619,7 +2665,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->dynamic($index, Response::MODEL_INDEX); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') +Http::get('/v1/databases/:databaseId/collections/:collectionId/indexes') ->alias('/v1/database/collections/:collectionId/indexes', ['databaseId' => 'default']) ->desc('List indexes') ->groups(['api', 'database']) @@ -2636,9 +2682,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') ->param('queries', [], new Indexes(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Indexes::ALLOWED_ATTRIBUTES), true) ->inject('response') ->inject('dbForProject') - ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Authorization $authorization) { /** @var Document $database */ - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2668,7 +2715,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') if ($cursor) { $indexId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->find('indexes', [ + $cursorDocument = $authorization->skip(fn () => $dbForProject->find('indexes', [ Query::equal('collectionInternalId', [$collection->getInternalId()]), Query::equal('databaseInternalId', [$database->getInternalId()]), Query::equal('key', [$indexId]), @@ -2689,7 +2736,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') ]), Response::MODEL_INDEX_LIST); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') +Http::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) ->desc('Get index') ->groups(['api', 'database']) @@ -2706,9 +2753,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->param('key', null, new Key(), 'Index Key.') ->inject('response') ->inject('dbForProject') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2728,7 +2776,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') }); -App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') +Http::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) ->desc('Delete index') ->groups(['api', 'database']) @@ -2749,9 +2797,10 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2792,7 +2841,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') $response->noContent(); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/documents') +Http::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->alias('/v1/database/collections/:collectionId/documents', ['databaseId' => 'default']) ->desc('Create document') ->groups(['api', 'database']) @@ -2823,7 +2872,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->inject('queueForEvents') ->inject('queueForUsage') ->inject('mode') - ->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, Usage $queueForUsage, string $mode) { + ->inject('authorization') + ->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, Usage $queueForUsage, string $mode, Authorization $authorization) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -2835,16 +2885,16 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, '$id is not allowed for creating new documents, try update instead'); } - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); @@ -2882,8 +2932,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!Authorization::isRole($role)) { - throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', Authorization::getRoles()) . ')'); + if (!$authorization->isRole($role)) { + throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $authorization->getRoles()) . ')'); } } } @@ -2894,17 +2944,16 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') $data['$permissions'] = $permissions; $document = new Document($data); - $checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database) { + $checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database, $authorization) { $documentSecurity = $collection->getAttribute('documentSecurity', false); - $validator = new Authorization($permission); - $valid = $validator->isValid($collection->getPermissionsByType($permission)); + $valid = $authorization->isValid(new Input($permission, $collection->getPermissionsByType($permission))); if (($permission === Database::PERMISSION_UPDATE && !$documentSecurity) || !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } if ($permission === Database::PERMISSION_UPDATE) { - $valid = $valid || $validator->isValid($document->getUpdate()); + $valid = $valid || $authorization->isValid($document->getUpdate()); if ($documentSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -2931,7 +2980,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( + $relatedCollection = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -2945,7 +2994,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') $relation = new Document($relation); } if ($relation instanceof Document) { - $current = Authorization::skip( + $current = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId(), $relation->getId()) ); @@ -2985,7 +3034,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') } // Add $collectionId and $databaseId for all documents - $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { + $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, $authorization) { $document->setAttribute('$databaseId', $database->getId()); $document->setAttribute('$collectionId', $collection->getId()); @@ -3005,7 +3054,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( + $relatedCollection = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -3044,7 +3093,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE), 1); // per collection }); -App::get('/v1/databases/:databaseId/collections/:collectionId/documents') +Http::get('/v1/databases/:databaseId/collections/:collectionId/documents') ->alias('/v1/database/collections/:collectionId/documents', ['databaseId' => 'default']) ->desc('List documents') ->groups(['api', 'database']) @@ -3063,16 +3112,17 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); @@ -3096,7 +3146,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') if ($cursor) { $documentId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); + $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Document '{$documentId}' for the 'cursor' value not found."); @@ -3115,7 +3165,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') } // Add $collectionId and $databaseId for all documents - $processDocument = (function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database): bool { + $processDocument = (function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, $authorization): bool { if ($document->isEmpty()) { return false; } @@ -3142,7 +3192,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId)); + $relatedCollection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId)); foreach ($relations as $index => $doc) { if ($doc instanceof Document) { @@ -3199,7 +3249,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') ]), Response::MODEL_DOCUMENT_LIST); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') +Http::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) ->desc('Get document') ->groups(['api', 'database']) @@ -3220,17 +3270,18 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); @@ -3250,7 +3301,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen } // Add $collectionId and $databaseId for all documents - $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { + $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, $authorization) { if ($document->isEmpty()) { return; } @@ -3274,7 +3325,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( + $relatedCollection = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -3291,7 +3342,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen $response->dynamic($document, Response::MODEL_DOCUMENT); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/logs') +Http::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/logs') ->alias('/v1/database/collections/:collectionId/documents/:documentId/logs', ['databaseId' => 'default']) ->desc('List document logs') ->groups(['api', 'database']) @@ -3311,9 +3362,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -3395,7 +3447,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ]), Response::MODEL_LOG_LIST); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) ->desc('Update document') ->groups(['api', 'database']) @@ -3425,7 +3477,8 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->inject('dbForProject') ->inject('queueForEvents') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, string $mode) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Authorization $authorization) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -3433,16 +3486,16 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum throw new Exception(Exception::DOCUMENT_MISSING_PAYLOAD); } - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); @@ -3450,7 +3503,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum // Read permission should not be required for update /** @var Document $document */ - $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); + $document = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); @@ -3464,7 +3517,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ]); // Users can only manage their own roles, API keys and Admin users can manage any - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); if (!$isAPIKey && !$isPrivilegedUser && !\is_null($permissions)) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { @@ -3477,7 +3530,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!Authorization::isRole($role)) { + if (!$authorization->isRole($role)) { throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); } } @@ -3492,7 +3545,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum $data['$permissions'] = $permissions; $newDocument = new Document($data); - $setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database) { + $setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, $authorization) { $relationships = \array_filter( $collection->getAttribute('attributes', []), fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP @@ -3514,7 +3567,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( + $relatedCollection = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -3529,7 +3582,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum $relation = new Document($relation); } if ($relation instanceof Document) { - $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument( + $oldDocument = $authorization->skip(fn () => $dbForProject->getDocument( 'database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId(), $relation->getId() )); @@ -3578,7 +3631,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum } // Add $collectionId and $databaseId for all documents - $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { + $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, $authorization) { $document->setAttribute('$databaseId', $database->getId()); $document->setAttribute('$collectionId', $collection->getId()); @@ -3598,7 +3651,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( + $relatedCollection = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -3631,7 +3684,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->setPayload($response->getPayload(), sensitive: $relationships); }); -App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') +Http::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) ->desc('Delete document') ->groups(['api', 'database']) @@ -3660,24 +3713,25 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->inject('queueForEvents') ->inject('queueForUsage') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Usage $queueForUsage, string $mode) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Usage $queueForUsage, string $mode, Authorization $authorization) { + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } // Read permission should not be required for delete - $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); + $document = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); @@ -3691,7 +3745,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu }); // Add $collectionId and $databaseId for all documents - $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { + $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, $authorization) { $document->setAttribute('$databaseId', $database->getId()); $document->setAttribute('$collectionId', $collection->getId()); @@ -3711,7 +3765,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( + $relatedCollection = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -3751,7 +3805,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu $response->noContent(); }); -App::get('/v1/databases/usage') +Http::get('/v1/databases/usage') ->desc('Get databases usage stats') ->groups(['api', 'database', 'usage']) ->label('scope', 'collections.read') @@ -3764,7 +3818,8 @@ App::get('/v1/databases/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), '`Date range.', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $range, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) { $periods = Config::getParam('usage', []); $stats = $usage = []; @@ -3776,7 +3831,7 @@ App::get('/v1/databases/usage') METRIC_DATABASES_STORAGE ]; - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -3832,7 +3887,7 @@ App::get('/v1/databases/usage') ]), Response::MODEL_USAGE_DATABASES); }); -App::get('/v1/databases/:databaseId/usage') +Http::get('/v1/databases/:databaseId/usage') ->desc('Get database usage stats') ->groups(['api', 'database', 'usage']) ->label('scope', 'collections.read') @@ -3846,7 +3901,8 @@ App::get('/v1/databases/:databaseId/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), '`Date range.', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $databaseId, string $range, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $databaseId, string $range, Response $response, Database $dbForProject, Authorization $authorization) { $database = $dbForProject->getDocument('databases', $databaseId); @@ -3863,7 +3919,7 @@ App::get('/v1/databases/:databaseId/usage') str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_STORAGE) ]; - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -3918,7 +3974,7 @@ App::get('/v1/databases/:databaseId/usage') ]), Response::MODEL_USAGE_DATABASE); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/usage') +Http::get('/v1/databases/:databaseId/collections/:collectionId/usage') ->alias('/v1/database/:collectionId/usage', ['databaseId' => 'default']) ->desc('Get collection usage stats') ->groups(['api', 'database', 'usage']) @@ -3934,7 +3990,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage') ->param('collectionId', '', new UID(), 'Collection ID.') ->inject('response') ->inject('dbForProject') - ->action(function (string $databaseId, string $range, string $collectionId, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $databaseId, string $range, string $collectionId, Response $response, Database $dbForProject, Authorization $authorization) { $database = $dbForProject->getDocument('databases', $databaseId); $collectionDocument = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); @@ -3951,7 +4008,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage') str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collectionDocument->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), ]; - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 9c3e6782b4..d0aa60ad06 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -2,6 +2,7 @@ use Ahc\Jwt\JWT; use Appwrite\Auth\Auth; +use Appwrite\Auth\Authentication; use Appwrite\Event\Build; use Appwrite\Event\Delete; use Appwrite\Event\Event; @@ -20,11 +21,11 @@ use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Database\Validator\Queries\Deployments; use Appwrite\Utopia\Database\Validator\Queries\Executions; use Appwrite\Utopia\Database\Validator\Queries\Functions; +use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model\Rule; use Executor\Executor; use MaxMind\Db\Reader; -use Utopia\App; use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Database; @@ -37,24 +38,25 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Roles; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Validator\AnyOf; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Assoc; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Nullable; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Storage\Device; use Utopia\Storage\Validator\File; use Utopia\Storage\Validator\FileExt; use Utopia\Storage\Validator\FileSize; use Utopia\Storage\Validator\Upload; -use Utopia\Swoole\Request; use Utopia\System\System; -use Utopia\Validator\AnyOf; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Assoc; -use Utopia\Validator\Boolean; -use Utopia\Validator\Nullable; -use Utopia\Validator\Range; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub; use Utopia\VCS\Exception\RepositoryNotFound; @@ -133,7 +135,7 @@ $redeployVcs = function (Request $request, Document $function, Document $project ->setTemplate($template); }; -App::post('/v1/functions') +Http::post('/v1/functions') ->groups(['api', 'functions']) ->desc('Create function') ->label('scope', 'functions.write') @@ -171,8 +173,8 @@ App::post('/v1/functions') ->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification( $plan, Config::getParam('runtime-specifications', []), - App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), - App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) + System::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), + System::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) ), 'Runtime specification for the function and builds.', true, ['plan']) ->inject('request') ->inject('response') @@ -183,7 +185,8 @@ App::post('/v1/functions') ->inject('queueForBuilds') ->inject('dbForConsole') ->inject('gitHub') - ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) { + ->inject('authorization') + ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github, Authorization $authorization) use ($redeployVcs) { $functionId = ($functionId == 'unique()') ? ID::unique() : $functionId; $allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', ''))); @@ -247,7 +250,7 @@ App::post('/v1/functions') 'specification' => $specification ])); - $schedule = Authorization::skip( + $schedule = $authorization->skip( fn () => $dbForConsole->createDocument('schedules', new Document([ 'region' => System::getEnv('_APP_REGION', 'default'), // Todo replace with projects region 'resourceType' => 'function', @@ -329,7 +332,7 @@ App::post('/v1/functions') $routeSubdomain = ID::unique(); $domain = "{$routeSubdomain}.{$functionsDomain}"; - $rule = Authorization::skip( + $rule = $authorization->skip( fn () => $dbForConsole->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), @@ -396,7 +399,7 @@ App::post('/v1/functions') ->dynamic($function, Response::MODEL_FUNCTION); }); -App::get('/v1/functions') +Http::get('/v1/functions') ->groups(['api', 'functions']) ->desc('List functions') ->label('scope', 'functions.read') @@ -450,7 +453,7 @@ App::get('/v1/functions') ]), Response::MODEL_FUNCTION_LIST); }); -App::get('/v1/functions/runtimes') +Http::get('/v1/functions/runtimes') ->groups(['api', 'functions']) ->desc('List runtimes') ->label('scope', 'functions.read') @@ -483,7 +486,7 @@ App::get('/v1/functions/runtimes') ]), Response::MODEL_RUNTIME_LIST); }); -App::get('/v1/functions/specifications') +Http::get('/v1/functions/specifications') ->groups(['api', 'functions']) ->desc('List available function runtime specifications') ->label('scope', 'functions.read') @@ -519,7 +522,7 @@ App::get('/v1/functions/specifications') ]), Response::MODEL_SPECIFICATION_LIST); }); -App::get('/v1/functions/:functionId') +Http::get('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Get function') ->label('scope', 'functions.read') @@ -543,7 +546,7 @@ App::get('/v1/functions/:functionId') $response->dynamic($function, Response::MODEL_FUNCTION); }); -App::get('/v1/functions/:functionId/usage') +Http::get('/v1/functions/:functionId/usage') ->desc('Get function usage') ->groups(['api', 'functions', 'usage']) ->label('scope', 'functions.read') @@ -557,7 +560,8 @@ App::get('/v1/functions/:functionId/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $functionId, string $range, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $functionId, string $range, Response $response, Database $dbForProject, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); @@ -580,7 +584,7 @@ App::get('/v1/functions/:functionId/usage') str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS) ]; - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -647,7 +651,7 @@ App::get('/v1/functions/:functionId/usage') ]), Response::MODEL_USAGE_FUNCTION); }); -App::get('/v1/functions/usage') +Http::get('/v1/functions/usage') ->desc('Get functions usage') ->groups(['api', 'functions']) ->label('scope', 'functions.read') @@ -660,7 +664,8 @@ App::get('/v1/functions/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $range, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) { $periods = Config::getParam('usage', []); $stats = $usage = []; @@ -678,7 +683,7 @@ App::get('/v1/functions/usage') METRIC_EXECUTIONS_MB_SECONDS, ]; - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -746,7 +751,7 @@ App::get('/v1/functions/usage') ]), Response::MODEL_USAGE_FUNCTIONS); }); -App::put('/v1/functions/:functionId') +Http::put('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Update function') ->label('scope', 'functions.write') @@ -780,8 +785,8 @@ App::put('/v1/functions/:functionId') ->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification( $plan, Config::getParam('runtime-specifications', []), - App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), - App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) + System::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), + System::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) ), 'Runtime specification for the function and builds.', true, ['plan']) ->inject('request') ->inject('response') @@ -791,7 +796,8 @@ App::put('/v1/functions/:functionId') ->inject('queueForBuilds') ->inject('dbForConsole') ->inject('gitHub') - ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) { + ->inject('authorization') + ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github, Authorization $authorization) use ($redeployVcs) { // TODO: If only branch changes, re-deploy $function = $dbForProject->getDocument('functions', $functionId); @@ -894,7 +900,7 @@ App::put('/v1/functions/:functionId') // Enforce Cold Start if spec limits change. if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deployment'))) { - $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); + $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); try { $executor->deleteRuntime($project->getId(), $function->getAttribute('deployment')); } catch (\Throwable $th) { @@ -941,14 +947,14 @@ App::put('/v1/functions/:functionId') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $queueForEvents->setParam('functionId', $function->getId()); $response->dynamic($function, Response::MODEL_FUNCTION); }); -App::get('/v1/functions/:functionId/deployments/:deploymentId/download') +Http::get('/v1/functions/:functionId/deployments/:deploymentId/download') ->groups(['api', 'functions']) ->desc('Download deployment') ->label('scope', 'functions.read') @@ -1033,7 +1039,7 @@ App::get('/v1/functions/:functionId/deployments/:deploymentId/download') } }); -App::patch('/v1/functions/:functionId/deployments/:deploymentId') +Http::patch('/v1/functions/:functionId/deployments/:deploymentId') ->groups(['api', 'functions']) ->desc('Update deployment') ->label('scope', 'functions.write') @@ -1053,7 +1059,8 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId') ->inject('dbForProject') ->inject('queueForEvents') ->inject('dbForConsole') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForConsole) { + ->inject('authorization') + ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForConsole, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); $deployment = $dbForProject->getDocument('deployments', $deploymentId); @@ -1086,7 +1093,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $queueForEvents ->setParam('functionId', $function->getId()) @@ -1095,7 +1102,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId') $response->dynamic($function, Response::MODEL_FUNCTION); }); -App::delete('/v1/functions/:functionId') +Http::delete('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Delete function') ->label('scope', 'functions.write') @@ -1114,7 +1121,8 @@ App::delete('/v1/functions/:functionId') ->inject('queueForDeletes') ->inject('queueForEvents') ->inject('dbForConsole') - ->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForConsole) { + ->inject('authorization') + ->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForConsole, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); @@ -1131,7 +1139,7 @@ App::delete('/v1/functions/:functionId') $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) @@ -1142,7 +1150,7 @@ App::delete('/v1/functions/:functionId') $response->noContent(); }); -App::post('/v1/functions/:functionId/deployments') +Http::post('/v1/functions/:functionId/deployments') ->groups(['api', 'functions']) ->desc('Create deployment') ->label('scope', 'functions.write') @@ -1361,7 +1369,7 @@ App::post('/v1/functions/:functionId/deployments') ->dynamic($deployment, Response::MODEL_DEPLOYMENT); }); -App::get('/v1/functions/:functionId/deployments') +Http::get('/v1/functions/:functionId/deployments') ->groups(['api', 'functions']) ->desc('List deployments') ->label('scope', 'functions.read') @@ -1438,7 +1446,7 @@ App::get('/v1/functions/:functionId/deployments') ]), Response::MODEL_DEPLOYMENT_LIST); }); -App::get('/v1/functions/:functionId/deployments/:deploymentId') +Http::get('/v1/functions/:functionId/deployments/:deploymentId') ->groups(['api', 'functions']) ->desc('Get deployment') ->label('scope', 'functions.read') @@ -1481,7 +1489,7 @@ App::get('/v1/functions/:functionId/deployments/:deploymentId') $response->dynamic($deployment, Response::MODEL_DEPLOYMENT); }); -App::delete('/v1/functions/:functionId/deployments/:deploymentId') +Http::delete('/v1/functions/:functionId/deployments/:deploymentId') ->groups(['api', 'functions']) ->desc('Delete deployment') ->label('scope', 'functions.write') @@ -1545,7 +1553,7 @@ App::delete('/v1/functions/:functionId/deployments/:deploymentId') $response->noContent(); }); -App::post('/v1/functions/:functionId/deployments/:deploymentId/build') +Http::post('/v1/functions/:functionId/deployments/:deploymentId/build') ->alias('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') ->groups(['api', 'functions']) ->desc('Rebuild deployment') @@ -1568,7 +1576,8 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/build') ->inject('queueForEvents') ->inject('queueForBuilds') ->inject('deviceForFunctions') - ->action(function (string $functionId, string $deploymentId, string $buildId, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions) { + ->inject('authorization') + ->action(function (string $functionId, string $deploymentId, string $buildId, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -1614,7 +1623,7 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/build') $response->noContent(); }); -App::patch('/v1/functions/:functionId/deployments/:deploymentId/build') +Http::patch('/v1/functions/:functionId/deployments/:deploymentId/build') ->groups(['api', 'functions']) ->desc('Cancel deployment') ->label('scope', 'functions.write') @@ -1632,7 +1641,8 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId/build') ->inject('dbForProject') ->inject('project') ->inject('queueForEvents') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -1645,7 +1655,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId/build') throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } - $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + $build = $authorization->skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); if ($build->isEmpty()) { $buildId = ID::unique(); @@ -1687,7 +1697,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId/build') $dbForProject->purgeCachedDocument('deployments', $deployment->getId()); try { - $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); + $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); $executor->deleteRuntime($project->getId(), $deploymentId . "-build"); } catch (\Throwable $th) { // Don't throw if the deployment doesn't exist @@ -1703,7 +1713,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId/build') $response->dynamic($build, Response::MODEL_BUILD); }); -App::post('/v1/functions/:functionId/executions') +Http::post('/v1/functions/:functionId/executions') ->groups(['api', 'functions']) ->desc('Create execution') ->label('scope', 'execution.write') @@ -1733,7 +1743,9 @@ App::post('/v1/functions/:functionId/executions') ->inject('queueForUsage') ->inject('queueForFunctions') ->inject('geodb') - ->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForConsole, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb) { + ->inject('authorization') + ->inject('authentication') + ->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForConsole, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, Authorization $authorization, Authentication $authentication) { $async = \strval($async) === 'true' || \strval($async) === '1'; if (!$async && !is_null($scheduledAt)) { @@ -1763,10 +1775,10 @@ App::post('/v1/functions/:functionId/executions') throw new Exception($validator->getDescription(), 400); } - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); @@ -1782,7 +1794,7 @@ App::post('/v1/functions/:functionId/executions') throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); } - $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); + $deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); if ($deployment->getAttribute('resourceId') !== $function->getId()) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); @@ -1793,7 +1805,7 @@ App::post('/v1/functions/:functionId/executions') } /** Check if build has completed */ - $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + $build = $authorization->skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); if ($build->isEmpty()) { throw new Exception(Exception::BUILD_NOT_FOUND); } @@ -1802,10 +1814,8 @@ App::post('/v1/functions/:functionId/executions') throw new Exception(Exception::BUILD_NOT_READY); } - $validator = new Authorization('execute'); - - if (!$validator->isValid($function->getAttribute('execute'))) { // Check if user has write access to execute function - throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription()); + if (!$authorization->isValid(new Input('execute', $function->getAttribute('execute')))) { // Check if user has write access to execute function + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } $jwt = ''; // initialize @@ -1815,7 +1825,7 @@ App::post('/v1/functions/:functionId/executions') foreach ($sessions as $session) { /** @var Utopia\Database\Document $session */ - if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + if ($session->getAttribute('secret') == Auth::hash($authentication->getSecret())) { // If current session delete the cookies too $current = $session; } } @@ -1899,8 +1909,9 @@ App::post('/v1/functions/:functionId/executions') ->setContext('function', $function); if ($async) { + if (is_null($scheduledAt)) { - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); $queueForFunctions ->setType('http') ->setExecution($execution) @@ -1941,7 +1952,7 @@ App::post('/v1/functions/:functionId/executions') ->setAttribute('scheduleInternalId', $schedule->getInternalId()) ->setAttribute('scheduledAt', $scheduledAt); - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); } return $response @@ -2027,8 +2038,7 @@ App::post('/v1/functions/:functionId/executions') runtimeEntrypoint: $command, cpus: $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT, memory: $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT, - logging: $function->getAttribute('logging', true), - requestTimeout: 30 + logging: $function->getAttribute('logging', true) ); $headersFiltered = []; @@ -2069,10 +2079,10 @@ App::post('/v1/functions/:functionId/executions') ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT))) ; - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -2105,7 +2115,7 @@ App::post('/v1/functions/:functionId/executions') ->dynamic($execution, Response::MODEL_EXECUTION); }); -App::get('/v1/functions/:functionId/executions') +Http::get('/v1/functions/:functionId/executions') ->groups(['api', 'functions']) ->desc('List executions') ->label('scope', 'execution.read') @@ -2122,11 +2132,12 @@ App::get('/v1/functions/:functionId/executions') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + ->inject('authorization') + ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { + $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); @@ -2169,7 +2180,7 @@ App::get('/v1/functions/:functionId/executions') $results = $dbForProject->find('executions', $queries); $total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT); - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); if (!$isPrivilegedUser && !$isAppUser) { @@ -2186,7 +2197,7 @@ App::get('/v1/functions/:functionId/executions') ]), Response::MODEL_EXECUTION_LIST); }); -App::get('/v1/functions/:functionId/executions/:executionId') +Http::get('/v1/functions/:functionId/executions/:executionId') ->groups(['api', 'functions']) ->desc('Get execution') ->label('scope', 'execution.read') @@ -2202,11 +2213,12 @@ App::get('/v1/functions/:functionId/executions/:executionId') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, string $mode) { - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + ->inject('authorization') + ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { + $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); @@ -2222,7 +2234,7 @@ App::get('/v1/functions/:functionId/executions/:executionId') throw new Exception(Exception::EXECUTION_NOT_FOUND); } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); if (!$isPrivilegedUser && !$isAppUser) { @@ -2233,7 +2245,7 @@ App::get('/v1/functions/:functionId/executions/:executionId') $response->dynamic($execution, Response::MODEL_EXECUTION); }); -App::delete('/v1/functions/:functionId/executions/:executionId') +Http::delete('/v1/functions/:functionId/executions/:executionId') ->groups(['api', 'functions']) ->desc('Delete execution') ->label('scope', 'execution.write') @@ -2252,7 +2264,8 @@ App::delete('/v1/functions/:functionId/executions/:executionId') ->inject('dbForProject') ->inject('dbForConsole') ->inject('queueForEvents') - ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForConsole, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForConsole, Event $queueForEvents, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -2289,7 +2302,7 @@ App::delete('/v1/functions/:functionId/executions/:executionId') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); } } @@ -2303,7 +2316,7 @@ App::delete('/v1/functions/:functionId/executions/:executionId') // Variables -App::post('/v1/functions/:functionId/variables') +Http::post('/v1/functions/:functionId/variables') ->desc('Create variable') ->groups(['api', 'functions']) ->label('scope', 'functions.write') @@ -2322,7 +2335,8 @@ App::post('/v1/functions/:functionId/variables') ->inject('response') ->inject('dbForProject') ->inject('dbForConsole') - ->action(function (string $functionId, string $key, string $value, Response $response, Database $dbForProject, Database $dbForConsole) { + ->inject('authorization') + ->action(function (string $functionId, string $key, string $value, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -2360,14 +2374,14 @@ App::post('/v1/functions/:functionId/variables') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($variable, Response::MODEL_VARIABLE); }); -App::get('/v1/functions/:functionId/variables') +Http::get('/v1/functions/:functionId/variables') ->desc('List variables') ->groups(['api', 'functions']) ->label('scope', 'functions.read') @@ -2394,7 +2408,7 @@ App::get('/v1/functions/:functionId/variables') ]), Response::MODEL_VARIABLE_LIST); }); -App::get('/v1/functions/:functionId/variables/:variableId') +Http::get('/v1/functions/:functionId/variables/:variableId') ->desc('Get variable') ->groups(['api', 'functions']) ->label('scope', 'functions.read') @@ -2433,7 +2447,7 @@ App::get('/v1/functions/:functionId/variables/:variableId') $response->dynamic($variable, Response::MODEL_VARIABLE); }); -App::put('/v1/functions/:functionId/variables/:variableId') +Http::put('/v1/functions/:functionId/variables/:variableId') ->desc('Update variable') ->groups(['api', 'functions']) ->label('scope', 'functions.write') @@ -2453,7 +2467,8 @@ App::put('/v1/functions/:functionId/variables/:variableId') ->inject('response') ->inject('dbForProject') ->inject('dbForConsole') - ->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForConsole) { + ->inject('authorization') + ->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); @@ -2489,12 +2504,12 @@ App::put('/v1/functions/:functionId/variables/:variableId') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $response->dynamic($variable, Response::MODEL_VARIABLE); }); -App::delete('/v1/functions/:functionId/variables/:variableId') +Http::delete('/v1/functions/:functionId/variables/:variableId') ->desc('Delete variable') ->groups(['api', 'functions']) ->label('scope', 'functions.write') @@ -2511,7 +2526,8 @@ App::delete('/v1/functions/:functionId/variables/:variableId') ->inject('response') ->inject('dbForProject') ->inject('dbForConsole') - ->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForConsole) { + ->inject('authorization') + ->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -2537,12 +2553,12 @@ App::delete('/v1/functions/:functionId/variables/:variableId') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $response->noContent(); }); -App::get('/v1/functions/templates') +Http::get('/v1/functions/templates') ->groups(['api']) ->desc('List function templates') ->label('scope', 'public') @@ -2580,7 +2596,7 @@ App::get('/v1/functions/templates') ]), Response::MODEL_TEMPLATE_FUNCTION_LIST); }); -App::get('/v1/functions/templates/:templateId') +Http::get('/v1/functions/templates/:templateId') ->desc('Get function template') ->label('scope', 'public') ->label('sdk.namespace', 'functions') @@ -2595,9 +2611,10 @@ App::get('/v1/functions/templates/:templateId') ->action(function (string $templateId, Response $response) { $templates = Config::getParam('function-templates', []); - $template = array_shift(\array_filter($templates, function ($template) use ($templateId) { + $array = \array_filter($templates, function ($template) use ($templateId) { return $template['id'] === $templateId; - })); + }); + $template = array_shift($array); if (empty($template)) { throw new Exception(Exception::FUNCTION_TEMPLATE_NOT_FOUND); diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index f79f433b5c..0e7ddc783d 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -14,27 +14,28 @@ use GraphQL\Validator\Rules\DisableIntrospection; use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\QueryDepth; use Swoole\Coroutine\WaitGroup; -use Utopia\App; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use Utopia\Http\Http; +use Utopia\Http\Validator\JSON; +use Utopia\Http\Validator\Text; use Utopia\System\System; -use Utopia\Validator\JSON; -use Utopia\Validator\Text; -App::init() +Http::init() ->groups(['graphql']) ->inject('project') - ->action(function (Document $project) { + ->inject('authorization') + ->action(function (Document $project, Authorization $authorization) { if ( array_key_exists('graphql', $project->getAttribute('apis', [])) && !$project->getAttribute('apis', [])['graphql'] - && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + && !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } }); -App::get('/v1/graphql') +Http::get('/v1/graphql') ->desc('GraphQL endpoint') ->groups(['graphql']) ->label('scope', 'graphql') @@ -74,7 +75,7 @@ App::get('/v1/graphql') ->json($output); }); -App::post('/v1/graphql/mutation') +Http::post('/v1/graphql/mutation') ->desc('GraphQL endpoint') ->groups(['graphql']) ->label('scope', 'graphql') @@ -119,7 +120,7 @@ App::post('/v1/graphql/mutation') ->json($output); }); -App::post('/v1/graphql') +Http::post('/v1/graphql') ->desc('GraphQL endpoint') ->groups(['graphql']) ->label('scope', 'graphql') @@ -156,7 +157,6 @@ App::post('/v1/graphql') if (\str_starts_with($type, 'multipart/form-data')) { $query = parseMultipart($query, $request); } - $output = execute($schema, $promiseAdapter, $query); $response @@ -205,7 +205,7 @@ function execute( $validations[] = new QueryComplexity($maxComplexity); $validations[] = new QueryDepth($maxDepth); } - if (App::getMode() === App::MODE_TYPE_PRODUCTION) { + if (Http::getMode() === Http::MODE_TYPE_PRODUCTION) { $flags = DebugFlag::NONE; } @@ -306,9 +306,10 @@ function processResult($result, $debugFlags): array ); } -App::shutdown() +Http::shutdown() ->groups(['schema']) ->inject('project') - ->action(function (Document $project) { - Schema::setDirty($project->getId()); + ->inject('schemaVariable') + ->action(function (Document $project, Schema $schemaVariable) { + $schemaVariable->setDirty($project->getId()); }); diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index f4581df8e4..47d80dbf71 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -3,12 +3,19 @@ use Appwrite\ClamAV\Network; use Appwrite\Event\Event; use Appwrite\Extend\Exception; +use Appwrite\Utopia\Queue\Connections; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Config\Config; +use Utopia\Database\Adapter\MariaDB; +use Utopia\Database\Adapter\MySQL; use Utopia\Database\Document; use Utopia\Domains\Validator\PublicDomain; -use Utopia\Pools\Group; +use Utopia\Http\Http; +use Utopia\Http\Validator\Domain; +use Utopia\Http\Validator\Integer; +use Utopia\Http\Validator\Multiple; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Queue\Client; use Utopia\Queue\Connection; use Utopia\Registry\Registry; @@ -16,13 +23,8 @@ use Utopia\Storage\Device; use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; use Utopia\System\System; -use Utopia\Validator\Domain; -use Utopia\Validator\Integer; -use Utopia\Validator\Multiple; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; -App::get('/v1/health') +Http::get('/v1/health') ->desc('Get HTTP') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -45,7 +47,7 @@ App::get('/v1/health') $response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); }); -App::get('/v1/health/version') +Http::get('/v1/health/version') ->desc('Get version') ->groups(['api', 'health']) ->label('scope', 'public') @@ -57,7 +59,7 @@ App::get('/v1/health/version') $response->dynamic(new Document([ 'version' => APP_VERSION_STABLE ]), Response::MODEL_HEALTH_VERSION); }); -App::get('/v1/health/db') +Http::get('/v1/health/db') ->desc('Get DB') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -70,21 +72,34 @@ App::get('/v1/health/db') ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) ->inject('response') ->inject('pools') - ->action(function (Response $response, Group $pools) { + ->inject('connections') + ->action(function (Response $response, array $pools, Connections $connections) { $output = []; $configs = [ - 'Console.DB' => Config::getParam('pools-console'), - 'Projects.DB' => Config::getParam('pools-database'), + 'console' => Config::getParam('pools-console'), + 'database' => Config::getParam('pools-database'), ]; foreach ($configs as $key => $config) { foreach ($config as $database) { - try { - $adapter = $pools->get($database)->pop()->getResource(); + $checkStart = \microtime(true); + + try { + + $pool = $pools['pools-'.$key.'-'.$database]['pool']; + $dsn = $pools['pools-'.$key.'-'.$database]['dsn']; + + $connection = $pool->get(); + $connections->add($connection, $pool); + $adapter = match ($dsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + $adapter->setDatabase($dsn->getPath()); - $checkStart = \microtime(true); if ($adapter->ping()) { $output[] = new Document([ @@ -111,7 +126,7 @@ App::get('/v1/health/db') ]), Response::MODEL_HEALTH_STATUS_LIST); }); -App::get('/v1/health/cache') +Http::get('/v1/health/cache') ->desc('Get cache') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -124,7 +139,8 @@ App::get('/v1/health/cache') ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) ->inject('response') ->inject('pools') - ->action(function (Response $response, Group $pools) { + ->inject('connections') + ->action(function (Response $response, array $pools, Connections $connections) { $output = []; @@ -134,10 +150,15 @@ App::get('/v1/health/cache') foreach ($configs as $key => $config) { foreach ($config as $database) { + $checkStart = \microtime(true); try { - $adapter = $pools->get($database)->pop()->getResource(); + $pool = $pools['pools-cache-' . $database]['pool']; + $dsn = $pools['pools-cache-' . $database]['dsn']; + $connection = $pool->get(); + $connections->add($connection, $pool); + + $adapter = new Connection\Redis($dsn->getHost(), $dsn->getPort()); - $checkStart = \microtime(true); if ($adapter->ping()) { $output[] = new Document([ @@ -158,6 +179,8 @@ App::get('/v1/health/cache') 'status' => 'fail', 'ping' => \round((\microtime(true) - $checkStart) / 1000) ]); + } finally { + $connections->reclaim(); } } } @@ -168,7 +191,7 @@ App::get('/v1/health/cache') ]), Response::MODEL_HEALTH_STATUS_LIST); }); -App::get('/v1/health/queue') +Http::get('/v1/health/queue') ->desc('Get queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -181,7 +204,8 @@ App::get('/v1/health/queue') ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) ->inject('response') ->inject('pools') - ->action(function (Response $response, Group $pools) { + ->inject('connections') + ->action(function (Response $response, array $pools, Connections $connections) { $output = []; @@ -190,12 +214,16 @@ App::get('/v1/health/queue') ]; foreach ($configs as $key => $config) { + $checkStart = \microtime(true); + foreach ($config as $database) { try { - $adapter = $pools->get($database)->pop()->getResource(); - - $checkStart = \microtime(true); + $pool = $pools['pools-queue-' . $database]['pool']; + $dsn = $pools['pools-queue-' . $database]['dsn']; + $connection = $pool->get(); + $connections->add($connection, $pool); + $adapter = new Connection\Redis($dsn->getHost(), $dsn->getPort()); if ($adapter->ping()) { $output[] = new Document([ 'name' => $key . " ($database)", @@ -215,6 +243,8 @@ App::get('/v1/health/queue') 'status' => 'fail', 'ping' => \round((\microtime(true) - $checkStart) / 1000) ]); + } finally { + $connections->reclaim(); } } } @@ -225,7 +255,7 @@ App::get('/v1/health/queue') ]), Response::MODEL_HEALTH_STATUS_LIST); }); -App::get('/v1/health/pubsub') +Http::get('/v1/health/pubsub') ->desc('Get pubsub') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -238,7 +268,8 @@ App::get('/v1/health/pubsub') ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) ->inject('response') ->inject('pools') - ->action(function (Response $response, Group $pools) { + ->inject('connections') + ->action(function (Response $response, array $pools, Connections $connections) { $output = []; @@ -249,7 +280,12 @@ App::get('/v1/health/pubsub') foreach ($configs as $key => $config) { foreach ($config as $database) { try { - $adapter = $pools->get($database)->pop()->getResource(); + $pool = $pools['pools-pubsub-' . $database]['pool']; + + $connection = $pool->get(); + $connections->add($connection, $pool); + + $adapter = new Connection\Redis($connection); $checkStart = \microtime(true); @@ -272,6 +308,8 @@ App::get('/v1/health/pubsub') 'status' => 'fail', 'ping' => \round((\microtime(true) - $checkStart) / 1000) ]); + } finally { + $connections->reclaim(); } } } @@ -282,7 +320,7 @@ App::get('/v1/health/pubsub') ]), Response::MODEL_HEALTH_STATUS_LIST); }); -App::get('/v1/health/time') +Http::get('/v1/health/time') ->desc('Get time') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -339,7 +377,7 @@ App::get('/v1/health/time') $response->dynamic(new Document($output), Response::MODEL_HEALTH_TIME); }); -App::get('/v1/health/queue/webhooks') +Http::get('/v1/health/queue/webhooks') ->desc('Get webhooks queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -366,7 +404,7 @@ App::get('/v1/health/queue/webhooks') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/logs') +Http::get('/v1/health/queue/logs') ->desc('Get logs queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -393,7 +431,7 @@ App::get('/v1/health/queue/logs') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/certificate') +Http::get('/v1/health/certificate') ->desc('Get the SSL certificate for a domain') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -443,7 +481,7 @@ App::get('/v1/health/certificate') ]), Response::MODEL_HEALTH_CERTIFICATE); }, ['response']); -App::get('/v1/health/queue/certificates') +Http::get('/v1/health/queue/certificates') ->desc('Get certificates queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -470,7 +508,7 @@ App::get('/v1/health/queue/certificates') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/builds') +Http::get('/v1/health/queue/builds') ->desc('Get builds queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -497,7 +535,7 @@ App::get('/v1/health/queue/builds') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/databases') +Http::get('/v1/health/queue/databases') ->desc('Get databases queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -525,7 +563,7 @@ App::get('/v1/health/queue/databases') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/deletes') +Http::get('/v1/health/queue/deletes') ->desc('Get deletes queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -552,7 +590,7 @@ App::get('/v1/health/queue/deletes') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/mails') +Http::get('/v1/health/queue/mails') ->desc('Get mails queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -579,7 +617,7 @@ App::get('/v1/health/queue/mails') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/messaging') +Http::get('/v1/health/queue/messaging') ->desc('Get messaging queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -606,7 +644,7 @@ App::get('/v1/health/queue/messaging') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/migrations') +Http::get('/v1/health/queue/migrations') ->desc('Get migrations queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -633,7 +671,7 @@ App::get('/v1/health/queue/migrations') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/functions') +Http::get('/v1/health/queue/functions') ->desc('Get functions queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -660,7 +698,7 @@ App::get('/v1/health/queue/functions') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/usage') +Http::get('/v1/health/queue/usage') ->desc('Get usage queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -687,7 +725,7 @@ App::get('/v1/health/queue/usage') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }); -App::get('/v1/health/queue/usage-dump') +Http::get('/v1/health/queue/usage-dump') ->desc('Get usage dump queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -714,7 +752,7 @@ App::get('/v1/health/queue/usage-dump') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }); -App::get('/v1/health/storage/local') +Http::get('/v1/health/storage/local') ->desc('Get local storage') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -757,7 +795,7 @@ App::get('/v1/health/storage/local') $response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); }); -App::get('/v1/health/storage') +Http::get('/v1/health/storage') ->desc('Get storage') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -798,7 +836,7 @@ App::get('/v1/health/storage') $response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); }); -App::get('/v1/health/anti-virus') +Http::get('/v1/health/anti-virus') ->desc('Get antivirus') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -837,7 +875,7 @@ App::get('/v1/health/anti-virus') $response->dynamic(new Document($output), Response::MODEL_HEALTH_ANTIVIRUS); }); -App::get('/v1/health/queue/failed/:name') +Http::get('/v1/health/queue/failed/:name') ->desc('Get number of failed queue jobs') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -878,7 +916,7 @@ App::get('/v1/health/queue/failed/:name') $response->dynamic(new Document([ 'size' => $failed ]), Response::MODEL_HEALTH_QUEUE); }); -App::get('/v1/health/stats') // Currently only used internally +Http::get('/v1/health/stats') // Currently only used internally ->desc('Get system stats') ->groups(['api', 'health']) ->label('scope', 'root') diff --git a/app/controllers/api/locale.php b/app/controllers/api/locale.php index 2917bc8416..fcaf0c03cb 100644 --- a/app/controllers/api/locale.php +++ b/app/controllers/api/locale.php @@ -3,12 +3,12 @@ use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; -use Utopia\App; use Utopia\Config\Config; use Utopia\Database\Document; +use Utopia\Http\Http; use Utopia\Locale\Locale; -App::get('/v1/locale') +Http::get('/v1/locale') ->desc('Get user locale') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -68,8 +68,8 @@ App::get('/v1/locale') $response->dynamic(new Document($output), Response::MODEL_LOCALE); }); -App::get('/v1/locale/codes') - ->desc('List locale codes') +Http::get('/v1/locale/codes') + ->desc('List Locale Codes') ->groups(['api', 'locale']) ->label('scope', 'locale.read') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) @@ -90,7 +90,7 @@ App::get('/v1/locale/codes') ]), Response::MODEL_LOCALE_CODE_LIST); }); -App::get('/v1/locale/countries') +Http::get('/v1/locale/countries') ->desc('List countries') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -123,7 +123,7 @@ App::get('/v1/locale/countries') $response->dynamic(new Document(['countries' => $output, 'total' => \count($output)]), Response::MODEL_COUNTRY_LIST); }); -App::get('/v1/locale/countries/eu') +Http::get('/v1/locale/countries/eu') ->desc('List EU countries') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -158,7 +158,7 @@ App::get('/v1/locale/countries/eu') $response->dynamic(new Document(['countries' => $output, 'total' => \count($output)]), Response::MODEL_COUNTRY_LIST); }); -App::get('/v1/locale/countries/phones') +Http::get('/v1/locale/countries/phones') ->desc('List countries phone codes') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -192,7 +192,7 @@ App::get('/v1/locale/countries/phones') $response->dynamic(new Document(['phones' => $output, 'total' => \count($output)]), Response::MODEL_PHONE_LIST); }); -App::get('/v1/locale/continents') +Http::get('/v1/locale/continents') ->desc('List continents') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -224,7 +224,7 @@ App::get('/v1/locale/continents') $response->dynamic(new Document(['continents' => $output, 'total' => \count($output)]), Response::MODEL_CONTINENT_LIST); }); -App::get('/v1/locale/currencies') +Http::get('/v1/locale/currencies') ->desc('List currencies') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -247,7 +247,7 @@ App::get('/v1/locale/currencies') }); -App::get('/v1/locale/languages') +Http::get('/v1/locale/languages') ->desc('List languages') ->groups(['api', 'locale']) ->label('scope', 'locale.read') diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 7da0348a8f..8beb38c7ca 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -20,7 +20,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Targets; use Appwrite\Utopia\Database\Validator\Queries\Topics; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; -use Utopia\App; use Utopia\Audit\Audit; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -30,25 +29,27 @@ use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Queries; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\Roles; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Integer; +use Utopia\Http\Validator\JSON; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Locale\Locale; use Utopia\System\System; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Boolean; -use Utopia\Validator\Integer; -use Utopia\Validator\JSON; -use Utopia\Validator\Range; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; use function Swoole\Coroutine\batch; -App::post('/v1/messaging/providers/mailgun') +Http::post('/v1/messaging/providers/mailgun') ->desc('Create Mailgun provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -135,7 +136,7 @@ App::post('/v1/messaging/providers/mailgun') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/sendgrid') +Http::post('/v1/messaging/providers/sendgrid') ->desc('Create Sendgrid provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -210,7 +211,7 @@ App::post('/v1/messaging/providers/sendgrid') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/smtp') +Http::post('/v1/messaging/providers/smtp') ->desc('Create SMTP provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -298,7 +299,7 @@ App::post('/v1/messaging/providers/smtp') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/msg91') +Http::post('/v1/messaging/providers/msg91') ->desc('Create Msg91 provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -374,7 +375,7 @@ App::post('/v1/messaging/providers/msg91') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/telesign') +Http::post('/v1/messaging/providers/telesign') ->desc('Create Telesign provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -451,7 +452,7 @@ App::post('/v1/messaging/providers/telesign') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/textmagic') +Http::post('/v1/messaging/providers/textmagic') ->desc('Create Textmagic provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -528,7 +529,7 @@ App::post('/v1/messaging/providers/textmagic') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/twilio') +Http::post('/v1/messaging/providers/twilio') ->desc('Create Twilio provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -605,7 +606,7 @@ App::post('/v1/messaging/providers/twilio') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/vonage') +Http::post('/v1/messaging/providers/vonage') ->desc('Create Vonage provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -682,7 +683,7 @@ App::post('/v1/messaging/providers/vonage') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/fcm') +Http::post('/v1/messaging/providers/fcm') ->desc('Create FCM provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -745,7 +746,7 @@ App::post('/v1/messaging/providers/fcm') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/apns') +Http::post('/v1/messaging/providers/apns') ->desc('Create APNS provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -831,7 +832,7 @@ App::post('/v1/messaging/providers/apns') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::get('/v1/messaging/providers') +Http::get('/v1/messaging/providers') ->desc('List providers') ->groups(['api', 'messaging']) ->label('scope', 'providers.read') @@ -846,7 +847,8 @@ App::get('/v1/messaging/providers') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('dbForProject') ->inject('response') - ->action(function (array $queries, string $search, Database $dbForProject, Response $response) { + ->inject('authorization') + ->action(function (array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -867,7 +869,7 @@ App::get('/v1/messaging/providers') if ($cursor) { $providerId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId)); + $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('providers', $providerId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Provider '{$providerId}' for the 'cursor' value not found."); @@ -882,7 +884,7 @@ App::get('/v1/messaging/providers') ]), Response::MODEL_PROVIDER_LIST); }); -App::get('/v1/messaging/providers/:providerId/logs') +Http::get('/v1/messaging/providers/:providerId/logs') ->desc('List provider logs') ->groups(['api', 'messaging']) ->label('scope', 'providers.read') @@ -970,7 +972,7 @@ App::get('/v1/messaging/providers/:providerId/logs') ]), Response::MODEL_LOG_LIST); }); -App::get('/v1/messaging/providers/:providerId') +Http::get('/v1/messaging/providers/:providerId') ->desc('Get provider') ->groups(['api', 'messaging']) ->label('scope', 'providers.read') @@ -994,7 +996,7 @@ App::get('/v1/messaging/providers/:providerId') $response->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/mailgun/:providerId') +Http::patch('/v1/messaging/providers/mailgun/:providerId') ->desc('Update Mailgun provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1100,7 +1102,7 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/sendgrid/:providerId') +Http::patch('/v1/messaging/providers/sendgrid/:providerId') ->desc('Update Sendgrid provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1191,7 +1193,7 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/smtp/:providerId') +Http::patch('/v1/messaging/providers/smtp/:providerId') ->desc('Update SMTP provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1313,7 +1315,7 @@ App::patch('/v1/messaging/providers/smtp/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/msg91/:providerId') +Http::patch('/v1/messaging/providers/msg91/:providerId') ->desc('Update Msg91 provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1393,7 +1395,7 @@ App::patch('/v1/messaging/providers/msg91/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/telesign/:providerId') +Http::patch('/v1/messaging/providers/telesign/:providerId') ->desc('Update Telesign provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1475,7 +1477,7 @@ App::patch('/v1/messaging/providers/telesign/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/textmagic/:providerId') +Http::patch('/v1/messaging/providers/textmagic/:providerId') ->desc('Update Textmagic provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1557,7 +1559,7 @@ App::patch('/v1/messaging/providers/textmagic/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/twilio/:providerId') +Http::patch('/v1/messaging/providers/twilio/:providerId') ->desc('Update Twilio provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1639,7 +1641,7 @@ App::patch('/v1/messaging/providers/twilio/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/vonage/:providerId') +Http::patch('/v1/messaging/providers/vonage/:providerId') ->desc('Update Vonage provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1721,7 +1723,7 @@ App::patch('/v1/messaging/providers/vonage/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/fcm/:providerId') +Http::patch('/v1/messaging/providers/fcm/:providerId') ->desc('Update FCM provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1790,7 +1792,7 @@ App::patch('/v1/messaging/providers/fcm/:providerId') }); -App::patch('/v1/messaging/providers/apns/:providerId') +Http::patch('/v1/messaging/providers/apns/:providerId') ->desc('Update APNS provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1885,7 +1887,7 @@ App::patch('/v1/messaging/providers/apns/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::delete('/v1/messaging/providers/:providerId') +Http::delete('/v1/messaging/providers/:providerId') ->desc('Delete provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.delete') @@ -1920,7 +1922,7 @@ App::delete('/v1/messaging/providers/:providerId') ->noContent(); }); -App::post('/v1/messaging/topics') +Http::post('/v1/messaging/topics') ->desc('Create topic') ->groups(['api', 'messaging']) ->label('audits.event', 'topic.create') @@ -1963,7 +1965,7 @@ App::post('/v1/messaging/topics') ->dynamic($topic, Response::MODEL_TOPIC); }); -App::get('/v1/messaging/topics') +Http::get('/v1/messaging/topics') ->desc('List topics') ->groups(['api', 'messaging']) ->label('scope', 'topics.read') @@ -1978,7 +1980,8 @@ App::get('/v1/messaging/topics') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('dbForProject') ->inject('response') - ->action(function (array $queries, string $search, Database $dbForProject, Response $response) { + ->inject('authorization') + ->action(function (array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -1999,7 +2002,7 @@ App::get('/v1/messaging/topics') if ($cursor) { $topicId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Topic '{$topicId}' for the 'cursor' value not found."); @@ -2014,7 +2017,7 @@ App::get('/v1/messaging/topics') ]), Response::MODEL_TOPIC_LIST); }); -App::get('/v1/messaging/topics/:topicId/logs') +Http::get('/v1/messaging/topics/:topicId/logs') ->desc('List topic logs') ->groups(['api', 'messaging']) ->label('scope', 'topics.read') @@ -2031,7 +2034,8 @@ App::get('/v1/messaging/topics/:topicId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $topicId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $topicId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { $topic = $dbForProject->getDocument('topics', $topicId); if ($topic->isEmpty()) { @@ -2103,7 +2107,7 @@ App::get('/v1/messaging/topics/:topicId/logs') ]), Response::MODEL_LOG_LIST); }); -App::get('/v1/messaging/topics/:topicId') +Http::get('/v1/messaging/topics/:topicId') ->desc('Get topic') ->groups(['api', 'messaging']) ->label('scope', 'topics.read') @@ -2128,7 +2132,7 @@ App::get('/v1/messaging/topics/:topicId') ->dynamic($topic, Response::MODEL_TOPIC); }); -App::patch('/v1/messaging/topics/:topicId') +Http::patch('/v1/messaging/topics/:topicId') ->desc('Update topic') ->groups(['api', 'messaging']) ->label('audits.event', 'topic.update') @@ -2172,7 +2176,7 @@ App::patch('/v1/messaging/topics/:topicId') ->dynamic($topic, Response::MODEL_TOPIC); }); -App::delete('/v1/messaging/topics/:topicId') +Http::delete('/v1/messaging/topics/:topicId') ->desc('Delete topic') ->groups(['api', 'messaging']) ->label('audits.event', 'topic.delete') @@ -2212,7 +2216,7 @@ App::delete('/v1/messaging/topics/:topicId') ->noContent(); }); -App::post('/v1/messaging/topics/:topicId/subscribers') +Http::post('/v1/messaging/topics/:topicId/subscribers') ->desc('Create subscriber') ->groups(['api', 'messaging']) ->label('audits.event', 'subscriber.create') @@ -2232,28 +2236,27 @@ App::post('/v1/messaging/topics/:topicId/subscribers') ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Response $response) { + ->inject('authorization') + ->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Response $response, Authorization $authorization) { $subscriberId = $subscriberId == 'unique()' ? ID::unique() : $subscriberId; - $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($topic->isEmpty()) { throw new Exception(Exception::TOPIC_NOT_FOUND); } - $validator = new Authorization('subscribe'); - - if (!$validator->isValid($topic->getAttribute('subscribe'))) { - throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription()); + if (!$authorization->isValid(new Input('subscribe', $topic->getAttribute('subscribe')))) { + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } - $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); + $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId)); if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); } - $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); + $user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); $subscriber = new Document([ '$id' => $subscriberId, @@ -2286,7 +2289,7 @@ App::post('/v1/messaging/topics/:topicId/subscribers') default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE), }; - Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute( + $authorization->skip(fn () => $dbForProject->increaseDocumentAttribute( 'topics', $topicId, $totalAttribute, @@ -2308,7 +2311,7 @@ App::post('/v1/messaging/topics/:topicId/subscribers') ->dynamic($subscriber, Response::MODEL_SUBSCRIBER); }); -App::get('/v1/messaging/topics/:topicId/subscribers') +Http::get('/v1/messaging/topics/:topicId/subscribers') ->desc('List subscribers') ->groups(['api', 'messaging']) ->label('scope', 'subscribers.read') @@ -2324,7 +2327,8 @@ App::get('/v1/messaging/topics/:topicId/subscribers') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('dbForProject') ->inject('response') - ->action(function (string $topicId, array $queries, string $search, Database $dbForProject, Response $response) { + ->inject('authorization') + ->action(function (string $topicId, array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -2335,7 +2339,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers') $queries[] = Query::search('search', $search); } - $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($topic->isEmpty()) { throw new Exception(Exception::TOPIC_NOT_FOUND); @@ -2353,7 +2357,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers') if ($cursor) { $subscriberId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId)); + $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Subscriber '{$subscriberId}' for the 'cursor' value not found."); @@ -2364,10 +2368,10 @@ App::get('/v1/messaging/topics/:topicId/subscribers') $subscribers = $dbForProject->find('subscribers', $queries); - $subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject) { - return function () use ($subscriber, $dbForProject) { - $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); - $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); + $subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject, $authorization) { + return function () use ($subscriber, $dbForProject, $authorization) { + $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); + $user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); return $subscriber ->setAttribute('target', $target) @@ -2382,7 +2386,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers') ]), Response::MODEL_SUBSCRIBER_LIST); }); -App::get('/v1/messaging/subscribers/:subscriberId/logs') +Http::get('/v1/messaging/subscribers/:subscriberId/logs') ->desc('List subscriber logs') ->groups(['api', 'messaging']) ->label('scope', 'subscribers.read') @@ -2399,7 +2403,8 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $subscriberId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $subscriberId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { $subscriber = $dbForProject->getDocument('subscribers', $subscriberId); if ($subscriber->isEmpty()) { @@ -2471,7 +2476,7 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs') ]), Response::MODEL_LOG_LIST); }); -App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') +Http::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->desc('Get subscriber') ->groups(['api', 'messaging']) ->label('scope', 'subscribers.read') @@ -2486,8 +2491,9 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->param('subscriberId', '', new UID(), 'Subscriber ID.') ->inject('dbForProject') ->inject('response') - ->action(function (string $topicId, string $subscriberId, Database $dbForProject, Response $response) { - $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + ->inject('authorization') + ->action(function (string $topicId, string $subscriberId, Database $dbForProject, Response $response, Authorization $authorization) { + $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($topic->isEmpty()) { throw new Exception(Exception::TOPIC_NOT_FOUND); @@ -2499,8 +2505,8 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') throw new Exception(Exception::SUBSCRIBER_NOT_FOUND); } - $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); - $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); + $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); + $user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); $subscriber ->setAttribute('target', $target) @@ -2510,7 +2516,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->dynamic($subscriber, Response::MODEL_SUBSCRIBER); }); -App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') +Http::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->desc('Delete subscriber') ->groups(['api', 'messaging']) ->label('audits.event', 'subscriber.delete') @@ -2529,8 +2535,9 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Response $response) { - $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + ->inject('authorization') + ->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Response $response, Authorization $authorization) { + $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($topic->isEmpty()) { throw new Exception(Exception::TOPIC_NOT_FOUND); @@ -2553,7 +2560,7 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE), }; - Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute( + $authorization->skip(fn () => $dbForProject->decreaseDocumentAttribute( 'topics', $topicId, $totalAttribute, @@ -2569,7 +2576,7 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->noContent(); }); -App::post('/v1/messaging/messages/email') +Http::post('/v1/messaging/messages/email') ->desc('Create email') ->groups(['api', 'messaging']) ->label('audits.event', 'message.create') @@ -2721,7 +2728,7 @@ App::post('/v1/messaging/messages/email') ->dynamic($message, Response::MODEL_MESSAGE); }); -App::post('/v1/messaging/messages/sms') +Http::post('/v1/messaging/messages/sms') ->desc('Create SMS') ->groups(['api', 'messaging']) ->label('audits.event', 'message.create') @@ -2837,7 +2844,7 @@ App::post('/v1/messaging/messages/sms') ->dynamic($message, Response::MODEL_MESSAGE); }); -App::post('/v1/messaging/messages/push') +Http::post('/v1/messaging/messages/push') ->desc('Create push notification') ->groups(['api', 'messaging']) ->label('audits.event', 'message.create') @@ -3013,7 +3020,7 @@ App::post('/v1/messaging/messages/push') ->dynamic($message, Response::MODEL_MESSAGE); }); -App::get('/v1/messaging/messages') +Http::get('/v1/messaging/messages') ->desc('List messages') ->groups(['api', 'messaging']) ->label('scope', 'messages.read') @@ -3028,7 +3035,8 @@ App::get('/v1/messaging/messages') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('dbForProject') ->inject('response') - ->action(function (array $queries, string $search, Database $dbForProject, Response $response) { + ->inject('authorization') + ->action(function (array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -3049,7 +3057,7 @@ App::get('/v1/messaging/messages') if ($cursor) { $messageId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('messages', $messageId)); + $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('messages', $messageId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Message '{$messageId}' for the 'cursor' value not found."); @@ -3064,7 +3072,7 @@ App::get('/v1/messaging/messages') ]), Response::MODEL_MESSAGE_LIST); }); -App::get('/v1/messaging/messages/:messageId/logs') +Http::get('/v1/messaging/messages/:messageId/logs') ->desc('List message logs') ->groups(['api', 'messaging']) ->label('scope', 'messages.read') @@ -3081,7 +3089,8 @@ App::get('/v1/messaging/messages/:messageId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $messageId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $messageId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { $message = $dbForProject->getDocument('messages', $messageId); if ($message->isEmpty()) { @@ -3153,7 +3162,7 @@ App::get('/v1/messaging/messages/:messageId/logs') ]), Response::MODEL_LOG_LIST); }); -App::get('/v1/messaging/messages/:messageId/targets') +Http::get('/v1/messaging/messages/:messageId/targets') ->desc('List message targets') ->groups(['api', 'messaging']) ->label('scope', 'messages.read') @@ -3218,7 +3227,7 @@ App::get('/v1/messaging/messages/:messageId/targets') ]), Response::MODEL_TARGET_LIST); }); -App::get('/v1/messaging/messages/:messageId') +Http::get('/v1/messaging/messages/:messageId') ->desc('Get message') ->groups(['api', 'messaging']) ->label('scope', 'messages.read') @@ -3242,7 +3251,7 @@ App::get('/v1/messaging/messages/:messageId') $response->dynamic($message, Response::MODEL_MESSAGE); }); -App::patch('/v1/messaging/messages/email/:messageId') +Http::patch('/v1/messaging/messages/email/:messageId') ->desc('Update email') ->groups(['api', 'messaging']) ->label('audits.event', 'message.update') @@ -3442,7 +3451,7 @@ App::patch('/v1/messaging/messages/email/:messageId') ->dynamic($message, Response::MODEL_MESSAGE); }); -App::patch('/v1/messaging/messages/sms/:messageId') +Http::patch('/v1/messaging/messages/sms/:messageId') ->desc('Update SMS') ->groups(['api', 'messaging']) ->label('audits.event', 'message.update') @@ -3597,7 +3606,7 @@ App::patch('/v1/messaging/messages/sms/:messageId') ->dynamic($message, Response::MODEL_MESSAGE); }); -App::patch('/v1/messaging/messages/push/:messageId') +Http::patch('/v1/messaging/messages/push/:messageId') ->desc('Update push notification') ->groups(['api', 'messaging']) ->label('audits.event', 'message.update') @@ -3835,7 +3844,7 @@ App::patch('/v1/messaging/messages/push/:messageId') ->dynamic($message, Response::MODEL_MESSAGE); }); -App::delete('/v1/messaging/messages/:messageId') +Http::delete('/v1/messaging/messages/:messageId') ->desc('Delete message') ->groups(['api', 'messaging']) ->label('audits.event', 'message.delete') diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 374a5575a7..552a4293f1 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -9,7 +9,6 @@ use Appwrite\Role; use Appwrite\Utopia\Database\Validator\Queries\Migrations; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -17,21 +16,22 @@ use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Host; +use Utopia\Http\Validator\Integer; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\URL; +use Utopia\Http\Validator\WhiteList; use Utopia\Migration\Sources\Appwrite; use Utopia\Migration\Sources\Firebase; use Utopia\Migration\Sources\NHost; use Utopia\Migration\Sources\Supabase; use Utopia\System\System; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Host; -use Utopia\Validator\Integer; -use Utopia\Validator\Text; -use Utopia\Validator\URL; -use Utopia\Validator\WhiteList; include_once __DIR__ . '/../shared/api.php'; -App::post('/v1/migrations/appwrite') +Http::post('/v1/migrations/appwrite') ->groups(['api', 'migrations']) ->desc('Migrate Appwrite data') ->label('scope', 'migrations.write') @@ -85,7 +85,7 @@ App::post('/v1/migrations/appwrite') ->dynamic($migration, Response::MODEL_MIGRATION); }); -App::post('/v1/migrations/firebase/oauth') +Http::post('/v1/migrations/firebase/oauth') ->groups(['api', 'migrations']) ->desc('Migrate Firebase data (OAuth)') ->label('scope', 'migrations.write') @@ -187,7 +187,7 @@ App::post('/v1/migrations/firebase/oauth') ->dynamic($migration, Response::MODEL_MIGRATION); }); -App::post('/v1/migrations/firebase') +Http::post('/v1/migrations/firebase') ->groups(['api', 'migrations']) ->desc('Migrate Firebase data (Service Account)') ->label('scope', 'migrations.write') @@ -247,7 +247,7 @@ App::post('/v1/migrations/firebase') ->dynamic($migration, Response::MODEL_MIGRATION); }); -App::post('/v1/migrations/supabase') +Http::post('/v1/migrations/supabase') ->groups(['api', 'migrations']) ->desc('Migrate Supabase data') ->label('scope', 'migrations.write') @@ -307,7 +307,7 @@ App::post('/v1/migrations/supabase') ->dynamic($migration, Response::MODEL_MIGRATION); }); -App::post('/v1/migrations/nhost') +Http::post('/v1/migrations/nhost') ->groups(['api', 'migrations']) ->desc('Migrate NHost data') ->label('scope', 'migrations.write') @@ -369,7 +369,7 @@ App::post('/v1/migrations/nhost') ->dynamic($migration, Response::MODEL_MIGRATION); }); -App::get('/v1/migrations') +Http::get('/v1/migrations') ->groups(['api', 'migrations']) ->desc('List migrations') ->label('scope', 'migrations.read') @@ -422,7 +422,7 @@ App::get('/v1/migrations') ]), Response::MODEL_MIGRATION_LIST); }); -App::get('/v1/migrations/:migrationId') +Http::get('/v1/migrations/:migrationId') ->groups(['api', 'migrations']) ->desc('Get migration') ->label('scope', 'migrations.read') @@ -446,7 +446,7 @@ App::get('/v1/migrations/:migrationId') $response->dynamic($migration, Response::MODEL_MIGRATION); }); -App::get('/v1/migrations/appwrite/report') +Http::get('/v1/migrations/appwrite/report') ->groups(['api', 'migrations']) ->desc('Generate a report on Appwrite data') ->label('scope', 'migrations.write') @@ -488,7 +488,7 @@ App::get('/v1/migrations/appwrite/report') ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); -App::get('/v1/migrations/firebase/report') +Http::get('/v1/migrations/firebase/report') ->groups(['api', 'migrations']) ->desc('Generate a report on Firebase data') ->label('scope', 'migrations.write') @@ -535,7 +535,7 @@ App::get('/v1/migrations/firebase/report') ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); -App::get('/v1/migrations/firebase/report/oauth') +Http::get('/v1/migrations/firebase/report/oauth') ->groups(['api', 'migrations']) ->desc('Generate a report on Firebase data using OAuth') ->label('scope', 'migrations.write') @@ -626,7 +626,7 @@ App::get('/v1/migrations/firebase/report/oauth') ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); -App::get('/v1/migrations/firebase/connect') +Http::get('/v1/migrations/firebase/connect') ->desc('Authorize with Firebase') ->groups(['api', 'migrations']) ->label('scope', 'migrations.write') @@ -668,7 +668,7 @@ App::get('/v1/migrations/firebase/connect') ->redirect($url); }); -App::get('/v1/migrations/firebase/redirect') +Http::get('/v1/migrations/firebase/redirect') ->desc('Capture and receive data on Firebase authorization') ->groups(['api', 'migrations']) ->label('scope', 'public') @@ -780,8 +780,8 @@ App::get('/v1/migrations/firebase/redirect') ->redirect($redirect); }); -App::get('/v1/migrations/firebase/projects') - ->desc('List Firebase projects') +Http::get('/v1/migrations/firebase/projects') + ->desc('List Firebase Projects') ->groups(['api', 'migrations']) ->label('scope', 'migrations.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -869,8 +869,8 @@ App::get('/v1/migrations/firebase/projects') ]), Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST); }); -App::get('/v1/migrations/firebase/deauthorize') - ->desc('Revoke Appwrite\'s authorization to access Firebase projects') +Http::get('/v1/migrations/firebase/deauthorize') + ->desc('Revoke Appwrite\'s authorization to access Firebase Projects') ->groups(['api', 'migrations']) ->label('scope', 'migrations.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -897,7 +897,7 @@ App::get('/v1/migrations/firebase/deauthorize') $response->noContent(); }); -App::get('/v1/migrations/supabase/report') +Http::get('/v1/migrations/supabase/report') ->groups(['api', 'migrations']) ->desc('Generate a report on Supabase Data') ->label('scope', 'migrations.write') @@ -940,7 +940,7 @@ App::get('/v1/migrations/supabase/report') ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); -App::get('/v1/migrations/nhost/report') +Http::get('/v1/migrations/nhost/report') ->groups(['api', 'migrations']) ->desc('Generate a report on NHost Data') ->label('scope', 'migrations.write') @@ -983,7 +983,7 @@ App::get('/v1/migrations/nhost/report') ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); -App::patch('/v1/migrations/:migrationId') +Http::patch('/v1/migrations/:migrationId') ->groups(['api', 'migrations']) ->desc('Retry migration') ->label('scope', 'migrations.write') @@ -1028,7 +1028,7 @@ App::patch('/v1/migrations/:migrationId') $response->noContent(); }); -App::delete('/v1/migrations/:migrationId') +Http::delete('/v1/migrations/:migrationId') ->groups(['api', 'migrations']) ->desc('Delete migration') ->label('scope', 'migrations.write') diff --git a/app/controllers/api/project.php b/app/controllers/api/project.php index 6053326308..8a6f4d942e 100644 --- a/app/controllers/api/project.php +++ b/app/controllers/api/project.php @@ -2,7 +2,6 @@ use Appwrite\Extend\Exception; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate as DuplicateException; @@ -13,10 +12,11 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime as DateTimeValidator; use Utopia\Database\Validator\UID; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; +use Utopia\Http\Http; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; -App::get('/v1/project/usage') +Http::get('/v1/project/usage') ->desc('Get project usage stats') ->groups(['api', 'usage']) ->label('scope', 'projects.read') @@ -31,7 +31,8 @@ App::get('/v1/project/usage') ->param('period', '1d', new WhiteList(['1h', '1d']), 'Period used', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $startDate, string $endDate, string $period, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $startDate, string $endDate, string $period, Response $response, Database $dbForProject, Authorization $authorization) { $stats = $total = $usage = []; $format = 'Y-m-d 00:00:00'; $firstDay = (new DateTime($startDate))->format($format); @@ -78,7 +79,7 @@ App::get('/v1/project/usage') '1d' => 'Y-m-d\T00:00:00.000P', }; - Authorization::skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) { + $authorization->skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) { foreach ($metrics['total'] as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -296,8 +297,6 @@ App::get('/v1/project/usage') 'buildsStorageTotal' => $total[METRIC_BUILDS_STORAGE], 'deploymentsStorageTotal' => $total[METRIC_DEPLOYMENTS_STORAGE], 'executionsBreakdown' => $executionsBreakdown, - 'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown, - 'buildsMbSecondsBreakdown' => $buildsMbSecondsBreakdown, 'bucketsBreakdown' => $bucketsBreakdown, 'databasesStorageBreakdown' => $databasesStorageBreakdown, 'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown, @@ -308,8 +307,8 @@ App::get('/v1/project/usage') // Variables -App::post('/v1/project/variables') - ->desc('Create variable') +Http::post('/v1/project/variables') + ->desc('Create Variable') ->groups(['api']) ->label('scope', 'projects.write') ->label('audits.event', 'variable.create') @@ -363,8 +362,8 @@ App::post('/v1/project/variables') ->dynamic($variable, Response::MODEL_VARIABLE); }); -App::get('/v1/project/variables') - ->desc('List variables') +Http::get('/v1/project/variables') + ->desc('List Variables') ->groups(['api']) ->label('scope', 'projects.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -388,8 +387,8 @@ App::get('/v1/project/variables') ]), Response::MODEL_VARIABLE_LIST); }); -App::get('/v1/project/variables/:variableId') - ->desc('Get variable') +Http::get('/v1/project/variables/:variableId') + ->desc('Get Variable') ->groups(['api']) ->label('scope', 'projects.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -412,8 +411,8 @@ App::get('/v1/project/variables/:variableId') $response->dynamic($variable, Response::MODEL_VARIABLE); }); -App::put('/v1/project/variables/:variableId') - ->desc('Update variable') +Http::put('/v1/project/variables/:variableId') + ->desc('Update Variable') ->groups(['api']) ->label('scope', 'projects.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -458,8 +457,8 @@ App::put('/v1/project/variables/:variableId') $response->dynamic($variable, Response::MODEL_VARIABLE); }); -App::delete('/v1/project/variables/:variableId') - ->desc('Delete variable') +Http::delete('/v1/project/variables/:variableId') + ->desc('Delete Variable') ->groups(['api']) ->label('scope', 'projects.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 3a8c232195..da5b4b882c 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -13,14 +13,16 @@ use Appwrite\Network\Validator\Origin; use Appwrite\Template\Template; use Appwrite\Utopia\Database\Validator\ProjectId; use Appwrite\Utopia\Database\Validator\Queries\Projects; +use Appwrite\Utopia\Queue\Connections; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use PHPMailer\PHPMailer\PHPMailer; use Utopia\Abuse\Adapters\Database\TimeLimit; -use Utopia\App; use Utopia\Audit\Audit; use Utopia\Cache\Cache; use Utopia\Config\Config; +use Utopia\Database\Adapter\MariaDB; +use Utopia\Database\Adapter\MySQL; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -30,24 +32,25 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Domains\Validator\PublicDomain; use Utopia\DSN\DSN; +use Utopia\Http\Http; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Hostname; +use Utopia\Http\Validator\Integer; +use Utopia\Http\Validator\Multiple; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\URL; +use Utopia\Http\Validator\WhiteList; use Utopia\Locale\Locale; -use Utopia\Pools\Group; use Utopia\System\System; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Boolean; -use Utopia\Validator\Hostname; -use Utopia\Validator\Integer; -use Utopia\Validator\Multiple; -use Utopia\Validator\Range; -use Utopia\Validator\Text; -use Utopia\Validator\URL; -use Utopia\Validator\WhiteList; -App::init() +Http::init() ->groups(['projects']) ->inject('project') ->action(function (Document $project) { @@ -56,7 +59,7 @@ App::init() } }); -App::post('/v1/projects') +Http::post('/v1/projects') ->desc('Create project') ->groups(['api', 'projects']) ->label('audits.event', 'projects.create') @@ -86,8 +89,9 @@ App::post('/v1/projects') ->inject('cache') ->inject('pools') ->inject('hooks') - ->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Request $request, Response $response, Database $dbForConsole, Cache $cache, Group $pools, Hooks $hooks) { - + ->inject('authorization') + ->inject('connections') + ->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Request $request, Response $response, Database $dbForConsole, Cache $cache, array $pools, Hooks $hooks, Authorization $authorization, Connections $connections) { $team = $dbForConsole->getDocument('teams', $teamId); if ($team->isEmpty()) { @@ -189,8 +193,21 @@ App::post('/v1/projects') $dsn = new DSN('mysql://' . $dsn); } - $adapter = $pools->get($dsn->getHost())->pop()->getResource(); + $pool = $pools['pools-database-' . $dsn->getHost()]['pool']; + $connectionDsn = $pools['pools-database-' . $dsn->getHost()]['dsn']; + $connection = $pool->get(); + $connections->add($connection, $pool); + + $adapter = match ($connectionDsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + + $adapter->setDatabase($connectionDsn->getPath()); + $dbForProject = new Database($adapter, $cache); + $dbForProject->setAuthorization($authorization); if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { $dbForProject @@ -208,10 +225,8 @@ App::post('/v1/projects') $audit = new Audit($dbForProject); $audit->setup(); - $abuse = new TimeLimit('', 0, 1, $dbForProject); $abuse->setup(); - /** @var array $collections */ $collections = Config::getParam('collections', [])['projects'] ?? []; @@ -234,17 +249,17 @@ App::post('/v1/projects') // Collection already exists } } - + $connections->reclaim(); // Hook allowing instant project mirroring during migration // Outside of migration, hook is not registered and has no effect - $hooks->trigger('afterProjectCreation', [ $project, $pools, $cache ]); + $hooks->trigger('afterProjectCreation', [$project, $pools, $cache]); $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($project, Response::MODEL_PROJECT); }); -App::get('/v1/projects') +Http::get('/v1/projects') ->desc('List projects') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -259,7 +274,6 @@ App::get('/v1/projects') ->inject('response') ->inject('dbForConsole') ->action(function (array $queries, string $search, Response $response, Database $dbForConsole) { - try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -297,7 +311,7 @@ App::get('/v1/projects') ]), Response::MODEL_PROJECT_LIST); }); -App::get('/v1/projects/:projectId') +Http::get('/v1/projects/:projectId') ->desc('Get project') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -311,7 +325,6 @@ App::get('/v1/projects/:projectId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -321,7 +334,7 @@ App::get('/v1/projects/:projectId') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId') +Http::patch('/v1/projects/:projectId') ->desc('Update project') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -345,31 +358,34 @@ App::patch('/v1/projects/:projectId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project - ->setAttribute('name', $name) - ->setAttribute('description', $description) - ->setAttribute('logo', $logo) - ->setAttribute('url', $url) - ->setAttribute('legalName', $legalName) - ->setAttribute('legalCountry', $legalCountry) - ->setAttribute('legalState', $legalState) - ->setAttribute('legalCity', $legalCity) - ->setAttribute('legalAddress', $legalAddress) - ->setAttribute('legalTaxId', $legalTaxId) - ->setAttribute('search', implode(' ', [$projectId, $name]))); + $project = $dbForConsole->updateDocument( + 'projects', + $project->getId(), + $project + ->setAttribute('name', $name) + ->setAttribute('description', $description) + ->setAttribute('logo', $logo) + ->setAttribute('url', $url) + ->setAttribute('legalName', $legalName) + ->setAttribute('legalCountry', $legalCountry) + ->setAttribute('legalState', $legalState) + ->setAttribute('legalCity', $legalCity) + ->setAttribute('legalAddress', $legalAddress) + ->setAttribute('legalTaxId', $legalTaxId) + ->setAttribute('search', implode(' ', [$projectId, $name])) + ); $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/team') - ->desc('Update project team') +Http::patch('/v1/projects/:projectId/team') + ->desc('Update Project Team') ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -383,7 +399,6 @@ App::patch('/v1/projects/:projectId/team') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $teamId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); $team = $dbForConsole->getDocument('teams', $teamId); @@ -436,7 +451,7 @@ App::patch('/v1/projects/:projectId/team') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/service') +Http::patch('/v1/projects/:projectId/service') ->desc('Update service status') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -452,7 +467,6 @@ App::patch('/v1/projects/:projectId/service') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $service, bool $status, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -467,7 +481,7 @@ App::patch('/v1/projects/:projectId/service') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/service/all') +Http::patch('/v1/projects/:projectId/service/all') ->desc('Update all service status') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -482,7 +496,6 @@ App::patch('/v1/projects/:projectId/service/all') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, bool $status, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -501,7 +514,7 @@ App::patch('/v1/projects/:projectId/service/all') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/api') +Http::patch('/v1/projects/:projectId/api') ->desc('Update API status') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -517,7 +530,6 @@ App::patch('/v1/projects/:projectId/api') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $api, bool $status, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -532,7 +544,7 @@ App::patch('/v1/projects/:projectId/api') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/api/all') +Http::patch('/v1/projects/:projectId/api/all') ->desc('Update all API status') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -547,7 +559,6 @@ App::patch('/v1/projects/:projectId/api/all') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, bool $status, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -566,7 +577,7 @@ App::patch('/v1/projects/:projectId/api/all') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/oauth2') +Http::patch('/v1/projects/:projectId/oauth2') ->desc('Update project OAuth2') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -584,7 +595,6 @@ App::patch('/v1/projects/:projectId/oauth2') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $provider, ?string $appId, ?string $secret, ?bool $enabled, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -610,7 +620,7 @@ App::patch('/v1/projects/:projectId/oauth2') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/session-alerts') +Http::patch('/v1/projects/:projectId/auth/session-alerts') ->desc('Update project sessions emails') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -641,7 +651,7 @@ App::patch('/v1/projects/:projectId/auth/session-alerts') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/limit') +Http::patch('/v1/projects/:projectId/auth/limit') ->desc('Update project users limit') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -656,7 +666,6 @@ App::patch('/v1/projects/:projectId/auth/limit') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -666,13 +675,17 @@ App::patch('/v1/projects/:projectId/auth/limit') $auths = $project->getAttribute('auths', []); $auths['limit'] = $limit; - $dbForConsole->updateDocument('projects', $project->getId(), $project - ->setAttribute('auths', $auths)); + $dbForConsole->updateDocument( + 'projects', + $project->getId(), + $project + ->setAttribute('auths', $auths) + ); $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/duration') +Http::patch('/v1/projects/:projectId/auth/duration') ->desc('Update project authentication duration') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -687,7 +700,6 @@ App::patch('/v1/projects/:projectId/auth/duration') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, int $duration, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -697,13 +709,17 @@ App::patch('/v1/projects/:projectId/auth/duration') $auths = $project->getAttribute('auths', []); $auths['duration'] = $duration; - $dbForConsole->updateDocument('projects', $project->getId(), $project - ->setAttribute('auths', $auths)); + $dbForConsole->updateDocument( + 'projects', + $project->getId(), + $project + ->setAttribute('auths', $auths) + ); $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/:method') +Http::patch('/v1/projects/:projectId/auth/:method') ->desc('Update project auth method status. Use this endpoint to enable or disable a given auth method for this project.') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -719,10 +735,9 @@ App::patch('/v1/projects/:projectId/auth/:method') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $method, bool $status, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); - $auth = Config::getParam('auth')[$method] ?? []; - $authKey = $auth['key'] ?? ''; + $authConfig = Config::getParam('auth')[$method] ?? []; + $authKey = $authConfig['key'] ?? ''; $status = ($status === '1' || $status === 'true' || $status === 1 || $status === true); if ($project->isEmpty()) { @@ -737,7 +752,7 @@ App::patch('/v1/projects/:projectId/auth/:method') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/password-history') +Http::patch('/v1/projects/:projectId/auth/password-history') ->desc('Update authentication password history. Use this endpoint to set the number of password history to save and 0 to disable password history.') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -752,7 +767,6 @@ App::patch('/v1/projects/:projectId/auth/password-history') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -762,13 +776,17 @@ App::patch('/v1/projects/:projectId/auth/password-history') $auths = $project->getAttribute('auths', []); $auths['passwordHistory'] = $limit; - $dbForConsole->updateDocument('projects', $project->getId(), $project - ->setAttribute('auths', $auths)); + $dbForConsole->updateDocument( + 'projects', + $project->getId(), + $project + ->setAttribute('auths', $auths) + ); $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/password-dictionary') +Http::patch('/v1/projects/:projectId/auth/password-dictionary') ->desc('Update authentication password dictionary status. Use this endpoint to enable or disable the dicitonary check for user password') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -783,7 +801,6 @@ App::patch('/v1/projects/:projectId/auth/password-dictionary') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, bool $enabled, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -793,13 +810,17 @@ App::patch('/v1/projects/:projectId/auth/password-dictionary') $auths = $project->getAttribute('auths', []); $auths['passwordDictionary'] = $enabled; - $dbForConsole->updateDocument('projects', $project->getId(), $project - ->setAttribute('auths', $auths)); + $dbForConsole->updateDocument( + 'projects', + $project->getId(), + $project + ->setAttribute('auths', $auths) + ); $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/personal-data') +Http::patch('/v1/projects/:projectId/auth/personal-data') ->desc('Enable or disable checking user passwords for similarity with their personal data.') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -814,7 +835,6 @@ App::patch('/v1/projects/:projectId/auth/personal-data') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, bool $enabled, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -824,13 +844,17 @@ App::patch('/v1/projects/:projectId/auth/personal-data') $auths = $project->getAttribute('auths', []); $auths['personalDataCheck'] = $enabled; - $dbForConsole->updateDocument('projects', $project->getId(), $project - ->setAttribute('auths', $auths)); + $dbForConsole->updateDocument( + 'projects', + $project->getId(), + $project + ->setAttribute('auths', $auths) + ); $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/max-sessions') +Http::patch('/v1/projects/:projectId/auth/max-sessions') ->desc('Update project user sessions limit') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -845,7 +869,6 @@ App::patch('/v1/projects/:projectId/auth/max-sessions') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -855,13 +878,17 @@ App::patch('/v1/projects/:projectId/auth/max-sessions') $auths = $project->getAttribute('auths', []); $auths['maxSessions'] = $limit; - $dbForConsole->updateDocument('projects', $project->getId(), $project - ->setAttribute('auths', $auths)); + $dbForConsole->updateDocument( + 'projects', + $project->getId(), + $project + ->setAttribute('auths', $auths) + ); $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/mock-numbers') +Http::patch('/v1/projects/:projectId/auth/mock-numbers') ->desc('Update the mock numbers for the project') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -900,7 +927,7 @@ App::patch('/v1/projects/:projectId/auth/mock-numbers') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::delete('/v1/projects/:projectId') +Http::delete('/v1/projects/:projectId') ->desc('Delete project') ->groups(['api', 'projects']) ->label('audits.event', 'projects.delete') @@ -935,7 +962,7 @@ App::delete('/v1/projects/:projectId') // Webhooks -App::post('/v1/projects/:projectId/webhooks') +Http::post('/v1/projects/:projectId/webhooks') ->desc('Create webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -956,14 +983,13 @@ App::post('/v1/projects/:projectId/webhooks') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $security = (bool) filter_var($security, FILTER_VALIDATE_BOOLEAN); + $security = (bool)filter_var($security, FILTER_VALIDATE_BOOLEAN); $webhook = new Document([ '$id' => ID::unique(), @@ -993,7 +1019,7 @@ App::post('/v1/projects/:projectId/webhooks') ->dynamic($webhook, Response::MODEL_WEBHOOK); }); -App::get('/v1/projects/:projectId/webhooks') +Http::get('/v1/projects/:projectId/webhooks') ->desc('List webhooks') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1007,7 +1033,6 @@ App::get('/v1/projects/:projectId/webhooks') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1025,7 +1050,7 @@ App::get('/v1/projects/:projectId/webhooks') ]), Response::MODEL_WEBHOOK_LIST); }); -App::get('/v1/projects/:projectId/webhooks/:webhookId') +Http::get('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Get webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1040,7 +1065,6 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1059,7 +1083,7 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId') $response->dynamic($webhook, Response::MODEL_WEBHOOK); }); -App::put('/v1/projects/:projectId/webhooks/:webhookId') +Http::put('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Update webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1081,7 +1105,6 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $webhookId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1118,7 +1141,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId') $response->dynamic($webhook, Response::MODEL_WEBHOOK); }); -App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') +Http::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') ->desc('Update webhook signature key') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1133,7 +1156,6 @@ App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1157,7 +1179,7 @@ App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') $response->dynamic($webhook, Response::MODEL_WEBHOOK); }); -App::delete('/v1/projects/:projectId/webhooks/:webhookId') +Http::delete('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Delete webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1171,7 +1193,6 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1196,7 +1217,7 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId') // Keys -App::post('/v1/projects/:projectId/keys') +Http::post('/v1/projects/:projectId/keys') ->desc('Create key') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1213,7 +1234,6 @@ App::post('/v1/projects/:projectId/keys') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1246,7 +1266,7 @@ App::post('/v1/projects/:projectId/keys') ->dynamic($key, Response::MODEL_KEY); }); -App::get('/v1/projects/:projectId/keys') +Http::get('/v1/projects/:projectId/keys') ->desc('List keys') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1260,7 +1280,6 @@ App::get('/v1/projects/:projectId/keys') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1278,7 +1297,7 @@ App::get('/v1/projects/:projectId/keys') ]), Response::MODEL_KEY_LIST); }); -App::get('/v1/projects/:projectId/keys/:keyId') +Http::get('/v1/projects/:projectId/keys/:keyId') ->desc('Get key') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1293,7 +1312,6 @@ App::get('/v1/projects/:projectId/keys/:keyId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $keyId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1312,7 +1330,7 @@ App::get('/v1/projects/:projectId/keys/:keyId') $response->dynamic($key, Response::MODEL_KEY); }); -App::put('/v1/projects/:projectId/keys/:keyId') +Http::put('/v1/projects/:projectId/keys/:keyId') ->desc('Update key') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1330,7 +1348,6 @@ App::put('/v1/projects/:projectId/keys/:keyId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1358,7 +1375,7 @@ App::put('/v1/projects/:projectId/keys/:keyId') $response->dynamic($key, Response::MODEL_KEY); }); -App::delete('/v1/projects/:projectId/keys/:keyId') +Http::delete('/v1/projects/:projectId/keys/:keyId') ->desc('Delete key') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1372,7 +1389,6 @@ App::delete('/v1/projects/:projectId/keys/:keyId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $keyId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1397,7 +1413,7 @@ App::delete('/v1/projects/:projectId/keys/:keyId') // JWT Keys -App::post('/v1/projects/:projectId/jwts') +Http::post('/v1/projects/:projectId/jwts') ->groups(['api', 'projects']) ->desc('Create JWT') ->label('scope', 'projects.write') @@ -1432,7 +1448,7 @@ App::post('/v1/projects/:projectId/jwts') // Platforms -App::post('/v1/projects/:projectId/platforms') +Http::post('/v1/projects/:projectId/platforms') ->desc('Create platform') ->groups(['api', 'projects']) ->label('audits.event', 'platforms.create') @@ -1483,7 +1499,7 @@ App::post('/v1/projects/:projectId/platforms') ->dynamic($platform, Response::MODEL_PLATFORM); }); -App::get('/v1/projects/:projectId/platforms') +Http::get('/v1/projects/:projectId/platforms') ->desc('List platforms') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1497,7 +1513,6 @@ App::get('/v1/projects/:projectId/platforms') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1515,7 +1530,7 @@ App::get('/v1/projects/:projectId/platforms') ]), Response::MODEL_PLATFORM_LIST); }); -App::get('/v1/projects/:projectId/platforms/:platformId') +Http::get('/v1/projects/:projectId/platforms/:platformId') ->desc('Get platform') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1530,7 +1545,6 @@ App::get('/v1/projects/:projectId/platforms/:platformId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $platformId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1549,7 +1563,7 @@ App::get('/v1/projects/:projectId/platforms/:platformId') $response->dynamic($platform, Response::MODEL_PLATFORM); }); -App::put('/v1/projects/:projectId/platforms/:platformId') +Http::put('/v1/projects/:projectId/platforms/:platformId') ->desc('Update platform') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1596,7 +1610,7 @@ App::put('/v1/projects/:projectId/platforms/:platformId') $response->dynamic($platform, Response::MODEL_PLATFORM); }); -App::delete('/v1/projects/:projectId/platforms/:platformId') +Http::delete('/v1/projects/:projectId/platforms/:platformId') ->desc('Delete platform') ->groups(['api', 'projects']) ->label('audits.event', 'platforms.delete') @@ -1611,7 +1625,6 @@ App::delete('/v1/projects/:projectId/platforms/:platformId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $platformId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1636,7 +1649,7 @@ App::delete('/v1/projects/:projectId/platforms/:platformId') // CUSTOM SMTP and Templates -App::patch('/v1/projects/:projectId/smtp') +Http::patch('/v1/projects/:projectId/smtp') ->desc('Update SMTP') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1659,7 +1672,6 @@ App::patch('/v1/projects/:projectId/smtp') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, bool $enabled, string $senderName, string $senderEmail, string $replyTo, string $host, int $port, string $username, string $password, string $secure, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1726,7 +1738,7 @@ App::patch('/v1/projects/:projectId/smtp') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::post('/v1/projects/:projectId/smtp/tests') +Http::post('/v1/projects/:projectId/smtp/tests') ->desc('Create SMTP test') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1785,7 +1797,7 @@ App::post('/v1/projects/:projectId/smtp/tests') return $response->noContent(); }); -App::get('/v1/projects/:projectId/templates/sms/:type/:locale') +Http::get('/v1/projects/:projectId/templates/sms/:type/:locale') ->desc('Get custom SMS template') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1801,7 +1813,6 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { - throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); $project = $dbForConsole->getDocument('projects', $projectId); @@ -1811,7 +1822,7 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale') } $templates = $project->getAttribute('templates', []); - $template = $templates['sms.' . $type . '-' . $locale] ?? null; + $template = $templates['sms.' . $type . '-' . $locale] ?? null; if (is_null($template)) { $template = [ @@ -1826,7 +1837,7 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale') }); -App::get('/v1/projects/:projectId/templates/email/:type/:locale') +Http::get('/v1/projects/:projectId/templates/email/:type/:locale') ->desc('Get custom email template') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1842,7 +1853,6 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1850,7 +1860,7 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale') } $templates = $project->getAttribute('templates', []); - $template = $templates['email.' . $type . '-' . $locale] ?? null; + $template = $templates['email.' . $type . '-' . $locale] ?? null; $localeObj = new Locale($locale); if (is_null($template)) { @@ -1858,7 +1868,7 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale') $message ->setParam('{{hello}}', $localeObj->getText("emails.{$type}.hello")) ->setParam('{{footer}}', $localeObj->getText("emails.{$type}.footer")) - ->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body'), escapeHtml: false) + ->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body'), escape: false) ->setParam('{{thanks}}', $localeObj->getText("emails.{$type}.thanks")) ->setParam('{{signature}}', $localeObj->getText("emails.{$type}.signature")) ->setParam('{{direction}}', $localeObj->getText('settings.direction')); @@ -1878,7 +1888,7 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale') $response->dynamic(new Document($template), Response::MODEL_EMAIL_TEMPLATE); }); -App::patch('/v1/projects/:projectId/templates/sms/:type/:locale') +Http::patch('/v1/projects/:projectId/templates/sms/:type/:locale') ->desc('Update custom SMS template') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1895,7 +1905,6 @@ App::patch('/v1/projects/:projectId/templates/sms/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, string $message, Response $response, Database $dbForConsole) { - throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); $project = $dbForConsole->getDocument('projects', $projectId); @@ -1918,7 +1927,7 @@ App::patch('/v1/projects/:projectId/templates/sms/:type/:locale') ]), Response::MODEL_SMS_TEMPLATE); }); -App::patch('/v1/projects/:projectId/templates/email/:type/:locale') +Http::patch('/v1/projects/:projectId/templates/email/:type/:locale') ->desc('Update custom email templates') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1939,7 +1948,6 @@ App::patch('/v1/projects/:projectId/templates/email/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1968,7 +1976,7 @@ App::patch('/v1/projects/:projectId/templates/email/:type/:locale') ]), Response::MODEL_EMAIL_TEMPLATE); }); -App::delete('/v1/projects/:projectId/templates/sms/:type/:locale') +Http::delete('/v1/projects/:projectId/templates/sms/:type/:locale') ->desc('Reset custom SMS template') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1984,7 +1992,6 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { - throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); $project = $dbForConsole->getDocument('projects', $projectId); @@ -1994,7 +2001,7 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale') } $templates = $project->getAttribute('templates', []); - $template = $templates['sms.' . $type . '-' . $locale] ?? null; + $template = $templates['sms.' . $type . '-' . $locale] ?? null; if (is_null($template)) { throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION); @@ -2011,7 +2018,7 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale') ]), Response::MODEL_SMS_TEMPLATE); }); -App::delete('/v1/projects/:projectId/templates/email/:type/:locale') +Http::delete('/v1/projects/:projectId/templates/email/:type/:locale') ->desc('Reset custom email template') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -2027,7 +2034,6 @@ App::delete('/v1/projects/:projectId/templates/email/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -2035,7 +2041,7 @@ App::delete('/v1/projects/:projectId/templates/email/:type/:locale') } $templates = $project->getAttribute('templates', []); - $template = $templates['email.' . $type . '-' . $locale] ?? null; + $template = $templates['email.' . $type . '-' . $locale] ?? null; if (is_null($template)) { throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION); diff --git a/app/controllers/api/proxy.php b/app/controllers/api/proxy.php index 84484a7209..3ca019c9ce 100644 --- a/app/controllers/api/proxy.php +++ b/app/controllers/api/proxy.php @@ -7,7 +7,6 @@ use Appwrite\Extend\Exception; use Appwrite\Network\Validator\CNAME; use Appwrite\Utopia\Database\Validator\Queries\Rules; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Query as QueryException; @@ -15,13 +14,14 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\UID; use Utopia\Domains\Domain; +use Utopia\Http\Http; +use Utopia\Http\Validator\Domain as ValidatorDomain; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Logger\Log; use Utopia\System\System; -use Utopia\Validator\Domain as ValidatorDomain; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; -App::post('/v1/proxy/rules') +Http::post('/v1/proxy/rules') ->groups(['api', 'proxy']) ->desc('Create rule') ->label('scope', 'rules.write') @@ -147,7 +147,7 @@ App::post('/v1/proxy/rules') ->dynamic($rule, Response::MODEL_PROXY_RULE); }); -App::get('/v1/proxy/rules') +Http::get('/v1/proxy/rules') ->groups(['api', 'proxy']) ->desc('List rules') ->label('scope', 'rules.read') @@ -210,7 +210,7 @@ App::get('/v1/proxy/rules') ]), Response::MODEL_PROXY_RULE_LIST); }); -App::get('/v1/proxy/rules/:ruleId') +Http::get('/v1/proxy/rules/:ruleId') ->groups(['api', 'proxy']) ->desc('Get rule') ->label('scope', 'rules.read') @@ -239,7 +239,7 @@ App::get('/v1/proxy/rules/:ruleId') $response->dynamic($rule, Response::MODEL_PROXY_RULE); }); -App::delete('/v1/proxy/rules/:ruleId') +Http::delete('/v1/proxy/rules/:ruleId') ->groups(['api', 'proxy']) ->desc('Delete rule') ->label('scope', 'rules.write') @@ -276,8 +276,8 @@ App::delete('/v1/proxy/rules/:ruleId') $response->noContent(); }); -App::patch('/v1/proxy/rules/:ruleId/verification') - ->desc('Update rule verification status') +Http::patch('/v1/proxy/rules/:ruleId/verification') + ->desc('Update Rule Verification Status') ->groups(['api', 'proxy']) ->label('scope', 'rules.write') ->label('event', 'rules.[ruleId].update') diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index ca248c2628..e0408acb37 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -11,8 +11,8 @@ use Appwrite\OpenSSL\OpenSSL; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Database\Validator\Queries\Buckets; use Appwrite\Utopia\Database\Validator\Queries\Files; +use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; @@ -23,8 +23,16 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\HexColor; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Image\Image; use Utopia\Storage\Compression\Algorithms\GZIP; use Utopia\Storage\Compression\Algorithms\Zstd; @@ -35,16 +43,9 @@ use Utopia\Storage\Validator\File; use Utopia\Storage\Validator\FileExt; use Utopia\Storage\Validator\FileSize; use Utopia\Storage\Validator\Upload; -use Utopia\Swoole\Request; use Utopia\System\System; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Boolean; -use Utopia\Validator\HexColor; -use Utopia\Validator\Range; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; -App::post('/v1/storage/buckets') +Http::post('/v1/storage/buckets') ->desc('Create bucket') ->groups(['api', 'storage']) ->label('scope', 'buckets.write') @@ -142,7 +143,7 @@ App::post('/v1/storage/buckets') ->dynamic($bucket, Response::MODEL_BUCKET); }); -App::get('/v1/storage/buckets') +Http::get('/v1/storage/buckets') ->desc('List buckets') ->groups(['api', 'storage']) ->label('scope', 'buckets.read') @@ -196,7 +197,7 @@ App::get('/v1/storage/buckets') ]), Response::MODEL_BUCKET_LIST); }); -App::get('/v1/storage/buckets/:bucketId') +Http::get('/v1/storage/buckets/:bucketId') ->desc('Get bucket') ->groups(['api', 'storage']) ->label('scope', 'buckets.read') @@ -221,7 +222,7 @@ App::get('/v1/storage/buckets/:bucketId') $response->dynamic($bucket, Response::MODEL_BUCKET); }); -App::put('/v1/storage/buckets/:bucketId') +Http::put('/v1/storage/buckets/:bucketId') ->desc('Update bucket') ->groups(['api', 'storage']) ->label('scope', 'buckets.write') @@ -288,7 +289,7 @@ App::put('/v1/storage/buckets/:bucketId') $response->dynamic($bucket, Response::MODEL_BUCKET); }); -App::delete('/v1/storage/buckets/:bucketId') +Http::delete('/v1/storage/buckets/:bucketId') ->desc('Delete bucket') ->groups(['api', 'storage']) ->label('scope', 'buckets.write') @@ -329,7 +330,7 @@ App::delete('/v1/storage/buckets/:bucketId') $response->noContent(); }); -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->alias('/v1/storage/files', ['bucketId' => 'default']) ->desc('Create file') ->groups(['api', 'storage']) @@ -361,19 +362,19 @@ App::post('/v1/storage/buckets/:bucketId/files') ->inject('mode') ->inject('deviceForFiles') ->inject('deviceForLocal') - ->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, string $mode, Device $deviceForFiles, Device $deviceForLocal) { + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, string $mode, Device $deviceForFiles, Device $deviceForLocal, Authorization $authorization) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } - $validator = new Authorization(Database::PERMISSION_CREATE); - if (!$validator->isValid($bucket->getCreate())) { + if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -397,7 +398,7 @@ App::post('/v1/storage/buckets/:bucketId/files') } // Users can only manage their own roles, API keys and Admin users can manage any - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { @@ -410,7 +411,7 @@ App::post('/v1/storage/buckets/:bucketId/files') $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!Authorization::isRole($role)) { + if (!$authorization->isRole($role)) { throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); } } @@ -630,11 +631,10 @@ App::post('/v1/storage/buckets/:bucketId/files') * However as with chunk upload even if we are updating, we are essentially creating a file * adding it's new chunk so we validate create permission instead of update */ - $validator = new Authorization(Database::PERMISSION_CREATE); - if (!$validator->isValid($bucket->getCreate())) { + if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) { throw new Exception(Exception::USER_UNAUTHORIZED); } - $file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); + $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); } } else { if ($file->isEmpty()) { @@ -669,11 +669,10 @@ App::post('/v1/storage/buckets/:bucketId/files') * However as with chunk upload even if we are updating, we are essentially creating a file * adding it's new chunk so we validate create permission instead of update */ - $validator = new Authorization(Database::PERMISSION_CREATE); - if (!$validator->isValid($bucket->getCreate())) { + if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) { throw new Exception(Exception::USER_UNAUTHORIZED); } - $file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); + $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); } } @@ -690,7 +689,7 @@ App::post('/v1/storage/buckets/:bucketId/files') ->dynamic($file, Response::MODEL_FILE); }); -App::get('/v1/storage/buckets/:bucketId/files') +Http::get('/v1/storage/buckets/:bucketId/files') ->alias('/v1/storage/files', ['bucketId' => 'default']) ->desc('List files') ->groups(['api', 'storage']) @@ -708,19 +707,19 @@ App::get('/v1/storage/buckets/:bucketId/files') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $bucketId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + ->inject('authorization') + ->action(function (string $bucketId, array $queries, string $search, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_READ); - $valid = $validator->isValid($bucket->getRead()); + $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -749,7 +748,7 @@ App::get('/v1/storage/buckets/:bucketId/files') if ($fileSecurity && !$valid) { $cursorDocument = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($cursorDocument->isEmpty()) { @@ -765,8 +764,8 @@ App::get('/v1/storage/buckets/:bucketId/files') $files = $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries); $total = $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT); } else { - $files = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries)); - $total = Authorization::skip(fn () => $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT)); + $files = $authorization->skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries)); + $total = $authorization->skip(fn () => $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT)); } $response->dynamic(new Document([ @@ -775,7 +774,7 @@ App::get('/v1/storage/buckets/:bucketId/files') ]), Response::MODEL_FILE_LIST); }); -App::get('/v1/storage/buckets/:bucketId/files/:fileId') +Http::get('/v1/storage/buckets/:bucketId/files/:fileId') ->alias('/v1/storage/files/:fileId', ['bucketId' => 'default']) ->desc('Get file') ->groups(['api', 'storage']) @@ -792,19 +791,19 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_READ); - $valid = $validator->isValid($bucket->getRead()); + $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -812,7 +811,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId') if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -822,7 +821,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId') $response->dynamic($file, Response::MODEL_FILE); }); -App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') +Http::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') ->alias('/v1/storage/files/:fileId/preview', ['bucketId' => 'default']) ->desc('Get file preview') ->groups(['api', 'storage']) @@ -857,24 +856,24 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') ->inject('mode') ->inject('deviceForFiles') ->inject('deviceForLocal') - ->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceForFiles, Device $deviceForLocal) { + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceForFiles, Device $deviceForLocal, Authorization $authorization) { if (!\extension_loaded('imagick')) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); } - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_READ); - $valid = $validator->isValid($bucket->getRead()); + $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -882,7 +881,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -998,7 +997,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') unset($image); }); -App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') +Http::get('/v1/storage/buckets/:bucketId/files/:fileId/download') ->alias('/v1/storage/files/:fileId/download', ['bucketId' => 'default']) ->desc('Get file for download') ->groups(['api', 'storage']) @@ -1017,20 +1016,20 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') ->inject('dbForProject') ->inject('mode') ->inject('deviceForFiles') - ->action(function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, string $mode, Device $deviceForFiles) { + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, string $mode, Device $deviceForFiles, Authorization $authorization) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_READ); - $valid = $validator->isValid($bucket->getRead()); + $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -1038,7 +1037,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -1138,7 +1137,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') } }); -App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') +Http::get('/v1/storage/buckets/:bucketId/files/:fileId/view') ->alias('/v1/storage/files/:fileId/view', ['bucketId' => 'default']) ->desc('Get file for view') ->groups(['api', 'storage']) @@ -1157,19 +1156,19 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') ->inject('dbForProject') ->inject('mode') ->inject('deviceForFiles') - ->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, string $mode, Device $deviceForFiles) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, string $mode, Device $deviceForFiles, Authorization $authorization) { + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_READ); - $valid = $validator->isValid($bucket->getRead()); + $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -1177,7 +1176,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -1290,7 +1289,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') } }); -App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') +Http::get('/v1/storage/buckets/:bucketId/files/:fileId/push') ->desc('Get file for push notification') ->groups(['api', 'storage']) ->label('scope', 'public') @@ -1306,8 +1305,9 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') ->inject('project') ->inject('mode') ->inject('deviceForFiles') - ->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Document $project, string $mode, Device $deviceForFiles) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Document $project, string $mode, Device $deviceForFiles, Authorization $authorization) { + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); @@ -1325,14 +1325,14 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') throw new Exception(Exception::USER_UNAUTHORIZED); } - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); if ($file->isEmpty()) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); @@ -1443,7 +1443,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') } }); -App::put('/v1/storage/buckets/:bucketId/files/:fileId') +Http::put('/v1/storage/buckets/:bucketId/files/:fileId') ->alias('/v1/storage/files/:fileId', ['bucketId' => 'default']) ->desc('Update file') ->groups(['api', 'storage']) @@ -1470,26 +1470,26 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') ->inject('user') ->inject('mode') ->inject('queueForEvents') - ->action(function (string $bucketId, string $fileId, ?string $name, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, ?string $name, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $queueForEvents, Authorization $authorization) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_UPDATE); - $valid = $validator->isValid($bucket->getUpdate()); + $valid = $authorization->isValid(new Input(Database::PERMISSION_UPDATE, $bucket->getUpdate())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } // Read permission should not be required for update - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); if ($file->isEmpty()) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); @@ -1503,7 +1503,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') ]); // Users can only manage their own roles, API keys and Admin users can manage any - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles) && !\is_null($permissions)) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { @@ -1516,7 +1516,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!Authorization::isRole($role)) { + if (!$authorization->isRole($role)) { throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); } } @@ -1536,7 +1536,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') if ($fileSecurity && !$valid) { $file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file); } else { - $file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); + $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); } $queueForEvents @@ -1548,8 +1548,8 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') $response->dynamic($file, Response::MODEL_FILE); }); -App::delete('/v1/storage/buckets/:bucketId/files/:fileId') - ->desc('Delete file') +Http::delete('/v1/storage/buckets/:bucketId/files/:fileId') + ->desc('Delete File') ->groups(['api', 'storage']) ->label('scope', 'files.write') ->label('event', 'buckets.[bucketId].files.[fileId].delete') @@ -1572,32 +1572,32 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') ->inject('mode') ->inject('deviceForFiles') ->inject('queueForDeletes') - ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceForFiles, Delete $queueForDeletes) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceForFiles, Delete $queueForDeletes, Authorization $authorization) { + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_DELETE); - $valid = $validator->isValid($bucket->getDelete()); + $valid = $authorization->isValid(new Input(Database::PERMISSION_DELETE, $bucket->getDelete())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } // Read permission should not be required for delete - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); if ($file->isEmpty()) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } // Make sure we don't delete the file before the document permission check occurs - if ($fileSecurity && !$valid && !$validator->isValid($file->getDelete())) { + if ($fileSecurity && !$valid && !$authorization->isValid(new Input(Database::PERMISSION_DELETE, $file->getDelete()))) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -1621,7 +1621,7 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') if ($fileSecurity && !$valid) { $deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $deleted = Authorization::skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $deleted = $authorization->skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if (!$deleted) { @@ -1641,7 +1641,7 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') $response->noContent(); }); -App::get('/v1/storage/usage') +Http::get('/v1/storage/usage') ->desc('Get storage usage stats') ->groups(['api', 'storage']) ->label('scope', 'files.read') @@ -1654,7 +1654,8 @@ App::get('/v1/storage/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $range, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) { $periods = Config::getParam('usage', []); $stats = $usage = []; @@ -1666,7 +1667,7 @@ App::get('/v1/storage/usage') ]; $total = []; - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -1720,7 +1721,7 @@ App::get('/v1/storage/usage') ]), Response::MODEL_USAGE_STORAGE); }); -App::get('/v1/storage/:bucketId/usage') +Http::get('/v1/storage/:bucketId/usage') ->desc('Get bucket usage stats') ->groups(['api', 'storage']) ->label('scope', 'files.read') @@ -1734,7 +1735,8 @@ App::get('/v1/storage/:bucketId/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $bucketId, string $range, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $bucketId, string $range, Response $response, Database $dbForProject, Authorization $authorization) { $bucket = $dbForProject->getDocument('buckets', $bucketId); @@ -1750,8 +1752,7 @@ App::get('/v1/storage/:bucketId/usage') str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE), ]; - - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index f98cdd721c..16529d3dbd 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1,6 +1,7 @@ desc('Create team') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].create') @@ -65,15 +66,16 @@ App::post('/v1/teams') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - $isAppUser = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAppUser = Auth::isAppUser($authorization->getRoles()); $teamId = $teamId == 'unique()' ? ID::unique() : $teamId; try { - $team = Authorization::skip(fn () => $dbForProject->createDocument('teams', new Document([ + $team = $authorization->skip(fn () => $dbForProject->createDocument('teams', new Document([ '$id' => $teamId, '$permissions' => [ Permission::read(Role::team($teamId)), @@ -132,7 +134,7 @@ App::post('/v1/teams') ->dynamic($team, Response::MODEL_TEAM); }); -App::get('/v1/teams') +Http::get('/v1/teams') ->desc('List teams') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -190,7 +192,7 @@ App::get('/v1/teams') ]), Response::MODEL_TEAM_LIST); }); -App::get('/v1/teams/:teamId') +Http::get('/v1/teams/:teamId') ->desc('Get team') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -217,7 +219,7 @@ App::get('/v1/teams/:teamId') $response->dynamic($team, Response::MODEL_TEAM); }); -App::get('/v1/teams/:teamId/prefs') +Http::get('/v1/teams/:teamId/prefs') ->desc('Get team preferences') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -245,7 +247,7 @@ App::get('/v1/teams/:teamId/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -App::put('/v1/teams/:teamId') +Http::put('/v1/teams/:teamId') ->desc('Update name') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].update') @@ -288,7 +290,7 @@ App::put('/v1/teams/:teamId') $response->dynamic($team, Response::MODEL_TEAM); }); -App::put('/v1/teams/:teamId/prefs') +Http::put('/v1/teams/:teamId/prefs') ->desc('Update preferences') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].update.prefs') @@ -324,7 +326,7 @@ App::put('/v1/teams/:teamId/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -App::delete('/v1/teams/:teamId') +Http::delete('/v1/teams/:teamId') ->desc('Delete team') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].delete') @@ -373,7 +375,7 @@ App::delete('/v1/teams/:teamId') $response->noContent(); }); -App::post('/v1/teams/:teamId/memberships') +Http::post('/v1/teams/:teamId/memberships') ->desc('Create team membership') ->groups(['api', 'teams', 'auth']) ->label('event', 'teams.[teamId].memberships.[membershipId].create') @@ -405,9 +407,10 @@ App::post('/v1/teams/:teamId/memberships') ->inject('queueForMails') ->inject('queueForMessaging') ->inject('queueForEvents') - ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents) { - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + ->inject('authorization') + ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, Authorization $authorization) { + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); $url = htmlentities($url); if (empty($url)) { @@ -419,8 +422,8 @@ App::post('/v1/teams/:teamId/memberships') if (empty($userId) && empty($email) && empty($phone)) { throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'At least one of userId, email, or phone is required'); } - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - $isAppUser = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAppUser = Auth::isAppUser($authorization->getRoles()); if (!$isPrivilegedUser && !$isAppUser && empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED); @@ -480,7 +483,7 @@ App::post('/v1/teams/:teamId/memberships') try { $userId = ID::unique(); - $invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([ + $invitee = $authorization->skip(fn () => $dbForProject->createDocument('users', new Document([ '$id' => $userId, '$permissions' => [ Permission::read(Role::any()), @@ -516,7 +519,7 @@ App::post('/v1/teams/:teamId/memberships') } } - $isOwner = Authorization::isRole('team:' . $team->getId() . '/owner'); + $isOwner = $authorization->isRole('team:' . $team->getId() . '/owner'); if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server) throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to send invitations for this team'); @@ -548,12 +551,12 @@ App::post('/v1/teams/:teamId/memberships') if ($isPrivilegedUser || $isAppUser) { // Allow admin to create membership try { - $membership = Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)); + $membership = $authorization->skip(fn () => $dbForProject->createDocument('memberships', $membership)); } catch (Duplicate $th) { throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS); } - Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); + $authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); $dbForProject->purgeCachedDocument('users', $invitee->getId()); } else { @@ -575,7 +578,7 @@ App::post('/v1/teams/:teamId/memberships') $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); $message - ->setParam('{{body}}', $body, escapeHtml: false) + ->setParam('{{body}}', $body, escape: false) ->setParam('{{hello}}', $locale->getText("emails.invitation.hello")) ->setParam('{{footer}}', $locale->getText("emails.invitation.footer")) ->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks")) @@ -693,7 +696,7 @@ App::post('/v1/teams/:teamId/memberships') ); }); -App::get('/v1/teams/:teamId/memberships') +Http::get('/v1/teams/:teamId/memberships') ->desc('List team memberships') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -796,7 +799,7 @@ App::get('/v1/teams/:teamId/memberships') ]), Response::MODEL_MEMBERSHIP_LIST); }); -App::get('/v1/teams/:teamId/memberships/:membershipId') +Http::get('/v1/teams/:teamId/memberships/:membershipId') ->desc('Get team membership') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -852,7 +855,7 @@ App::get('/v1/teams/:teamId/memberships/:membershipId') $response->dynamic($membership, Response::MODEL_MEMBERSHIP); }); -App::patch('/v1/teams/:teamId/memberships/:membershipId') +Http::patch('/v1/teams/:teamId/memberships/:membershipId') ->desc('Update membership') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].memberships.[membershipId].update') @@ -874,7 +877,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $team = $dbForProject->getDocument('teams', $teamId); if ($team->isEmpty()) { @@ -891,9 +895,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') throw new Exception(Exception::USER_NOT_FOUND); } - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - $isAppUser = Auth::isAppUser(Authorization::getRoles()); - $isOwner = Authorization::isRole('team:' . $team->getId() . '/owner'); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAppUser = Auth::isAppUser($authorization->getRoles()); + $isOwner = $authorization->isRole('team:' . $team->getId() . '/owner'); if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server) throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to modify roles'); @@ -924,7 +928,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') ); }); -App::patch('/v1/teams/:teamId/memberships/:membershipId/status') +Http::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->desc('Update team membership status') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].memberships.[membershipId].update.status') @@ -950,7 +954,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->inject('project') ->inject('geodb') ->inject('queueForEvents') - ->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents) { + ->inject('authorization') + ->inject('authentication') + ->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Authorization $authorization, Authentication $authentication) { $protocol = $request->getProtocol(); $membership = $dbForProject->getDocument('memberships', $membershipId); @@ -959,7 +965,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') throw new Exception(Exception::MEMBERSHIP_NOT_FOUND); } - $team = Authorization::skip(fn () => $dbForProject->getDocument('teams', $teamId)); + $team = $authorization->skip(fn () => $dbForProject->getDocument('teams', $teamId)); if ($team->isEmpty()) { throw new Exception(Exception::TEAM_NOT_FOUND); @@ -994,11 +1000,11 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->setAttribute('confirm', true) ; - Authorization::skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true))); + $authorization->skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true))); // Log user in - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); @@ -1028,13 +1034,13 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') $dbForProject->purgeCachedDocument('users', $user->getId()); - Authorization::setRole(Role::user($userId)->toString()); + $authorization->addRole(Role::user($userId)->toString()); $membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership); $dbForProject->purgeCachedDocument('users', $user->getId()); - Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); + $authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); $queueForEvents ->setParam('userId', $user->getId()) @@ -1044,13 +1050,13 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') if (!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) + ->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $secret)])) ; } $response - ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ; $response->dynamic( @@ -1062,7 +1068,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ); }); -App::delete('/v1/teams/:teamId/memberships/:membershipId') +Http::delete('/v1/teams/:teamId/memberships/:membershipId') ->desc('Delete team membership') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].memberships.[membershipId].delete') @@ -1080,7 +1086,8 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $membership = $dbForProject->getDocument('memberships', $membershipId); @@ -1115,7 +1122,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') $dbForProject->purgeCachedDocument('users', $user->getId()); if ($membership->getAttribute('confirm')) { // Count only confirmed members - Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0)); + $authorization->skip(fn () => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0)); } $queueForEvents @@ -1128,7 +1135,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') $response->noContent(); }); -App::get('/v1/teams/:teamId/logs') +Http::get('/v1/teams/:teamId/logs') ->desc('List team logs') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -1145,7 +1152,8 @@ App::get('/v1/teams/:teamId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $teamId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $teamId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { $team = $dbForProject->getDocument('teams', $teamId); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 571df4fdb2..3d080fd98e 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -22,7 +22,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Users; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; -use Utopia\App; use Utopia\Audit\Audit; use Utopia\Config\Config; use Utopia\Database\Database; @@ -39,15 +38,16 @@ use Utopia\Database\Validator\Queries; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Assoc; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Integer; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Locale\Locale; use Utopia\System\System; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Assoc; -use Utopia\Validator\Boolean; -use Utopia\Validator\Integer; -use Utopia\Validator\Range; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; /** TODO: Remove function when we move to using utopia/platform */ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks): Document @@ -180,7 +180,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e return $user; } -App::post('/v1/users') +Http::post('/v1/users') ->desc('Create user') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -211,7 +211,7 @@ App::post('/v1/users') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/bcrypt') +Http::post('/v1/users/bcrypt') ->desc('Create user with bcrypt password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -242,7 +242,7 @@ App::post('/v1/users/bcrypt') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/md5') +Http::post('/v1/users/md5') ->desc('Create user with MD5 password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -273,7 +273,7 @@ App::post('/v1/users/md5') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/argon2') +Http::post('/v1/users/argon2') ->desc('Create user with Argon2 password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -304,7 +304,7 @@ App::post('/v1/users/argon2') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/sha') +Http::post('/v1/users/sha') ->desc('Create user with SHA password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -342,7 +342,7 @@ App::post('/v1/users/sha') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/phpass') +Http::post('/v1/users/phpass') ->desc('Create user with PHPass password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -373,7 +373,7 @@ App::post('/v1/users/phpass') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/scrypt') +Http::post('/v1/users/scrypt') ->desc('Create user with Scrypt password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -417,7 +417,7 @@ App::post('/v1/users/scrypt') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/scrypt-modified') +Http::post('/v1/users/scrypt-modified') ->desc('Create user with Scrypt modified password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -451,7 +451,7 @@ App::post('/v1/users/scrypt-modified') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/:userId/targets') +Http::post('/v1/users/:userId/targets') ->desc('Create user target') ->groups(['api', 'users']) ->label('audits.event', 'target.create') @@ -540,7 +540,7 @@ App::post('/v1/users/:userId/targets') ->dynamic($target, Response::MODEL_TARGET); }); -App::get('/v1/users') +Http::get('/v1/users') ->desc('List users') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -594,7 +594,7 @@ App::get('/v1/users') ]), Response::MODEL_USER_LIST); }); -App::get('/v1/users/:userId') +Http::get('/v1/users/:userId') ->desc('Get user') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -619,7 +619,7 @@ App::get('/v1/users/:userId') $response->dynamic($user, Response::MODEL_USER); }); -App::get('/v1/users/:userId/prefs') +Http::get('/v1/users/:userId/prefs') ->desc('Get user preferences') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -646,7 +646,7 @@ App::get('/v1/users/:userId/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -App::get('/v1/users/:userId/targets/:targetId') +Http::get('/v1/users/:userId/targets/:targetId') ->desc('Get user target') ->groups(['api', 'users']) ->label('scope', 'targets.read') @@ -678,7 +678,7 @@ App::get('/v1/users/:userId/targets/:targetId') $response->dynamic($target, Response::MODEL_TARGET); }); -App::get('/v1/users/:userId/sessions') +Http::get('/v1/users/:userId/sessions') ->desc('List user sessions') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -719,7 +719,7 @@ App::get('/v1/users/:userId/sessions') ]), Response::MODEL_SESSION_LIST); }); -App::get('/v1/users/:userId/memberships') +Http::get('/v1/users/:userId/memberships') ->desc('List user memberships') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -758,7 +758,7 @@ App::get('/v1/users/:userId/memberships') ]), Response::MODEL_MEMBERSHIP_LIST); }); -App::get('/v1/users/:userId/logs') +Http::get('/v1/users/:userId/logs') ->desc('List user logs') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -775,7 +775,8 @@ App::get('/v1/users/:userId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $userId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $userId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { $user = $dbForProject->getDocument('users', $userId); @@ -847,7 +848,7 @@ App::get('/v1/users/:userId/logs') ]), Response::MODEL_LOG_LIST); }); -App::get('/v1/users/:userId/targets') +Http::get('/v1/users/:userId/targets') ->desc('List user targets') ->groups(['api', 'users']) ->label('scope', 'targets.read') @@ -902,7 +903,7 @@ App::get('/v1/users/:userId/targets') ]), Response::MODEL_TARGET_LIST); }); -App::get('/v1/users/identities') +Http::get('/v1/users/identities') ->desc('List identities') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -956,7 +957,7 @@ App::get('/v1/users/identities') ]), Response::MODEL_IDENTITY_LIST); }); -App::patch('/v1/users/:userId/status') +Http::patch('/v1/users/:userId/status') ->desc('Update user status') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.status') @@ -992,7 +993,7 @@ App::patch('/v1/users/:userId/status') $response->dynamic($user, Response::MODEL_USER); }); -App::put('/v1/users/:userId/labels') +Http::put('/v1/users/:userId/labels') ->desc('Update user labels') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.labels') @@ -1029,7 +1030,7 @@ App::put('/v1/users/:userId/labels') $response->dynamic($user, Response::MODEL_USER); }); -App::patch('/v1/users/:userId/verification/phone') +Http::patch('/v1/users/:userId/verification/phone') ->desc('Update phone verification') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.verification') @@ -1064,7 +1065,7 @@ App::patch('/v1/users/:userId/verification/phone') $response->dynamic($user, Response::MODEL_USER); }); -App::patch('/v1/users/:userId/name') +Http::patch('/v1/users/:userId/name') ->desc('Update name') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.name') @@ -1101,7 +1102,7 @@ App::patch('/v1/users/:userId/name') $response->dynamic($user, Response::MODEL_USER); }); -App::patch('/v1/users/:userId/password') +Http::patch('/v1/users/:userId/password') ->desc('Update password') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.password') @@ -1178,7 +1179,7 @@ App::patch('/v1/users/:userId/password') $response->dynamic($user, Response::MODEL_USER); }); -App::patch('/v1/users/:userId/email') +Http::patch('/v1/users/:userId/email') ->desc('Update email') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.email') @@ -1273,7 +1274,7 @@ App::patch('/v1/users/:userId/email') $response->dynamic($user, Response::MODEL_USER); }); -App::patch('/v1/users/:userId/phone') +Http::patch('/v1/users/:userId/phone') ->desc('Update phone') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.phone') @@ -1356,7 +1357,7 @@ App::patch('/v1/users/:userId/phone') $response->dynamic($user, Response::MODEL_USER); }); -App::patch('/v1/users/:userId/verification') +Http::patch('/v1/users/:userId/verification') ->desc('Update email verification') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.verification') @@ -1391,7 +1392,7 @@ App::patch('/v1/users/:userId/verification') $response->dynamic($user, Response::MODEL_USER); }); -App::patch('/v1/users/:userId/prefs') +Http::patch('/v1/users/:userId/prefs') ->desc('Update user preferences') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.prefs') @@ -1424,7 +1425,7 @@ App::patch('/v1/users/:userId/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -App::patch('/v1/users/:userId/targets/:targetId') +Http::patch('/v1/users/:userId/targets/:targetId') ->desc('Update user target') ->groups(['api', 'users']) ->label('audits.event', 'target.update') @@ -1518,7 +1519,7 @@ App::patch('/v1/users/:userId/targets/:targetId') ->dynamic($target, Response::MODEL_TARGET); }); -App::patch('/v1/users/:userId/mfa') +Http::patch('/v1/users/:userId/mfa') ->desc('Update MFA') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.mfa') @@ -1556,7 +1557,7 @@ App::patch('/v1/users/:userId/mfa') $response->dynamic($user, Response::MODEL_USER); }); -App::get('/v1/users/:userId/mfa/factors') +Http::get('/v1/users/:userId/mfa/factors') ->desc('List factors') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -1589,7 +1590,7 @@ App::get('/v1/users/:userId/mfa/factors') $response->dynamic($factors, Response::MODEL_MFA_FACTORS); }); -App::get('/v1/users/:userId/mfa/recovery-codes') +Http::get('/v1/users/:userId/mfa/recovery-codes') ->desc('Get MFA recovery codes') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -1624,7 +1625,7 @@ App::get('/v1/users/:userId/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -App::patch('/v1/users/:userId/mfa/recovery-codes') +Http::patch('/v1/users/:userId/mfa/recovery-codes') ->desc('Create MFA recovery codes') ->groups(['api', 'users']) ->label('event', 'users.[userId].create.mfa.recovery-codes') @@ -1670,7 +1671,7 @@ App::patch('/v1/users/:userId/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -App::put('/v1/users/:userId/mfa/recovery-codes') +Http::put('/v1/users/:userId/mfa/recovery-codes') ->desc('Regenerate MFA recovery codes') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.mfa.recovery-codes') @@ -1715,7 +1716,7 @@ App::put('/v1/users/:userId/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -App::delete('/v1/users/:userId/mfa/authenticators/:type') +Http::delete('/v1/users/:userId/mfa/authenticators/:type') ->desc('Delete authenticator') ->groups(['api', 'users']) ->label('event', 'users.[userId].delete.mfa') @@ -1757,7 +1758,7 @@ App::delete('/v1/users/:userId/mfa/authenticators/:type') $response->noContent(); }); -App::post('/v1/users/:userId/sessions') +Http::post('/v1/users/:userId/sessions') ->desc('Create session') ->groups(['api', 'users']) ->label('event', 'users.[userId].sessions.[sessionId].create') @@ -1827,7 +1828,7 @@ App::post('/v1/users/:userId/sessions') ->dynamic($session, Response::MODEL_SESSION); }); -App::post('/v1/users/:userId/tokens') +Http::post('/v1/users/:userId/tokens') ->desc('Create token') ->groups(['api', 'users']) ->label('event', 'users.[userId].tokens.[tokenId].create') @@ -1884,7 +1885,7 @@ App::post('/v1/users/:userId/tokens') ->dynamic($token, Response::MODEL_TOKEN); }); -App::delete('/v1/users/:userId/sessions/:sessionId') +Http::delete('/v1/users/:userId/sessions/:sessionId') ->desc('Delete user session') ->groups(['api', 'users']) ->label('event', 'users.[userId].sessions.[sessionId].delete') @@ -1927,7 +1928,7 @@ App::delete('/v1/users/:userId/sessions/:sessionId') $response->noContent(); }); -App::delete('/v1/users/:userId/sessions') +Http::delete('/v1/users/:userId/sessions') ->desc('Delete user sessions') ->groups(['api', 'users']) ->label('event', 'users.[userId].sessions.delete') @@ -1969,7 +1970,7 @@ App::delete('/v1/users/:userId/sessions') $response->noContent(); }); -App::delete('/v1/users/:userId') +Http::delete('/v1/users/:userId') ->desc('Delete user') ->groups(['api', 'users']) ->label('event', 'users.[userId].delete') @@ -2011,7 +2012,7 @@ App::delete('/v1/users/:userId') $response->noContent(); }); -App::delete('/v1/users/:userId/targets/:targetId') +Http::delete('/v1/users/:userId/targets/:targetId') ->desc('Delete user target') ->groups(['api', 'users']) ->label('audits.event', 'target.delete') @@ -2062,7 +2063,7 @@ App::delete('/v1/users/:userId/targets/:targetId') $response->noContent(); }); -App::delete('/v1/users/identities/:identityId') +Http::delete('/v1/users/identities/:identityId') ->desc('Delete identity') ->groups(['api', 'users']) ->label('event', 'users.[userId].identities.[identityId].delete') @@ -2097,7 +2098,7 @@ App::delete('/v1/users/identities/:identityId') return $response->noContent(); }); -App::post('/v1/users/:userId/jwts') +Http::post('/v1/users/:userId/jwts') ->desc('Create user JWT') ->groups(['api', 'users']) ->label('scope', 'users.write') @@ -2147,7 +2148,7 @@ App::post('/v1/users/:userId/jwts') ])]), Response::MODEL_JWT); }); -App::get('/v1/users/usage') +Http::get('/v1/users/usage') ->desc('Get users usage stats') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -2160,8 +2161,8 @@ App::get('/v1/users/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) ->inject('response') ->inject('dbForProject') - ->inject('register') - ->action(function (string $range, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) { $periods = Config::getParam('usage', []); $stats = $usage = []; @@ -2171,7 +2172,7 @@ App::get('/v1/users/usage') METRIC_SESSIONS, ]; - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $count => $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index f3381490ec..987d84bb7e 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -8,7 +8,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Installations; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Appwrite\Vcs\Comment; -use Utopia\App; use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Database; @@ -32,17 +31,17 @@ use Utopia\Detector\Adapter\Python; use Utopia\Detector\Adapter\Ruby; use Utopia\Detector\Adapter\Swift; use Utopia\Detector\Detector; +use Utopia\Http\Http; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Host; +use Utopia\Http\Validator\Text; use Utopia\System\System; -use Utopia\Validator\Boolean; -use Utopia\Validator\Host; -use Utopia\Validator\Text; use Utopia\VCS\Adapter\Git\GitHub; use Utopia\VCS\Exception\RepositoryNotFound; use function Swoole\Coroutine\batch; -$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForConsole, Build $queueForBuilds, callable $getProjectDB, Request $request) { - $errors = []; +$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForConsole, Build $queueForBuilds, callable $getProjectDB, Request $request, Authorization $auth) { foreach ($repositories as $resource) { try { $resourceType = $resource->getAttribute('resourceType'); @@ -52,11 +51,11 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId } $projectId = $resource->getAttribute('projectId'); - $project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId)); + $project = $auth->skip(fn () => $dbForConsole->getDocument('projects', $projectId)); $dbForProject = $getProjectDB($project); $functionId = $resource->getAttribute('resourceId'); - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + $function = $auth->skip(fn () => $dbForProject->getDocument('functions', $functionId)); $functionInternalId = $function->getInternalId(); $deploymentId = ID::unique(); @@ -102,8 +101,8 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $latestCommentId = ''; - if (!empty($providerPullRequestId) && $function->getAttribute('providerSilentMode', false) === false) { - $latestComment = Authorization::skip(fn () => $dbForConsole->findOne('vcsComments', [ + if (!empty($providerPullRequestId) && $function->getAttribute('providerSilentMode', false)) { + $latestComment = $auth->skip(fn () => $dbForConsole->findOne('vcsComments', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::equal('providerPullRequestId', [$providerPullRequestId]), Query::orderDesc('$createdAt'), @@ -124,7 +123,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId if (!empty($latestCommentId)) { $teamId = $project->getAttribute('teamId', ''); - $latestComment = Authorization::skip(fn () => $dbForConsole->createDocument('vcsComments', new Document([ + $latestComment = $auth->skip(fn () => $dbForConsole->createDocument('vcsComments', new Document([ '$id' => ID::unique(), '$permissions' => [ Permission::read(Role::team(ID::custom($teamId))), @@ -145,7 +144,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId } } } elseif (!empty($providerBranch)) { - $latestComments = Authorization::skip(fn () => $dbForConsole->find('vcsComments', [ + $latestComments = $auth->skip(fn () => $dbForConsole->find('vcsComments', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::equal('providerBranch', [$providerBranch]), Query::orderDesc('$createdAt'), @@ -262,8 +261,8 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId } }; -App::get('/v1/vcs/github/authorize') - ->desc('Install GitHub app') +Http::get('/v1/vcs/github/authorize') + ->desc('Install GitHub App') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') ->label('sdk.namespace', 'vcs') @@ -304,8 +303,8 @@ App::get('/v1/vcs/github/authorize') ->redirect($url); }); -App::get('/v1/vcs/github/callback') - ->desc('Capture installation and authorization from GitHub app') +Http::get('/v1/vcs/github/callback') + ->desc('Capture installation and authorization from GitHub App') ->groups(['api', 'vcs']) ->label('scope', 'public') ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -464,7 +463,7 @@ App::get('/v1/vcs/github/callback') ->redirect($redirectSuccess); }); -App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents') +Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents') ->desc('Get files and directories of a VCS repository') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') @@ -525,7 +524,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro ]), Response::MODEL_VCS_CONTENT_LIST); }); -App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection') +Http::post('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection') ->desc('Detect runtime settings from source code') ->groups(['api', 'vcs']) ->label('scope', 'vcs.write') @@ -597,8 +596,8 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:pr $response->dynamic(new Document($detection), Response::MODEL_DETECTION); }); -App::get('/v1/vcs/github/installations/:installationId/providerRepositories') - ->desc('List repositories') +Http::get('/v1/vcs/github/installations/:installationId/providerRepositories') + ->desc('List Repositories') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') ->label('sdk.namespace', 'vcs') @@ -692,7 +691,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories') ]), Response::MODEL_PROVIDER_REPOSITORY_LIST); }); -App::post('/v1/vcs/github/installations/:installationId/providerRepositories') +Http::post('/v1/vcs/github/installations/:installationId/providerRepositories') ->desc('Create repository') ->groups(['api', 'vcs']) ->label('scope', 'vcs.write') @@ -793,7 +792,7 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories') $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); }); -App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId') +Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId') ->desc('Get repository') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') @@ -842,8 +841,8 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); }); -App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/branches') - ->desc('List repository branches') +Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/branches') + ->desc('List Repository Branches') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') ->label('sdk.namespace', 'vcs') @@ -891,8 +890,8 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro ]), Response::MODEL_BRANCH_LIST); }); -App::post('/v1/vcs/github/events') - ->desc('Create event') +Http::post('/v1/vcs/github/events') + ->desc('Create Event') ->groups(['api', 'vcs']) ->label('scope', 'public') ->inject('gitHub') @@ -901,8 +900,9 @@ App::post('/v1/vcs/github/events') ->inject('dbForConsole') ->inject('getProjectDB') ->inject('queueForBuilds') + ->inject('auth') ->action( - function (GitHub $github, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) { + function (GitHub $github, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $auth) use ($createGitDeployments) { $payload = $request->getRawPayload(); $signatureRemote = $request->getHeader('x-hub-signature-256', ''); $signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', ''); @@ -936,14 +936,14 @@ App::post('/v1/vcs/github/events') $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); //find functionId from functions table - $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::limit(100), ])); // create new deployment only on push and not when branch is created if (!$providerBranchCreated) { - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForConsole, $queueForBuilds, $getProjectDB, $request); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $auth); } } elseif ($event == $github::EVENT_INSTALLATION) { if ($parsedPayload["action"] == "deleted") { @@ -956,13 +956,13 @@ App::post('/v1/vcs/github/events') ]); foreach ($installations as $installation) { - $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('installationInternalId', [$installation->getInternalId()]), Query::limit(1000) ])); foreach ($repositories as $repository) { - Authorization::skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId())); + $auth->skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId())); } $dbForConsole->deleteDocument('installations', $installation->getId()); @@ -994,12 +994,12 @@ App::post('/v1/vcs/github/events') $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; $providerCommitMessage = $commitDetails["commitMessage"] ?? ''; - $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') ])); - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $auth); } elseif ($parsedPayload["action"] == "closed") { // Allowed external contributions cleanup @@ -1008,7 +1008,7 @@ App::post('/v1/vcs/github/events') $external = $parsedPayload["external"] ?? true; if ($external) { - $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') ])); @@ -1019,7 +1019,7 @@ App::post('/v1/vcs/github/events') if (\in_array($providerPullRequestId, $providerPullRequestIds)) { $providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]); $repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds); - $repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); + $repository = $auth->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); } } } @@ -1030,7 +1030,7 @@ App::post('/v1/vcs/github/events') } ); -App::get('/v1/vcs/installations') +Http::get('/v1/vcs/installations') ->desc('List installations') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') @@ -1090,7 +1090,7 @@ App::get('/v1/vcs/installations') ]), Response::MODEL_INSTALLATION_LIST); }); -App::get('/v1/vcs/installations/:installationId') +Http::get('/v1/vcs/installations/:installationId') ->desc('Get installation') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') @@ -1119,8 +1119,8 @@ App::get('/v1/vcs/installations/:installationId') $response->dynamic($installation, Response::MODEL_INSTALLATION); }); -App::delete('/v1/vcs/installations/:installationId') - ->desc('Delete installation') +Http::delete('/v1/vcs/installations/:installationId') + ->desc('Delete Installation') ->groups(['api', 'vcs']) ->label('scope', 'vcs.write') ->label('sdk.namespace', 'vcs') @@ -1152,7 +1152,7 @@ App::delete('/v1/vcs/installations/:installationId') $response->noContent(); }); -App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositoryId') +Http::patch('/v1/vcs/github/installations/:installationId/repositories/:repositoryId') ->desc('Authorize external deployment') ->groups(['api', 'vcs']) ->label('scope', 'vcs.write') @@ -1172,14 +1172,15 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor ->inject('dbForConsole') ->inject('getProjectDB') ->inject('queueForBuilds') - ->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) { + ->inject('auth') + ->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $auth) use ($createGitDeployments) { $installation = $dbForConsole->getDocument('installations', $installationId); if ($installation->isEmpty()) { throw new Exception(Exception::INSTALLATION_NOT_FOUND); } - $repository = Authorization::skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [ + $repository = $auth->skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [ Query::equal('projectInternalId', [$project->getInternalId()]) ])); @@ -1196,7 +1197,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor // TODO: Delete from array when PR is closed - $repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); + $repository = $auth->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); @@ -1220,7 +1221,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor $providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? ''; $providerCommitHash = $pullRequestResponse['head']['sha'] ?? ''; - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForConsole, $queueForBuilds, $getProjectDB, $request); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $auth); $response->noContent(); }); diff --git a/app/controllers/general.php b/app/controllers/general.php index 04554a940e..1e403ef93a 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1,7 +1,5 @@ getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml'); + $route?->label('error', __DIR__ . '/../views/general/error.phtml'); $host = $request->getHostname() ?? ''; - $route = Authorization::skip( + $rule = $auth->skip( fn () => $dbForConsole->find('rules', [ Query::equal('domain', [$host]), Query::limit(1) ]) )[0] ?? null; - if ($route === null) { + if ($rule === null) { if ($host === System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) { throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.'); } @@ -73,12 +72,12 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo } // Act as API - no Proxy logic - $utopia->getRoute()?->label('error', ''); + $route?->label('error', ''); return false; } - $projectId = $route->getAttribute('projectId'); - $project = Authorization::skip( + $projectId = $rule->getAttribute('projectId'); + $project = $auth->skip( fn () => $dbForConsole->getDocument('projects', $projectId) ); if (array_key_exists('proxy', $project->getAttribute('services', []))) { @@ -89,16 +88,16 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo } // Skip Appwrite Router for ACME challenge. Nessessary for certificate generation - $path = ($swooleRequest->server['request_uri'] ?? '/'); + $path = ($request->getURI() ?? '/'); if (\str_starts_with($path, '/.well-known/acme-challenge')) { return false; } - $type = $route->getAttribute('resourceType'); + $type = $rule->getAttribute('resourceType'); if ($type === 'function') { - $utopia->getRoute()?->label('sdk.namespace', 'functions'); - $utopia->getRoute()?->label('sdk.method', 'createExecution'); + $route->label('sdk.namespace', 'functions'); + $route->label('sdk.method', 'createExecution'); if (System::getEnv('_APP_OPTIONS_FUNCTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS if ($request->getProtocol() !== 'https') { @@ -110,26 +109,25 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo } } - $functionId = $route->getAttribute('resourceId'); - $projectId = $route->getAttribute('projectId'); + $functionId = $rule->getAttribute('resourceId'); + $projectId = $rule->getAttribute('projectId'); - $path = ($swooleRequest->server['request_uri'] ?? '/'); - $query = ($swooleRequest->server['query_string'] ?? ''); + $path = ($request->getURI() ?? '/'); + $query = ($request->getQueryString() ?? ''); if (!empty($query)) { $path .= '?' . $query; } - - $body = $swooleRequest->getContent() ?? ''; - $method = $swooleRequest->server['request_method']; + $body = $request->getRawPayload() ?? ''; + $method = $request->getMethod(); $requestHeaders = $request->getHeaders(); - $project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId)); + $project = $auth->skip(fn () => $dbForConsole->getDocument('projects', $projectId)); $dbForProject = $getProjectDB($project); - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + $function = $auth->skip(fn () => $dbForProject->getDocument('functions', $functionId)); if ($function->isEmpty() || !$function->getAttribute('enabled')) { throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND); @@ -145,7 +143,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); } - $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); + $deployment = $auth->skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); if ($deployment->getAttribute('resourceId') !== $function->getId()) { throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); @@ -156,7 +154,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo } /** Check if build has completed */ - $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + $build = $auth->skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); if ($build->isEmpty()) { throw new AppwriteException(AppwriteException::BUILD_NOT_FOUND); } @@ -217,7 +215,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo 'deploymentInternalId' => $deployment->getInternalId(), 'deploymentId' => $deployment->getId(), 'trigger' => 'http', // http / schedule / event - 'status' => 'processing', // waiting / processing / completed / failed + 'status' => 'processing', // waiting / processing / completed / failed 'responseStatusCode' => 0, 'responseHeaders' => [], 'requestPath' => $path, @@ -313,7 +311,6 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo cpus: $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT, memory: $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT, logging: $function->getAttribute('logging', true), - requestTimeout: 30 ); $headersFiltered = []; @@ -331,7 +328,6 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo $execution->setAttribute('logs', $executionResponse['logs']); $execution->setAttribute('errors', $executionResponse['errors']); $execution->setAttribute('duration', $executionResponse['duration']); - } catch (\Throwable $th) { $durationEnd = \microtime(true); @@ -366,7 +362,8 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo ->trigger() ; - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + /** @var Document $execution */ + $execution = $auth->skip(fn () => $dbForProject->createDocument('executions', $execution)); } $execution->setAttribute('logs', ''); @@ -398,18 +395,15 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo return true; } elseif ($type === 'api') { - $utopia->getRoute()?->label('error', ''); + $route?->label('error', ''); return false; } else { throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type); } - - $utopia->getRoute()?->label('error', ''); - return false; } /* -App::init() +Http::init() ->groups(['api']) ->inject('project') ->inject('mode') @@ -420,7 +414,7 @@ App::init() }); */ -App::init() +Http::init() ->groups(['database', 'functions', 'storage', 'messaging']) ->inject('project') ->inject('request') @@ -433,12 +427,11 @@ App::init() } }); -App::init() +Http::init() ->groups(['api', 'web']) - ->inject('utopia') - ->inject('swooleRequest') ->inject('request') ->inject('response') + ->inject('route') ->inject('console') ->inject('project') ->inject('dbForConsole') @@ -450,7 +443,27 @@ App::init() ->inject('queueForUsage') ->inject('queueForEvents') ->inject('queueForCertificates') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, Usage $queueForUsage, Event $queueForEvents, Certificate $queueForCertificates) { + ->inject('authorization') + ->action(function ( + Request $request, + Response $response, + Route $route, + Document $console, + Document $project, + Database $dbForConsole, + $getProjectDB, + Locale $locale, + array $localeCodes, + array $clients, + /** + * @disregard P1009 Undefined type + */ + Reader $geodb, + Usage $queueForUsage, + Event $queueForEvents, + Certificate $queueForCertificates, + Authorization $authorization + ) { /* * Appwrite Router */ @@ -458,7 +471,7 @@ App::init() $mainDomain = System::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { + if (router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization)) { return; } } @@ -466,8 +479,8 @@ App::init() /* * Request format */ - $route = $utopia->getRoute(); - Request::setRoute($route); + //$route = $utopia->getRoute(); + //Request::setRoute($route); if ($route === null) { return $response @@ -499,7 +512,7 @@ App::init() } elseif (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) { Console::warning('Skipping SSL certificates generation on ACME challenge.'); } else { - Authorization::disable(); + $authorization->disable(); $envDomain = System::getEnv('_APP_DOMAIN', ''); $mainDomain = null; @@ -538,12 +551,12 @@ App::init() } $domains[$domain->get()] = true; - Authorization::reset(); // ensure authorization is re-enabled + $authorization->reset(); // ensure authorization is re-enabled } Config::setParam('domains', $domains); } - $localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', '')); + $localeParam = (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')); if (\in_array($localeParam, $localeCodes)) { $locale->setDefault($localeParam); } @@ -571,7 +584,7 @@ App::init() Config::setParam( 'domainVerification', ($selfDomain->getRegisterable() === $endDomain->getRegisterable()) && - $endDomain->getRegisterable() !== '' + $endDomain->getRegisterable() !== '' ); $isLocalHost = $request->getHostname() === 'localhost' || $request->getHostname() === 'localhost:' . $request->getPort(); @@ -586,8 +599,8 @@ App::init() ? null : ( $isConsoleProject && $isConsoleRootSession - ? '.' . $selfDomain->getRegisterable() - : '.' . $request->getHostname() + ? '.' . $selfDomain->getRegisterable() + : '.' . $request->getHostname() ) ); @@ -606,7 +619,7 @@ App::init() $response->addFilter(new ResponseV18()); } if (version_compare($responseFormat, APP_VERSION_STABLE, '>')) { - $response->addHeader('X-Appwrite-Warning', "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is ". APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks"); + $response->addHeader('X-Appwrite-Warning', "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is " . APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks"); } } @@ -617,7 +630,9 @@ App::init() * @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers */ if (System::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS - if ($request->getProtocol() !== 'https' && ($swooleRequest->header['host'] ?? '') !== 'localhost' && ($swooleRequest->header['host'] ?? '') !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations + if ($request->getProtocol() !== 'https' // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations + && ($request->getHeader('host') ?? '') !== 'localhost' + && ($request->getHeader('host') ?? '') !== APP_HOSTNAME_INTERNAL) { if ($request->getMethod() !== Request::METHOD_GET) { throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.'); } @@ -657,9 +672,8 @@ App::init() } }); -App::options() - ->inject('utopia') - ->inject('swooleRequest') +Http::options() + ->inject('route') ->inject('request') ->inject('response') ->inject('dbForConsole') @@ -667,7 +681,8 @@ App::options() ->inject('queueForEvents') ->inject('queueForUsage') ->inject('geodb') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { + ->inject('authorization') + ->action(function (Route $route, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb, Authorization $authorization) { /* * Appwrite Router */ @@ -675,7 +690,7 @@ App::options() $mainDomain = System::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { + if (router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization)) { return; } } @@ -692,18 +707,25 @@ App::options() ->noContent(); }); -App::error() +Http::error() ->inject('error') - ->inject('utopia') + ->inject('user') + ->inject('route') ->inject('request') ->inject('response') ->inject('project') ->inject('logger') ->inject('log') + ->inject('authorization') + ->inject('connections') ->inject('queueForUsage') - ->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Usage $queueForUsage) { + ->action(function (Throwable $error, Document $user, ?Route $route, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Authorization $authorization, Connections $connections, Usage $queueForUsage) { $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); - $route = $utopia->getRoute(); + + if (is_null($route)) { + $route = new Route($request->getMethod(), $request->getURI()); + } + $class = \get_class($error); $code = $error->getCode(); $message = $error->getMessage(); @@ -724,9 +746,9 @@ App::error() Console::error('[Error] File: ' . $file); Console::error('[Error] Line: ' . $line); } - switch ($class) { - case 'Utopia\Exception': + case 'Utopia\Servers\Exception': + case 'Utopia\Http\Exception': $error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error); switch ($code) { case 400: @@ -771,35 +793,36 @@ App::error() } else { $publish = $error->getCode() === 0 || $error->getCode() >= 500; } - if ($error->getCode() >= 400 && $error->getCode() < 500) { // Register error logger $providerName = System::getEnv('_APP_EXPERIMENT_LOGGING_PROVIDER', ''); $providerConfig = System::getEnv('_APP_EXPERIMENT_LOGGING_CONFIG', ''); - try { - $loggingProvider = new DSN($providerConfig ?? ''); - $providerName = $loggingProvider->getScheme(); + if (!(empty($providerName) || empty($providerConfig))) { + try { + $loggingProvider = new DSN($providerConfig); + $providerName = $loggingProvider->getScheme(); - if (!empty($providerName) && $providerName === 'sentry') { - $key = $loggingProvider->getPassword(); - $projectId = $loggingProvider->getUser() ?? ''; - $host = 'https://' . $loggingProvider->getHost(); + if (!empty($providerName) && $providerName === 'sentry') { + $key = $loggingProvider->getPassword(); + $projectId = $loggingProvider->getUser() ?? ''; + $host = 'https://' . $loggingProvider->getHost(); - $adapter = new Sentry($projectId, $key, $host); - $logger = new Logger($adapter); - $logger->setSample(0.04); - $publish = true; - } else { - throw new \Exception('Invalid experimental logging provider'); + $adapter = new Sentry($projectId, $key, $host); + $logger = new Logger($adapter); + $logger->setSample(0.04); + $publish = true; + } else { + throw new \Exception('Invalid experimental logging provider'); + } + } catch (\Throwable $th) { + Console::warning('Failed to initialize logging provider: ' . $th->getMessage()); } - } catch (\Throwable $th) { - Console::warning('Failed to initialize logging provider: ' . $th->getMessage()); } } if ($publish && $project->getId() !== 'console') { - if (!Auth::isPrivilegedUser(Authorization::getRoles())) { + if (!Auth::isPrivilegedUser($authorization->getRoles())) { $fileSize = 0; $file = $request->getFiles('file'); if (!empty($file)) { @@ -818,14 +841,7 @@ App::error() } - if ($logger && $publish) { - try { - /** @var Utopia\Database\Document $user */ - $user = $utopia->getResource('user'); - } catch (\Throwable) { - // All good, user is optional information for logger - } - + if ($logger && ($publish || $error->getCode() === 0)) { if (isset($user) && !$user->isEmpty()) { $log->setUser(new User($user->getId())); } @@ -855,7 +871,7 @@ App::error() $log->addExtra('file', $error->getFile()); $log->addExtra('line', $error->getLine()); $log->addExtra('trace', $error->getTraceAsString()); - $log->addExtra('roles', Authorization::getRoles()); + $log->addExtra('roles', $authorization->getRoles()); $action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD"); $log->setAction($action); @@ -873,7 +889,7 @@ App::error() /** Wrap all exceptions inside Appwrite\Extend\Exception */ if (!($error instanceof AppwriteException)) { - $error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error); + $error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, (int)$code, $error); } switch ($code) { // Don't show 500 errors! @@ -893,14 +909,14 @@ App::error() break; default: $code = 500; // All other errors get the generic 500 server error status code - $message = 'Server Error'; + $message = (Http::getMode() === Http::MODE_TYPE_DEVELOPMENT) ? $message : 'Server Error'; } //$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised $type = $error->getType(); - $output = ((App::isDevelopment())) ? [ + $output = ((Http::isDevelopment())) ? [ 'message' => $message, 'code' => $code, 'file' => $file, @@ -928,7 +944,7 @@ App::error() $layout ->setParam('title', $project->getAttribute('name') . ' - Error') - ->setParam('development', App::isDevelopment()) + ->setParam('development', Http::isDevelopment()) ->setParam('projectName', $project->getAttribute('name')) ->setParam('projectURL', $project->getAttribute('url')) ->setParam('message', $output['message'] ?? '') @@ -939,18 +955,18 @@ App::error() $response->html($layout->render()); } + $connections->reclaim(); + $response->dynamic( new Document($output), - $utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR + Http::isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR ); }); -App::get('/robots.txt') +Http::get('/robots.txt') ->desc('Robots.txt File') ->label('scope', 'public') ->label('docs', false) - ->inject('utopia') - ->inject('swooleRequest') ->inject('request') ->inject('response') ->inject('dbForConsole') @@ -958,7 +974,9 @@ App::get('/robots.txt') ->inject('queueForEvents') ->inject('queueForUsage') ->inject('geodb') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { + ->inject('route') + ->inject('authorization') + ->action(function (Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb, ?Route $route, Authorization $authorization) { $host = $request->getHostname() ?? ''; $mainDomain = System::getEnv('_APP_DOMAIN', ''); @@ -966,16 +984,17 @@ App::get('/robots.txt') $template = new View(__DIR__ . '/../views/general/robots.phtml'); $response->text($template->render(false)); } else { - router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb); + if (is_null($route)) { + $route = new Route($request->getMethod(), $request->getURI()); + } + router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization); } }); -App::get('/humans.txt') +Http::get('/humans.txt') ->desc('Humans.txt File') ->label('scope', 'public') ->label('docs', false) - ->inject('utopia') - ->inject('swooleRequest') ->inject('request') ->inject('response') ->inject('dbForConsole') @@ -983,7 +1002,9 @@ App::get('/humans.txt') ->inject('queueForEvents') ->inject('queueForUsage') ->inject('geodb') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { + ->inject('route') + ->inject('authorization') + ->action(function (Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb, Route $route, Authorization $authorization) { $host = $request->getHostname() ?? ''; $mainDomain = System::getEnv('_APP_DOMAIN', ''); @@ -991,11 +1012,11 @@ App::get('/humans.txt') $template = new View(__DIR__ . '/../views/general/humans.phtml'); $response->text($template->render(false)); } else { - router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb); + router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization); } }); -App::get('/.well-known/acme-challenge/*') +Http::get('/.well-known/acme-challenge/*') ->desc('SSL Verification') ->label('scope', 'public') ->label('docs', false) @@ -1045,16 +1066,32 @@ App::get('/.well-known/acme-challenge/*') $response->text($content); }); -include_once __DIR__ . '/shared/api.php'; -include_once __DIR__ . '/shared/api/auth.php'; - -App::wildcard() +Http::wildcard() ->groups(['api']) ->label('scope', 'global') ->action(function () { throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); }); -foreach (Config::getParam('services', []) as $service) { - include_once $service['controller']; -} +include_once 'mock.php'; +include_once 'shared/api.php'; +include_once 'shared/api/auth.php'; +include_once 'api/account.php'; +include_once 'api/avatars.php'; +include_once 'api/console.php'; +include_once 'api/databases.php'; +include_once 'api/functions.php'; +include_once 'api/graphql.php'; +include_once 'api/health.php'; +include_once 'api/locale.php'; +include_once 'api/messaging.php'; +include_once 'api/migrations.php'; +include_once 'api/project.php'; +include_once 'api/projects.php'; +include_once 'api/proxy.php'; +include_once 'api/storage.php'; +include_once 'api/teams.php'; +include_once 'api/users.php'; +include_once 'api/vcs.php'; +include_once 'web/console.php'; +include_once 'web/home.php'; diff --git a/app/controllers/mock.php b/app/controllers/mock.php index fdb1d80dcc..70ca5e9048 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -3,9 +3,7 @@ global $utopia, $request, $response; use Appwrite\Extend\Exception; -use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; @@ -13,13 +11,15 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Route; +use Utopia\Http\Validator\Host; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\System\System; -use Utopia\Validator\Host; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub; -App::get('/v1/mock/tests/general/oauth2') +Http::get('/v1/mock/tests/general/oauth2') ->desc('OAuth Login') ->groups(['mock']) ->label('scope', 'public') @@ -35,7 +35,7 @@ App::get('/v1/mock/tests/general/oauth2') $response->redirect($redirectURI . '?' . \http_build_query(['code' => 'abcdef', 'state' => $state])); }); -App::get('/v1/mock/tests/general/oauth2/token') +Http::get('/v1/mock/tests/general/oauth2/token') ->desc('OAuth2 Token') ->groups(['mock']) ->label('scope', 'public') @@ -81,7 +81,7 @@ App::get('/v1/mock/tests/general/oauth2/token') } }); -App::get('/v1/mock/tests/general/oauth2/user') +Http::get('/v1/mock/tests/general/oauth2/user') ->desc('OAuth2 User') ->groups(['mock']) ->label('scope', 'public') @@ -101,7 +101,7 @@ App::get('/v1/mock/tests/general/oauth2/user') ]); }); -App::get('/v1/mock/tests/general/oauth2/success') +Http::get('/v1/mock/tests/general/oauth2/success') ->desc('OAuth2 Success') ->groups(['mock']) ->label('scope', 'public') @@ -114,7 +114,7 @@ App::get('/v1/mock/tests/general/oauth2/success') ]); }); -App::get('/v1/mock/tests/general/oauth2/failure') +Http::get('/v1/mock/tests/general/oauth2/failure') ->desc('OAuth2 Failure') ->groups(['mock']) ->label('scope', 'public') @@ -129,7 +129,7 @@ App::get('/v1/mock/tests/general/oauth2/failure') ]); }); -App::patch('/v1/mock/functions-v2') +Http::patch('/v1/mock/functions-v2') ->desc('Update Function Version to V2 (outdated code syntax)') ->groups(['mock', 'api', 'functions']) ->label('scope', 'functions.write') @@ -155,7 +155,7 @@ App::patch('/v1/mock/functions-v2') $response->noContent(); }); -App::post('/v1/mock/api-key-unprefixed') +Http::post('/v1/mock/api-key-unprefixed') ->desc('Create API Key (without standard prefix)') ->groups(['mock', 'api', 'projects']) ->label('scope', 'projects.write') @@ -204,7 +204,7 @@ App::post('/v1/mock/api-key-unprefixed') ->dynamic($key, Response::MODEL_KEY); }); -App::get('/v1/mock/github/callback') +Http::get('/v1/mock/github/callback') ->desc('Create installation document using GitHub installation id') ->groups(['mock', 'api', 'vcs']) ->label('scope', 'public') @@ -264,15 +264,13 @@ App::get('/v1/mock/github/callback') ]); }); -App::shutdown() +Http::shutdown() ->groups(['mock']) - ->inject('utopia') + ->inject('route') ->inject('response') - ->inject('request') - ->action(function (App $utopia, Response $response, Request $request) { + ->action(function (Route $route, Response $response) { $result = []; - $route = $utopia->getRoute(); $path = APP_STORAGE_CACHE . '/tests.json'; $tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : []; diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 357d73adc8..f35643dad5 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -15,11 +15,11 @@ use Appwrite\Event\Usage; use Appwrite\Extend\Exception; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Messaging\Adapter\Realtime; +use Appwrite\Utopia\Queue\Connections; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\Database\TimeLimit; -use Utopia\App; use Utopia\Cache\Adapter\Filesystem; use Utopia\Cache\Cache; use Utopia\Config\Config; @@ -28,8 +28,11 @@ use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; +use Utopia\Http\Http; +use Utopia\Http\Route; +use Utopia\Http\Validator\WhiteList; use Utopia\System\System; -use Utopia\Validator\WhiteList; $parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) { preg_match_all('/{(.*?)}/', $label, $matches); @@ -150,9 +153,9 @@ $databaseListener = function (string $event, Document $document, Document $proje } }; -App::init() +Http::init() ->groups(['api']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('dbForConsole') ->inject('project') @@ -160,9 +163,8 @@ App::init() ->inject('session') ->inject('servers') ->inject('mode') - ->action(function (App $utopia, Request $request, Database $dbForConsole, Document $project, Document $user, ?Document $session, array $servers, string $mode) { - $route = $utopia->getRoute(); - + ->inject('authorization') + ->action(function (Route $route, Request $request, Database $dbForConsole, Document $project, Document $user, ?Document $session, array $servers, string $mode, Authorization $authorization) { if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } @@ -241,8 +243,8 @@ App::init() $role = Auth::USER_ROLE_APPS; $scopes = \array_merge($roles[$role]['scopes'], $tokenScopes); - Authorization::setRole(Auth::USER_ROLE_APPS); - Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys. + $authorization->addRole(Auth::USER_ROLE_APPS); + $authorization->setDefaultStatus(false); // Cancel security segmentation for API keys. } } elseif ($keyType === API_KEY_STANDARD) { // No underline means no prefix. Backwards compatibility. @@ -267,8 +269,8 @@ App::init() throw new Exception(Exception::PROJECT_KEY_EXPIRED); } - Authorization::setRole(Auth::USER_ROLE_APPS); - Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys. + $authorization->addRole(Auth::USER_ROLE_APPS); + $authorization->setDefaultStatus(false); // Cancel security segmentation for API keys. $accessedAt = $key->getAttribute('accessedAt', ''); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) { @@ -295,10 +297,10 @@ App::init() } } - Authorization::setRole($role); + $authorization->addRole($role); - foreach (Auth::getRoles($user) as $authRole) { - Authorization::setRole($authRole); + foreach (Auth::getRoles($user, $authorization) as $authRole) { + $authorization->addRole($authRole); } $service = $route->getLabel('sdk.namespace', ''); @@ -306,7 +308,7 @@ App::init() if ( array_key_exists($service, $project->getAttribute('services', [])) && !$project->getAttribute('services', [])[$service] - && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + && !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles())) ) { throw new Exception(Exception::GENERAL_SERVICE_DISABLED); } @@ -341,9 +343,9 @@ App::init() } }); -App::init() +Http::init() ->groups(['api']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('response') ->inject('project') @@ -357,14 +359,12 @@ App::init() ->inject('queueForUsage') ->inject('dbForProject') ->inject('mode') - ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, string $mode) use ($databaseListener) { - - $route = $utopia->getRoute(); - + ->inject('authorization') + ->action(function (Route $route, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, string $mode, Authorization $authorization) use ($databaseListener) { if ( array_key_exists('rest', $project->getAttribute('apis', [])) && !$project->getAttribute('apis', [])['rest'] - && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + && !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } @@ -394,7 +394,7 @@ App::init() $closestLimit = null; - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -458,7 +458,7 @@ App::init() $useCache = $route->getLabel('cache', false); if ($useCache) { $key = md5($request->getURI() . '*' . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER); - $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); + $cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key)); $cache = new Cache( new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId()) ); @@ -471,19 +471,18 @@ App::init() if ($type === 'bucket') { $bucketId = $parts[1] ?? null; - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_READ); - $valid = $validator->isValid($bucket->getRead()); - + $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -494,7 +493,7 @@ App::init() if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -518,7 +517,7 @@ App::init() } }); -App::init() +Http::init() ->groups(['session']) ->inject('user') ->inject('request') @@ -538,14 +537,12 @@ App::init() * Delete older sessions if the number of sessions have crossed * the session limit set for the project */ -App::shutdown() +Http::shutdown() ->groups(['session']) - ->inject('utopia') - ->inject('request') ->inject('response') ->inject('project') ->inject('dbForProject') - ->action(function (App $utopia, Request $request, Response $response, Document $project, Database $dbForProject) { + ->action(function (Response $response, Document $project, Database $dbForProject) { $sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT; $session = $response->getPayload(); $userId = $session['userId'] ?? ''; @@ -572,9 +569,9 @@ App::shutdown() $dbForProject->purgeCachedDocument('users', $userId); }); -App::shutdown() +Http::shutdown() ->groups(['api']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('response') ->inject('project') @@ -590,7 +587,29 @@ App::shutdown() ->inject('queueForFunctions') ->inject('mode') ->inject('dbForConsole') - ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Database $dbForProject, Func $queueForFunctions, string $mode, Database $dbForConsole) use ($parseLabel) { + ->inject('authorization') + ->action(function ( + Route $route, + Request $request, + Response $response, + Document $project, + Document $user, + Event $queueForEvents, + Audit $queueForAudits, + Usage $queueForUsage, + Delete $queueForDeletes, + EventDatabase $queueForDatabase, + Build $queueForBuilds, + Messaging $queueForMessaging, + Database $dbForProject, + Func $queueForFunctions, + string $mode, + Database $dbForConsole, + Authorization $authorization, + ) use ($parseLabel) { + if (!empty($user) && !$user->isEmpty() && empty($user->getInternalId())) { + $user = $authorization->skip(fn () => $dbForProject->getDocument('users', $user->getId())); + } $responsePayload = $response->getPayload(); @@ -650,7 +669,6 @@ App::shutdown() } } - $route = $utopia->getRoute(); $requestParams = $route->getParamsValues(); /** @@ -720,11 +738,11 @@ App::shutdown() $key = md5($request->getURI() . '*' . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER); $signature = md5($data['payload']); - $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); + $cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key)); $accessedAt = $cacheLog->getAttribute('accessedAt', ''); $now = DateTime::now(); if ($cacheLog->isEmpty()) { - Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([ + $authorization->skip(fn () => $dbForProject->createDocument('cache', new Document([ '$id' => $key, 'resource' => $resource, 'resourceType' => $resourceType, @@ -734,7 +752,7 @@ App::shutdown() ]))); } elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) { $cacheLog->setAttribute('accessedAt', $now); - Authorization::skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog)); + $authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog)); } if ($signature !== $cacheLog->getAttribute('signature')) { @@ -746,10 +764,8 @@ App::shutdown() } } - - if ($project->getId() !== 'console') { - if (!Auth::isPrivilegedUser(Authorization::getRoles())) { + if (!Auth::isPrivilegedUser($authorization->getRoles())) { $fileSize = 0; $file = $request->getFiles('file'); if (!empty($file)) { @@ -774,7 +790,7 @@ App::shutdown() $accessedAt = $project->getAttribute('accessedAt', ''); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { $project->setAttribute('accessedAt', DateTime::now()); - Authorization::skip(fn () => $dbForConsole->updateDocument('projects', $project->getId(), $project)); + $authorization->skip(fn () => $dbForConsole->updateDocument('projects', $project->getId(), $project)); } } @@ -795,10 +811,16 @@ App::shutdown() } }); -App::init() +Http::init() ->groups(['usage']) ->action(function () { if (System::getEnv('_APP_USAGE_STATS', 'enabled') !== 'enabled') { throw new Exception(Exception::GENERAL_USAGE_DISABLED); } }); + +Http::shutdown() + ->inject('connections') + ->action(function (Connections $connections) { + $connections->reclaim(); + }); diff --git a/app/controllers/shared/api/auth.php b/app/controllers/shared/api/auth.php index 53aacabe21..224dcb3392 100644 --- a/app/controllers/shared/api/auth.php +++ b/app/controllers/shared/api/auth.php @@ -4,13 +4,14 @@ use Appwrite\Auth\Auth; use Appwrite\Extend\Exception; use Appwrite\Utopia\Request; use MaxMind\Db\Reader; -use Utopia\App; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use Utopia\Http\Http; +use Utopia\Http\Route; use Utopia\System\System; -App::init() +Http::init() ->groups(['mfaProtected']) ->inject('session') ->action(function (Document $session) { @@ -29,13 +30,14 @@ App::init() } }); -App::init() +Http::init() ->groups(['auth']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('project') ->inject('geodb') - ->action(function (App $utopia, Request $request, Document $project, Reader $geodb) { + ->inject('authorization') + ->action(function (Route $route, Request $request, Document $project, Reader $geodb, Authorization $authorization) { $denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', ''); if (!empty($denylist && $project->getId() === 'console')) { $countries = explode(',', $denylist); @@ -46,15 +48,17 @@ App::init() } } - $route = $utopia->match($request); - - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - $isAppUser = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAppUser = Auth::isAppUser($authorization->getRoles()); if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs return; } + if ($route->getLabel('sdk.namespace', '') === 'graphql') { // Skip for graphQL recursive call + return; + } + $auths = $project->getAttribute('auths', []); switch ($route->getLabel('auth.type', '')) { case 'emailPassword': diff --git a/app/controllers/web/console.php b/app/controllers/web/console.php index c02e140270..60be4acf54 100644 --- a/app/controllers/web/console.php +++ b/app/controllers/web/console.php @@ -2,9 +2,9 @@ use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; -use Utopia\App; +use Utopia\Http\Http; -App::init() +Http::init() ->groups(['web']) ->inject('request') ->inject('response') @@ -16,7 +16,7 @@ App::init() ; }); -App::get('/') +Http::get('/') ->alias('auth/*') ->alias('/invite') ->alias('/login') diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index 27b2614c37..3577061d69 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -1,10 +1,10 @@ desc('Get Version') ->groups(['home', 'web']) ->label('scope', 'public') diff --git a/app/http.php b/app/http.php index bec772c770..320621ad33 100644 --- a/app/http.php +++ b/app/http.php @@ -1,335 +1,242 @@ set([ - 'worker_num' => $workerNumber, - 'open_http2_protocol' => true, - 'http_compression' => true, - 'http_compression_level' => 6, - 'package_max_length' => $payloadSize, - 'buffer_output_size' => $payloadSize, - ]); +$server = new Server('0.0.0.0', '80', [ + 'open_http2_protocol' => true, + 'http_compression' => true, + 'http_compression_level' => 6, + 'package_max_length' => $payloadSize, + 'buffer_output_size' => $payloadSize, + // Server + // 'log_level' => 0, + 'dispatch_mode' => 2, + 'worker_num' => $workerNumber, + 'reactor_num' => swoole_cpu_num() * 2, + 'open_cpu_affinity' => true, + // Coroutine + 'enable_coroutine' => true, + 'send_yield' => true, + 'tcp_fastopen' => true, +]); -$http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) { - Console::success('Worker ' . ++$workerId . ' started successfully'); -}); - -$http->on(Constant::EVENT_BEFORE_RELOAD, function ($server, $workerId) { - Console::success('Starting reload...'); -}); - -$http->on(Constant::EVENT_AFTER_RELOAD, function ($server, $workerId) { - Console::success('Reload completed...'); -}); - -include __DIR__ . '/controllers/general.php'; - -$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $register) { - $app = new App('UTC'); - - go(function () use ($register, $app) { - $pools = $register->get('pools'); - /** @var Group $pools */ - App::setResource('pools', fn () => $pools); - - // wait for database to be ready - $attempts = 0; - $max = 10; - $sleep = 1; - - do { - try { - $attempts++; - $dbForConsole = $app->getResource('dbForConsole'); - /** @var Utopia\Database\Database $dbForConsole */ - break; // leave the do-while if successful - } catch (\Throwable $e) { - Console::warning("Database not ready. Retrying connection ({$attempts})..."); - if ($attempts >= $max) { - throw new \Exception('Failed to connect to database: ' . $e->getMessage()); - } - sleep($sleep); - } - } while ($attempts < $max); - - Console::success('[Setup] - Server database init started...'); +$http = new Http($server, $container, 'UTC'); +$http->setRequestClass(Request::class); +$http->setResponseClass(Response::class); +Http::onStart() + ->inject('authorization') + ->inject('cache') + ->inject('pools') + ->inject('connections') + ->action(function (Authorization $authorization, Cache $cache, array $pools, Connections $connections) { try { - Console::success('[Setup] - Creating database: appwrite...'); - $dbForConsole->create(); + // wait for database to be ready + $attempts = 0; + $max = 15; + $sleep = 2; + + do { + try { + $attempts++; + $pool = $pools['pools-console-console']['pool']; + $dsn = $pools['pools-console-console']['dsn']; + $connection = $pool->get(); + $connections->add($connection, $pool); + + $adapter = match ($dsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + + $adapter->setDatabase($dsn->getPath()); + + $dbForConsole = new Database($adapter, $cache); + $dbForConsole->setAuthorization($authorization); + + $dbForConsole + ->setNamespace('_console') + ->setMetadata('host', \gethostname()) + ->setMetadata('project', 'console') + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); + + $dbForConsole->ping(); + break; // leave the do-while if successful + } catch (\Throwable $e) { + Console::warning("Database not ready. Retrying connection ({$attempts})..."); + if ($attempts >= $max) { + throw new \Exception('Failed to connect to database: ' . $e->getMessage()); + } + sleep($sleep); + } + } while ($attempts < $max); + + Console::success('[Setup] - Server database init started...'); + + try { + Console::success('[Setup] - Creating database: appwrite...'); + $dbForConsole->create(); + } catch (\Throwable $e) { + Console::success('[Setup] - Skip: metadata table already exists'); + return true; + } + + if ($dbForConsole->getCollection(Audit::COLLECTION)->isEmpty()) { + $audit = new Audit($dbForConsole); + $audit->setup(); + } + + if ($dbForConsole->getCollection(TimeLimit::COLLECTION)->isEmpty()) { + $abuse = new TimeLimit("", 0, 1, $dbForConsole); + $abuse->setup(); + } + + /** @var array $collections */ + $collections = Config::getParam('collections', []); + $consoleCollections = $collections['console']; + foreach ($consoleCollections as $key => $collection) { + if (($collection['$collection'] ?? '') !== Database::METADATA) { + continue; + } + if (!$dbForConsole->getCollection($key)->isEmpty()) { + continue; + } + + Console::success('[Setup] - Creating collection: ' . $collection['$id'] . '...'); + + $attributes = []; + $indexes = []; + + foreach ($collection['attributes'] as $attribute) { + $attributes[] = new Document([ + '$id' => ID::custom($attribute['$id']), + 'type' => $attribute['type'], + 'size' => $attribute['size'], + 'required' => $attribute['required'], + 'signed' => $attribute['signed'], + 'array' => $attribute['array'], + 'filters' => $attribute['filters'], + 'default' => $attribute['default'] ?? null, + 'format' => $attribute['format'] ?? '' + ]); + } + + foreach ($collection['indexes'] as $index) { + $indexes[] = new Document([ + '$id' => ID::custom($index['$id']), + 'type' => $index['type'], + 'attributes' => $index['attributes'], + 'lengths' => $index['lengths'], + 'orders' => $index['orders'], + ]); + } + + $dbForConsole->createCollection($key, $attributes, $indexes); + } + + if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDatabase(), 'bucket_1')) { + Console::success('[Setup] - Creating default bucket...'); + $dbForConsole->createDocument('buckets', new Document([ + '$id' => ID::custom('default'), + '$collection' => ID::custom('buckets'), + 'name' => 'Default', + 'maximumFileSize' => (int) System::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB + 'allowedFileExtensions' => [], + 'enabled' => true, + 'compression' => 'gzip', + 'encryption' => true, + 'antivirus' => true, + 'fileSecurity' => true, + '$permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'search' => 'buckets Default', + ])); + + $bucket = $dbForConsole->getDocument('buckets', 'default'); + + Console::success('[Setup] - Creating files collection for default bucket...'); + + $files = $collections['buckets']['files'] ?? []; + if (empty($files)) { + throw new Exception('Files collection is not configured.'); + } + + $attributes = []; + $indexes = []; + + foreach ($files['attributes'] as $attribute) { + $attributes[] = new Document([ + '$id' => ID::custom($attribute['$id']), + 'type' => $attribute['type'], + 'size' => $attribute['size'], + 'required' => $attribute['required'], + 'signed' => $attribute['signed'], + 'array' => $attribute['array'], + 'filters' => $attribute['filters'], + 'default' => $attribute['default'] ?? null, + 'format' => $attribute['format'] ?? '' + ]); + } + + foreach ($files['indexes'] as $index) { + $indexes[] = new Document([ + '$id' => ID::custom($index['$id']), + 'type' => $index['type'], + 'attributes' => $index['attributes'], + 'lengths' => $index['lengths'], + 'orders' => $index['orders'], + ]); + } + + $dbForConsole->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes); + } + + $connections->reclaim(); + + Console::success('[Setup] - Server database init completed...'); + Console::success('Server started successfully'); } catch (\Throwable $e) { - Console::success('[Setup] - Skip: metadata table already exists'); + Console::warning('Database not ready: ' . $e->getMessage()); + exit(1); } - - if ($dbForConsole->getCollection(Audit::COLLECTION)->isEmpty()) { - $audit = new Audit($dbForConsole); - $audit->setup(); - } - - if ($dbForConsole->getCollection(TimeLimit::COLLECTION)->isEmpty()) { - $adapter = new TimeLimit("", 0, 1, $dbForConsole); - $adapter->setup(); - } - - /** @var array $collections */ - $collections = Config::getParam('collections', []); - $consoleCollections = $collections['console']; - foreach ($consoleCollections as $key => $collection) { - if (($collection['$collection'] ?? '') !== Database::METADATA) { - continue; - } - if (!$dbForConsole->getCollection($key)->isEmpty()) { - continue; - } - - Console::success('[Setup] - Creating collection: ' . $collection['$id'] . '...'); - - $attributes = []; - $indexes = []; - - foreach ($collection['attributes'] as $attribute) { - $attributes[] = new Document([ - '$id' => ID::custom($attribute['$id']), - 'type' => $attribute['type'], - 'size' => $attribute['size'], - 'required' => $attribute['required'], - 'signed' => $attribute['signed'], - 'array' => $attribute['array'], - 'filters' => $attribute['filters'], - 'default' => $attribute['default'] ?? null, - 'format' => $attribute['format'] ?? '' - ]); - } - - foreach ($collection['indexes'] as $index) { - $indexes[] = new Document([ - '$id' => ID::custom($index['$id']), - 'type' => $index['type'], - 'attributes' => $index['attributes'], - 'lengths' => $index['lengths'], - 'orders' => $index['orders'], - ]); - } - - $dbForConsole->createCollection($key, $attributes, $indexes); - } - - if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDatabase(), 'bucket_1')) { - Console::success('[Setup] - Creating default bucket...'); - $dbForConsole->createDocument('buckets', new Document([ - '$id' => ID::custom('default'), - '$collection' => ID::custom('buckets'), - 'name' => 'Default', - 'maximumFileSize' => (int) System::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB - 'allowedFileExtensions' => [], - 'enabled' => true, - 'compression' => 'gzip', - 'encryption' => true, - 'antivirus' => true, - 'fileSecurity' => true, - '$permissions' => [ - Permission::create(Role::any()), - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'search' => 'buckets Default', - ])); - - $bucket = $dbForConsole->getDocument('buckets', 'default'); - - Console::success('[Setup] - Creating files collection for default bucket...'); - $files = $collections['buckets']['files'] ?? []; - if (empty($files)) { - throw new Exception('Files collection is not configured.'); - } - - $attributes = []; - $indexes = []; - - foreach ($files['attributes'] as $attribute) { - $attributes[] = new Document([ - '$id' => ID::custom($attribute['$id']), - 'type' => $attribute['type'], - 'size' => $attribute['size'], - 'required' => $attribute['required'], - 'signed' => $attribute['signed'], - 'array' => $attribute['array'], - 'filters' => $attribute['filters'], - 'default' => $attribute['default'] ?? null, - 'format' => $attribute['format'] ?? '' - ]); - } - - foreach ($files['indexes'] as $index) { - $indexes[] = new Document([ - '$id' => ID::custom($index['$id']), - 'type' => $index['type'], - 'attributes' => $index['attributes'], - 'lengths' => $index['lengths'], - 'orders' => $index['orders'], - ]); - } - - $dbForConsole->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes); - } - - $pools->reclaim(); - - Console::success('[Setup] - Server database init completed...'); }); - Console::success('Server started successfully (max payload is ' . number_format($payloadSize) . ' bytes)'); - Console::info("Master pid {$http->master_pid}, manager pid {$http->manager_pid}"); - - // listen ctrl + c - Process::signal(2, function () use ($http) { - Console::log('Stop by Ctrl+C'); - $http->shutdown(); +Http::init() + ->inject('authorization') + ->action(function (Authorization $authorization) { + $authorization->cleanRoles(); + $authorization->addRole(Role::any()->toString()); }); -}); - -$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register) { - App::setResource('swooleRequest', fn () => $swooleRequest); - App::setResource('swooleResponse', fn () => $swooleResponse); - - $request = new Request($swooleRequest); - $response = new Response($swooleResponse); - - if (Files::isFileLoaded($request->getURI())) { - $time = (60 * 60 * 24 * 365 * 2); // 45 days cache - - $response - ->setContentType(Files::getFileMimeType($request->getURI())) - ->addHeader('Cache-Control', 'public, max-age=' . $time) - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time) . ' GMT') // 45 days cache - ->send(Files::getFileContents($request->getURI())); - - return; - } - - $app = new App('UTC'); - - $pools = $register->get('pools'); - App::setResource('pools', fn () => $pools); - - try { - Authorization::cleanRoles(); - Authorization::setRole(Role::any()->toString()); - - $app->run($request, $response); - } catch (\Throwable $th) { - $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); - - $logger = $app->getResource("logger"); - if ($logger) { - try { - /** @var Utopia\Database\Document $user */ - $user = $app->getResource('user'); - } catch (\Throwable $_th) { - // All good, user is optional information for logger - } - - $route = $app->getRoute(); - - $log = $app->getResource("log"); - - if (isset($user) && !$user->isEmpty()) { - $log->setUser(new User($user->getId())); - } - - $log->setNamespace("http"); - $log->setServer(\gethostname()); - $log->setVersion($version); - $log->setType(Log::TYPE_ERROR); - $log->setMessage($th->getMessage()); - - $log->addTag('method', $route->getMethod()); - $log->addTag('url', $route->getPath()); - $log->addTag('verboseType', get_class($th)); - $log->addTag('code', $th->getCode()); - // $log->addTag('projectId', $project->getId()); // TODO: Figure out how to get ProjectID, if it becomes relevant - $log->addTag('hostname', $request->getHostname()); - $log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', ''))); - - $log->addExtra('file', $th->getFile()); - $log->addExtra('line', $th->getLine()); - $log->addExtra('trace', $th->getTraceAsString()); - $log->addExtra('roles', Authorization::getRoles()); - - $action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD"); - $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::error('[Error] Type: ' . get_class($th)); - Console::error('[Error] Message: ' . $th->getMessage()); - Console::error('[Error] File: ' . $th->getFile()); - Console::error('[Error] Line: ' . $th->getLine()); - - $swooleResponse->setStatusCode(500); - - $output = ((App::isDevelopment())) ? [ - 'message' => 'Error: ' . $th->getMessage(), - 'code' => 500, - 'file' => $th->getFile(), - 'line' => $th->getLine(), - 'trace' => $th->getTrace(), - 'version' => $version, - ] : [ - 'message' => 'Error: Server Error', - 'code' => 500, - 'version' => $version, - ]; - - $swooleResponse->end(\json_encode($output)); - } finally { - $pools->reclaim(); - } -}); $http->start(); diff --git a/app/init.php b/app/init.php index f94c9665e7..0f9e8f0ef6 100644 --- a/app/init.php +++ b/app/init.php @@ -1,26 +1,12 @@ $value], JSON_PRESERVE_ZERO_FRACTION); - }, - function (mixed $value) { - if (is_null($value)) { - return; - } +$container = new Container(); +$registry = new Registry(); - 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 Authorization::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 Authorization::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 Authorization::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 Authorization::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 Authorization::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 Authorization::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 = Authorization::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->skipValidation(fn () => $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; - } -); - -/** - * DB Formats - */ -Structure::addFormat(APP_DATABASE_ATTRIBUTE_EMAIL, function () { - return new Email(); -}, Database::VAR_STRING); - -Structure::addFormat(APP_DATABASE_ATTRIBUTE_DATETIME, function () { - return new DatetimeValidator(); -}, Database::VAR_DATETIME); - -Structure::addFormat(APP_DATABASE_ATTRIBUTE_ENUM, function ($attribute) { - $elements = $attribute['formatOptions']['elements']; - return new WhiteList($elements, true); -}, Database::VAR_STRING); - -Structure::addFormat(APP_DATABASE_ATTRIBUTE_IP, function () { - return new IP(); -}, Database::VAR_STRING); - -Structure::addFormat(APP_DATABASE_ATTRIBUTE_URL, function () { - return new URL(); -}, Database::VAR_STRING); - -Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function ($attribute) { - $min = $attribute['formatOptions']['min'] ?? -INF; - $max = $attribute['formatOptions']['max'] ?? INF; - return new Range($min, $max, Range::TYPE_INTEGER); -}, Database::VAR_INTEGER); - -Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function ($attribute) { - $min = $attribute['formatOptions']['min'] ?? -INF; - $max = $attribute['formatOptions']['max'] ?? INF; - return new Range($min, $max, Range::TYPE_FLOAT); -}, Database::VAR_FLOAT); - -/* - * Registry - */ -$register->set('logger', function () { +$registry->set('logger', function () { // Register error logger $providerName = System::getEnv('_APP_LOGGING_PROVIDER', ''); $providerConfig = System::getEnv('_APP_LOGGING_CONFIG', ''); @@ -776,12 +105,12 @@ $register->set('logger', function () { default => ['key' => $loggingProvider->getHost()], }; } catch (Throwable $th) { - // Fallback for older Appwrite versions up to 1.5.x that use _APP_LOGGING_PROVIDER and _APP_LOGGING_CONFIG environment variables Console::warning('Using deprecated logging configuration. Please update your configuration to use DSN format.' . $th->getMessage()); + // Fallback for older Appwrite versions up to 1.5.x that use _APP_LOGGING_PROVIDER and _APP_LOGGING_CONFIG environment variables $configChunks = \explode(";", $providerConfig); $providerConfig = match ($providerName) { - 'sentry' => [ 'key' => $configChunks[0], 'projectId' => $configChunks[1] ?? '', 'host' => '',], + 'sentry' => ['key' => $configChunks[0], 'projectId' => $configChunks[1] ?? '', 'host' => '',], 'logowl' => ['ticket' => $configChunks[0] ?? '', 'host' => ''], default => ['key' => $providerConfig], }; @@ -812,200 +141,167 @@ $register->set('logger', function () { return; } - return new Logger($adapter); + $logger = new Logger($adapter); + $logger->setSample(0.4); + return $logger; }); -$register->set('pools', function () { - $group = new Group(); +$registry->set('geodb', function () { + /** + * @disregard P1009 Undefined type + */ + return new Reader(__DIR__ . '/assets/dbip/dbip-country-lite-2024-09.mmdb'); +}); - $fallbackForDB = 'db_main=' . AppwriteURL::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=' . AppwriteURL::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', ''), - ]); +$registry->set('hooks', function () { + return new Hooks(); +}); - $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'], - ], - ]; +$registry->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', ''), + ]); - $maxConnections = System::getEnv('_APP_CONNECTIONS_MAX', 151); - $instanceConnections = $maxConnections / System::getEnv('_APP_POOL_CLIENTS', 14); + $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'], + ], + ]; - $multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled'; + $pools = []; + $poolSize = (int)System::getEnv('_APP_POOL_SIZE', 64); - if ($multiprocessing) { - $workerCount = swoole_cpu_num() * intval(System::getEnv('_APP_WORKER_PER_CORE', 6)); - } else { - $workerCount = 1; - } + foreach ($connections as $key => $connection) { + $dsns = $connection['dsns'] ?? ''; + $multiple = $connection['multiple'] ?? false; + $schemes = $connection['schemes'] ?? []; + $dsns = explode(',', $connection['dsns'] ?? ''); + $config = []; - 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); - } + foreach ($dsns as &$dsn) { + $dsn = explode('=', $dsn); + $name = ($multiple) ? $key . '_' . $dsn[0] : $key; + $config[] = $name; + $dsn = $dsn[1] ?? ''; - $poolSize = (int)($instanceConnections / $workerCount); + if (empty($dsn)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Missing value for DSN connection in {$key}"); + } - foreach ($connections as $key => $connection) { - $type = $connection['type'] ?? ''; - $multiple = $connection['multiple'] ?? false; - $schemes = $connection['schemes'] ?? []; - $config = []; - $dsns = explode(',', $connection['dsns'] ?? ''); - foreach ($dsns as &$dsn) { - $dsn = explode('=', $dsn); - $name = ($multiple) ? $key . '_' . $dsn[0] : $key; - $dsn = $dsn[1] ?? ''; - $config[] = $name; - if (empty($dsn)) { - //throw new Exception(Exception::GENERAL_SERVER_ERROR, "Missing value for DSN connection in {$key}"); - continue; - } + $dsn = new DSN($dsn); + $dsnHost = $dsn->getHost(); + $dsnPort = $dsn->getPort(); + $dsnUser = $dsn->getUser(); + $dsnPass = $dsn->getPassword(); + $dsnScheme = $dsn->getScheme(); + $dsnDatabase = $dsn->getPath(); - $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"); + } - 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, - /** - * Get Resource - * - * Creation could be reused across connection types like database, cache, queue, etc. - * - * Resource assignment to an adapter will happen below. - */ - $resource = match ($dsnScheme) { - 'mysql', - 'mariadb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) { - return new PDOProxy(function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) { - return new PDO("mysql:host={$dsnHost};port={$dsnPort};dbname={$dsnDatabase};charset=utf8mb4", $dsnUser, $dsnPass, array( - 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 - )); - }); - }, - 'redis' => function () use ($dsnHost, $dsnPort, $dsnPass) { - $redis = new Redis(); - @$redis->pconnect($dsnHost, (int)$dsnPort); - if ($dsnPass) { - $redis->auth($dsnPass); - } - $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); - - return $redis; - }, - default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Invalid scheme'), - }; - - $pool = new Pool($name, $poolSize, function () use ($type, $resource, $dsn) { - // Get Adapter - switch ($type) { - case 'database': - $adapter = match ($dsn->getScheme()) { - 'mariadb' => new MariaDB($resource()), - 'mysql' => new MySQL($resource()), - default => null - }; - - $adapter->setDatabase($dsn->getPath()); + ]), + $poolSize + ); break; - case 'pubsub': - $adapter = $resource(); - break; - case 'queue': - $adapter = match ($dsn->getScheme()) { - 'redis' => new Queue\Connection\Redis($dsn->getHost(), $dsn->getPort()), - default => null - }; - break; - case 'cache': - $adapter = match ($dsn->getScheme()) { - 'redis' => new RedisCache($resource()), - default => null - }; + case 'redis': + $pool = new RedisPool( + (new RedisConfig()) + ->withHost($dsnHost) + ->withPort((int)$dsnPort) + ->withAuth($dsnPass ?? ''), + $poolSize + ); break; default: - throw new Exception(Exception::GENERAL_SERVER_ERROR, "Server error: Missing adapter implementation."); + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid scheme"); } - return $adapter; - }); + $pools['pools-' . $key . '-' . $name] = [ + 'pool' => $pool, + 'dsn' => $dsn, + ]; + } - $group->add($pool); + Config::setParam('pools-' . $key, $config); } - Config::setParam('pools-' . $key, $config); - } + return function () use ($pools): array { + return $pools; + }; + })() +); - return $group; -}); - -$register->set('db', function () { - // This is usually for our workers or CLI commands scope - $dbHost = System::getEnv('_APP_DB_HOST', ''); - $dbPort = System::getEnv('_APP_DB_PORT', ''); - $dbUser = System::getEnv('_APP_DB_USER', ''); - $dbPass = System::getEnv('_APP_DB_PASS', ''); - $dbScheme = System::getEnv('_APP_DB_SCHEMA', ''); - - return new PDO( - "mysql:host={$dbHost};port={$dbPort};dbname={$dbScheme};charset=utf8mb4", - $dbUser, - $dbPass, - SQL::getPDOAttributes() - ); -}); - -$register->set('smtp', function () { +$registry->set('smtp', function () { $mail = new PHPMailer(true); $mail->isSMTP(); @@ -1033,739 +329,65 @@ $register->set('smtp', function () { return $mail; }); -$register->set('geodb', function () { - return new Reader(__DIR__ . '/assets/dbip/dbip-country-lite-2024-09.mmdb'); -}); -$register->set('passwordsDictionary', function () { - $content = \file_get_contents(__DIR__ . '/assets/security/10k-common-passwords'); - $content = explode("\n", $content); - $content = array_flip($content); - return $content; -}); -$register->set('promiseAdapter', function () { + +$registry->set('promiseAdapter', function () { return new Swoole(); }); -$register->set('hooks', function () { - return new Hooks(); -}); -/* - * Localization - */ -Locale::$exceptions = false; -$locales = Config::getParam('locale-codes', []); +$registry->set('db', function () { + // This is usually for our workers or CLI commands scope + $dbHost = System::getEnv('_APP_DB_HOST', ''); + $dbPort = System::getEnv('_APP_DB_PORT', ''); + $dbUser = System::getEnv('_APP_DB_USER', ''); + $dbPass = System::getEnv('_APP_DB_PASS', ''); + $dbScheme = System::getEnv('_APP_DB_SCHEMA', ''); -foreach ($locales as $locale) { - $code = $locale['code']; - - $path = __DIR__ . '/config/locale/translations/' . $code . '.json'; - - if (!\file_exists($path)) { - $path = __DIR__ . '/config/locale/translations/' . \substr($code, 0, 2) . '.json'; // if `ar-ae` doesn't exist, look for `ar` - if (!\file_exists($path)) { - $path = __DIR__ . '/config/locale/translations/en.json'; // if none translation exists, use default from `en.json` - } - } - - Locale::setLanguageFromJSON($code, $path); -} - -\stream_context_set_default([ // Set global user agent and http settings - 'http' => [ - 'method' => 'GET', - 'user_agent' => \sprintf( - APP_USERAGENT, - System::getEnv('_APP_VERSION', 'UNKNOWN'), - System::getEnv('_APP_EMAIL_SECURITY', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)) - ), - 'timeout' => 2, - ], -]); - -// Runtime Execution -App::setResource('log', fn () => new Log()); -App::setResource('logger', function ($register) { - return $register->get('logger'); -}, ['register']); - -App::setResource('hooks', function ($register) { - return $register->get('hooks'); -}, ['register']); - -App::setResource('register', fn () => $register); -App::setResource('locale', fn () => new Locale(System::getEnv('_APP_LOCALE', 'en'))); - -App::setResource('localeCodes', function () { - return array_map(fn ($locale) => $locale['code'], Config::getParam('locale-codes', [])); -}); - -// Queues -App::setResource('queue', function (Group $pools) { - return $pools->get('queue')->pop()->getResource(); -}, ['pools']); -App::setResource('queueForMessaging', function (Connection $queue) { - return new Messaging($queue); -}, ['queue']); -App::setResource('queueForMails', function (Connection $queue) { - return new Mail($queue); -}, ['queue']); -App::setResource('queueForBuilds', function (Connection $queue) { - return new Build($queue); -}, ['queue']); -App::setResource('queueForDatabase', function (Connection $queue) { - return new EventDatabase($queue); -}, ['queue']); -App::setResource('queueForDeletes', function (Connection $queue) { - return new Delete($queue); -}, ['queue']); -App::setResource('queueForEvents', function (Connection $queue) { - return new Event($queue); -}, ['queue']); -App::setResource('queueForAudits', function (Connection $queue) { - return new Audit($queue); -}, ['queue']); -App::setResource('queueForFunctions', function (Connection $queue) { - return new Func($queue); -}, ['queue']); -App::setResource('queueForUsage', function (Connection $queue) { - return new Usage($queue); -}, ['queue']); -App::setResource('queueForCertificates', function (Connection $queue) { - return new Certificate($queue); -}, ['queue']); -App::setResource('queueForMigrations', function (Connection $queue) { - return new Migration($queue); -}, ['queue']); -App::setResource('clients', function ($request, $console, $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) && !empty($node['hostname'])) - ) + return new PDO( + "mysql:host={$dbHost};port={$dbPort};dbname={$dbScheme};charset=utf8mb4", + $dbUser, + $dbPass, + SQL::getPDOAttributes() ); - - $clients = $clientsConsole; - $platforms = $project->getAttribute('platforms', []); - - foreach ($platforms as $node) { - if ( - isset($node['type']) && - ($node['type'] === Origin::CLIENT_TYPE_WEB || - $node['type'] === Origin::CLIENT_TYPE_FLUTTER_WEB) && - !empty($node['hostname']) - ) { - $clients[] = $node['hostname']; - } - } - - return \array_unique($clients); -}, ['request', 'console', 'project']); - -App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForConsole) { - /** @var Appwrite\Utopia\Request $request */ - /** @var Appwrite\Utopia\Response $response */ - /** @var Utopia\Database\Document $project */ - /** @var Utopia\Database\Database $dbForProject */ - /** @var Utopia\Database\Database $dbForConsole */ - /** @var string $mode */ - - 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('teamInternalId', $project->getAttribute('teamInternalId'), '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', 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)) { - $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 Document([]); - } - } - } - - $dbForProject->setMetadata('user', $user->getId()); - $dbForConsole->setMetadata('user', $user->getId()); - - return $user; -}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForConsole']); - -App::setResource('project', function ($dbForConsole, $request, $console) { - /** @var Appwrite\Utopia\Request $request */ - /** @var Utopia\Database\Database $dbForConsole */ - /** @var Utopia\Database\Document $console */ - - $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; -}, ['dbForConsole', 'request', 'console']); - -App::setResource('session', function (Document $user) { - if ($user->isEmpty()) { - return; - } - - $sessions = $user->getAttribute('sessions', []); - $sessionId = Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret); - - if (!$sessionId) { - return; - } - - foreach ($sessions as $session) {/** @var Document $session */ - if ($sessionId === $session->getId()) { - return $session; - } - } - - return; -}, ['user']); - -App::setResource('console', 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' => [ - 'mockNumbers' => [], - '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 - 'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled' - ], - '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', '') - ], - ]); -}, []); - -App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, Cache $cache, Document $project) { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForConsole; - } - - 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')); - } - - $dbAdapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - - $database = new Database($dbAdapter, $cache); - - $database - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()) - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); - - 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 ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } - - return $database; -}, ['pools', 'dbForConsole', 'cache', 'project']); - -App::setResource('dbForConsole', function (Group $pools, Cache $cache) { - $dbAdapter = $pools - ->get('console') - ->pop() - ->getResource(); - - $database = new Database($dbAdapter, $cache); - - $database - ->setNamespace('_console') - ->setMetadata('host', \gethostname()) - ->setMetadata('project', 'console') - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); - - return $database; -}, ['pools', 'cache']); - -App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) { - $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools - - return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForConsole; - } - - 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')); - } - - $configure = (function (Database $database) use ($project, $dsn) { - $database - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()) - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); - - if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } - }); - - if (isset($databases[$dsn->getHost()])) { - $database = $databases[$dsn->getHost()]; - $configure($database); - return $database; - } - - $dbAdapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - - $database = new Database($dbAdapter, $cache); - $databases[$dsn->getHost()] = $database; - $configure($database); - - return $database; - }; -}, ['pools', 'dbForConsole', 'cache']); - -App::setResource('cache', function (Group $pools) { - $list = Config::getParam('pools-cache', []); - $adapters = []; - - foreach ($list as $value) { - $adapters[] = $pools - ->get($value) - ->pop() - ->getResource() - ; - } - - return new Cache(new Sharding($adapters)); -}, ['pools']); - -App::setResource('deviceForLocal', function () { - return new Local(); }); -App::setResource('deviceForFiles', function ($project) { - return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); -}, ['project']); +// Autoload +class_exists(JWT::class, true); +class_exists(DSN::class, true); +class_exists(Log::class, true); +class_exists(TOTP::class, true); +class_exists(Mail::class, true); +class_exists(Func::class, true); +class_exists(Cache::class, true); +class_exists(Abuse::class, true); +class_exists(MySQL::class, true); +class_exists(Event::class, true); +class_exists(Audit::class, true); +class_exists(Usage::class, true); +class_exists(Local::class, true); +class_exists(Build::class, true); +class_exists(Locale::class, true); +class_exists(Delete::class, true); +class_exists(GitHub::class, true); +class_exists(Schema::class, true); +class_exists(Domain::class, true); +class_exists(Console::class, true); +class_exists(Request::class, true); +class_exists(MariaDB::class, true); +class_exists(Document::class, true); +class_exists(Sharding::class, true); +class_exists(Database::class, true); +class_exists(Hostname::class, true); +class_exists(TimeLimit::class, true); +class_exists(Migration::class, true); +class_exists(Messaging::class, true); +class_exists(CacheRedis::class, true); +class_exists(Connections::class, true); +class_exists(Certificate::class, true); +class_exists(EventDatabase::class, true); +class_exists(Authorization::class, true); +class_exists(Authentication::class, true); +class_exists(Queue\Connection\Redis::class, true); -App::setResource('deviceForFunctions', function ($project) { - return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()); -}, ['project']); +require_once __DIR__ . '/init/resources.php'; -App::setResource('deviceForBuilds', function ($project) { - return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); -}, ['project']); - -function getDevice($root): Device -{ - $connection = System::getEnv('_APP_CONNECTIONS_STORAGE', ''); - - if (!empty($connection)) { - $acl = 'private'; - $device = Storage::DEVICE_LOCAL; - $accessKey = ''; - $accessSecret = ''; - $bucket = ''; - $region = ''; - - try { - $dsn = new DSN($connection); - $device = $dsn->getScheme(); - $accessKey = $dsn->getUser() ?? ''; - $accessSecret = $dsn->getPassword() ?? ''; - $bucket = $dsn->getPath() ?? ''; - $region = $dsn->getParam('region'); - } catch (\Throwable $e) { - Console::warning($e->getMessage() . 'Invalid DSN. Defaulting to Local device.'); - } - - switch ($device) { - case Storage::DEVICE_S3: - return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl); - case STORAGE::DEVICE_DO_SPACES: - $device = new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl); - $device->setHttpVersion(S3::HTTP_VERSION_1_1); - return $device; - case Storage::DEVICE_BACKBLAZE: - return new Backblaze($root, $accessKey, $accessSecret, $bucket, $region, $acl); - case Storage::DEVICE_LINODE: - return new Linode($root, $accessKey, $accessSecret, $bucket, $region, $acl); - case Storage::DEVICE_WASABI: - return new Wasabi($root, $accessKey, $accessSecret, $bucket, $region, $acl); - case Storage::DEVICE_LOCAL: - default: - return new Local($root); - } - } else { - switch (strtolower(System::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) ?? '')) { - case Storage::DEVICE_LOCAL: - default: - return new Local($root); - case Storage::DEVICE_S3: - $s3AccessKey = System::getEnv('_APP_STORAGE_S3_ACCESS_KEY', ''); - $s3SecretKey = System::getEnv('_APP_STORAGE_S3_SECRET', ''); - $s3Region = System::getEnv('_APP_STORAGE_S3_REGION', ''); - $s3Bucket = System::getEnv('_APP_STORAGE_S3_BUCKET', ''); - $s3Acl = 'private'; - return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl); - case Storage::DEVICE_DO_SPACES: - $doSpacesAccessKey = System::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', ''); - $doSpacesSecretKey = System::getEnv('_APP_STORAGE_DO_SPACES_SECRET', ''); - $doSpacesRegion = System::getEnv('_APP_STORAGE_DO_SPACES_REGION', ''); - $doSpacesBucket = System::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', ''); - $doSpacesAcl = 'private'; - $device = new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl); - $device->setHttpVersion(S3::HTTP_VERSION_1_1); - return $device; - case Storage::DEVICE_BACKBLAZE: - $backblazeAccessKey = System::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', ''); - $backblazeSecretKey = System::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', ''); - $backblazeRegion = System::getEnv('_APP_STORAGE_BACKBLAZE_REGION', ''); - $backblazeBucket = System::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', ''); - $backblazeAcl = 'private'; - return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl); - case Storage::DEVICE_LINODE: - $linodeAccessKey = System::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', ''); - $linodeSecretKey = System::getEnv('_APP_STORAGE_LINODE_SECRET', ''); - $linodeRegion = System::getEnv('_APP_STORAGE_LINODE_REGION', ''); - $linodeBucket = System::getEnv('_APP_STORAGE_LINODE_BUCKET', ''); - $linodeAcl = 'private'; - return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl); - case Storage::DEVICE_WASABI: - $wasabiAccessKey = System::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', ''); - $wasabiSecretKey = System::getEnv('_APP_STORAGE_WASABI_SECRET', ''); - $wasabiRegion = System::getEnv('_APP_STORAGE_WASABI_REGION', ''); - $wasabiBucket = System::getEnv('_APP_STORAGE_WASABI_BUCKET', ''); - $wasabiAcl = 'private'; - return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl); - } - } -} - -App::setResource('mode', function ($request) { - /** @var Appwrite\Utopia\Request $request */ - - /** - * Defines the mode for the request: - * - 'default' => Requests for Client and Server Side - * - 'admin' => Request from the Console on non-console projects - */ - return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); -}, ['request']); - -App::setResource('geodb', function ($register) { - /** @var Utopia\Registry\Registry $register */ - return $register->get('geodb'); -}, ['register']); - -App::setResource('passwordsDictionary', function ($register) { - /** @var Utopia\Registry\Registry $register */ - return $register->get('passwordsDictionary'); -}, ['register']); - - -App::setResource('servers', function () { - $platforms = Config::getParam('platforms'); - $server = $platforms[APP_PLATFORM_SERVER]; - - $languages = array_map(function ($language) { - return strtolower($language['name']); - }, $server['sdks']); - - return $languages; -}); - -App::setResource('promiseAdapter', function ($register) { - return $register->get('promiseAdapter'); -}, ['register']); - -App::setResource('schema', function ($utopia, $dbForProject) { - - $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) { - $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']}"; - }, - ]; - - $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']); - -App::setResource('contributors', function () { - $path = 'app/config/contributors.json'; - $list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; - return $list; -}); - -App::setResource('employees', function () { - $path = 'app/config/employees.json'; - $list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; - return $list; -}); - -App::setResource('heroes', function () { - $path = 'app/config/heroes.json'; - $list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; - return $list; -}); - -App::setResource('gitHub', function (Cache $cache) { - return new VcsGitHub($cache); -}, ['cache']); - -App::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']); -App::setResource('plan', function (array $plan = []) { - return []; -}); +Models::init(); diff --git a/app/init/config.php b/app/init/config.php new file mode 100644 index 0000000000..bc8333b5d9 --- /dev/null +++ b/app/init/config.php @@ -0,0 +1,37 @@ + $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; + } +); diff --git a/app/init/database/formats.php b/app/init/database/formats.php new file mode 100644 index 0000000000..88e46655ac --- /dev/null +++ b/app/init/database/formats.php @@ -0,0 +1,43 @@ +getScheme(); + $accessKey = $dsn->getUser() ?? ''; + $accessSecret = $dsn->getPassword() ?? ''; + $bucket = $dsn->getPath() ?? ''; + $region = $dsn->getParam('region'); + } catch (\Throwable $e) { + Console::warning($e->getMessage() . 'Invalid DSN. Defaulting to Local device.'); + } + + switch ($device) { + case Storage::DEVICE_S3: + return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl); + case STORAGE::DEVICE_DO_SPACES: + return new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl); + case Storage::DEVICE_BACKBLAZE: + return new Backblaze($root, $accessKey, $accessSecret, $bucket, $region, $acl); + case Storage::DEVICE_LINODE: + return new Linode($root, $accessKey, $accessSecret, $bucket, $region, $acl); + case Storage::DEVICE_WASABI: + return new Wasabi($root, $accessKey, $accessSecret, $bucket, $region, $acl); + case Storage::DEVICE_LOCAL: + default: + return new Local($root); + } + } else { + switch (strtolower(System::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) ?? '')) { + case Storage::DEVICE_LOCAL: + default: + return new Local($root); + case Storage::DEVICE_S3: + $s3AccessKey = System::getEnv('_APP_STORAGE_S3_ACCESS_KEY', ''); + $s3SecretKey = System::getEnv('_APP_STORAGE_S3_SECRET', ''); + $s3Region = System::getEnv('_APP_STORAGE_S3_REGION', ''); + $s3Bucket = System::getEnv('_APP_STORAGE_S3_BUCKET', ''); + $s3Acl = 'private'; + return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl); + case Storage::DEVICE_DO_SPACES: + $doSpacesAccessKey = System::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', ''); + $doSpacesSecretKey = System::getEnv('_APP_STORAGE_DO_SPACES_SECRET', ''); + $doSpacesRegion = System::getEnv('_APP_STORAGE_DO_SPACES_REGION', ''); + $doSpacesBucket = System::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', ''); + $doSpacesAcl = 'private'; + return new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl); + case Storage::DEVICE_BACKBLAZE: + $backblazeAccessKey = System::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', ''); + $backblazeSecretKey = System::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', ''); + $backblazeRegion = System::getEnv('_APP_STORAGE_BACKBLAZE_REGION', ''); + $backblazeBucket = System::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', ''); + $backblazeAcl = 'private'; + return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl); + case Storage::DEVICE_LINODE: + $linodeAccessKey = System::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', ''); + $linodeSecretKey = System::getEnv('_APP_STORAGE_LINODE_SECRET', ''); + $linodeRegion = System::getEnv('_APP_STORAGE_LINODE_REGION', ''); + $linodeBucket = System::getEnv('_APP_STORAGE_LINODE_BUCKET', ''); + $linodeAcl = 'private'; + return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl); + case Storage::DEVICE_WASABI: + $wasabiAccessKey = System::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', ''); + $wasabiSecretKey = System::getEnv('_APP_STORAGE_WASABI_SECRET', ''); + $wasabiRegion = System::getEnv('_APP_STORAGE_WASABI_REGION', ''); + $wasabiBucket = System::getEnv('_APP_STORAGE_WASABI_BUCKET', ''); + $wasabiAcl = 'private'; + return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl); + } + } +} + +$log = new Dependency(); +$mode = new Dependency(); +$user = new Dependency(); +$plan = new Dependency(); +$pools = new Dependency(); +$geodb = new Dependency(); +$cache = new Dependency(); +$pools = new Dependency(); +$queue = new Dependency(); +$hooks = new Dependency(); +$logger = new Dependency(); +$locale = new Dependency(); +$schema = new Dependency(); +$github = new Dependency(); +$session = new Dependency(); +$console = new Dependency(); +$project = new Dependency(); +$clients = new Dependency(); +$servers = new Dependency(); +$register = new Dependency(); +$connections = new Dependency(); +$localeCodes = new Dependency(); +$getConsoleDB = new Dependency(); +$getProjectDB = new Dependency(); +$dbForProject = new Dependency(); +$dbForConsole = new Dependency(); +$queueForUsage = new Dependency(); +$queueForMails = new Dependency(); +$authorization = new Dependency(); +$authentication = new Dependency(); +$queueForBuilds = new Dependency(); +$deviceForLocal = new Dependency(); +$deviceForFiles = new Dependency(); +$queueForEvents = new Dependency(); +$queueForAudits = new Dependency(); +$promiseAdapter = new Dependency(); +$schemaVariable = new Dependency(); +$deviceForBuilds = new Dependency(); +$queueForDeletes = new Dependency(); +$requestTimestamp = new Dependency(); +$queueForDatabase = new Dependency(); +$queueForMessaging = new Dependency(); +$queueForFunctions = new Dependency(); +$queueForMigrations = new Dependency(); +$deviceForFunctions = new Dependency(); +$passwordsDictionary = new Dependency(); +$queueForCertificates = new Dependency(); + + +$plan + ->setName('plan') + ->setCallback(fn () => []); + +$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)); + }); + +$user + ->setName('user') + ->inject('mode') + ->inject('project') + ->inject('console') + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('dbForConsole') + ->inject('authorization') + ->inject('authentication') + ->setCallback(function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization, Authentication $authentication) { + $authorization->setDefaultStatus(true); + $authentication->setCookieName('a_session_' . $project->getId()); + + if (APP_MODE_ADMIN === $mode) { + $authentication->setCookieName('a_session_' . $console->getId()); + } + + $session = Auth::decodeSession( + $request->getCookie( + $authentication->getCookieName(), // Get sessions + $request->getCookie($authentication->getCookieName() . '_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[$authentication->getCookieName()])) ? $fallback[$authentication->getCookieName()] : '')); + } + + $authentication->setUnique($session['id'] ?? ''); + $authentication->setSecret($session['secret'] ?? ''); + + if (APP_MODE_ADMIN !== $mode) { + if ($project->isEmpty()) { + $user = new Document([]); + } else { + if ($project->getId() === 'console') { + $user = $dbForConsole->getDocument('users', $authentication->getUnique()); + } else { + $user = $dbForProject->getDocument('users', $authentication->getUnique()); + } + } + } else { + $user = $dbForConsole->getDocument('users', $authentication->getUnique()); + } + + if ( + $user->isEmpty() // Check a document has been found in the DB + || !Auth::sessionVerify($user->getAttribute('sessions', []), $authentication->getSecret()) + ) { // 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', 3600, 0); + try { + $payload = $jwt->decode($authJWT); + } catch (JWTException $error) { + $request->removeHeader('x-appwrite-jwt'); + throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage()); + } + + $jwtUserId = $payload['userId'] ?? ''; + if (!empty($jwtUserId)) { + $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 Document([]); + } + } + } + + // Adds logs to database queries + $dbForProject->setMetadata('user', $user->getId()); + $dbForConsole->setMetadata('user', $user->getId()); + + return $user; + }); + + +$session + ->setName('session') + ->inject('user') + ->inject('project') + ->inject('authorization') + ->inject('authentication') + ->setCallback(function (Document $user, Document $project, Authorization $authorization, Authentication $authentication) { + if ($user->isEmpty()) { + return; + } + + $sessions = $user->getAttribute('sessions', []); + $authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $sessionId = Auth::sessionVerify($user->getAttribute('sessions'), $authentication->getSecret(), $authDuration); + + if (!$sessionId) { + return; + } + + foreach ($sessions as $session) { + if ($sessionId === $session->getId()) { + return $session; + } + } + + return; + }); + +$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' => [ + 'mockNumbers' => [], + '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 + 'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled' + ], + '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', '') + ], + ]); + }); + + +$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; + }); + +$pools + ->setName('pools') + ->inject('registry') + ->setCallback(function (Registry $registry) { + return $registry->get('pools'); + }); + +$dbForProject + ->setName('dbForProject') + ->inject('cache') + ->inject('pools') + ->inject('project') + ->inject('dbForConsole') + ->inject('authorization') + ->inject('connections') + ->setCallback(function (Cache $cache, array $pools, Document $project, Database $dbForConsole, Authorization $authorization, Connections $connections) { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForConsole; + } + + 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['pools-database-' . $dsn->getHost()]['pool']; + $connectionDsn = $pools['pools-database-' . $dsn->getHost()]['dsn']; + + $connection = $pool->get(); + $connections->add($connection, $pool); + $adapter = match ($connectionDsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + + $adapter->setDatabase($connectionDsn->getPath()); + + $database = new Database($adapter, $cache); + + 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 ($dsn->getHost() === DATABASE_SHARED_TABLES) { + $database + ->setSharedTables(true) + ->setTenant($project->getInternalId()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getInternalId()); + } + + $database->setAuthorization($authorization); + return $database; + }); + +$dbForConsole + ->setName('dbForConsole') + ->inject('getConsoleDB') + ->inject('connections') + ->setCallback(function (callable $getConsoleDB, Connections $connections): Database { + [$connection,$pool, $database] = $getConsoleDB(); + $connections->add($connection, $pool); + + return $database; + }); + +$cache + ->setName('cache') + ->inject('pools') + ->inject('connections') + ->setCallback(function (array $pools, Connections $connections) { + $adapters = []; + $databases = Config::getParam('pools-cache'); + + foreach ($databases as $database) { + $pool = $pools['pools-cache-' . $database]['pool']; + $dsn = $pools['pools-cache-' . $database]['dsn']; + + $connection = $pool->get(); + $connections->add($connection, $pool); + + $adapters[] = new CacheRedis($connection); + } + + return new Cache(new Sharding($adapters)); + }); + +$authorization + ->setName('authorization') + ->setCallback(function (): Authorization { + return new Authorization(); + }); + +$authentication + ->setName('authentication') + ->setCallback(function (): Authentication { + return new Authentication(); + }); + +$register + ->setName('registry') + ->setCallback(function () use (&$registry): Registry { + return $registry; + }); + +$pools + ->setName('pools') + ->inject('registry') + ->setCallback(function (Registry $registry) { + return $registry->get('pools'); + }); + +$logger + ->setName('logger') + ->inject('registry') + ->setCallback(function (Registry $registry) { + return $registry->get('logger'); + }); + +$log + ->setName('log') + ->setCallback(function () { + return new Log(); + }); + +$connections + ->setName('connections') + ->setCallback(function () { + return new Connections(); + }); + +$locale + ->setName('locale') + ->setCallback(fn () => new Locale(System::getEnv('_APP_LOCALE', 'en'))); + +$localeCodes + ->setName('localeCodes') + ->setCallback(fn () => array_map(fn ($locale) => $locale['code'], Config::getParam('locale-codes', []))); + +$queue + ->setName('queue') + ->inject('pools') + ->inject('connections') + ->setCallback(function (array $pools, Connections $connections) { + $pool = $pools['pools-queue-queue']['pool']; + $connection = $pool->get(); + $connections->add($connection, $pool); + + return new Queue\Connection\Redis($connection); + }); + +$queueForMessaging + ->setName('queueForMessaging') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Messaging($queue); + }); + +$queueForMails + ->setName('queueForMails') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Mail($queue); + }); + +$queueForBuilds + ->setName('queueForBuilds') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Build($queue); + }); + +$queueForDatabase + ->setName('queueForDatabase') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new EventDatabase($queue); + }); + +$queueForDeletes + ->setName('queueForDeletes') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Delete($queue); + }); + +$queueForEvents + ->setName('queueForEvents') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Event($queue); + }); + +$queueForAudits + ->setName('queueForAudits') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Audit($queue); + }); + +$queueForFunctions + ->setName('queueForFunctions') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Func($queue); + }); + +$queueForUsage + ->setName('queueForUsage') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Usage($queue); + }); + +$queueForCertificates + ->setName('queueForCertificates') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Certificate($queue); + }); + +$queueForMigrations + ->setName('queueForMigrations') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Migration($queue); + }); + +$deviceForLocal + ->setName('deviceForLocal') + ->setCallback(function () { + return new Local(); + }); + +$deviceForFiles + ->setName('deviceForFiles') + ->inject('project') + ->setCallback(function ($project) { + return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); + }); + +$deviceForFunctions + ->setName('deviceForFunctions') + ->inject('project') + ->setCallback(function ($project) { + return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()); + }); + +$deviceForBuilds + ->setName('deviceForBuilds') + ->inject('project') + ->setCallback(function ($project) { + return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); + }); + +$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; + }); + +$servers + ->setName('servers') + ->setCallback(function () { + $platforms = Config::getParam('platforms'); + $server = $platforms[APP_PLATFORM_SERVER]; + + $languages = array_map(function ($language) { + return strtolower($language['name']); + }, $server['sdks']); + + return $languages; + }); + +$geodb + ->setName('geodb') + ->inject('registry') + ->setCallback(function (Registry $register) { + return $register->get('geodb'); + }); + +$passwordsDictionary + ->setName('passwordsDictionary') + ->setCallback(function () { + $content = file_get_contents(__DIR__ . '/../assets/security/10k-common-passwords'); + $content = explode("\n", $content); + $content = array_flip($content); + return $content; + }); + +$hooks + ->setName('hooks') + ->inject('registry') + ->setCallback(function (Registry $registry) { + return $registry->get('hooks'); + }); + +$github + ->setName('gitHub') + ->inject('cache') + ->setCallback(function (Cache $cache) { + return new GitHub($cache); + }); + +$requestTimestamp + ->setName('requestTimestamp') + ->inject('request') + ->setCallback(function ($request) { + $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; + }); +$getConsoleDB + ->setName('getConsoleDB') + ->inject('pools') + ->inject('cache') + ->inject('authorization') + ->inject('connections') + ->setCallback(function (array $pools, Cache $cache, Authorization $authorization) { + return function () use ($pools, $cache, $authorization): array { + $pool = $pools['pools-console-console']['pool']; + $dsn = $pools['pools-console-console']['dsn']; + $connection = $pool->get(); + + $adapter = match ($dsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + + $adapter->setDatabase($dsn->getPath()); + + $database = new Database($adapter, $cache); + $database->setAuthorization($authorization); + + $database + ->setNamespace('_console') + ->setMetadata('host', \gethostname()) + ->setMetadata('project', 'console') + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); + + return [$connection, $pool, $database]; + }; + }); + +$getProjectDB + ->setName('getProjectDB') + ->inject('pools') + ->inject('dbForConsole') + ->inject('cache') + ->inject('authorization') + ->inject('connections') + ->setCallback(function (array $pools, Database $dbForConsole, Cache $cache, Authorization $authorization, Connections $connections) { + $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools + + return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases, $authorization, $connections): Database { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForConsole; + } + + 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()]; + + if ($dsn->getHost() === DATABASE_SHARED_TABLES) { + $database + ->setSharedTables(true) + ->setTenant($project->getInternalId()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getInternalId()); + } + + return $database; + } + + $pool = $pools['pools-database-' . $dsn->getHost()]['pool']; + $connectionDsn = $pools['pools-database-' . $dsn->getHost()]['dsn']; + + $connection = $pool->get(); + $connections->add($connection, $pool); + $adapter = match ($connectionDsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + $adapter->setDatabase($connectionDsn->getPath()); + + $database = new Database($adapter, $cache); + $database->setAuthorization($authorization); + + $databases[$dsn->getHost()] = $database; + + if ($dsn->getHost() === DATABASE_SHARED_TABLES) { + $database + ->setSharedTables(true) + ->setTenant($project->getInternalId()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getInternalId()); + } + + return $database; + }; + }); + +$promiseAdapter + ->setName('promiseAdapter') + ->setCallback(function () use ($registry) { + return $registry->get('promiseAdapter'); + }); + +$schemaVariable + ->setName('schemaVariable') + ->setCallback(fn () => new Schema()); + +$schema + ->setName('schema') + ->inject('http') + ->inject('context') + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('authorization') + ->inject('schemaVariable') + ->setCallback(function (Http $http, Container $context, Request $request, Response $response, Database $dbForProject, Authorization $authorization, $schemaVariable) { + $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']}"; + }, + ]; + + $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']); + + 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']); + + return [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'documentId' => $documentId, + 'data' => $args, + 'permissions' => $permissions, + ]; + }, + ]; + + return $schemaVariable->build( + $http, + $request, + $response, + $context, + $complexity, + $attributes, + $urls, + $params, + ); + }); + +$container->set($log); +$container->set($mode); +$container->set($user); +$container->set($plan); +$container->set($cache); +$container->set($pools); +$container->set($queue); +$container->set($geodb); +$container->set($hooks); +$container->set($locale); +$container->set($schema); +$container->set($github); +$container->set($logger); +$container->set($session); +$container->set($console); +$container->set($project); +$container->set($clients); +$container->set($servers); +$container->set($register); +$container->set($connections); +$container->set($localeCodes); +$container->set($dbForProject); +$container->set($dbForConsole); +$container->set($getConsoleDB); +$container->set($getProjectDB); +$container->set($queueForUsage); +$container->set($queueForMails); +$container->set($authorization); +$container->set($authentication); +$container->set($schemaVariable); +$container->set($queueForBuilds); +$container->set($queueForEvents); +$container->set($queueForAudits); +$container->set($deviceForLocal); +$container->set($deviceForFiles); +$container->set($promiseAdapter); +$container->set($queueForDeletes); +$container->set($deviceForBuilds); +$container->set($queueForDatabase); +$container->set($requestTimestamp); +$container->set($queueForMessaging); +$container->set($queueForFunctions); +$container->set($queueForMigrations); +$container->set($deviceForFunctions); +$container->set($passwordsDictionary); +$container->set($queueForCertificates); diff --git a/app/realtime.php b/app/realtime.php index 39e00f7dda..ee6dd4fa81 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -5,136 +5,47 @@ use Appwrite\Extend\Exception; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Network\Validator\Origin; +use Appwrite\Utopia\Queue\Connections; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Swoole\Http\Request as SwooleRequest; +use Swoole\Http\Response as SwooleHttpResponse; use Swoole\Http\Response as SwooleResponse; use Swoole\Runtime; use Swoole\Table; use Swoole\Timer; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\Database\TimeLimit; -use Utopia\App; -use Utopia\Cache\Adapter\Sharding; -use Utopia\Cache\Cache; use Utopia\CLI\Console; -use Utopia\Config\Config; -use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization; -use Utopia\DSN\DSN; +use Utopia\DI\Container; +use Utopia\DI\Dependency; +use Utopia\Http\Adapter\Swoole\Request as UtopiaRequest; +use Utopia\Http\Adapter\Swoole\Response as HttpResponse; +use Utopia\Http\Adapter\Swoole\Response as UtopiaResponse; +use Utopia\Http\Http; use Utopia\Logger\Log; +use Utopia\Pools\Connection; +use Utopia\Registry\Registry; use Utopia\System\System; use Utopia\WebSocket\Adapter; use Utopia\WebSocket\Server; /** - * @var \Utopia\Registry\Registry $register + * @var Registry $registry + * @var Container $container */ +global $registry, $container; + + require_once __DIR__ . '/init.php'; Runtime::enableCoroutine(SWOOLE_HOOK_ALL); -// Allows overriding -if (!function_exists("getConsoleDB")) { - function getConsoleDB(): Database - { - global $register; - - /** @var \Utopia\Pools\Group $pools */ - $pools = $register->get('pools'); - - $dbAdapter = $pools - ->get('console') - ->pop() - ->getResource() - ; - - $database = new Database($dbAdapter, getCache()); - - $database - ->setNamespace('_console') - ->setMetadata('host', \gethostname()) - ->setMetadata('project', '_console'); - - return $database; - } -} - -// Allows overriding -if (!function_exists("getProjectDB")) { - function getProjectDB(Document $project): Database - { - global $register; - - /** @var \Utopia\Pools\Group $pools */ - $pools = $register->get('pools'); - - if ($project->isEmpty() || $project->getId() === 'console') { - return getConsoleDB(); - } - - 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 = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - - $database = new Database($adapter, getCache()); - - if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } - - $database - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()); - - return $database; - } -} - -// Allows overriding -if (!function_exists("getCache")) { - function getCache(): Cache - { - global $register; - - $pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */ - - $list = Config::getParam('pools-cache', []); - $adapters = []; - - foreach ($list as $value) { - $adapters[] = $pools - ->get($value) - ->pop() - ->getResource() - ; - } - - return new Cache(new Sharding($adapters)); - } -} - $realtime = new Realtime(); /** @@ -159,8 +70,8 @@ $adapter $server = new Server($adapter); -$logError = function (Throwable $error, string $action) use ($register) { - $logger = $register->get('logger'); +$logError = function (Throwable $error, string $action) use ($registry) { + $logger = $registry->get('logger'); if ($logger && !$error instanceof Exception) { $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); @@ -200,16 +111,16 @@ $logError = function (Throwable $error, string $action) use ($register) { $server->error($logError); -$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) { +$server->onStart(function () use ($stats, $container, $containerId, &$statsDocument, $logError) { sleep(5); // wait for the initial database schema to be ready Console::success('Server started successfully'); - + $authorization = $container->get('authorization'); /** * Create document for this worker to share stats across Containers. */ - go(function () use ($register, $containerId, &$statsDocument) { + go(function () use ($container, $containerId, &$statsDocument) { $attempts = 0; - $database = getConsoleDB(); + $database = $container->get('dbForConsole'); do { try { @@ -223,14 +134,15 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume 'value' => '{}' ]); - $statsDocument = Authorization::skip(fn () => $database->createDocument('realtime', $document)); + $authorization = $container->get('authorization'); + $statsDocument = $authorization->skip(fn () => $database->createDocument('realtime', $document)); break; } catch (Throwable) { Console::warning("Collection not ready. Retrying connection ({$attempts})..."); sleep(DATABASE_RECONNECT_SLEEP); } } while (true); - $register->get('pools')->reclaim(); + ($container->get('connections'))->reclaim(); }); /** @@ -238,7 +150,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume */ // TODO: Remove this if check once it doesn't cause issues for cloud if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') { - Timer::tick(5000, function () use ($register, $stats, &$statsDocument, $logError) { + Timer::tick(5000, function () use ($container, $stats, &$statsDocument, $logError, $authorization) { $payload = []; foreach ($stats as $projectId => $value) { $payload[$projectId] = $stats->get($projectId, 'connectionsTotal'); @@ -248,40 +160,43 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume } try { - $database = getConsoleDB(); + $database = $container->get('dbForConsole'); $statsDocument ->setAttribute('timestamp', DateTime::now()) ->setAttribute('value', json_encode($payload)); - Authorization::skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument)); + $authorization->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument)); } catch (Throwable $th) { call_user_func($logError, $th, "updateWorkerDocument"); } finally { - $register->get('pools')->reclaim(); + ($container->get('connections'))->reclaim(); + $container->refresh('dbForConsole'); } }); } }); -$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) { +$server->onWorkerStart(function (int $workerId) use ($server, $container, $stats, $realtime, $logError) { Console::success('Worker ' . $workerId . ' started successfully'); $attempts = 0; $start = time(); - Timer::tick(5000, function () use ($server, $register, $realtime, $stats, $logError) { + $authorization = $container->get('authorization'); + + Timer::tick(5000, function () use ($server, $container, $realtime, $stats, $logError, $authorization) { /** * Sending current connections to project channels on the console project every 5 seconds. */ // TODO: Remove this if check once it doesn't cause issues for cloud if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') { if ($realtime->hasSubscriber('console', Role::users()->toString(), 'project')) { - $database = getConsoleDB(); + $database = $container->get('dbForConsole'); $payload = []; - $list = Authorization::skip(fn () => $database->find('realtime', [ + $list = $authorization->skip(fn () => $database->find('realtime', [ Query::greaterThan('timestamp', DateTime::addSeconds(new \DateTime(), -15)), ])); @@ -321,8 +236,8 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, 'data' => $event['data'] ])); } - - $register->get('pools')->reclaim(); + ($container->get('connections'))->reclaim(); + $container->refresh('dbForConsole'); } } /** @@ -352,13 +267,26 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, while ($attempts < 300) { try { if ($attempts > 0) { - Console::error('Pub/sub connection lost (lasted ' . (time() - $start) . ' seconds, worker: ' . $workerId . '). - Attempting restart in 5 seconds (attempt #' . $attempts . ')'); + Console::error( + 'Pub/sub connection lost (lasted ' . (time() - $start) . ' seconds, worker: ' . $workerId . '). + Attempting restart in 5 seconds (attempt #' . $attempts . ')' + ); sleep(5); // 5 sec delay between connection attempts } + $start = time(); - $redis = $register->get('pools')->get('pubsub')->pop()->getResource(); /** @var Redis $redis */ + $pools = $container->get('pools'); + $pool = $pools['pools-pubsub-pubsub']['pool']; + + /** @var Connections $connections */ + $connections = $container->get('connections'); + $connection = $pool->get(); + $connections->add($connection, $pool); + + $redis = $connection; + + /** @var Redis $redis */ $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); if ($redis->ping(true)) { @@ -368,7 +296,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, Console::error('Pub/sub failed (worker: ' . $workerId . ')'); } - $redis->subscribe(['realtime'], function (Redis $redis, string $channel, string $payload) use ($server, $workerId, $stats, $register, $realtime) { + $redis->subscribe(['realtime'], function (Redis $redis, string $channel, string $payload) use ($server, $workerId, $stats, $realtime, $authorization, $container) { $event = json_decode($payload, true); if ($event['permissionsChanged'] && isset($event['userId'])) { @@ -377,25 +305,24 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) { $connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId])); - $consoleDatabase = getConsoleDB(); - $project = Authorization::skip(fn () => $consoleDatabase->getDocument('projects', $projectId)); - $database = getProjectDB($project); + $dbForConsole = $container->get('dbForConsole'); - $user = $database->getDocument('users', $userId); + $project = $authorization->skip(fn () => $dbForConsole->getDocument('projects', $projectId)); + $dbForProject = $container->get('getProjectDB')($project); - $roles = Auth::getRoles($user); + $user = $dbForProject->getDocument('users', $userId); + + $roles = Auth::getRoles($user, $authorization); $channels = $realtime->connections[$connection]['channels']; $realtime->unsubscribe($connection); $realtime->subscribe($projectId, $connection, $roles, $channels); - - $register->get('pools')->reclaim(); } } $receivers = $realtime->getSubscribers($event); - if (App::isDevelopment() && !empty($receivers)) { + if (Http::isDevelopment() && !empty($receivers)) { Console::log("[Debug][Worker {$workerId}] Receivers: " . count($receivers)); Console::log("[Debug][Worker {$workerId}] Receivers Connection IDs: " . json_encode($receivers)); Console::log("[Debug][Worker {$workerId}] Event: " . $payload); @@ -421,30 +348,40 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, sleep(DATABASE_RECONNECT_SLEEP); continue; } finally { - $register->get('pools')->reclaim(); + ($container->get('connections'))->reclaim(); + $container->refresh('dbForConsole'); } } Console::error('Failed to restart pub/sub...'); }); -$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $logError) { - $app = new App('UTC'); - $request = new Request($request); - $response = new Response(new SwooleResponse()); +$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $container, $stats, &$realtime, $logError) { + $authorization = $container->get('authorization'); + + $request = new Request(new UtopiaRequest($request)); + $response = new Response(new UtopiaResponse(new SwooleResponse())); + + $requestInjection = new Dependency(); + $responseInjection = new Dependency(); + + $requestInjection->setName('request')->setCallback(fn () => $request); + $responseInjection->setName('response')->setCallback(fn () => $response); + + $container->set($requestInjection); + $container->set($responseInjection); Console::info("Connection open (user: {$connection})"); - App::setResource('pools', fn () => $register->get('pools')); - App::setResource('request', fn () => $request); - App::setResource('response', fn () => $response); - try { + /** @var Document $project */ - $project = $app->getResource('project'); + $project = $container->refresh('project')->get('project'); + + $container->refresh('dbForProject'); /* - * Project Check + * Project Check */ if (empty($project->getId())) { throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing or unknown project ID'); @@ -453,15 +390,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, if ( array_key_exists('realtime', $project->getAttribute('apis', [])) && !$project->getAttribute('apis', [])['realtime'] - && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + && !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } - $dbForProject = getProjectDB($project); - $console = $app->getResource('console'); /** @var Document $console */ - $user = $app->getResource('user'); /** @var Document $user */ - + $dbForProject = $container->get('getProjectDB')($project); + /** @var Document $console */ + $console = $container->get('console'); + /** @var Document $user */ + $user = $container->refresh('user')->get('user'); /* * Abuse Check * @@ -490,7 +428,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription()); } - $roles = Auth::getRoles($user); + $authorization = $container->get('authorization'); + $roles = Auth::getRoles($user, $authorization); $channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId()); @@ -539,25 +478,33 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $server->send([$connection], json_encode($response)); $server->close($connection, $code); - if (App::isDevelopment()) { + if (Http::isDevelopment()) { Console::error('[Error] Connection Error'); Console::error('[Error] Code: ' . $response['data']['code']); Console::error('[Error] Message: ' . $response['data']['message']); } } finally { - $register->get('pools')->reclaim(); + $connections = $container->get('connections'); + $connections->reclaim(); } }); -$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) { +$server->onWorkerStop(function (int $workerId) use ($container) { + $connections = $container->get('connections'); + $connections->reclaim(); +}); + +$server->onMessage(function (int $connection, string $message) use ($server, $container, $realtime, $containerId) { try { - $response = new Response(new SwooleResponse()); + $response = new Response(new HttpResponse(new SwooleHttpResponse())); $projectId = $realtime->connections[$connection]['projectId']; - $database = getConsoleDB(); + $database = $container->get('dbForConsole'); + $authorization = $container->get('authorization'); + $authentication = $container->get('authentication'); if ($projectId !== 'console') { - $project = Authorization::skip(fn () => $database->getDocument('projects', $projectId)); - $database = getProjectDB($project); + $project = $authorization->skip(fn () => $database->getDocument('projects', $projectId)); + $database = $container->get('getProjectDB')($project); } else { $project = null; } @@ -595,20 +542,21 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re } $session = Auth::decodeSession($message['data']['session']); - Auth::$unique = $session['id'] ?? ''; - Auth::$secret = $session['secret'] ?? ''; - $user = $database->getDocument('users', Auth::$unique); + $authentication->setUnique($session['id'] ?? ''); + $authentication->setSecret($session['secret'] ?? ''); + + $user = $database->getDocument('users', $authentication->getUnique()); if ( empty($user->getId()) // Check a document has been found in the DB - || !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) // Validate user has valid login token + || !Auth::sessionVerify($user->getAttribute('sessions', []), $authentication->getSecret()) // Validate user has valid login token ) { // cookie not valid throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.'); } - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $authorization); $channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId()); $realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels); @@ -642,7 +590,8 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re $server->close($connection, $th->getCode()); } } finally { - $register->get('pools')->reclaim(); + ($container->get('connections'))->reclaim(); + $container->refresh('dbForConsole'); } }); diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index ad35135a6f..4365bf39fc 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -11,7 +11,8 @@ $httpsPort = $this->getParam('httpsPort', ''); $version = $this->getParam('version', ''); $organization = $this->getParam('organization', ''); $image = $this->getParam('image', ''); -?>services: +?> +services: traefik: image: traefik:2.11 container_name: appwrite-traefik @@ -853,7 +854,7 @@ $image = $this->getParam('image', ''); - MYSQL_USER=${_APP_DB_USER} - MYSQL_PASSWORD=${_APP_DB_PASS} - MARIADB_AUTO_UPGRADE=1 - command: 'mysqld --innodb-flush-method=fsync' + command: 'mysqld --innodb-flush-method=fsync --max_connections=5000' redis: image: redis:7.2.4-alpine diff --git a/app/worker.php b/app/worker.php index 6078a0127c..55e14280f6 100644 --- a/app/worker.php +++ b/app/worker.php @@ -2,278 +2,107 @@ require_once __DIR__ . '/init.php'; -use Appwrite\Event\Audit; -use Appwrite\Event\Build; -use Appwrite\Event\Certificate; -use Appwrite\Event\Database as EventDatabase; -use Appwrite\Event\Delete; -use Appwrite\Event\Event; -use Appwrite\Event\Func; -use Appwrite\Event\Mail; -use Appwrite\Event\Messaging; -use Appwrite\Event\Migration; -use Appwrite\Event\Usage; use Appwrite\Event\UsageDump; use Appwrite\Platform\Appwrite; +use Appwrite\Utopia\Queue\Connections; use Swoole\Runtime; -use Utopia\App; -use Utopia\Cache\Adapter\Sharding; -use Utopia\Cache\Cache; use Utopia\CLI\Console; -use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; -use Utopia\DSN\DSN; +use Utopia\DI\Dependency; use Utopia\Logger\Log; use Utopia\Logger\Logger; use Utopia\Platform\Service; -use Utopia\Pools\Group; use Utopia\Queue\Connection; use Utopia\Queue\Message; -use Utopia\Queue\Server; -use Utopia\Registry\Registry; +use Utopia\Queue\Worker; +use Utopia\Storage\Device\Local; use Utopia\System\System; -Authorization::disable(); +global $registry, $container; + Runtime::enableCoroutine(SWOOLE_HOOK_ALL); -Server::setResource('register', fn () => $register); +$project = new Dependency(); +$register = new Dependency(); +$dbForProject = new Dependency(); +$abuseRetention = new Dependency(); +$deviceForCache = new Dependency(); +$auditRetention = new Dependency(); +$queueForUsageDump = new Dependency(); +$executionRetention = new Dependency(); +$deviceForLocalFiles = new Dependency(); -Server::setResource('dbForConsole', function (Cache $cache, Registry $register) { - $pools = $register->get('pools'); - $database = $pools - ->get('console') - ->pop() - ->getResource(); +$register + ->setName('register') + ->setCallback(fn () => $registry); - $adapter = new Database($database, $cache); - $adapter->setNamespace('_console'); +$project + ->setName('project') + ->inject('message') + ->inject('dbForConsole') + ->setCallback(function (Message $message, Database $dbForConsole) { + $payload = $message->getPayload() ?? []; + $project = new Document($payload['project'] ?? []); - return $adapter; -}, ['cache', 'register']); - -Server::setResource('project', function (Message $message, Database $dbForConsole) { - $payload = $message->getPayload() ?? []; - $project = new Document($payload['project'] ?? []); - - if ($project->getId() === 'console') { - return $project; - } - - return $dbForConsole->getDocument('projects', $project->getId()); -}, ['message', 'dbForConsole']); - -Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForConsole) { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForConsole; - } - - $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 = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - - $database = new Database($adapter, $cache); - - 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 ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } - - return $database; -}, ['cache', 'register', 'message', 'project', 'dbForConsole']); - -Server::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) { - $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools - - return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases): Database { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForConsole; + if ($project->getId() === 'console') { + return $project; } - 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')); - } + return $dbForConsole->getDocument('projects', $project->getId()); + }); - if (isset($databases[$dsn->getHost()])) { - $database = $databases[$dsn->getHost()]; +$abuseRetention + ->setName('abuseRetention') + ->setCallback(function () { + return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400)); + }); - if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } +$auditRetention + ->setName('auditRetention') + ->setCallback(function () { + return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); + }); - return $database; - } +$executionRetention + ->setName('executionRetention') + ->setCallback(function () { + return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); + }); - $dbAdapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); +$queueForUsageDump + ->setName('queueForUsageDump') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new UsageDump($queue); + }); - $database = new Database($dbAdapter, $cache); +$deviceForCache + ->setName('deviceForCache') + ->inject('project') + ->setCallback(function (Document $project) { + return getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()); + }); - $databases[$dsn->getHost()] = $database; +$deviceForLocalFiles + ->setName('deviceForLocalFiles') + ->inject('project') + ->setCallback(function (Document $project) { + return new Local(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); + }); - if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } +$container->set($project); +$container->set($register); +$container->set($dbForProject); +$container->set($abuseRetention); +$container->set($auditRetention); +$container->set($deviceForCache); +$container->set($queueForUsageDump); +$container->set($executionRetention); +$container->set($deviceForLocalFiles); - return $database; - }; -}, ['pools', 'dbForConsole', 'cache']); - -Server::setResource('abuseRetention', function () { - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400)); -}); - -Server::setResource('auditRetention', function () { - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); -}); - -Server::setResource('executionRetention', function () { - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); -}); - -Server::setResource('cache', function (Registry $register) { - $pools = $register->get('pools'); - $list = Config::getParam('pools-cache', []); - $adapters = []; - - foreach ($list as $value) { - $adapters[] = $pools - ->get($value) - ->pop() - ->getResource() - ; - } - - return new Cache(new Sharding($adapters)); -}, ['register']); - -Server::setResource('log', fn () => new Log()); - -Server::setResource('queueForUsage', function (Connection $queue) { - return new Usage($queue); -}, ['queue']); - -Server::setResource('queueForUsageDump', function (Connection $queue) { - return new UsageDump($queue); -}, ['queue']); - -Server::setResource('queue', function (Group $pools) { - return $pools->get('queue')->pop()->getResource(); -}, ['pools']); - -Server::setResource('queueForDatabase', function (Connection $queue) { - return new EventDatabase($queue); -}, ['queue']); - -Server::setResource('queueForMessaging', function (Connection $queue) { - return new Messaging($queue); -}, ['queue']); - -Server::setResource('queueForMails', function (Connection $queue) { - return new Mail($queue); -}, ['queue']); - -Server::setResource('queueForBuilds', function (Connection $queue) { - return new Build($queue); -}, ['queue']); - -Server::setResource('queueForDeletes', function (Connection $queue) { - return new Delete($queue); -}, ['queue']); - -Server::setResource('queueForEvents', function (Connection $queue) { - return new Event($queue); -}, ['queue']); - -Server::setResource('queueForAudits', function (Connection $queue) { - return new Audit($queue); -}, ['queue']); - -Server::setResource('queueForFunctions', function (Connection $queue) { - return new Func($queue); -}, ['queue']); - -Server::setResource('queueForCertificates', function (Connection $queue) { - return new Certificate($queue); -}, ['queue']); - -Server::setResource('queueForMigrations', function (Connection $queue) { - return new Migration($queue); -}, ['queue']); - -Server::setResource('logger', function (Registry $register) { - return $register->get('logger'); -}, ['register']); - -Server::setResource('pools', function (Registry $register) { - return $register->get('pools'); -}, ['register']); - -Server::setResource('deviceForFunctions', function (Document $project) { - return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()); -}, ['project']); - -Server::setResource('deviceForFiles', function (Document $project) { - return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); -}, ['project']); - -Server::setResource('deviceForBuilds', function (Document $project) { - return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); -}, ['project']); - -Server::setResource('deviceForCache', function (Document $project) { - return getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()); -}, ['project']); - - -$pools = $register->get('pools'); $platform = new Appwrite(); $args = $platform->getEnv('argv'); @@ -292,6 +121,13 @@ if (\str_starts_with($workerName, 'databases')) { } try { + $connection = new Connection\Redis( + System::getEnv('_APP_REDIS_HOST', 'redis'), + System::getEnv('_APP_REDIS_PORT', '6379'), + System::getEnv('_APP_REDIS_USER', ''), + System::getEnv('_APP_REDIS_PASS', '') + ); + /** * Any worker can be configured with the following env vars: * - _APP_WORKERS_NUM The total number of worker processes @@ -300,32 +136,35 @@ try { */ $platform->init(Service::TYPE_WORKER, [ 'workersNum' => System::getEnv('_APP_WORKERS_NUM', 1), - 'connection' => $pools->get('queue')->pop()->getResource(), + 'connection' => $connection, 'workerName' => strtolower($workerName) ?? null, 'queueName' => $queueName ]); } catch (\Throwable $e) { - Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine()); + Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine()); } -$worker = $platform->getWorker(); - -$worker - ->shutdown() - ->inject('pools') - ->action(function (Group $pools) { - $pools->reclaim(); +Worker::init() + ->inject('authorization') + ->action(function (Authorization $authorization) { + $authorization->disable(); }); -$worker - ->error() +Worker::shutdown() + ->inject('connections') + ->action(function (Connections $connections) { + $connections->reclaim(); + }); + +Worker::error() ->inject('error') ->inject('logger') ->inject('log') - ->inject('pools') + ->inject('connections') ->inject('project') - ->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project) use ($queueName) { - $pools->reclaim(); + ->inject('authorization') + ->action(function (Throwable $error, ?Logger $logger, Log $log, Connections $connections, Document $project, Authorization $authorization) use ($queueName) { + $connections->reclaim(); $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); if ($logger) { @@ -341,7 +180,7 @@ $worker $log->addExtra('file', $error->getFile()); $log->addExtra('line', $error->getLine()); $log->addExtra('trace', $error->getTraceAsString()); - $log->addExtra('roles', Authorization::getRoles()); + $log->addExtra('roles', $authorization->getRoles()); $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); @@ -360,9 +199,7 @@ $worker Console::error('[Error] Line: ' . $error->getLine()); }); -$worker->workerStart() - ->action(function () use ($workerName) { - Console::info("Worker $workerName started"); - }); - -$worker->start(); +$platform + ->getWorker() + ->setContainer($container) + ->start(); diff --git a/composer.json b/composer.json index 26b8c3a7a3..e23806adc9 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "description": "End to end backend server for frontend and mobile apps.", "type": "project", "license": "BSD-3-Clause", + "minimum-stability": "stable", "authors": [ { "name": "Eldad Fux", @@ -14,7 +15,8 @@ "test": "vendor/bin/phpunit", "lint": "vendor/bin/pint --test", "format": "vendor/bin/pint", - "bench": "vendor/bin/phpbench run --report=benchmark" + "bench": "vendor/bin/phpbench run --report=benchmark", + "check": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit 1G app src tests" }, "autoload": { "psr-4": { @@ -45,33 +47,33 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.15.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "0.43.0", - "utopia-php/analytics": "0.10.*", - "utopia-php/audit": "0.43.0", + "utopia-php/abuse": "0.44.*", + "utopia-php/analytics": "0.13.*", + "utopia-php/audit": "0.44.*", "utopia-php/cache": "0.10.*", - "utopia-php/cli": "0.15.*", + "utopia-php/cli": "0.19.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.53.5-rc1", - "utopia-php/domains": "0.5.*", - "utopia-php/dsn": "0.2.1", - "utopia-php/framework": "0.33.*", + "utopia-php/database": "0.54.*", + "utopia-php/domains": "0.6.*", + "utopia-php/dsn": "0.2.*", + "utopia-php/framework": "1.0.*", "utopia-php/fetch": "0.2.*", "utopia-php/image": "0.6.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.12.*", - "utopia-php/migration": "0.5.*", - "utopia-php/orchestration": "0.9.*", - "utopia-php/platform": "0.7.*", + "utopia-php/migration": "0.4.*", + "utopia-php/orchestration": "0.15.*", + "utopia-php/platform": "0.8.*", + "utopia-php/view": "0.2.*", "utopia-php/pools": "0.5.*", "utopia-php/preloader": "0.2.*", - "utopia-php/queue": "0.7.*", + "utopia-php/queue": "0.8.*", "utopia-php/registry": "0.5.*", - "utopia-php/storage": "0.18.*", - "utopia-php/swoole": "0.8.*", + "utopia-php/storage": "0.19.*", "utopia-php/system": "0.8.*", - "utopia-php/vcs": "0.8.*", - "utopia-php/websocket": "0.1.*", + "utopia-php/vcs": "0.9.*", + "utopia-php/websocket": "0.2.*", "matomo/device-detector": "6.1.*", "dragonmantank/cron-expression": "3.3.2", "phpmailer/phpmailer": "6.9.1", @@ -88,7 +90,8 @@ "swoole/ide-helper": "5.1.2", "textalk/websocket": "1.5.7", "laravel/pint": "^1.14", - "phpbench/phpbench": "^1.2" + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "1.8.*" }, "provide": { "ext-phpiredis": "*" @@ -97,11 +100,5 @@ "platform": { "php": "8.3" } - }, - "repositories": { - "utopia-php/database": { - "type": "vcs", - "url": "https://github.com/utopia-php/database" - } } } diff --git a/composer.lock b/composer.lock index b3a94545b4..cff13d28a1 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": "7066d9ca32e7a1a60614effdc4701970", + "content-hash": "6017f815da50b7d4dabad66386e013e3", "packages": [ { "name": "adhocore/jwt", @@ -1429,16 +1429,16 @@ }, { "name": "utopia-php/abuse", - "version": "0.43.0", + "version": "0.44.0", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "6346a3b4c5177a43160035a7289e30fdfb0790d6" + "reference": "cd37bba23778f3a0dc54b4ba6ffb36d4d91ed8f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/6346a3b4c5177a43160035a7289e30fdfb0790d6", - "reference": "6346a3b4c5177a43160035a7289e30fdfb0790d6", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/cd37bba23778f3a0dc54b4ba6ffb36d4d91ed8f1", + "reference": "cd37bba23778f3a0dc54b4ba6ffb36d4d91ed8f1", "shasum": "" }, "require": { @@ -1446,7 +1446,7 @@ "ext-pdo": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "0.53.*" + "utopia-php/database": "0.54.*" }, "require-dev": { "laravel/pint": "1.5.*", @@ -1474,27 +1474,28 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/0.43.0" + "source": "https://github.com/utopia-php/abuse/tree/0.44.0" }, - "time": "2024-08-30T05:17:23+00:00" + "time": "2024-09-05T16:09:32+00:00" }, { "name": "utopia-php/analytics", - "version": "0.10.2", + "version": "0.13.0", "source": { "type": "git", "url": "https://github.com/utopia-php/analytics.git", - "reference": "14c805114736f44c26d6d24b176a2f8b93d86a1f" + "reference": "3dace02af5d4190623f88fb6e02f5559a99f14c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/analytics/zipball/14c805114736f44c26d6d24b176a2f8b93d86a1f", - "reference": "14c805114736f44c26d6d24b176a2f8b93d86a1f", + "url": "https://api.github.com/repos/utopia-php/analytics/zipball/3dace02af5d4190623f88fb6e02f5559a99f14c4", + "reference": "3dace02af5d4190623f88fb6e02f5559a99f14c4", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/cli": "^0.15.0" + "utopia-php/cli": "0.19.*", + "utopia-php/system": "0.8.*" }, "require-dev": { "laravel/pint": "dev-main", @@ -1520,27 +1521,27 @@ ], "support": { "issues": "https://github.com/utopia-php/analytics/issues", - "source": "https://github.com/utopia-php/analytics/tree/0.10.2" + "source": "https://github.com/utopia-php/analytics/tree/0.13.0" }, - "time": "2023-03-22T12:01:09+00:00" + "time": "2024-09-05T16:19:26+00:00" }, { "name": "utopia-php/audit", - "version": "0.43.0", + "version": "0.44.0", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e" + "reference": "69eee24e4d6cb8fdae31235d80b9a46b18092139" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e", - "reference": "cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/69eee24e4d6cb8fdae31235d80b9a46b18092139", + "reference": "69eee24e4d6cb8fdae31235d80b9a46b18092139", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/database": "0.53.*" + "utopia-php/database": "0.54.*" }, "require-dev": { "laravel/pint": "1.5.*", @@ -1567,9 +1568,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/0.43.0" + "source": "https://github.com/utopia-php/audit/tree/0.44.0" }, - "time": "2024-08-30T05:17:36+00:00" + "time": "2024-09-05T16:12:41+00:00" }, { "name": "utopia-php/cache", @@ -1623,27 +1624,29 @@ }, { "name": "utopia-php/cli", - "version": "0.15.0", + "version": "0.19.0", "source": { "type": "git", "url": "https://github.com/utopia-php/cli.git", - "reference": "ccb7c8125ffe0254fef8f25744bfa376eb7bd0ea" + "reference": "f8af1d6087f498bc1f0191750a118d357ded9948" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cli/zipball/ccb7c8125ffe0254fef8f25744bfa376eb7bd0ea", - "reference": "ccb7c8125ffe0254fef8f25744bfa376eb7bd0ea", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/f8af1d6087f498bc1f0191750a118d357ded9948", + "reference": "f8af1d6087f498bc1f0191750a118d357ded9948", "shasum": "" }, "require": { "php": ">=7.4", - "utopia-php/framework": "0.*.*" + "utopia-php/di": "0.1.*", + "utopia-php/framework": "1.0.*" }, "require-dev": { "laravel/pint": "1.2.*", + "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.3", "squizlabs/php_codesniffer": "^3.6", - "vimeo/psalm": "4.0.1" + "swoole/ide-helper": "4.8.8" }, "type": "library", "autoload": { @@ -1666,9 +1669,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cli/issues", - "source": "https://github.com/utopia-php/cli/tree/0.15.0" + "source": "https://github.com/utopia-php/cli/tree/0.19.0" }, - "time": "2023-03-01T05:55:14+00:00" + "time": "2024-09-05T15:46:56+00:00" }, { "name": "utopia-php/config", @@ -1723,16 +1726,16 @@ }, { "name": "utopia-php/database", - "version": "0.53.5-rc1", + "version": "0.54.1", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "689ba22063bf46def385da8695ba7a921e81e38d" + "reference": "c32d6eab5992c927cbf6fb4aad51d76fc5f64946" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/689ba22063bf46def385da8695ba7a921e81e38d", - "reference": "689ba22063bf46def385da8695ba7a921e81e38d", + "url": "https://api.github.com/repos/utopia-php/database/zipball/c32d6eab5992c927cbf6fb4aad51d76fc5f64946", + "reference": "c32d6eab5992c927cbf6fb4aad51d76fc5f64946", "shasum": "" }, "require": { @@ -1740,7 +1743,7 @@ "ext-pdo": "*", "php": ">=8.0", "utopia-php/cache": "0.10.*", - "utopia-php/framework": "0.33.*", + "utopia-php/framework": "1.0.*", "utopia-php/mongo": "0.3.*" }, "require-dev": { @@ -1751,7 +1754,7 @@ "phpunit/phpunit": "9.6.*", "rregeer/phpunit-coverage-check": "0.3.*", "swoole/ide-helper": "5.1.3", - "utopia-php/cli": "0.14.*" + "utopia-php/cli": "0.19.*" }, "type": "library", "autoload": { @@ -1759,38 +1762,7 @@ "Utopia\\Database\\": "src/Database" } }, - "autoload-dev": { - "psr-4": { - "Tests\\E2E\\": "tests/e2e", - "Tests\\Unit\\": "tests/unit" - } - }, - "scripts": { - "build": [ - "Composer\\Config::disableProcessTimeout", - "docker compose build" - ], - "start": [ - "Composer\\Config::disableProcessTimeout", - "docker compose up -d" - ], - "test": [ - "Composer\\Config::disableProcessTimeout", - "docker compose exec tests vendor/bin/phpunit --configuration phpunit.xml" - ], - "lint": [ - "./vendor/bin/pint --test" - ], - "format": [ - "./vendor/bin/pint" - ], - "check": [ - "./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 512M" - ], - "coverage": [ - "./vendor/bin/coverage-check ./tmp/clover.xml 90" - ] - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -1803,31 +1775,80 @@ "utopia" ], "support": { - "source": "https://github.com/utopia-php/database/tree/0.53.5-rc1", - "issues": "https://github.com/utopia-php/database/issues" + "issues": "https://github.com/utopia-php/database/issues", + "source": "https://github.com/utopia-php/database/tree/0.54.1" }, - "time": "2024-09-24T08:43:10+00:00" + "time": "2024-09-10T10:08:37+00:00" }, { - "name": "utopia-php/domains", - "version": "0.5.0", + "name": "utopia-php/di", + "version": "0.1.0", "source": { "type": "git", - "url": "https://github.com/utopia-php/domains.git", - "reference": "bf07f60326f8389f378ddf6fcde86217e5cfe18c" + "url": "https://github.com/utopia-php/di.git", + "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/domains/zipball/bf07f60326f8389f378ddf6fcde86217e5cfe18c", - "reference": "bf07f60326f8389f378ddf6fcde86217e5cfe18c", + "url": "https://api.github.com/repos/utopia-php/di/zipball/22490c95f7ac3898ed1c33f1b1b5dd577305ee31", + "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "laravel/pint": "^1.2", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.25", + "swoole/ide-helper": "4.8.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/", + "Tests\\E2E\\": "tests/e2e" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple and lite library for managing dependency injections", + "keywords": [ + "framework", + "http", + "php", + "upf" + ], + "support": { + "issues": "https://github.com/utopia-php/di/issues", + "source": "https://github.com/utopia-php/di/tree/0.1.0" + }, + "time": "2024-08-08T14:35:19+00:00" + }, + { + "name": "utopia-php/domains", + "version": "0.6.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/domains.git", + "reference": "5c70b0f524deeb1fccc3962ad1da650ae2c94cda" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/domains/zipball/5c70b0f524deeb1fccc3962ad1da650ae2c94cda", + "reference": "5c70b0f524deeb1fccc3962ad1da650ae2c94cda", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/framework": "0.*.*" + "utopia-php/framework": "1.0.*" }, "require-dev": { "laravel/pint": "1.2.*", + "phpstan/phpstan": "1.9.x-dev", "phpunit/phpunit": "^9.3" }, "type": "library", @@ -1864,9 +1885,9 @@ ], "support": { "issues": "https://github.com/utopia-php/domains/issues", - "source": "https://github.com/utopia-php/domains/tree/0.5.0" + "source": "https://github.com/utopia-php/domains/tree/0.6.0" }, - "time": "2024-01-03T22:04:27+00:00" + "time": "2024-09-05T16:21:11+00:00" }, { "name": "utopia-php/dsn", @@ -1956,26 +1977,30 @@ }, { "name": "utopia-php/framework", - "version": "0.33.8", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5" + "reference": "fc63ec61c720190a5ea5bb484c615145850951e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/a7f577540a25cb90896fef2b64767bf8d700f3c5", - "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5", + "url": "https://api.github.com/repos/utopia-php/http/zipball/fc63ec61c720190a5ea5bb484c615145850951e6", + "reference": "fc63ec61c720190a5ea5bb484c615145850951e6", "shasum": "" }, "require": { - "php": ">=8.0" + "ext-swoole": "*", + "php": ">=8.0", + "utopia-php/servers": "0.1.*" }, "require-dev": { + "ext-xdebug": "*", "laravel/pint": "^1.2", "phpbench/phpbench": "^1.2", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.5.25" + "phpunit/phpunit": "^9.5.25", + "swoole/ide-helper": "4.8.3" }, "type": "library", "autoload": { @@ -1987,17 +2012,18 @@ "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.8" + "source": "https://github.com/utopia-php/http/tree/1.0.2" }, - "time": "2024-08-15T14:10:09+00:00" + "time": "2024-09-10T09:04:19+00:00" }, { "name": "utopia-php/image", @@ -2100,16 +2126,16 @@ }, { "name": "utopia-php/logger", - "version": "0.6.1", + "version": "0.6.0", "source": { "type": "git", "url": "https://github.com/utopia-php/logger.git", - "reference": "7e8ff512c6f04577aba1df67c7b9628971946f9c" + "reference": "a2d1daeeb8f61fdec6d851950d9a021a3d05c9f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/logger/zipball/7e8ff512c6f04577aba1df67c7b9628971946f9c", - "reference": "7e8ff512c6f04577aba1df67c7b9628971946f9c", + "url": "https://api.github.com/repos/utopia-php/logger/zipball/a2d1daeeb8f61fdec6d851950d9a021a3d05c9f9", + "reference": "a2d1daeeb8f61fdec6d851950d9a021a3d05c9f9", "shasum": "" }, "require": { @@ -2148,9 +2174,9 @@ ], "support": { "issues": "https://github.com/utopia-php/logger/issues", - "source": "https://github.com/utopia-php/logger/tree/0.6.1" + "source": "https://github.com/utopia-php/logger/tree/0.6.0" }, - "time": "2024-09-20T14:02:12+00:00" + "time": "2024-05-23T13:37:54+00:00" }, { "name": "utopia-php/messaging", @@ -2205,16 +2231,16 @@ }, { "name": "utopia-php/migration", - "version": "0.5.2", + "version": "0.4.4", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "f18d44d4459f78c292dac9edde856fd156fe497a" + "reference": "a8a5d392bebf082faf289f4dfe09d9fd76844c33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/f18d44d4459f78c292dac9edde856fd156fe497a", - "reference": "f18d44d4459f78c292dac9edde856fd156fe497a", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/a8a5d392bebf082faf289f4dfe09d9fd76844c33", + "reference": "a8a5d392bebf082faf289f4dfe09d9fd76844c33", "shasum": "" }, "require": { @@ -2224,7 +2250,6 @@ "require-dev": { "laravel/pint": "1.*", "phpunit/phpunit": "9.*", - "utopia-php/cli": "^0.18.0", "vlucas/phpdotenv": "5.*" }, "type": "library", @@ -2247,9 +2272,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.5.2" + "source": "https://github.com/utopia-php/migration/tree/0.4.4" }, - "time": "2024-07-22T09:27:07+00:00" + "time": "2024-05-17T05:25:31+00:00" }, { "name": "utopia-php/mongo", @@ -2313,26 +2338,26 @@ }, { "name": "utopia-php/orchestration", - "version": "0.9.1", + "version": "0.15.0", "source": { "type": "git", "url": "https://github.com/utopia-php/orchestration.git", - "reference": "55f43513b3f940a3f4f9c2cde7682d0c2581beb0" + "reference": "cd55650ba5f13118c3580048e6dd86b604f9a5b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/orchestration/zipball/55f43513b3f940a3f4f9c2cde7682d0c2581beb0", - "reference": "55f43513b3f940a3f4f9c2cde7682d0c2581beb0", + "url": "https://api.github.com/repos/utopia-php/orchestration/zipball/cd55650ba5f13118c3580048e6dd86b604f9a5b3", + "reference": "cd55650ba5f13118c3580048e6dd86b604f9a5b3", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/cli": "0.15.*" + "utopia-php/cli": "0.19.*" }, "require-dev": { "laravel/pint": "^1.2", - "phpunit/phpunit": "^9.3", - "vimeo/psalm": "4.0.1" + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.3" }, "type": "library", "autoload": { @@ -2357,31 +2382,32 @@ ], "support": { "issues": "https://github.com/utopia-php/orchestration/issues", - "source": "https://github.com/utopia-php/orchestration/tree/0.9.1" + "source": "https://github.com/utopia-php/orchestration/tree/0.15.0" }, - "time": "2023-03-17T15:05:06+00:00" + "time": "2024-09-05T16:28:02+00:00" }, { "name": "utopia-php/platform", - "version": "0.7.0", + "version": "0.8.1", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52" + "reference": "95d57f38a4001c7189a66885c485ac635d305234" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/beeea0f2c9bce14a6869fc5c87a1047cdecb5c52", - "reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/95d57f38a4001c7189a66885c485ac635d305234", + "reference": "95d57f38a4001c7189a66885c485ac635d305234", "shasum": "" }, "require": { "ext-json": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/cli": "0.15.*", - "utopia-php/framework": "0.33.*", - "utopia-php/queue": "0.7.*" + "utopia-php/cli": "0.19.*", + "utopia-php/framework": "1.0.*", + "utopia-php/queue": "0.8.*", + "utopia-php/servers": "0.1.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2407,9 +2433,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.7.0" + "source": "https://github.com/utopia-php/platform/tree/0.8.1" }, - "time": "2024-05-08T17:00:55+00:00" + "time": "2024-09-06T02:33:27+00:00" }, { "name": "utopia-php/pools", @@ -2517,22 +2543,23 @@ }, { "name": "utopia-php/queue", - "version": "0.7.0", + "version": "0.8.0", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "917565256eb94bcab7246f7a746b1a486813761b" + "reference": "a518b271f8c158d6e66e36972f767189111033c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/917565256eb94bcab7246f7a746b1a486813761b", - "reference": "917565256eb94bcab7246f7a746b1a486813761b", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/a518b271f8c158d6e66e36972f767189111033c2", + "reference": "a518b271f8c158d6e66e36972f767189111033c2", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/cli": "0.15.*", - "utopia-php/framework": "0.*.*" + "utopia-php/cli": "0.19.*", + "utopia-php/di": "0.1.*", + "utopia-php/servers": "0.1.*" }, "require-dev": { "laravel/pint": "^0.2.3", @@ -2542,6 +2569,7 @@ "workerman/workerman": "^4.0" }, "suggest": { + "ext-redis": "Needed to support Redis connections", "ext-swoole": "Needed to support Swoole.", "workerman/workerman": "Needed to support Workerman." }, @@ -2572,9 +2600,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.7.0" + "source": "https://github.com/utopia-php/queue/tree/0.8.0" }, - "time": "2024-01-17T19:00:43+00:00" + "time": "2024-09-05T16:33:01+00:00" }, { "name": "utopia-php/registry", @@ -2629,17 +2657,70 @@ "time": "2021-03-10T10:45:22+00:00" }, { - "name": "utopia-php/storage", - "version": "0.18.5", + "name": "utopia-php/servers", + "version": "0.1.1", "source": { "type": "git", - "url": "https://github.com/utopia-php/storage.git", - "reference": "7d355c5e3ccc8ecebc0266f8ddd30088a43be919" + "url": "https://github.com/utopia-php/servers.git", + "reference": "fd5c8d32778f265256c1936372a071b944f5ba8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/7d355c5e3ccc8ecebc0266f8ddd30088a43be919", - "reference": "7d355c5e3ccc8ecebc0266f8ddd30088a43be919", + "url": "https://api.github.com/repos/utopia-php/servers/zipball/fd5c8d32778f265256c1936372a071b944f5ba8a", + "reference": "fd5c8d32778f265256c1936372a071b944f5ba8a", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "utopia-php/di": "0.1.*" + }, + "require-dev": { + "laravel/pint": "^0.2.3", + "phpstan/phpstan": "^1.8", + "phpunit/phpunit": "^9.5.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Servers\\": "src/Servers" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Team Appwrite", + "email": "team@appwrite.io" + } + ], + "description": "A base library for building Utopia style servers.", + "keywords": [ + "framework", + "php", + "servers", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/servers/issues", + "source": "https://github.com/utopia-php/servers/tree/0.1.1" + }, + "time": "2024-09-06T02:25:56+00:00" + }, + { + "name": "utopia-php/storage", + "version": "0.19.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/storage.git", + "reference": "5013b894a776874d6010753fc9349df2225c69af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/5013b894a776874d6010753fc9349df2225c69af", + "reference": "5013b894a776874d6010753fc9349df2225c69af", "shasum": "" }, "require": { @@ -2651,8 +2732,8 @@ "ext-zlib": "*", "ext-zstd": "*", "php": ">=8.0", - "utopia-php/framework": "0.*.*", - "utopia-php/system": "0.*.*" + "utopia-php/framework": "1.0.*", + "utopia-php/system": "0.8.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2679,60 +2760,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.18.5" + "source": "https://github.com/utopia-php/storage/tree/0.19.0" }, - "time": "2024-09-04T08:57:27+00:00" - }, - { - "name": "utopia-php/swoole", - "version": "0.8.2", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/swoole.git", - "reference": "5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/swoole/zipball/5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4", - "reference": "5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4", - "shasum": "" - }, - "require": { - "ext-swoole": "*", - "php": ">=8.0", - "utopia-php/framework": "0.33.*" - }, - "require-dev": { - "laravel/pint": "1.2.*", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.3", - "swoole/ide-helper": "5.0.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\Swoole\\": "src/Swoole" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "An extension for Utopia Framework to work with PHP Swoole as a PHP FPM alternative", - "keywords": [ - "framework", - "http", - "php", - "server", - "swoole", - "upf", - "utopia" - ], - "support": { - "issues": "https://github.com/utopia-php/swoole/issues", - "source": "https://github.com/utopia-php/swoole/tree/0.8.2" - }, - "time": "2024-02-01T14:54:12+00:00" + "time": "2024-09-05T17:00:24+00:00" }, { "name": "utopia-php/system", @@ -2792,23 +2822,24 @@ }, { "name": "utopia-php/vcs", - "version": "0.8.2", + "version": "0.9.0", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18" + "reference": "673abe2fef0750a841a4fa8fa6f99d4a602c68e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18", - "reference": "eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/673abe2fef0750a841a4fa8fa6f99d4a602c68e7", + "reference": "673abe2fef0750a841a4fa8fa6f99d4a602c68e7", "shasum": "" }, "require": { "adhocore/jwt": "^1.1", "php": ">=8.0", - "utopia-php/cache": "^0.10.0", - "utopia-php/framework": "0.*.*" + "utopia-php/cache": "0.10.*", + "utopia-php/framework": "1.0.*", + "utopia-php/system": "0.8.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2835,32 +2866,76 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/0.8.2" + "source": "https://github.com/utopia-php/vcs/tree/0.9.0" }, - "time": "2024-08-13T14:36:30+00:00" + "time": "2024-09-05T16:44:48+00:00" }, { - "name": "utopia-php/websocket", - "version": "0.1.0", + "name": "utopia-php/view", + "version": "0.2.0", "source": { "type": "git", - "url": "https://github.com/utopia-php/websocket.git", - "reference": "51fcb86171400d8aa40d76c54593481fd273dab5" + "url": "https://github.com/utopia-php/view.git", + "reference": "6ee55e83bc014c39ed6b69390f6d399116f65e88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/websocket/zipball/51fcb86171400d8aa40d76c54593481fd273dab5", - "reference": "51fcb86171400d8aa40d76c54593481fd273dab5", + "url": "https://api.github.com/repos/utopia-php/view/zipball/6ee55e83bc014c39ed6b69390f6d399116f65e88", + "reference": "6ee55e83bc014c39ed6b69390f6d399116f65e88", "shasum": "" }, "require": { "php": ">=8.0" }, "require-dev": { + "laravel/pint": "^1.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.25" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\View\\": "src/View" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple, light and advanced PHP rendering engine", + "keywords": [ + "php", + "view" + ], + "support": { + "issues": "https://github.com/utopia-php/view/issues", + "source": "https://github.com/utopia-php/view/tree/0.2.0" + }, + "time": "2024-04-01T17:21:29+00:00" + }, + { + "name": "utopia-php/websocket", + "version": "0.2.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/websocket.git", + "reference": "e9d0919b321744a61f12563f5791c47ba9f57810" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/websocket/zipball/e9d0919b321744a61f12563f5791c47ba9f57810", + "reference": "e9d0919b321744a61f12563f5791c47ba9f57810", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "^1.15", + "phpstan/phpstan": "^1.8", "phpunit/phpunit": "^9.5.5", - "swoole/ide-helper": "4.6.6", + "swoole/ide-helper": "5.1.2", "textalk/websocket": "1.5.2", - "vimeo/psalm": "^4.8.1", "workerman/workerman": "^4.0" }, "type": "library", @@ -2873,16 +2948,6 @@ "license": [ "MIT" ], - "authors": [ - { - "name": "Eldad Fux", - "email": "eldad@appwrite.io" - }, - { - "name": "Torsten Dittmann", - "email": "torsten@appwrite.io" - } - ], "description": "A simple abstraction for WebSocket servers.", "keywords": [ "framework", @@ -2893,9 +2958,9 @@ ], "support": { "issues": "https://github.com/utopia-php/websocket/issues", - "source": "https://github.com/utopia-php/websocket/tree/0.1.0" + "source": "https://github.com/utopia-php/websocket/tree/0.2.0" }, - "time": "2021-12-20T10:50:09+00:00" + "time": "2024-04-09T08:28:11+00:00" }, { "name": "webmozart/assert", @@ -4216,16 +4281,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.31.0", + "version": "1.30.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "249f15fb843bf240cf058372dad29e100cee6c17" + "reference": "51b95ec8670af41009e2b2b56873bad96682413e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/249f15fb843bf240cf058372dad29e100cee6c17", - "reference": "249f15fb843bf240cf058372dad29e100cee6c17", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/51b95ec8670af41009e2b2b56873bad96682413e", + "reference": "51b95ec8670af41009e2b2b56873bad96682413e", "shasum": "" }, "require": { @@ -4257,9 +4322,68 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.31.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.1" }, - "time": "2024-09-22T11:32:18+00:00" + "time": "2024-09-07T20:13:05+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.8.11", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "46e223dd68a620da18855c23046ddb00940b4014" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/46e223dd68a620da18855c23046ddb00940b4014", + "reference": "46e223dd68a620da18855c23046ddb00940b4014", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan/issues", + "source": "https://github.com/phpstan/phpstan/tree/1.8.11" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2022-10-24T15:45:13+00:00" }, { "name": "phpunit/php-code-coverage", @@ -5896,16 +6020,16 @@ }, { "name": "symfony/console", - "version": "v7.1.5", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee" + "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0fa539d12b3ccf068a722bbbffa07ca7079af9ee", - "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee", + "url": "https://api.github.com/repos/symfony/console/zipball/1eed7af6961d763e7832e874d7f9b21c3ea9c111", + "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111", "shasum": "" }, "require": { @@ -5969,7 +6093,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.5" + "source": "https://github.com/symfony/console/tree/v7.1.4" }, "funding": [ { @@ -5985,7 +6109,7 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:28:38+00:00" + "time": "2024-08-15T22:48:53+00:00" }, { "name": "symfony/deprecation-contracts", @@ -6056,16 +6180,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.1.5", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a" + "reference": "92a91985250c251de9b947a14bb2c9390b1a562c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/61fe0566189bf32e8cfee78335d8776f64a66f5a", - "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/92a91985250c251de9b947a14bb2c9390b1a562c", + "reference": "92a91985250c251de9b947a14bb2c9390b1a562c", "shasum": "" }, "require": { @@ -6102,7 +6226,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.1.5" + "source": "https://github.com/symfony/filesystem/tree/v7.1.2" }, "funding": [ { @@ -6118,7 +6242,7 @@ "type": "tidelift" } ], - "time": "2024-09-17T09:16:35+00:00" + "time": "2024-06-28T10:03:55+00:00" }, { "name": "symfony/finder", @@ -6567,16 +6691,16 @@ }, { "name": "symfony/process", - "version": "v7.1.5", + "version": "v7.1.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "5c03ee6369281177f07f7c68252a280beccba847" + "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847", - "reference": "5c03ee6369281177f07f7c68252a280beccba847", + "url": "https://api.github.com/repos/symfony/process/zipball/7f2f542c668ad6c313dc4a5e9c3321f733197eca", + "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca", "shasum": "" }, "require": { @@ -6608,7 +6732,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.1.5" + "source": "https://github.com/symfony/process/tree/v7.1.3" }, "funding": [ { @@ -6624,7 +6748,7 @@ "type": "tidelift" } ], - "time": "2024-09-19T21:48:23+00:00" + "time": "2024-07-26T12:44:47+00:00" }, { "name": "symfony/service-contracts", @@ -6711,16 +6835,16 @@ }, { "name": "symfony/string", - "version": "v7.1.5", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306" + "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306", - "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306", + "url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b", + "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b", "shasum": "" }, "require": { @@ -6778,7 +6902,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.5" + "source": "https://github.com/symfony/string/tree/v7.1.4" }, "funding": [ { @@ -6794,7 +6918,7 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:28:38+00:00" + "time": "2024-08-12T09:59:40+00:00" }, { "name": "textalk/websocket", @@ -7026,9 +7150,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "utopia-php/database": 5 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/docker-compose.yml b/docker-compose.yml index 6ecb0ecff8..c4bdffa1d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -682,6 +682,7 @@ services: entrypoint: maintenance <<: *x-logging container_name: appwrite-task-maintenance + restart: unless-stopped image: appwrite-dev networks: - appwrite @@ -782,6 +783,7 @@ services: entrypoint: schedule-functions <<: *x-logging container_name: appwrite-task-scheduler-functions + restart: unless-stopped image: appwrite-dev networks: - appwrite @@ -810,6 +812,7 @@ services: entrypoint: schedule-executions <<: *x-logging container_name: appwrite-task-scheduler-executions + restart: unless-stopped image: appwrite-dev networks: - appwrite @@ -837,6 +840,7 @@ services: entrypoint: schedule-messages <<: *x-logging container_name: appwrite-task-scheduler-messages + restart: unless-stopped image: appwrite-dev networks: - appwrite @@ -956,7 +960,7 @@ services: - MYSQL_USER=${_APP_DB_USER} - MYSQL_PASSWORD=${_APP_DB_PASS} - MARIADB_AUTO_UPGRADE=1 - command: "mysqld --innodb-flush-method=fsync" + command: 'mysqld --innodb-flush-method=fsync --max_connections=5000' redis: image: redis:7.2.4-alpine diff --git a/docs/tutorials/add-route.md b/docs/tutorials/add-route.md index ac6fd40bdb..0baa51b5c0 100644 --- a/docs/tutorials/add-route.md +++ b/docs/tutorials/add-route.md @@ -8,7 +8,7 @@ Setting an alias allows the route to be also accessible from the alias URL. The first parameter specifies the alias URL, the second parameter specifies default values for route parameters. ```php -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->alias('/v1/storage/files', ['bucketId' => 'default']) ``` @@ -17,7 +17,7 @@ App::post('/v1/storage/buckets/:bucketId/files') Used as an abstract description of the route. ```php -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->desc('Create File') ``` @@ -26,14 +26,14 @@ App::post('/v1/storage/buckets/:bucketId/files') Groups array is used to group one or more routes with one or more hooks functionality. ```php -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->groups(['api']) ``` In the above example groups() is used to define the current route as part of the routes that shares a common init middleware hook. ```php -App::init() +Http::init() ->groups(['api']) ->action( some code..... @@ -52,7 +52,7 @@ Appwrite uses different labels to achieve different things, for example: - scope - Defines the route permissions scope. ```php -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->label('scope', 'files.write') ``` @@ -66,7 +66,7 @@ App::post('/v1/storage/buckets/:bucketId/files') - audits.resource - Signals the extraction part of the resource. ```php -App::post('/v1/account/create') +Http::post('/v1/account/create') ->label('audits.event', 'account.create') ->label('audits.resource', 'user/{response.$id}') ->label('audits.userId', '{response.$id}') @@ -84,7 +84,7 @@ App::post('/v1/account/create') * sdk.offline.response.key - JSON property name that has the ID. Defaults to $id ```php -App::post('/v1/account/jwt') +Http::post('/v1/account/jwt') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'createJWT') @@ -100,7 +100,7 @@ App::post('/v1/account/jwt') - cache.resource - Identifies the cached resource. ```php -App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') +Http::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') ->label('cache', true) ->label('cache.resource', 'file/{request.fileId}') ``` @@ -115,7 +115,7 @@ When using the example below, we configure the abuse mechanism to allow this key constructed from the combination of the ip, http method, url, userId to hit the route maximum 60 times in 1 hour (60 seconds \* 60 minutes). ```php -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}') ->label('abuse-limit', 60) ->label('abuse-time', 3600) @@ -127,7 +127,7 @@ App::post('/v1/storage/buckets/:bucketId/files') Placeholders marked as `[]` are parsed and replaced with their real values. ```php -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->label('event', 'buckets.[bucketId].files.[fileId].create') ``` @@ -145,7 +145,7 @@ As the name implies, `param()` is used to define a request parameter. - An array of injections ```php -App::get('/v1/account/logs') +Http::get('/v1/account/logs') ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) ``` @@ -154,14 +154,14 @@ App::get('/v1/account/logs') inject is used to inject dependencies pre-bounded to the app. ```php -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->inject('user') ``` -In the example above, the user object is injected into the route pre-bounded using `App::setResource()`. +In the example above, the user object is injected into the route pre-bounded using `Http::setResource()`. ```php -App::setResource('user', function() { +Http::setResource('user', function() { some code... }); ``` @@ -170,7 +170,7 @@ some code... Action populates the actual route code and has to be very clear and understandable. A good route stays simple and doesn't contain complex logic. An action is where we describe our business needs in code, and combine different libraries to work together and tell our story. ```php -App::post('/v1/account/sessions/anonymous') +Http::post('/v1/account/sessions/anonymous') ->action(function (Request $request) { some code... }); diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000000..25771ef17c --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,11 @@ +parameters: + level: 0 + scanDirectories: + - vendor/swoole/ide-helper + excludePaths: + - tests/resources + ignoreErrors: + - '#Parameter \$geodb of anonymous function has invalid type MaxMind\\Db\\Reader\.#' + - '#Parameter \$geodb of function router\(\) has invalid type MaxMind\\Db\\Reader\.#' + - '#Instantiated class MaxMind\\Db\\Reader not found.#' + - '#Function scrypt not found\.#' diff --git a/src/Appwrite/Auth/Auth.php b/src/Appwrite/Auth/Auth.php index 1e8109622e..0135020765 100644 --- a/src/Appwrite/Auth/Auth.php +++ b/src/Appwrite/Auth/Auth.php @@ -91,37 +91,6 @@ class Auth */ public const MFA_RECENT_DURATION = 1800; // 30 mins - /** - * @var string - */ - public static $cookieName = 'a_session'; - - /** - * User Unique ID. - * - * @var string - */ - public static $unique = ''; - - /** - * User Secret Key. - * - * @var string - */ - public static $secret = ''; - - /** - * Set Cookie Name. - * - * @param $string - * - * @return string - */ - public static function setCookieName($string) - { - return self::$cookieName = $string; - } - /** * Encode Session. * @@ -439,13 +408,14 @@ class Auth * Returns all roles for a user. * * @param Document $user + * @param Authorization $auth * @return array */ - public static function getRoles(Document $user): array + public static function getRoles(Document $user, Authorization $auth): array { $roles = []; - if (!self::isPrivilegedUser(Authorization::getRoles()) && !self::isAppUser(Authorization::getRoles())) { + if (!self::isPrivilegedUser($auth->getRoles()) && !self::isAppUser($auth->getRoles())) { if ($user->getId()) { $roles[] = Role::user($user->getId())->toString(); $roles[] = Role::users()->toString(); diff --git a/src/Appwrite/Auth/Authentication.php b/src/Appwrite/Auth/Authentication.php new file mode 100644 index 0000000000..ef372309da --- /dev/null +++ b/src/Appwrite/Auth/Authentication.php @@ -0,0 +1,57 @@ +cookieName = $string; + } + + public function getCookieName(): string + { + return $this->cookieName; + } + + public function getUnique(): string + { + return $this->unique; + } + + public function setUnique(string $unique): void + { + $this->unique = $unique; + } + + public function getSecret(): string + { + return $this->secret; + } + + public function setSecret(string $secret): void + { + $this->secret = $secret; + } + +} diff --git a/src/Appwrite/Auth/Validator/MockNumber.php b/src/Appwrite/Auth/Validator/MockNumber.php index ac5ba89fc5..8f0f14c9da 100644 --- a/src/Appwrite/Auth/Validator/MockNumber.php +++ b/src/Appwrite/Auth/Validator/MockNumber.php @@ -2,8 +2,8 @@ namespace Appwrite\Auth\Validator; -use Utopia\Validator; -use Utopia\Validator\Text; +use Utopia\Http\Validator; +use Utopia\Http\Validator\Text; /** * MockNumber. @@ -52,6 +52,7 @@ class MockNumber extends Validator return false; } + $this->message = ''; return true; } diff --git a/src/Appwrite/Auth/Validator/Password.php b/src/Appwrite/Auth/Validator/Password.php index bfe5577889..913701f7a3 100644 --- a/src/Appwrite/Auth/Validator/Password.php +++ b/src/Appwrite/Auth/Validator/Password.php @@ -2,7 +2,7 @@ namespace Appwrite\Auth\Validator; -use Utopia\Validator; +use Utopia\Http\Validator; /** * Password. diff --git a/src/Appwrite/Auth/Validator/Phone.php b/src/Appwrite/Auth/Validator/Phone.php index 26aa687278..d5f6df60c8 100644 --- a/src/Appwrite/Auth/Validator/Phone.php +++ b/src/Appwrite/Auth/Validator/Phone.php @@ -2,7 +2,7 @@ namespace Appwrite\Auth\Validator; -use Utopia\Validator; +use Utopia\Http\Validator; /** * Phone. diff --git a/src/Appwrite/Event/Func.php b/src/Appwrite/Event/Func.php index 0cbaf17b60..2fbdb7e785 100644 --- a/src/Appwrite/Event/Func.php +++ b/src/Appwrite/Event/Func.php @@ -175,9 +175,9 @@ class Func extends Event * * @return string */ - public function getData(): string + public function getBody(): string { - return $this->data; + return $this->body; } /** diff --git a/src/Appwrite/Event/Validator/Event.php b/src/Appwrite/Event/Validator/Event.php index a3605e4df5..2d46bd7727 100644 --- a/src/Appwrite/Event/Validator/Event.php +++ b/src/Appwrite/Event/Validator/Event.php @@ -3,7 +3,7 @@ namespace Appwrite\Event\Validator; use Utopia\Config\Config; -use Utopia\Validator; +use Utopia\Http\Validator; class Event extends Validator { diff --git a/src/Appwrite/Functions/Validator/Headers.php b/src/Appwrite/Functions/Validator/Headers.php index 04003d535b..4c30a98045 100644 --- a/src/Appwrite/Functions/Validator/Headers.php +++ b/src/Appwrite/Functions/Validator/Headers.php @@ -2,7 +2,7 @@ namespace Appwrite\Functions\Validator; -use Utopia\Validator; +use Utopia\Http\Validator; /** * Headers. diff --git a/src/Appwrite/Functions/Validator/Payload.php b/src/Appwrite/Functions/Validator/Payload.php index acb461eabd..3b2ff4b918 100644 --- a/src/Appwrite/Functions/Validator/Payload.php +++ b/src/Appwrite/Functions/Validator/Payload.php @@ -2,7 +2,7 @@ namespace Appwrite\Functions\Validator; -use Utopia\Validator\Text; +use Utopia\Http\Validator\Text; class Payload extends Text { diff --git a/src/Appwrite/Functions/Validator/RuntimeSpecification.php b/src/Appwrite/Functions/Validator/RuntimeSpecification.php index 22311838f6..fa6efe90a4 100644 --- a/src/Appwrite/Functions/Validator/RuntimeSpecification.php +++ b/src/Appwrite/Functions/Validator/RuntimeSpecification.php @@ -2,7 +2,7 @@ namespace Appwrite\Functions\Validator; -use Utopia\Validator; +use Utopia\Http\Validator; class RuntimeSpecification extends Validator { diff --git a/src/Appwrite/GraphQL/Resolvers.php b/src/Appwrite/GraphQL/Resolvers.php index 8bc72af2f8..49d7c421f7 100644 --- a/src/Appwrite/GraphQL/Resolvers.php +++ b/src/Appwrite/GraphQL/Resolvers.php @@ -6,9 +6,12 @@ use Appwrite\GraphQL\Exception as GQLException; use Appwrite\Promises\Swoole; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; -use Utopia\App; +use Utopia\DI\Container; use Utopia\Exception; -use Utopia\Route; +use Utopia\Http\Http; +use Utopia\Http\Request as UtopiaHttpRequest; +use Utopia\Http\Response as UtopiaHttpResponse; +use Utopia\Http\Route; use Utopia\System\System; class Resolvers @@ -16,24 +19,21 @@ class Resolvers /** * Create a resolver for a given API {@see Route}. * - * @param App $utopia + * @param Http $http * @param ?Route $route * @return callable */ - public static function api( - App $utopia, + public function api( + Http $http, ?Route $route, + UtopiaHttpRequest $request, + UtopiaHttpResponse $response, + Container $container, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $route, $args, $context, $info) { - /** @var App $utopia */ - /** @var Response $response */ - /** @var Request $request */ - - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); + $resolver = $this; + return fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($http, $route, $args, $context, $container, $info, $request, $response, $resolver) { $path = $route->getPath(); foreach ($args as $key => $value) { if (\str_contains($path, '/:' . $key)) { @@ -46,14 +46,13 @@ class Resolvers switch ($route->getMethod()) { case 'GET': - $request->setQueryString($args); + $request->setQuery($args); break; default: $request->setPayload($args); break; } - - self::resolve($utopia, $request, $response, $resolve, $reject); + $resolver->resolve($http, $request, $response, $container, $resolve, $reject); } ); } @@ -61,20 +60,20 @@ class Resolvers /** * Create a resolver for a document in a specified database and collection with a specific method type. * - * @param App $utopia + * @param Http $http * @param string $databaseId * @param string $collectionId * @param string $methodType * @return callable */ - public static function document( - App $utopia, + public function document( + Http $http, string $databaseId, string $collectionId, string $methodType, ): callable { return [self::class, 'document' . \ucfirst($methodType)]( - $utopia, + $http, $databaseId, $collectionId ); @@ -83,28 +82,28 @@ class Resolvers /** * Create a resolver for getting a document in a specified database and collection. * - * @param App $utopia + * @param Http $http * @param string $databaseId * @param string $collectionId * @param callable $url * @return callable */ - public static function documentGet( - App $utopia, + public function documentGet( + Http $http, string $databaseId, string $collectionId, callable $url, + UtopiaHttpRequest $request, + UtopiaHttpResponse $response, + Container $container, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); - + $resolver = $this; + return fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $type, $args, $container, $request, $response, $resolver) { $request->setMethod('GET'); $request->setURI($url($databaseId, $collectionId, $args)); - self::resolve($utopia, $request, $response, $resolve, $reject); + $resolver->resolve($http, $request, $response, $container, $resolve, $reject); } ); } @@ -112,35 +111,35 @@ class Resolvers /** * Create a resolver for listing documents in a specified database and collection. * - * @param App $utopia + * @param Http $http * @param string $databaseId * @param string $collectionId * @param callable $url * @param callable $params * @return callable */ - public static function documentList( - App $utopia, + public function documentList( + Http $http, string $databaseId, string $collectionId, callable $url, callable $params, + UtopiaHttpRequest $request, + UtopiaHttpResponse $response, + Container $container, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); - + $resolver = $this; + return fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $params, $type, $args, $container, $request, $response, $resolver) { $request->setMethod('GET'); $request->setURI($url($databaseId, $collectionId, $args)); - $request->setQueryString($params($databaseId, $collectionId, $args)); + $request->setQuery($params($databaseId, $collectionId, $args)); $beforeResolve = function ($payload) { return $payload['documents']; }; - self::resolve($utopia, $request, $response, $resolve, $reject, $beforeResolve); + $resolver->resolve($http, $request, $response, $container, $resolve, $reject, $beforeResolve); } ); } @@ -148,31 +147,31 @@ class Resolvers /** * Create a resolver for creating a document in a specified database and collection. * - * @param App $utopia + * @param Http $http * @param string $databaseId * @param string $collectionId * @param callable $url * @param callable $params * @return callable */ - public static function documentCreate( - App $utopia, + public function documentCreate( + Http $http, string $databaseId, string $collectionId, callable $url, callable $params, + UtopiaHttpRequest $request, + UtopiaHttpResponse $response, + Container $container, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); - + $resolver = $this; + return fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $params, $type, $args, $container, $request, $response, $resolver) { $request->setMethod('POST'); $request->setURI($url($databaseId, $collectionId, $args)); $request->setPayload($params($databaseId, $collectionId, $args)); - self::resolve($utopia, $request, $response, $resolve, $reject); + $resolver->resolve($http, $request, $response, $container, $resolve, $reject); } ); } @@ -180,31 +179,31 @@ class Resolvers /** * Create a resolver for updating a document in a specified database and collection. * - * @param App $utopia + * @param Http $http * @param string $databaseId * @param string $collectionId * @param callable $url * @param callable $params * @return callable */ - public static function documentUpdate( - App $utopia, + public function documentUpdate( + Http $http, string $databaseId, string $collectionId, callable $url, callable $params, + UtopiaHttpRequest $request, + UtopiaHttpResponse $response, + Container $container, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); - + $resolver = $this; + return fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $params, $type, $args, $container, $request, $response, $resolver) { $request->setMethod('PATCH'); $request->setURI($url($databaseId, $collectionId, $args)); $request->setPayload($params($databaseId, $collectionId, $args)); - self::resolve($utopia, $request, $response, $resolve, $reject); + $resolver->resolve($http, $request, $response, $container, $resolve, $reject); } ); } @@ -212,34 +211,34 @@ class Resolvers /** * Create a resolver for deleting a document in a specified database and collection. * - * @param App $utopia + * @param Http $http * @param string $databaseId * @param string $collectionId * @param callable $url * @return callable */ - public static function documentDelete( - App $utopia, + public function documentDelete( + Http $http, string $databaseId, string $collectionId, callable $url, + UtopiaHttpRequest $request, + UtopiaHttpResponse $response, + Container $container, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); - + $resolver = $this; + return fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $type, $args, $container, $request, $response, $resolver) { $request->setMethod('DELETE'); $request->setURI($url($databaseId, $collectionId, $args)); - self::resolve($utopia, $request, $response, $resolve, $reject); + $resolver->resolve($http, $request, $response, $container, $resolve, $reject); } ); } /** - * @param App $utopia + * @param Http $http * @param Request $request * @param Response $response * @param callable $resolve @@ -249,10 +248,11 @@ class Resolvers * @return void * @throws Exception */ - private static function resolve( - App $utopia, + private function resolve( + Http $http, Request $request, Response $response, + Container $context, callable $resolve, callable $reject, ?callable $beforeResolve = null, @@ -263,14 +263,16 @@ class Resolvers $request->removeHeader('content-type'); } - $request = clone $request; - $utopia->setResource('request', static fn () => $request); $response->setContentType(Response::CONTENT_TYPE_NULL); try { - $route = $utopia->match($request, fresh: true); + $context + ->refresh('cache') + ->refresh('dbForProject') + ->refresh('dbForConsole') + ->refresh('getProjectDb'); - $utopia->execute($route, $request, $response); + $http->run(clone $context); } catch (\Throwable $e) { if ($beforeReject) { $e = $beforeReject($e); @@ -285,10 +287,12 @@ class Resolvers if ($beforeReject) { $payload = $beforeReject($payload); } - $reject(new GQLException( - message: $payload['message'], - code: $response->getStatusCode() - )); + $reject( + new GQLException( + message: $payload['message'], + code: $response->getStatusCode() + ) + ); return; } diff --git a/src/Appwrite/GraphQL/Schema.php b/src/Appwrite/GraphQL/Schema.php index 833ea9d032..2b05f08aee 100644 --- a/src/Appwrite/GraphQL/Schema.php +++ b/src/Appwrite/GraphQL/Schema.php @@ -3,54 +3,63 @@ namespace Appwrite\GraphQL; use Appwrite\GraphQL\Types\Mapper; +use Appwrite\Utopia\Response\Models; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema as GQLSchema; -use Utopia\App; +use Utopia\DI\Container; use Utopia\Exception; -use Utopia\Route; +use Utopia\Http\Adapter\Swoole\Response as UtopiaSwooleResponse; +use Utopia\Http\Http; +use Utopia\Http\Request; +use Utopia\Http\Response as UtopiaHttpResponse; +use Utopia\Http\Route; class Schema { - protected static ?GQLSchema $schema = null; - protected static array $dirty = []; + protected ?GQLSchema $schema = null; + protected array $dirty = []; /** * - * @param App $utopia - * @param callable $complexity Function to calculate complexity - * @param callable $attributes Function to get attributes - * @param array $urls Array of functions to get urls for specific method types - * @param array $params Array of functions to build parameters for specific method types + * @param Http $http + * @param callable $complexity Function to calculate complexity + * @param callable $attributes Function to get attributes + * @param array $urls Array of functions to get urls for specific method types + * @param array $params Array of functions to build parameters for specific method types * @return GQLSchema * @throws Exception */ - public static function build( - App $utopia, + public function build( + Http $http, + Request $request, + UtopiaHttpResponse $response, + Container $container, callable $complexity, callable $attributes, array $urls, array $params, ): GQLSchema { - App::setResource('utopia:graphql', static function () use ($utopia) { - return $utopia; - }); - - if (!empty(self::$schema)) { - return self::$schema; + if (!empty($this->schema)) { + return $this->schema; } - $api = static::api( - $utopia, + $api = $this->api( + $http, + $request, + $response, + $container, $complexity ); - //$collections = static::collections( - // $utopia, - // $complexity, - // $attributes, - // $urls, - // $params, - //); + // $collections = $this->collections( + // $http, + // $complexity, + // $request, + // $response, + // $attributes, + // $urls, + // $params, + // ); $queries = \array_merge_recursive( $api['query'], @@ -64,7 +73,7 @@ class Schema \ksort($queries); \ksort($mutations); - return static::$schema = new GQLSchema([ + return $this->schema = new GQLSchema([ 'query' => new ObjectType([ 'name' => 'Query', 'fields' => $queries @@ -80,21 +89,23 @@ class Schema * This function iterates all API routes and builds a GraphQL * schema defining types and resolvers for all response models. * - * @param App $utopia + * @param Http $http + * @param Request $request + * @param UtopiaSwooleResponse $response * @param callable $complexity * @return array - * @throws Exception + * @throws \Exception */ - protected static function api(App $utopia, callable $complexity): array + protected function api(Http $http, Request $request, UtopiaHttpResponse $response, Container $container, callable $complexity): array { - Mapper::init($utopia - ->getResource('response') - ->getModels()); + Mapper::init(Models::getModels()); + + $mapper = new Mapper(); $queries = []; $mutations = []; - foreach ($utopia->getRoutes() as $routes) { + foreach ($http->getRoutes() as $routes) { foreach ($routes as $route) { /** @var Route $route */ @@ -106,7 +117,7 @@ class Schema continue; } - foreach (Mapper::route($utopia, $route, $complexity) as $field) { + foreach ($mapper->route($http, $route, $request, $response, $container, $complexity) as $field) { switch ($route->getMethod()) { case 'GET': $queries[$name] = $field; @@ -134,7 +145,7 @@ class Schema * Iterates all of a projects attributes and builds GraphQL * queries and mutations for the collections they make up. * - * @param App $utopia + * @param Http $http * @param callable $complexity * @param callable $attributes * @param array $urls @@ -143,7 +154,7 @@ class Schema * @throws \Exception */ protected static function collections( - App $utopia, + Http $http, callable $complexity, callable $attributes, array $urls, @@ -194,36 +205,36 @@ class Schema $queryFields[$collectionId . 'Get'] = [ 'type' => $objectType, 'args' => Mapper::args('id'), - 'resolve' => Resolvers::documentGet( - $utopia, + /*'resolve' => Resolvers::documentGet( + $http, $databaseId, $collectionId, $urls['get'], - ) + )*/ ]; $queryFields[$collectionId . 'List'] = [ 'type' => Type::listOf($objectType), 'args' => Mapper::args('list'), - 'resolve' => Resolvers::documentList( - $utopia, + /*'resolve' => Resolvers::documentList( + $http, $databaseId, $collectionId, $urls['list'], $params['list'], - ), + ),*/ 'complexity' => $complexity, ]; $mutationFields[$collectionId . 'Create'] = [ 'type' => $objectType, 'args' => $attributes, - 'resolve' => Resolvers::documentCreate( - $utopia, + /*'resolve' => Resolvers::documentCreate( + $http, $databaseId, $collectionId, $urls['create'], $params['create'], - ) + )*/ ]; $mutationFields[$collectionId . 'Update'] = [ 'type' => $objectType, @@ -234,23 +245,23 @@ class Schema $attributes ) ), - 'resolve' => Resolvers::documentUpdate( - $utopia, + /*'resolve' => Resolvers::documentUpdate( + $http, $databaseId, $collectionId, $urls['update'], $params['update'], - ) + )*/ ]; $mutationFields[$collectionId . 'Delete'] = [ 'type' => Mapper::model('none'), 'args' => Mapper::args('id'), - 'resolve' => Resolvers::documentDelete( - $utopia, + /*'resolve' => Resolvers::documentDelete( + $http, $databaseId, $collectionId, $urls['delete'], - ) + )*/ ]; } $offset += $limit; @@ -262,8 +273,8 @@ class Schema ]; } - public static function setDirty(string $projectId): void + public function setDirty(string $projectId): void { - self::$dirty[$projectId] = true; + $this->dirty[$projectId] = true; } } diff --git a/src/Appwrite/GraphQL/Types/Mapper.php b/src/Appwrite/GraphQL/Types/Mapper.php index 36b246b28b..a15c6aa475 100644 --- a/src/Appwrite/GraphQL/Types/Mapper.php +++ b/src/Appwrite/GraphQL/Types/Mapper.php @@ -8,10 +8,13 @@ use Exception; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; -use Utopia\App; -use Utopia\Route; -use Utopia\Validator; -use Utopia\Validator\Nullable; +use Utopia\DI\Container; +use Utopia\Http\Adapter\Swoole\Response as UtopiaSwooleResponse; +use Utopia\Http\Http; +use Utopia\Http\Request; +use Utopia\Http\Route; +use Utopia\Http\Validator; +use Utopia\Http\Validator\Nullable; class Mapper { @@ -74,12 +77,15 @@ class Mapper return self::$args[$key] ?? []; } - public static function route( - App $utopia, + public function route( + Http $http, Route $route, + Request $request, + UtopiaSwooleResponse $response, + Container $container, callable $complexity ): iterable { - foreach (self::$blacklist as $blacklist) { + foreach (static::$blacklist as $blacklist) { if (\str_starts_with($route->getPath(), $blacklist)) { return; } @@ -101,7 +107,7 @@ class Mapper $list = true; } $parameterType = Mapper::param( - $utopia, + $container, $parameter['validator'], !$parameter['optional'], $parameter['injections'] @@ -116,7 +122,7 @@ class Mapper 'type' => $type, 'description' => $description, 'args' => $params, - 'resolve' => Resolvers::api($utopia, $route) + 'resolve' => (new Resolvers())->api($http, $route, $request, $response, $container) ]; if ($list) { @@ -205,7 +211,7 @@ class Mapper /** * Map a {@see Route} parameter to a GraphQL Type * - * @param App $utopia + * @param Container $container * @param Validator|callable $validator * @param bool $required * @param array $injections @@ -213,13 +219,13 @@ class Mapper * @throws Exception */ public static function param( - App $utopia, + Container $container, Validator|callable $validator, bool $required, array $injections ): Type { $validator = \is_callable($validator) - ? \call_user_func_array($validator, $utopia->getResources($injections)) + ? \call_user_func_array($validator, array_map(fn ($injection) => $container->get($injection), $injections)) : $validator; $isNullable = $validator instanceof Nullable; @@ -232,20 +238,20 @@ class Mapper case 'Appwrite\Network\Validator\CNAME': case 'Appwrite\Task\Validator\Cron': case 'Appwrite\Utopia\Database\Validator\CustomId': - case 'Utopia\Validator\Domain': + case 'Utopia\Http\Validator\Domain': case 'Appwrite\Network\Validator\Email': case 'Appwrite\Event\Validator\Event': case 'Appwrite\Event\Validator\FunctionEvent': - case 'Utopia\Validator\HexColor': - case 'Utopia\Validator\Host': - case 'Utopia\Validator\IP': + case 'Utopia\Http\Validator\HexColor': + case 'Utopia\Http\Validator\Host': + case 'Utopia\Http\Validator\IP': case 'Utopia\Database\Validator\Key': - case 'Utopia\Validator\Origin': + case 'Utopia\Http\Validator\Origin': case 'Appwrite\Auth\Validator\Password': - case 'Utopia\Validator\Text': + case 'Utopia\Http\Validator\Text': case 'Utopia\Database\Validator\UID': - case 'Utopia\Validator\URL': - case 'Utopia\Validator\WhiteList': + case 'Utopia\Http\Validator\URL': + case 'Utopia\Http\Validator\WhiteList': default: $type = Type::string(); break; @@ -273,29 +279,29 @@ class Mapper case 'Appwrite\Utopia\Database\Validator\Queries\Variables': $type = Type::listOf(Type::string()); break; - case 'Utopia\Validator\Boolean': + case 'Utopia\Http\Validator\Boolean': $type = Type::boolean(); break; - case 'Utopia\Validator\ArrayList': + case 'Utopia\Http\Validator\ArrayList': $type = Type::listOf(self::param( - $utopia, + $container, $validator->getValidator(), $required, $injections )); break; - case 'Utopia\Validator\Integer': - case 'Utopia\Validator\Numeric': - case 'Utopia\Validator\Range': + case 'Utopia\Http\Validator\Integer': + case 'Utopia\Http\Validator\Numeric': + case 'Utopia\Http\Validator\Range': $type = Type::int(); break; - case 'Utopia\Validator\FloatValidator': + case 'Utopia\Http\Validator\FloatValidator': $type = Type::float(); break; - case 'Utopia\Validator\Assoc': + case 'Utopia\Http\Validator\Assoc': $type = Types::assoc(); break; - case 'Utopia\Validator\JSON': + case 'Utopia\Http\Validator\JSON': $type = Types::json(); break; case 'Utopia\Storage\Validator\File': diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index cee1b2d263..5b215ec04f 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -98,10 +98,10 @@ abstract class Migration */ protected array $collections; - public function __construct() + public function __construct(Authorization $auth) { - Authorization::disable(); - Authorization::setDefaultStatus(false); + $auth->disable(); + $auth->setDefaultStatus(false); $this->collections = Config::getParam('collections', []); @@ -176,25 +176,23 @@ abstract class Migration Console::log('Migrating Collection ' . $collection['$id'] . ':'); foreach ($this->documentsIterator($collection['$id']) as $document) { - go(function (Document $document, callable $callback) { - if (empty($document->getId()) || empty($document->getCollection())) { - return; - } + if (empty($document->getId()) || empty($document->getCollection())) { + return; + } - $old = $document->getArrayCopy(); - $new = call_user_func($callback, $document); + $old = $document->getArrayCopy(); + $new = call_user_func($callback, $document); - if (is_null($new) || $new->getArrayCopy() == $old) { - return; - } + if (is_null($new) || $new->getArrayCopy() == $old) { + return; + } - try { - $this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document); - } catch (\Throwable $th) { - Console::error('Failed to update document: ' . $th->getMessage()); - return; - } - }, $document, $callback); + try { + $this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document); + } catch (\Throwable $th) { + Console::error('Failed to update document: ' . $th->getMessage()); + return; + } } } } diff --git a/src/Appwrite/Network/Validator/CNAME.php b/src/Appwrite/Network/Validator/CNAME.php index e1ae061c84..e9e2b586a5 100644 --- a/src/Appwrite/Network/Validator/CNAME.php +++ b/src/Appwrite/Network/Validator/CNAME.php @@ -2,7 +2,7 @@ namespace Appwrite\Network\Validator; -use Utopia\Validator; +use Utopia\Http\Validator; class CNAME extends Validator { diff --git a/src/Appwrite/Network/Validator/Email.php b/src/Appwrite/Network/Validator/Email.php index 3209a4aada..bae0ff0bbf 100644 --- a/src/Appwrite/Network/Validator/Email.php +++ b/src/Appwrite/Network/Validator/Email.php @@ -2,14 +2,14 @@ namespace Appwrite\Network\Validator; -use Utopia\Validator; +use Utopia\Http\Validator; /** * Email * * Validate that an variable is a valid email address * - * @package Utopia\Validator + * @package Utopia\Http\Validator */ class Email extends Validator { diff --git a/src/Appwrite/Network/Validator/Origin.php b/src/Appwrite/Network/Validator/Origin.php index d41e9af2ad..573a59b844 100644 --- a/src/Appwrite/Network/Validator/Origin.php +++ b/src/Appwrite/Network/Validator/Origin.php @@ -2,8 +2,8 @@ namespace Appwrite\Network\Validator; -use Utopia\Validator; -use Utopia\Validator\Hostname; +use Utopia\Http\Validator; +use Utopia\Http\Validator\Hostname; class Origin extends Validator { diff --git a/src/Appwrite/Platform/Tasks/Doctor.php b/src/Appwrite/Platform/Tasks/Doctor.php index 82d1ca2d59..3bf9e0d33b 100644 --- a/src/Appwrite/Platform/Tasks/Doctor.php +++ b/src/Appwrite/Platform/Tasks/Doctor.php @@ -3,13 +3,17 @@ namespace Appwrite\Platform\Tasks; use Appwrite\ClamAV\Network; -use Utopia\App; +use Appwrite\Utopia\Queue\Connections; use Utopia\CLI\Console; use Utopia\Config\Config; +use Utopia\Database\Adapter\MariaDB; +use Utopia\Database\Adapter\MySQL; use Utopia\Domains\Domain; use Utopia\DSN\DSN; +use Utopia\Http\Http; use Utopia\Logger\Logger; use Utopia\Platform\Action; +use Utopia\Queue\Connection\Redis; use Utopia\Registry\Registry; use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; @@ -27,10 +31,11 @@ class Doctor extends Action $this ->desc('Validate server health') ->inject('register') - ->callback(fn (Registry $register) => $this->action($register)); + ->inject('connections') + ->callback(fn (Registry $register, Connections $connections) => $this->action($register, $connections)); } - public function action(Registry $register): void + public function action(Registry $register, Connections $connections): void { Console::log(" __ ____ ____ _ _ ____ __ ____ ____ __ __ / _\ ( _ \( _ \/ )( \( _ \( )(_ _)( __) ( )/ \ @@ -125,22 +130,41 @@ class Doctor extends Action //throw $th; } - $pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */ + /** @var array $pools */ + $pools = $register->get('pools'); $configs = [ - 'Console.DB' => Config::getParam('pools-console'), - 'Projects.DB' => Config::getParam('pools-database'), + 'Console.DB' => [ + 'prefix' => 'console', + 'databases' => Config::getParam('pools-console') + ], + 'Database.DB' => [ + 'prefix' => 'database', + 'databases' => Config::getParam('pools-database') + ], ]; + foreach ($configs as $key => $config) { - foreach ($config as $database) { + foreach ($config['databases'] as $database) { try { - $adapter = $pools->get($database)->pop()->getResource(); + $pool = $pools['pools-' . $config['prefix'] . '-' . $database]['pool']; + $dsn = $pools['pools-' . $config['prefix'] . '-' . $database]['dsn']; + + $connection = $pool->get(); + $connections->add($connection, $pool); + $adapter = match ($dsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + $adapter->setDatabase($dsn->getPath()); + if ($adapter->ping()) { - Console::success('🟢 ' . str_pad("{$key}({$database})", 50, '.') . 'connected'); + Console::success('🟢 ' . str_pad("$key({$database})", 50, '.') . 'connected'); } else { - Console::error('🔴 ' . str_pad("{$key}({$database})", 47, '.') . 'disconnected'); + Console::error('🔴 ' . str_pad("$key({$database})", 47, '.') . 'disconnected'); } } catch (\Throwable $th) { Console::error('🔴 ' . str_pad("{$key}.({$database})", 47, '.') . 'disconnected'); @@ -148,25 +172,39 @@ class Doctor extends Action } } - $pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */ + /** @var array $pools */ + $pools = $register->get('pools'); $configs = [ - 'Cache' => Config::getParam('pools-cache'), - 'Queue' => Config::getParam('pools-queue'), - 'PubSub' => Config::getParam('pools-pubsub'), + 'Cache' => [ + 'prefix' => 'cache', + 'databases' => Config::getParam('pools-cache') + ], + 'Queue' => [ + 'prefix' => 'queue', + 'databases' => Config::getParam('pools-queue') + ], + 'PubSub' => [ + 'prefix' => 'pubsub', + 'databases' => Config::getParam('pools-pubsub') + ], ]; - foreach ($configs as $key => $config) { - foreach ($config as $pool) { + foreach ($config['databases'] as $database) { try { - $adapter = $pools->get($pool)->pop()->getResource(); + $pool = $pools['pools-' . $config['prefix'] . '-' . $database]['pool']; + $dsn = $pools['pools-' . $config['prefix'] . '-' . $database]['dsn']; + $connection = $pool->get(); + $connections->add($connection, $pool); + + $adapter = new Redis($dsn->getHost(), $dsn->getPort()); if ($adapter->ping()) { - Console::success('🟢 ' . str_pad("{$key}({$pool})", 50, '.') . 'connected'); + Console::success('🟢 ' . str_pad("{$key}({$database})", 50, '.') . 'connected'); } else { - Console::error('🔴 ' . str_pad("{$key}({$pool})", 47, '.') . 'disconnected'); + Console::error('🔴 ' . str_pad("{$key}({$database})", 47, '.') . 'disconnected'); } } catch (\Throwable $th) { - Console::error('🔴 ' . str_pad("{$key}({$pool})", 47, '.') . 'disconnected'); + Console::error('🔴 ' . str_pad("{$key}({$database})", 47, '.') . 'disconnected'); } } } @@ -258,7 +296,7 @@ class Doctor extends Action } try { - if (App::isProduction()) { + if (Http::isProduction()) { Console::log(''); $version = \json_decode(@\file_get_contents(System::getEnv('_APP_HOME', 'http://localhost') . '/version'), true); diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php index 4abd267684..a1a73e6385 100644 --- a/src/Appwrite/Platform/Tasks/Install.php +++ b/src/Appwrite/Platform/Tasks/Install.php @@ -8,9 +8,9 @@ use Appwrite\Docker\Env; use Appwrite\Utopia\View; use Utopia\CLI\Console; use Utopia\Config\Config; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Text; use Utopia\Platform\Action; -use Utopia\Validator\Boolean; -use Utopia\Validator\Text; class Install extends Action { @@ -213,8 +213,7 @@ class Install extends Action } $env = ''; - $stdout = ''; - $stderr = ''; + $output = ''; foreach ($input as $key => $value) { if ($value) { @@ -225,13 +224,13 @@ class Install extends Action $exit = 0; if (!$noStart) { Console::log("Running \"docker compose up -d --remove-orphans --renew-anon-volumes\""); - $exit = Console::execute("$env docker compose --project-directory $this->path up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr); + $exit = Console::execute("$env docker compose --project-directory $this->path up -d --remove-orphans --renew-anon-volumes", '', $output); } if ($exit !== 0) { $message = 'Failed to install Appwrite dockers'; Console::error($message); - Console::error($stderr); + Console::error($output); Console::exit($exit); } else { $message = 'Appwrite installed successfully'; diff --git a/src/Appwrite/Platform/Tasks/Migrate.php b/src/Appwrite/Platform/Tasks/Migrate.php index dcba59bb1d..55be0b81c2 100644 --- a/src/Appwrite/Platform/Tasks/Migrate.php +++ b/src/Appwrite/Platform/Tasks/Migrate.php @@ -10,10 +10,10 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Http\Validator\Text; use Utopia\Platform\Action; use Utopia\Registry\Registry; use Utopia\System\System; -use Utopia\Validator\Text; class Migrate extends Action { @@ -30,12 +30,15 @@ class Migrate extends Action ->desc('Migrate Appwrite to new version') /** @TODO APP_VERSION_STABLE needs to be defined */ ->param('version', APP_VERSION_STABLE, new Text(8), 'Version to migrate to.', true) + ->inject('cache') ->inject('dbForConsole') ->inject('getProjectDB') ->inject('register') - ->callback(function ($version, $dbForConsole, $getProjectDB, Registry $register) { - \Co\run(function () use ($version, $dbForConsole, $getProjectDB, $register) { - $this->action($version, $dbForConsole, $getProjectDB, $register); + ->inject('authorization') + ->inject('console') + ->callback(function ($version, $dbForConsole, $getProjectDB, Registry $register, Authorization $authorization, Document $console) { + \Co\run(function () use ($version, $dbForConsole, $getProjectDB, $register, $authorization, $console) { + $this->action($version, $dbForConsole, $getProjectDB, $register, $authorization, $console); }); }); } @@ -58,13 +61,12 @@ class Migrate extends Action } } - public function action(string $version, Database $dbForConsole, callable $getProjectDB, Registry $register) + public function action(string $version, Database $dbForConsole, callable $getProjectDB, Registry $register, Authorization $auth, Document $console) { - Authorization::disable(); + $auth->disable(); if (!array_key_exists($version, Migration::$versions)) { Console::error("Version {$version} not found."); Console::exit(1); - return; } @@ -77,12 +79,8 @@ class Migrate extends Action 10 ); - $app = new App('UTC'); - Console::success('Starting Data Migration to version ' . $version); - $console = $app->getResource('console'); - $limit = 30; $sum = 30; $offset = 0; @@ -101,7 +99,7 @@ class Migrate extends Action $class = 'Appwrite\\Migration\\Version\\' . Migration::$versions[$version]; /** @var Migration $migration */ - $migration = new $class(); + $migration = new $class($auth, ); while (!empty($projects)) { foreach ($projects as $project) { diff --git a/src/Appwrite/Platform/Tasks/QueueCount.php b/src/Appwrite/Platform/Tasks/QueueCount.php index b02165c1d2..63f829073a 100644 --- a/src/Appwrite/Platform/Tasks/QueueCount.php +++ b/src/Appwrite/Platform/Tasks/QueueCount.php @@ -3,11 +3,11 @@ namespace Appwrite\Platform\Tasks; use Utopia\CLI\Console; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Platform\Action; use Utopia\Queue\Client; use Utopia\Queue\Connection; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; class QueueCount extends Action { diff --git a/src/Appwrite/Platform/Tasks/QueueRetry.php b/src/Appwrite/Platform/Tasks/QueueRetry.php index b6139dc177..63f6c8e11e 100644 --- a/src/Appwrite/Platform/Tasks/QueueRetry.php +++ b/src/Appwrite/Platform/Tasks/QueueRetry.php @@ -3,11 +3,11 @@ namespace Appwrite\Platform\Tasks; use Utopia\CLI\Console; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\Wildcard; use Utopia\Platform\Action; use Utopia\Queue\Client; use Utopia\Queue\Connection; -use Utopia\Validator\Text; -use Utopia\Validator\Wildcard; class QueueRetry extends Action { diff --git a/src/Appwrite/Platform/Tasks/SSL.php b/src/Appwrite/Platform/Tasks/SSL.php index 5af0cb6cd8..ad4098d9ee 100644 --- a/src/Appwrite/Platform/Tasks/SSL.php +++ b/src/Appwrite/Platform/Tasks/SSL.php @@ -5,10 +5,10 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Event\Certificate; use Utopia\CLI\Console; use Utopia\Database\Document; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Hostname; use Utopia\Platform\Action; use Utopia\System\System; -use Utopia\Validator\Boolean; -use Utopia\Validator\Hostname; class SSL extends Action { diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 79a05dcd13..8dc037897d 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -2,44 +2,44 @@ namespace Appwrite\Platform\Tasks; +use Appwrite\Utopia\Queue\Connections; use Swoole\Timer; use Utopia\CLI\Console; -use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception; use Utopia\Database\Query; use Utopia\Platform\Action; -use Utopia\Pools\Group; use Utopia\System\System; -use function Swoole\Coroutine\run; - abstract class ScheduleBase extends Action { protected const UPDATE_TIMER = 10; //seconds protected const ENQUEUE_TIMER = 60; //seconds protected array $schedules = []; + protected Connections $connections; abstract public static function getName(): string; + abstract public static function getSupportedResource(): string; abstract protected function enqueueResources( - Group $pools, - Database $dbForConsole + array $pools, + callable $getConsoleDB ); public function __construct() { + $this->connections = new Connections(); $type = static::getSupportedResource(); $this ->desc("Execute {$type}s scheduled in Appwrite") ->inject('pools') - ->inject('dbForConsole') + ->inject('getConsoleDB') ->inject('getProjectDB') - ->callback(fn (Group $pools, Database $dbForConsole, callable $getProjectDB) => $this->action($pools, $dbForConsole, $getProjectDB)); + ->callback(fn (array $pools, callable $getConsoleDB, callable $getProjectDB) => $this->action($pools, $getConsoleDB, $getProjectDB)); } /** @@ -47,11 +47,12 @@ abstract class ScheduleBase extends Action * 2. Create timer that sync all changes from 'schedules' collection to local copy. Only reading changes thanks to 'resourceUpdatedAt' attribute * 3. Create timer that prepares coroutines for soon-to-execute schedules. When it's ready, coroutine sleeps until exact time before sending request to worker. */ - public function action(Group $pools, Database $dbForConsole, callable $getProjectDB): void + public function action(array $pools, callable $getConsoleDB, callable $getProjectDB): void { Console::title(\ucfirst(static::getSupportedResource()) . ' scheduler V1'); Console::success(APP_NAME . ' ' . \ucfirst(static::getSupportedResource()) . ' scheduler v1 has started'); + [$_, $_, $dbForConsole] = $getConsoleDB(); /** * Extract only necessary attributes to lower memory used. * @@ -127,76 +128,74 @@ abstract class ScheduleBase extends Action $latestDocument = \end($results); } - $pools->reclaim(); Console::success("{$total} resources were loaded in " . (\microtime(true) - $loadStart) . " seconds"); Console::success("Starting timers at " . DateTime::now()); - run(function () use ($dbForConsole, &$lastSyncUpdate, $getSchedule, $pools) { - /** - * The timer synchronize $schedules copy with database collection. - */ - Timer::tick(static::UPDATE_TIMER * 1000, function () use ($dbForConsole, &$lastSyncUpdate, $getSchedule, $pools) { - $time = DateTime::now(); - $timerStart = \microtime(true); - $limit = 1000; - $sum = $limit; - $total = 0; - $latestDocument = null; + Timer::tick(static::UPDATE_TIMER * 1000, function () use ($getConsoleDB, &$lastSyncUpdate, $getSchedule, $pools) { + [$connection,$pool, $dbForConsole] = $getConsoleDB(); + $connections = new Connections(); + $connections->add($connection, $pool); - Console::log("Sync tick: Running at $time"); + $time = DateTime::now(); + $timerStart = \microtime(true); - while ($sum === $limit) { - $paginationQueries = [Query::limit($limit)]; + $limit = 1000; + $sum = $limit; + $total = 0; + $latestDocument = null; - if ($latestDocument) { - $paginationQueries[] = Query::cursorAfter($latestDocument); - } + Console::log("Sync tick: Running at $time"); - $results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [ - Query::equal('region', [System::getEnv('_APP_REGION', 'default')]), - Query::equal('resourceType', [static::getSupportedResource()]), - Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate), - ])); + while ($sum === $limit) { + $paginationQueries = [Query::limit($limit)]; - $sum = count($results); - $total = $total + $sum; - - foreach ($results as $document) { - $localDocument = $this->schedules[$document->getInternalId()] ?? null; - - // Check if resource has been updated since last sync - $org = $localDocument !== null ? \strtotime($localDocument['resourceUpdatedAt']) : null; - $new = \strtotime($document['resourceUpdatedAt']); - - if (!$document['active']) { - Console::info("Removing: {$document['resourceId']}"); - unset($this->schedules[$document->getInternalId()]); - } elseif ($new !== $org) { - Console::info("Updating: {$document['resourceId']}"); - $this->schedules[$document->getInternalId()] = $getSchedule($document); - } - } - - $latestDocument = \end($results); + if ($latestDocument) { + $paginationQueries[] = Query::cursorAfter($latestDocument); } - $lastSyncUpdate = $time; - $timerEnd = \microtime(true); + $results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [ + Query::equal('region', [System::getEnv('_APP_REGION', 'default')]), + Query::equal('resourceType', [static::getSupportedResource()]), + Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate), + ])); - $pools->reclaim(); + $sum = count($results); + $total = $total + $sum; - Console::log("Sync tick: {$total} schedules were updated in " . ($timerEnd - $timerStart) . " seconds"); - }); + foreach ($results as $document) { + $localDocument = $this->schedules[$document->getInternalId()] ?? null; - Timer::tick( - static::ENQUEUE_TIMER * 1000, - fn () => $this->enqueueResources($pools, $dbForConsole) - ); + // Check if resource has been updated since last sync + $org = $localDocument !== null ? \strtotime($localDocument['resourceUpdatedAt']) : null; + $new = \strtotime($document['resourceUpdatedAt']); - $this->enqueueResources($pools, $dbForConsole); + if (!$document['active']) { + Console::info("Removing: {$document['resourceId']}"); + unset($this->schedules[$document->getInternalId()]); + } elseif ($new !== $org) { + Console::info("Updating: {$document['resourceId']}"); + $this->schedules[$document->getInternalId()] = $getSchedule($document); + } + } + + $latestDocument = \end($results); + } + + $lastSyncUpdate = $time; + $timerEnd = \microtime(true); + + $connections->reclaim(); + Console::log("Sync tick: {$total} schedules were updated in " . ($timerEnd - $timerStart) . " seconds"); }); + + Timer::tick( + static::ENQUEUE_TIMER * 1000, + fn () => $this->enqueueResources($pools, $getConsoleDB) + ); + + $this->enqueueResources($pools, $getConsoleDB); } } diff --git a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php index 682d796585..b67a892b2e 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php @@ -4,8 +4,7 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Event\Func; use Swoole\Coroutine as Co; -use Utopia\Database\Database; -use Utopia\Pools\Group; +use Utopia\Queue\Connection\Redis; class ScheduleExecutions extends ScheduleBase { @@ -22,11 +21,16 @@ class ScheduleExecutions extends ScheduleBase return 'execution'; } - protected function enqueueResources(Group $pools, Database $dbForConsole): void + protected function enqueueResources(array $pools, callable $getConsoleDB): void { - $queue = $pools->get('queue')->pop(); - $connection = $queue->getResource(); - $queueForFunctions = new Func($connection); + [$connection,$pool, $dbForConsole] = $getConsoleDB(); + $this->connections->add($connection, $pool); + + $queuePool = $pools['pools-queue-queue']['pool']; + $queueConnection = $queuePool->get(); + $this->connections->add($queueConnection, $queuePool); + + $queueForFunctions = new Func(new Redis($queueConnection)); $intervalEnd = (new \DateTime())->modify('+' . self::ENQUEUE_TIMER . ' seconds'); foreach ($this->schedules as $schedule) { @@ -52,7 +56,7 @@ class ScheduleExecutions extends ScheduleBase $delay = $scheduledAt->getTimestamp() - (new \DateTime())->getTimestamp(); - \go(function () use ($queueForFunctions, $schedule, $delay, $data) { + \go(function () use ($queueForFunctions, $schedule, $delay, $data, $dbForConsole) { Co::sleep($delay); $queueForFunctions->setType('schedule') @@ -67,16 +71,16 @@ class ScheduleExecutions extends ScheduleBase ->setProject($schedule['project']) ->setUserId($data['userId'] ?? '') ->trigger(); - }); - $dbForConsole->deleteDocument( - 'schedules', - $schedule['$id'], - ); + $dbForConsole->deleteDocument( + 'schedules', + $schedule['$id'], + ); + }); unset($this->schedules[$schedule['$internalId']]); } - $queue->reclaim(); + $this->connections->reclaim(); } } diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php index e2c278714f..450551400e 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php @@ -5,9 +5,8 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Event\Func; use Cron\CronExpression; use Utopia\CLI\Console; -use Utopia\Database\Database; use Utopia\Database\DateTime; -use Utopia\Pools\Group; +use Utopia\Queue\Connection\Redis; class ScheduleFunctions extends ScheduleBase { @@ -26,7 +25,7 @@ class ScheduleFunctions extends ScheduleBase return 'function'; } - protected function enqueueResources(Group $pools, Database $dbForConsole): void + protected function enqueueResources(array $pools, callable $getConsoleDB): void { $timerStart = \microtime(true); $time = DateTime::now(); @@ -68,8 +67,11 @@ class ScheduleFunctions extends ScheduleBase \go(function () use ($delay, $scheduleKeys, $pools) { \sleep($delay); // in seconds - $queue = $pools->get('queue')->pop(); - $connection = $queue->getResource(); + $pool = $pools['pools-queue-queue']['pool']; + $connection = $pool->get(); + $this->connections->add($connection, $pool); + + $queueConnection = new Redis($connection); foreach ($scheduleKeys as $scheduleKey) { // Ensure schedule was not deleted @@ -79,7 +81,7 @@ class ScheduleFunctions extends ScheduleBase $schedule = $this->schedules[$scheduleKey]; - $queueForFunctions = new Func($connection); + $queueForFunctions = new Func($queueConnection); $queueForFunctions ->setType('schedule') @@ -90,7 +92,8 @@ class ScheduleFunctions extends ScheduleBase ->trigger(); } - $queue->reclaim(); + $this->connections->reclaim(); + // $queue->reclaim(); // TODO: Do in try/catch/finally, or add to connectons resource }); } diff --git a/src/Appwrite/Platform/Tasks/ScheduleMessages.php b/src/Appwrite/Platform/Tasks/ScheduleMessages.php index 167f1282ed..72e1a5f786 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleMessages.php +++ b/src/Appwrite/Platform/Tasks/ScheduleMessages.php @@ -3,8 +3,7 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Event\Messaging; -use Utopia\Database\Database; -use Utopia\Pools\Group; +use Utopia\Queue\Connection\Redis; class ScheduleMessages extends ScheduleBase { @@ -21,8 +20,11 @@ class ScheduleMessages extends ScheduleBase return 'message'; } - protected function enqueueResources(Group $pools, Database $dbForConsole): void + protected function enqueueResources(array $pools, callable $getConsoleDB): void { + [$connection,$pool, $dbForConsole] = $getConsoleDB(); + $this->connections->add($connection, $pool); + foreach ($this->schedules as $schedule) { if (!$schedule['active']) { continue; @@ -35,10 +37,15 @@ class ScheduleMessages extends ScheduleBase continue; } - \go(function () use ($schedule, $pools, $dbForConsole) { - $queue = $pools->get('queue')->pop(); - $connection = $queue->getResource(); - $queueForMessaging = new Messaging($connection); + \go(function () use ($now, $schedule, $pools, $dbForConsole) { + $pool = $pools['pools-queue-queue']['pool']; + $dsn = $pools['pools-queue-queue']['dsn']; + $connection = $pool->get(); + $this->connections->add($connection, $pool); + + $queueConnection = new Redis($dsn->getHost(), $dsn->getPort()); + + $queueForMessaging = new Messaging($queueConnection); $queueForMessaging ->setType(MESSAGE_SEND_TYPE_EXTERNAL) @@ -51,8 +58,7 @@ class ScheduleMessages extends ScheduleBase $schedule['$id'], ); - $queue->reclaim(); - + $this->connections->reclaim(); unset($this->schedules[$schedule['$internalId']]); }); } diff --git a/src/Appwrite/Platform/Tasks/Specs.php b/src/Appwrite/Platform/Tasks/Specs.php index e171f2f405..6e69798d98 100644 --- a/src/Appwrite/Platform/Tasks/Specs.php +++ b/src/Appwrite/Platform/Tasks/Specs.php @@ -6,21 +6,26 @@ use Appwrite\Specification\Format\OpenAPI3; use Appwrite\Specification\Format\Swagger2; use Appwrite\Specification\Specification; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Models; use Exception; -use Swoole\Http\Response as HttpResponse; -use Utopia\App; +use Swoole\Http\Request as SwooleHttpRequest; +use Swoole\Http\Response as SwooleHttpResponse; use Utopia\Cache\Adapter\None; use Utopia\Cache\Cache; use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Adapter\MySQL; use Utopia\Database\Database; +use Utopia\DI\Container; +use Utopia\DI\Dependency; +use Utopia\Http\Adapter\FPM\Server; +use Utopia\Http\Adapter\Swoole\Request; +use Utopia\Http\Adapter\Swoole\Response as HttpResponse; +use Utopia\Http\Http; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Platform\Action; -use Utopia\Registry\Registry; -use Utopia\Request; use Utopia\System\System; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; class Specs extends Action { @@ -35,21 +40,32 @@ class Specs extends Action ->desc('Generate Appwrite API specifications') ->param('version', 'latest', new Text(16), 'Spec version', true) ->param('mode', 'normal', new WhiteList(['normal', 'mocks']), 'Spec Mode', true) - ->inject('register') - ->callback(fn (string $version, string $mode, Registry $register) => $this->action($version, $mode, $register)); + ->inject('context') + ->callback(fn (string $version, string $mode, Container $context) => $this->action($version, $mode, $context)); } - public function action(string $version, string $mode, Registry $register): void + public function action(string $version, string $mode, Container $container): void { - $appRoutes = App::getRoutes(); - $response = new Response(new HttpResponse()); + $appRoutes = Http::getRoutes(); + $response = new Response(new HttpResponse(new SwooleHttpResponse())); $mocks = ($mode === 'mocks'); + $requestDependency = new Dependency(); + $responseDependency = new Dependency(); + $dbForConsole = new Dependency(); + $dbForProject = new Dependency(); + // Mock dependencies - App::setResource('request', fn () => new Request()); - App::setResource('response', fn () => $response); - App::setResource('dbForConsole', fn () => new Database(new MySQL(''), new Cache(new None()))); - App::setResource('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None()))); + $requestDependency->setName('request')->setCallback(fn () => new Request(new SwooleHttpRequest())); + $responseDependency->setName('response')->setCallback(fn () => $response); + $dbForConsole->setName('dbForConsole')->setCallback(fn () => new Database(new MySQL(''), new Cache(new None()))); + $dbForProject->setName('dbForProject')->setCallback(fn () => new Database(new MySQL(''), new Cache(new None()))); + + $container + ->set($requestDependency) + ->set($responseDependency) + ->set($dbForProject) + ->set($dbForConsole); $platforms = [ 'client' => APP_PLATFORM_CLIENT, @@ -241,7 +257,7 @@ class Specs extends Action ]; } - $models = $response->getModels(); + $models = Models::getModels(); foreach ($models as $key => $value) { if ($platform !== APP_PLATFORM_CONSOLE && !$value->isPublic()) { @@ -249,7 +265,7 @@ class Specs extends Action } } - $arguments = [new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0]; + $arguments = [new Http(new Server(), $container, 'UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0]; foreach (['swagger2', 'open-api3'] as $format) { $formatInstance = match ($format) { 'swagger2' => new Swagger2(...$arguments), diff --git a/src/Appwrite/Platform/Tasks/Upgrade.php b/src/Appwrite/Platform/Tasks/Upgrade.php index 341ce42fc4..608b924c06 100644 --- a/src/Appwrite/Platform/Tasks/Upgrade.php +++ b/src/Appwrite/Platform/Tasks/Upgrade.php @@ -3,8 +3,8 @@ namespace Appwrite\Platform\Tasks; use Utopia\CLI\Console; -use Utopia\Validator\Boolean; -use Utopia\Validator\Text; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Text; class Upgrade extends Install { diff --git a/src/Appwrite/Platform/Workers/Audits.php b/src/Appwrite/Platform/Workers/Audits.php index 86ca59d3fd..d21f1c267d 100644 --- a/src/Appwrite/Platform/Workers/Audits.php +++ b/src/Appwrite/Platform/Workers/Audits.php @@ -9,6 +9,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Authorization; use Utopia\Database\Exception\Structure; +use Utopia\Database\Validator\Authorization as ValidatorAuthorization; use Utopia\Platform\Action; use Utopia\Queue\Message; @@ -28,7 +29,8 @@ class Audits extends Action ->desc('Audits worker') ->inject('message') ->inject('dbForProject') - ->callback(fn ($message, $dbForProject) => $this->action($message, $dbForProject)); + ->inject('authorization') + ->callback(fn ($message, $dbForProject, ValidatorAuthorization $authorization) => $this->action($message, $dbForProject, $authorization)); } @@ -41,7 +43,7 @@ class Audits extends Action * @throws Authorization * @throws Structure */ - public function action(Message $message, Database $dbForProject): void + public function action(Message $message, Database $dbForProject, ValidatorAuthorization $auth): void { $payload = $message->getPayload() ?? []; diff --git a/src/Appwrite/Platform/Workers/Builds.php b/src/Appwrite/Platform/Workers/Builds.php index 5dd2f7f886..9274e7430c 100644 --- a/src/Appwrite/Platform/Workers/Builds.php +++ b/src/Appwrite/Platform/Workers/Builds.php @@ -54,7 +54,8 @@ class Builds extends Action ->inject('dbForProject') ->inject('deviceForFunctions') ->inject('log') - ->callback(fn ($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log)); + ->inject('authorization') + ->callback(fn ($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log, Authorization $authorization) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log, $authorization)); } /** @@ -67,10 +68,11 @@ class Builds extends Action * @param Database $dbForProject * @param Device $deviceForFunctions * @param Log $log + * @param Authorization $auth * @return void * @throws \Utopia\Database\Exception */ - public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void + public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log, Authorization $auth): void { $payload = $message->getPayload() ?? []; @@ -92,7 +94,7 @@ class Builds extends Action case BUILD_TYPE_RETRY: Console::info('Creating build for deployment: ' . $deployment->getId()); $github = new GitHub($cache); - $this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log); + $this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log, $auth); break; default: @@ -113,11 +115,12 @@ class Builds extends Action * @param Document $deployment * @param Document $template * @param Log $log + * @param Authorization $auth * @return void * @throws \Utopia\Database\Exception * @throws Exception */ - protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void + protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log, Authorization $auth): void { $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); @@ -221,20 +224,18 @@ class Builds extends Action $templateRootDirectory = \ltrim($templateRootDirectory, '/'); if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateVersion)) { - $stdout = ''; - $stderr = ''; - + $output = ''; // Clone template repo $tmpTemplateDirectory = '/tmp/builds/' . $buildId . '-template'; $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory); - $exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr); + $exit = Console::execute($gitCloneCommandForTemplate, '', $output); if ($exit !== 0) { - throw new \Exception('Unable to clone code repository: ' . $stderr); + throw new \Exception('Unable to clone code repository: ' . $output); } // Ensure directories - Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $stdout, $stderr); + Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $output); $tmpPathFile = $tmpTemplateDirectory . '/code.tar.gz'; @@ -245,7 +246,7 @@ class Builds extends Action } $tarParamDirectory = \escapeshellarg($tmpTemplateDirectory . (empty($templateRootDirectory) ? '' : '/' . $templateRootDirectory)); - Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax + Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $output); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax $source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); $result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions); @@ -254,7 +255,7 @@ class Builds extends Action throw new \Exception("Unable to move file"); } - Console::execute('rm -rf ' . \escapeshellarg($tmpTemplateDirectory), '', $stdout, $stderr); + Console::execute('rm -rf ' . \escapeshellarg($tmpTemplateDirectory), '', $output); $directorySize = $deviceForFunctions->getFileSize($source); $build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source)); @@ -276,6 +277,7 @@ class Builds extends Action $branchName = $deployment->getAttribute('providerBranch'); $commitHash = $deployment->getAttribute('providerCommitHash', ''); + $output = ''; $cloneVersion = $branchName; $cloneType = GitHub::CLONE_TYPE_BRANCH; @@ -283,22 +285,18 @@ class Builds extends Action $cloneVersion = $commitHash; $cloneType = GitHub::CLONE_TYPE_COMMIT; } - $gitCloneCommand = $github->generateCloneCommand($cloneOwner, $cloneRepository, $cloneVersion, $cloneType, $tmpDirectory, $rootDirectory); - $stdout = ''; - $stderr = ''; - - Console::execute('mkdir -p ' . \escapeshellarg('/tmp/builds/' . $buildId), '', $stdout, $stderr); + Console::execute('mkdir -p ' . \escapeshellarg('/tmp/builds/' . $buildId), '', $output); if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') { Console::info('Build has been canceled'); return; } - $exit = Console::execute($gitCloneCommand, '', $stdout, $stderr); + $exit = Console::execute($gitCloneCommand, '', $output); if ($exit !== 0) { - throw new \Exception('Unable to clone code repository: ' . $stderr); + throw new \Exception('Unable to clone code repository: ' . $output); } // Local refactoring for function folder with spaces @@ -306,10 +304,10 @@ class Builds extends Action $rootDirectoryWithoutSpaces = str_replace(' ', '', $rootDirectory); $from = $tmpDirectory . '/' . $rootDirectory; $to = $tmpDirectory . '/' . $rootDirectoryWithoutSpaces; - $exit = Console::execute('mv "' . \escapeshellarg($from) . '" "' . \escapeshellarg($to) . '"', '', $stdout, $stderr); + $exit = Console::execute('mv "' . \escapeshellarg($from) . '" "' . \escapeshellarg($to) . '"', '', $output); if ($exit !== 0) { - throw new \Exception('Unable to move function with spaces' . $stderr); + throw new \Exception('Unable to move function with spaces' . $output); } $rootDirectory = $rootDirectoryWithoutSpaces; } @@ -330,33 +328,33 @@ class Builds extends Action $tmpTemplateDirectory = '/tmp/builds/' . $buildId . '/template'; $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory); - $exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr); + $exit = Console::execute($gitCloneCommandForTemplate, '', $output); if ($exit !== 0) { - throw new \Exception('Unable to clone code repository: ' . $stderr); + throw new \Exception('Unable to clone code repository: ' . $output); } // Ensure directories - Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $stdout, $stderr); - Console::execute('mkdir -p ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $stdout, $stderr); + Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $output); + Console::execute('mkdir -p ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $output); // Merge template into user repo - Console::execute('rsync -av --exclude \'.git\' ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory . '/') . ' ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $stdout, $stderr); + Console::execute('rsync -av --exclude \'.git\' ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory . '/') . ' ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $output); // Commit and push - $exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . \escapeshellarg($tmpDirectory) . ' && git add . && git commit -m "Create ' . \escapeshellarg($function->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $stdout, $stderr); + $exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . \escapeshellarg($tmpDirectory) . ' && git add . && git commit -m "Create ' . \escapeshellarg($function->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $output); if ($exit !== 0) { - throw new \Exception('Unable to push code repository: ' . $stderr); + throw new \Exception('Unable to push code repository: ' . $output); } - $exit = Console::execute('cd ' . \escapeshellarg($tmpDirectory) . ' && git rev-parse HEAD', '', $stdout, $stderr); + $exit = Console::execute('cd ' . \escapeshellarg($tmpDirectory) . ' && git rev-parse HEAD', '', $output); if ($exit !== 0) { - throw new \Exception('Unable to get vcs commit SHA: ' . $stderr); + throw new \Exception('Unable to get vcs commit SHA: ' . $output); } - $providerCommitHash = \trim($stdout); + $providerCommitHash = \trim($output); $authorUrl = "https://github.com/$cloneOwner"; $deployment->setAttribute('providerCommitHash', $providerCommitHash ?? ''); @@ -399,7 +397,7 @@ class Builds extends Action } $tarParamDirectory = '/tmp/builds/' . $buildId . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory); - Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax + Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $output); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax $source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); $result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions); @@ -408,7 +406,7 @@ class Builds extends Action throw new \Exception("Unable to move file"); } - Console::execute('rm -rf ' . \escapeshellarg($tmpPath), '', $stdout, $stderr); + Console::execute('rm -rf ' . \escapeshellarg($tmpPath), '', $output); $build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source)); @@ -663,7 +661,7 @@ class Builds extends Action ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $auth->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); } catch (\Throwable $th) { if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') { Console::info('Build has been canceled'); diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index 58dc1dd28a..4a8d928ba2 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -11,7 +11,6 @@ use Appwrite\Template\Template; use Appwrite\Utopia\Response\Model\Rule; use Exception; use Throwable; -use Utopia\App; use Utopia\CLI\Console; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -22,6 +21,7 @@ use Utopia\Database\Exception\Structure; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Domains\Domain; +use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\Logger\Log; use Utopia\Platform\Action; @@ -330,30 +330,26 @@ class Certificates extends Action * * @param string $folder Folder into which certificates should be generated * @param string $domain Domain to generate certificate for - * @return array Named array with keys 'stdout' and 'stderr', both string + * @return string output * @throws Exception */ - private function issueCertificate(string $folder, string $domain, string $email): array + private function issueCertificate(string $folder, string $domain, string $email): string { - $stdout = ''; - $stderr = ''; + $output = ''; - $staging = (App::isProduction()) ? '' : ' --dry-run'; + $staging = (Http::isProduction()) ? '' : ' --dry-run'; $exit = Console::execute("certbot certonly -v --webroot --noninteractive --agree-tos{$staging}" . " --email " . $email . " --cert-name " . $folder . " -w " . APP_STORAGE_CERTIFICATES - . " -d {$domain}", '', $stdout, $stderr); + . " -d {$domain}", '', $output); // Unexpected error, usually 5XX, API limits, ... if ($exit !== 0) { - throw new Exception('Failed to issue a certificate with message: ' . $stderr); + throw new Exception('Failed to issue a certificate with message: ' . $output); } - return [ - 'stdout' => $stdout, - 'stderr' => $stderr - ]; + return $output; } /** @@ -381,7 +377,7 @@ class Certificates extends Action * @return void * @throws Exception */ - private function applyCertificateFiles(string $folder, string $domain, array $letsEncryptData): void + private function applyCertificateFiles(string $folder, string $domain, string $letsEncryptData): void { // Prepare folder in storage for domain @@ -394,19 +390,19 @@ class Certificates extends Action // Move generated files if (!@\rename('/etc/letsencrypt/live/' . $folder . '/cert.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem')) { - throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $letsEncryptData); } if (!@\rename('/etc/letsencrypt/live/' . $folder . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/chain.pem')) { - throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $letsEncryptData); } if (!@\rename('/etc/letsencrypt/live/' . $folder . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/fullchain.pem')) { - throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $letsEncryptData); } if (!@\rename('/etc/letsencrypt/live/' . $folder . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/privkey.pem')) { - throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $letsEncryptData); } $config = \implode(PHP_EOL, [ diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 4f8953a270..478b8bb80d 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -22,6 +22,7 @@ use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization as ValidatorAuthorization; use Utopia\DSN\DSN; use Utopia\Logger\Log; use Utopia\Platform\Action; @@ -54,14 +55,15 @@ class Deletes extends Action ->inject('executionRetention') ->inject('auditRetention') ->inject('log') - ->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log) => $this->action($message, $dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $abuseRetention, $executionRetention, $auditRetention, $log)); + ->inject('authorization') + ->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log, ValidatorAuthorization $authorization) => $this->action($message, $dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $abuseRetention, $executionRetention, $auditRetention, $log, $authorization)); } /** * @throws Exception * @throws Throwable */ - public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log): void + public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log, ValidatorAuthorization $auth): void { $payload = $message->getPayload() ?? []; @@ -117,7 +119,7 @@ class Deletes extends Action break; case DELETE_TYPE_AUDIT: if (!$project->isEmpty()) { - $this->deleteAuditLogs($project, $getProjectDB, $auditRetention); + $this->deleteAuditLogs($project, $getProjectDB, $auditRetention, $auth); } if (!$document->isEmpty()) { @@ -125,7 +127,7 @@ class Deletes extends Action } break; case DELETE_TYPE_ABUSE: - $this->deleteAbuseLogs($project, $getProjectDB, $abuseRetention); + $this->deleteAbuseLogs($project, $getProjectDB, $abuseRetention, $auth); break; case DELETE_TYPE_REALTIME: $this->deleteRealtimeUsage($dbForConsole, $datetime); @@ -693,7 +695,7 @@ class Deletes extends Action * @return void * @throws Exception */ - private function deleteAbuseLogs(Document $project, callable $getProjectDB, string $abuseRetention): void + private function deleteAbuseLogs(Document $project, callable $getProjectDB, string $abuseRetention, ValidatorAuthorization $auth): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); @@ -714,7 +716,7 @@ class Deletes extends Action * @return void * @throws Exception */ - private function deleteAuditLogs(Document $project, callable $getProjectDB, string $auditRetention): void + private function deleteAuditLogs(Document $project, callable $getProjectDB, string $auditRetention, ValidatorAuthorization $auth): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php index e48f96ea90..51bdb56cf0 100644 --- a/src/Appwrite/Platform/Workers/Mails.php +++ b/src/Appwrite/Platform/Workers/Mails.php @@ -84,13 +84,13 @@ class Mails extends Action $bodyTemplate = __DIR__ . '/../../../../app/config/locale/templates/email-base.tpl'; } $bodyTemplate = Template::fromFile($bodyTemplate); - $bodyTemplate->setParam('{{body}}', $body, escapeHtml: false); + $bodyTemplate->setParam('{{body}}', $body, escape: false); foreach ($variables as $key => $value) { // TODO: hotfix for redirect param - $bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: $key !== 'redirect'); + $bodyTemplate->setParam('{{' . $key . '}}', $value, escape: $key !== 'redirect'); } foreach ($this->richTextParams as $key => $value) { - $bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: false); + $bodyTemplate->setParam('{{' . $key . '}}', $value, escape: false); } $body = $bodyTemplate->render(); diff --git a/src/Appwrite/Promises/Promise.php b/src/Appwrite/Promises/Promise.php index a6b1aa79d5..f12590dfed 100644 --- a/src/Appwrite/Promises/Promise.php +++ b/src/Appwrite/Promises/Promise.php @@ -12,7 +12,7 @@ abstract class Promise private mixed $result; - public function __construct(?callable $executor = null) + final public function __construct(?callable $executor = null) { if (\is_null($executor)) { return; diff --git a/src/Appwrite/Promises/Swoole.php b/src/Appwrite/Promises/Swoole.php index c258ef6a5e..8cddd567f3 100644 --- a/src/Appwrite/Promises/Swoole.php +++ b/src/Appwrite/Promises/Swoole.php @@ -6,23 +6,16 @@ use Swoole\Coroutine\Channel; class Swoole extends Promise { - public function __construct(?callable $executor = null) - { - parent::__construct($executor); - } - protected function execute( callable $executor, callable $resolve, callable $reject ): void { - \go(function () use ($executor, $resolve, $reject) { - try { - $executor($resolve, $reject); - } catch (\Throwable $exception) { - $reject($exception); - } - }); + try { + $executor($resolve, $reject); + } catch (\Throwable $exception) { + $reject($exception); + } } /** diff --git a/src/Appwrite/Specification/Format.php b/src/Appwrite/Specification/Format.php index 30ce6470e1..e2048971be 100644 --- a/src/Appwrite/Specification/Format.php +++ b/src/Appwrite/Specification/Format.php @@ -3,13 +3,13 @@ namespace Appwrite\Specification; use Appwrite\Utopia\Response\Model; -use Utopia\App; use Utopia\Config\Config; -use Utopia\Route; +use Utopia\Http\Http; +use Utopia\Http\Route; abstract class Format { - protected App $app; + protected Http $http; /** * @var Route[] @@ -50,9 +50,9 @@ abstract class Format ] ]; - public function __construct(App $app, array $services, array $routes, array $models, array $keys, int $authCount) + public function __construct(Http $http, array $services, array $routes, array $models, array $keys, int $authCount) { - $this->app = $app; + $this->http = $http; $this->services = $services; $this->routes = $routes; $this->models = $models; diff --git a/src/Appwrite/Specification/Format/OpenAPI3.php b/src/Appwrite/Specification/Format/OpenAPI3.php index 3074d59b7c..c9186ee0ac 100644 --- a/src/Appwrite/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/Specification/Format/OpenAPI3.php @@ -7,11 +7,11 @@ use Appwrite\Template\Template; use Appwrite\Utopia\Response\Model; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; -use Utopia\Validator; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Nullable; -use Utopia\Validator\Range; -use Utopia\Validator\WhiteList; +use Utopia\Http\Validator; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Nullable; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\WhiteList; class OpenAPI3 extends Format { @@ -238,8 +238,11 @@ class OpenAPI3 extends Format } if ($route->getLabel('sdk.response.code', 500) === 204) { - $temp['responses'][(string)$route->getLabel('sdk.response.code', '500')]['description'] = 'No content'; - unset($temp['responses'][(string)$route->getLabel('sdk.response.code', '500')]['schema']); + $labelCode = (string)$route->getLabel('sdk.response.code', '500'); + $temp['responses'][$labelCode]['description'] = 'No content'; + if (isset($temp['responses'][$labelCode]['schema'])) { + unset($temp['responses'][$labelCode]['schema']); + } } if ((!empty($scope))) { // && 'public' != $scope @@ -269,10 +272,10 @@ class OpenAPI3 extends Format $bodyRequired = []; foreach ($route->getParams() as $name => $param) { // Set params - /** - * @var \Utopia\Validator $validator - */ - $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator']; + $injections = array_map(fn ($injection) => $this->http->getContainer()->get($injection), $param['injections'] ?? []); + + /** @var Validator $validator */ + $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $injections) : $param['validator']; $node = [ 'name' => $name, @@ -289,11 +292,11 @@ class OpenAPI3 extends Format switch ((!empty($validator)) ? \get_class($validator) : '') { case 'Utopia\Database\Validator\UID': - case 'Utopia\Validator\Text': + case 'Utopia\Http\Validator\Text': $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; - case 'Utopia\Validator\Boolean': + case 'Utopia\Http\Validator\Boolean': $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = false; break; @@ -314,15 +317,15 @@ class OpenAPI3 extends Format $node['schema']['format'] = 'email'; $node['schema']['x-example'] = 'email@example.com'; break; - case 'Utopia\Validator\Host': - case 'Utopia\Validator\URL': + case 'Utopia\Http\Validator\Host': + case 'Utopia\Http\Validator\URL': $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'url'; $node['schema']['x-example'] = 'https://example.com'; break; - case 'Utopia\Validator\JSON': - case 'Utopia\Validator\Mock': - case 'Utopia\Validator\Assoc': + case 'Utopia\Http\Validator\JSON': + case 'Utopia\Http\Validator\Mock': + case 'Utopia\Http\Validator\Assoc': $param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default']; $node['schema']['type'] = 'object'; $node['schema']['x-example'] = '{}'; @@ -332,7 +335,7 @@ class OpenAPI3 extends Format $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'binary'; break; - case 'Utopia\Validator\ArrayList': + case 'Utopia\Http\Validator\ArrayList': /** @var ArrayList $validator */ $node['schema']['type'] = 'array'; $node['schema']['items'] = [ @@ -394,25 +397,25 @@ class OpenAPI3 extends Format $node['schema']['format'] = 'phone'; $node['schema']['x-example'] = '+12065550100'; // In the US, 555 is reserved like example.com break; - case 'Utopia\Validator\Range': + case 'Utopia\Http\Validator\Range': /** @var Range $validator */ $node['schema']['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType(); $node['schema']['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float'; $node['schema']['x-example'] = $validator->getMin(); break; - case 'Utopia\Validator\Numeric': - case 'Utopia\Validator\Integer': + case 'Utopia\Http\Validator\Numeric': + case 'Utopia\Http\Validator\Integer': $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'int32'; break; - case 'Utopia\Validator\FloatValidator': + case 'Utopia\Http\Validator\FloatValidator': $node['schema']['type'] = 'number'; $node['schema']['format'] = 'float'; break; - case 'Utopia\Validator\Length': + case 'Utopia\Http\Validator\Length': $node['schema']['type'] = $validator->getType(); break; - case 'Utopia\Validator\WhiteList': + case 'Utopia\Http\Validator\WhiteList': /** @var WhiteList $validator */ $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = $validator->getList()[0]; diff --git a/src/Appwrite/Specification/Format/Swagger2.php b/src/Appwrite/Specification/Format/Swagger2.php index 2eab7807b3..81e0f2c41d 100644 --- a/src/Appwrite/Specification/Format/Swagger2.php +++ b/src/Appwrite/Specification/Format/Swagger2.php @@ -7,10 +7,10 @@ use Appwrite\Template\Template; use Appwrite\Utopia\Response\Model; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; -use Utopia\Validator; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Nullable; -use Utopia\Validator\Range; +use Utopia\Http\Validator; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Nullable; +use Utopia\Http\Validator\Range; class Swagger2 extends Format { @@ -270,8 +270,10 @@ class Swagger2 extends Format ); foreach ($parameters as $name => $param) { // Set params + $injections = array_map(fn ($injection) => $this->http->getContainer()->get($injection), $param['injections'] ?? []); + /** @var Validator $validator */ - $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator']; + $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $injections) : $param['validator']; $node = [ 'name' => $name, @@ -287,18 +289,18 @@ class Swagger2 extends Format } $validatorClass = (!empty($validator)) ? \get_class($validator) : ''; - if ($validatorClass === 'Utopia\Validator\AnyOf') { + if ($validatorClass === 'Utopia\Http\Validator\AnyOf') { $validator = $param['validator']->getValidators()[0]; $validatorClass = \get_class($validator); } switch ($validatorClass) { - case 'Utopia\Validator\Text': + case 'Utopia\Http\Validator\Text': case 'Utopia\Database\Validator\UID': $node['type'] = $validator->getType(); $node['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; - case 'Utopia\Validator\Boolean': + case 'Utopia\Http\Validator\Boolean': $node['type'] = $validator->getType(); $node['x-example'] = false; break; @@ -319,13 +321,13 @@ class Swagger2 extends Format $node['format'] = 'email'; $node['x-example'] = 'email@example.com'; break; - case 'Utopia\Validator\Host': - case 'Utopia\Validator\URL': + case 'Utopia\Http\Validator\Host': + case 'Utopia\Http\Validator\URL': $node['type'] = $validator->getType(); $node['format'] = 'url'; $node['x-example'] = 'https://example.com'; break; - case 'Utopia\Validator\ArrayList': + case 'Utopia\Http\Validator\ArrayList': /** @var ArrayList $validator */ $node['type'] = 'array'; $node['collectionFormat'] = 'multi'; @@ -333,9 +335,9 @@ class Swagger2 extends Format 'type' => $validator->getValidator()->getType(), ]; break; - case 'Utopia\Validator\JSON': - case 'Utopia\Validator\Mock': - case 'Utopia\Validator\Assoc': + case 'Utopia\Http\Validator\JSON': + case 'Utopia\Http\Validator\Mock': + case 'Utopia\Http\Validator\Assoc': $node['type'] = 'object'; $node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default']; $node['x-example'] = '{}'; @@ -406,26 +408,26 @@ class Swagger2 extends Format $node['format'] = 'phone'; $node['x-example'] = '+12065550100'; break; - case 'Utopia\Validator\Range': + case 'Utopia\Http\Validator\Range': /** @var Range $validator */ $node['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType(); $node['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float'; $node['x-example'] = $validator->getMin(); break; - case 'Utopia\Validator\Numeric': - case 'Utopia\Validator\Integer': + case 'Utopia\Http\Validator\Numeric': + case 'Utopia\Http\Validator\Integer': $node['type'] = $validator->getType(); $node['format'] = 'int32'; break; - case 'Utopia\Validator\FloatValidator': + case 'Utopia\Http\Validator\FloatValidator': $node['type'] = 'number'; $node['format'] = 'float'; break; - case 'Utopia\Validator\Length': + case 'Utopia\Http\Validator\Length': $node['type'] = $validator->getType(); break; - case 'Utopia\Validator\WhiteList': - /** @var \Utopia\Validator\WhiteList $validator */ + case 'Utopia\Http\Validator\WhiteList': + /** @var \Utopia\Http\Validator\WhiteList $validator */ $node['type'] = $validator->getType(); $node['x-example'] = $validator->getList()[0]; diff --git a/src/Appwrite/Task/Validator/Cron.php b/src/Appwrite/Task/Validator/Cron.php index 03bd1c5220..afa19c50a6 100644 --- a/src/Appwrite/Task/Validator/Cron.php +++ b/src/Appwrite/Task/Validator/Cron.php @@ -3,7 +3,7 @@ namespace Appwrite\Task\Validator; use Cron\CronExpression; -use Utopia\Validator; +use Utopia\Http\Validator; class Cron extends Validator { diff --git a/src/Appwrite/Utopia/Database/Validator/CompoundUID.php b/src/Appwrite/Utopia/Database/Validator/CompoundUID.php index 3f23500952..b851d8ba85 100644 --- a/src/Appwrite/Utopia/Database/Validator/CompoundUID.php +++ b/src/Appwrite/Utopia/Database/Validator/CompoundUID.php @@ -3,7 +3,7 @@ namespace Appwrite\Utopia\Database\Validator; use Utopia\Database\Validator\UID; -use Utopia\Validator; +use Utopia\Http\Validator; class CompoundUID extends Validator { diff --git a/src/Appwrite/Utopia/Database/Validator/ProjectId.php b/src/Appwrite/Utopia/Database/Validator/ProjectId.php index 46b0cdf53e..28abe176fe 100644 --- a/src/Appwrite/Utopia/Database/Validator/ProjectId.php +++ b/src/Appwrite/Utopia/Database/Validator/ProjectId.php @@ -2,7 +2,7 @@ namespace Appwrite\Utopia\Database\Validator; -use Utopia\Validator; +use Utopia\Http\Validator; class ProjectId extends Validator { diff --git a/src/Appwrite/Utopia/Queue/Connections.php b/src/Appwrite/Utopia/Queue/Connections.php new file mode 100644 index 0000000000..e873373566 --- /dev/null +++ b/src/Appwrite/Utopia/Queue/Connections.php @@ -0,0 +1,36 @@ +connections[] = ['connection' => $connection, 'pool' => $pool]; + return $this; + } + + /** + * @return self + */ + public function reclaim(): self + { + foreach ($this->connections as $id => $resource) { + $pool = $resource['pool']; + $connection = $resource['connection']; + $pool->put($connection); + unset($this->connections[$id]); + } + + return $this; + } +} diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index 3f0a196d5e..6be9701baa 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -3,11 +3,10 @@ namespace Appwrite\Utopia; use Appwrite\Utopia\Request\Filter; -use Swoole\Http\Request as SwooleRequest; -use Utopia\Route; -use Utopia\Swoole\Request as UtopiaRequest; +use Utopia\Http\Adapter\Swoole\Request as HttpRequest; +use Utopia\Http\Route; -class Request extends UtopiaRequest +class Request extends HttpRequest { /** * @var array @@ -15,9 +14,12 @@ class Request extends UtopiaRequest private array $filters = []; private static ?Route $route = null; - public function __construct(SwooleRequest $request) + /** + * Request constructor. + */ + public function __construct(HttpRequest $request) { - parent::__construct($request); + parent::__construct($request->swoole); } /** @@ -113,6 +115,16 @@ class Request extends UtopiaRequest return self::$route !== null; } + + public function removeHeader(string $key): static + { + if (isset($this->headers[$key])) { + unset($this->headers[$key]); + } + + return parent::removeHeader($key); + } + /** * Get headers * @@ -122,10 +134,14 @@ class Request extends UtopiaRequest */ public function getHeaders(): array { - $headers = $this->generateHeaders(); + if ($this->headers !== null) { + return $this->headers; + } + + $this->headers = $this->generateHeaders(); if (empty($this->swoole->cookie)) { - return $headers; + return $this->headers; } $cookieHeaders = []; @@ -134,10 +150,10 @@ class Request extends UtopiaRequest } if (!empty($cookieHeaders)) { - $headers['cookie'] = \implode('; ', $cookieHeaders); + $this->headers['cookie'] = \implode('; ', $cookieHeaders); } - return $headers; + return $this->headers; } /** diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 6cc2639f51..376c327b37 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -4,122 +4,20 @@ namespace Appwrite\Utopia; use Appwrite\Utopia\Fetch\BodyMultipart; use Appwrite\Utopia\Response\Filter; -use Appwrite\Utopia\Response\Model; -use Appwrite\Utopia\Response\Model\Account; -use Appwrite\Utopia\Response\Model\AlgoArgon2; -use Appwrite\Utopia\Response\Model\AlgoBcrypt; -use Appwrite\Utopia\Response\Model\AlgoMd5; -use Appwrite\Utopia\Response\Model\AlgoPhpass; -use Appwrite\Utopia\Response\Model\AlgoScrypt; -use Appwrite\Utopia\Response\Model\AlgoScryptModified; -use Appwrite\Utopia\Response\Model\AlgoSha; -use Appwrite\Utopia\Response\Model\Any; -use Appwrite\Utopia\Response\Model\Attribute; -use Appwrite\Utopia\Response\Model\AttributeBoolean; -use Appwrite\Utopia\Response\Model\AttributeDatetime; -use Appwrite\Utopia\Response\Model\AttributeEmail; -use Appwrite\Utopia\Response\Model\AttributeEnum; -use Appwrite\Utopia\Response\Model\AttributeFloat; -use Appwrite\Utopia\Response\Model\AttributeInteger; -use Appwrite\Utopia\Response\Model\AttributeIP; -use Appwrite\Utopia\Response\Model\AttributeList; -use Appwrite\Utopia\Response\Model\AttributeRelationship; -use Appwrite\Utopia\Response\Model\AttributeString; -use Appwrite\Utopia\Response\Model\AttributeURL; -use Appwrite\Utopia\Response\Model\AuthProvider; -use Appwrite\Utopia\Response\Model\BaseList; -use Appwrite\Utopia\Response\Model\Branch; -use Appwrite\Utopia\Response\Model\Bucket; -use Appwrite\Utopia\Response\Model\Build; -use Appwrite\Utopia\Response\Model\Collection; -use Appwrite\Utopia\Response\Model\ConsoleVariables; -use Appwrite\Utopia\Response\Model\Continent; -use Appwrite\Utopia\Response\Model\Country; -use Appwrite\Utopia\Response\Model\Currency; -use Appwrite\Utopia\Response\Model\Database; -use Appwrite\Utopia\Response\Model\Deployment; -use Appwrite\Utopia\Response\Model\Detection; -use Appwrite\Utopia\Response\Model\Document as ModelDocument; -use Appwrite\Utopia\Response\Model\Error; -use Appwrite\Utopia\Response\Model\ErrorDev; -use Appwrite\Utopia\Response\Model\Execution; -use Appwrite\Utopia\Response\Model\File; -use Appwrite\Utopia\Response\Model\Func; -use Appwrite\Utopia\Response\Model\Headers; -use Appwrite\Utopia\Response\Model\HealthAntivirus; -use Appwrite\Utopia\Response\Model\HealthCertificate; -use Appwrite\Utopia\Response\Model\HealthQueue; -use Appwrite\Utopia\Response\Model\HealthStatus; -use Appwrite\Utopia\Response\Model\HealthTime; -use Appwrite\Utopia\Response\Model\HealthVersion; -use Appwrite\Utopia\Response\Model\Identity; -use Appwrite\Utopia\Response\Model\Index; -use Appwrite\Utopia\Response\Model\Installation; -use Appwrite\Utopia\Response\Model\JWT; -use Appwrite\Utopia\Response\Model\Key; -use Appwrite\Utopia\Response\Model\Language; -use Appwrite\Utopia\Response\Model\Locale; -use Appwrite\Utopia\Response\Model\LocaleCode; -use Appwrite\Utopia\Response\Model\Log; -use Appwrite\Utopia\Response\Model\Membership; -use Appwrite\Utopia\Response\Model\Message; -use Appwrite\Utopia\Response\Model\Metric; -use Appwrite\Utopia\Response\Model\MetricBreakdown; -use Appwrite\Utopia\Response\Model\MFAChallenge; -use Appwrite\Utopia\Response\Model\MFAFactors; -use Appwrite\Utopia\Response\Model\MFARecoveryCodes; -use Appwrite\Utopia\Response\Model\MFAType; -use Appwrite\Utopia\Response\Model\Migration; -use Appwrite\Utopia\Response\Model\MigrationFirebaseProject; -use Appwrite\Utopia\Response\Model\MigrationReport; -use Appwrite\Utopia\Response\Model\Mock; -use Appwrite\Utopia\Response\Model\MockNumber; -use Appwrite\Utopia\Response\Model\None; -use Appwrite\Utopia\Response\Model\Phone; -use Appwrite\Utopia\Response\Model\Platform; -use Appwrite\Utopia\Response\Model\Preferences; -use Appwrite\Utopia\Response\Model\Project; -use Appwrite\Utopia\Response\Model\Provider; -use Appwrite\Utopia\Response\Model\ProviderRepository; -use Appwrite\Utopia\Response\Model\Rule; -use Appwrite\Utopia\Response\Model\Runtime; -use Appwrite\Utopia\Response\Model\Session; -use Appwrite\Utopia\Response\Model\Specification; -use Appwrite\Utopia\Response\Model\Subscriber; -use Appwrite\Utopia\Response\Model\Target; -use Appwrite\Utopia\Response\Model\Team; -use Appwrite\Utopia\Response\Model\TemplateEmail; -use Appwrite\Utopia\Response\Model\TemplateFunction; -use Appwrite\Utopia\Response\Model\TemplateRuntime; -use Appwrite\Utopia\Response\Model\TemplateSMS; -use Appwrite\Utopia\Response\Model\TemplateVariable; -use Appwrite\Utopia\Response\Model\Token; -use Appwrite\Utopia\Response\Model\Topic; -use Appwrite\Utopia\Response\Model\UsageBuckets; -use Appwrite\Utopia\Response\Model\UsageCollection; -use Appwrite\Utopia\Response\Model\UsageDatabase; -use Appwrite\Utopia\Response\Model\UsageDatabases; -use Appwrite\Utopia\Response\Model\UsageFunction; -use Appwrite\Utopia\Response\Model\UsageFunctions; -use Appwrite\Utopia\Response\Model\UsageProject; -use Appwrite\Utopia\Response\Model\UsageStorage; -use Appwrite\Utopia\Response\Model\UsageUsers; -use Appwrite\Utopia\Response\Model\User; -use Appwrite\Utopia\Response\Model\Variable; -use Appwrite\Utopia\Response\Model\VcsContent; -use Appwrite\Utopia\Response\Model\Webhook; +use Appwrite\Utopia\Response\Models; use Exception; use JsonException; -use Swoole\Http\Response as SwooleHTTPResponse; // Keep last use Utopia\Database\Document; -use Utopia\Swoole\Response as SwooleResponse; +use Utopia\Http\Adapter\Swoole\Response as HttpResponse; + +// Keep last /** * @method int getStatusCode() * @method Response setStatusCode(int $code = 200) */ -class Response extends SwooleResponse +class Response extends HttpResponse { // General public const MODEL_NONE = 'none'; @@ -330,162 +228,9 @@ class Response extends SwooleResponse * * @param float $time */ - public function __construct(SwooleHTTPResponse $response) + public function __construct(HttpResponse $response) { - $this - // General - ->setModel(new None()) - ->setModel(new Any()) - ->setModel(new Error()) - ->setModel(new ErrorDev()) - // Lists - ->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_DOCUMENT)) - ->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'collections', self::MODEL_COLLECTION)) - ->setModel(new BaseList('Databases List', self::MODEL_DATABASE_LIST, 'databases', self::MODEL_DATABASE)) - ->setModel(new BaseList('Indexes List', self::MODEL_INDEX_LIST, 'indexes', self::MODEL_INDEX)) - ->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER)) - ->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION)) - ->setModel(new BaseList('Identities List', self::MODEL_IDENTITY_LIST, 'identities', self::MODEL_IDENTITY)) - ->setModel(new BaseList('Logs List', self::MODEL_LOG_LIST, 'logs', self::MODEL_LOG)) - ->setModel(new BaseList('Files List', self::MODEL_FILE_LIST, 'files', self::MODEL_FILE)) - ->setModel(new BaseList('Buckets List', self::MODEL_BUCKET_LIST, 'buckets', self::MODEL_BUCKET)) - ->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM)) - ->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP)) - ->setModel(new BaseList('Functions List', self::MODEL_FUNCTION_LIST, 'functions', self::MODEL_FUNCTION)) - ->setModel(new BaseList('Function Templates List', self::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', self::MODEL_TEMPLATE_FUNCTION)) - ->setModel(new BaseList('Installations List', self::MODEL_INSTALLATION_LIST, 'installations', self::MODEL_INSTALLATION)) - ->setModel(new BaseList('Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_LIST, 'providerRepositories', self::MODEL_PROVIDER_REPOSITORY)) - ->setModel(new BaseList('Branches List', self::MODEL_BRANCH_LIST, 'branches', self::MODEL_BRANCH)) - ->setModel(new BaseList('Runtimes List', self::MODEL_RUNTIME_LIST, 'runtimes', self::MODEL_RUNTIME)) - ->setModel(new BaseList('Deployments List', self::MODEL_DEPLOYMENT_LIST, 'deployments', self::MODEL_DEPLOYMENT)) - ->setModel(new BaseList('Executions List', self::MODEL_EXECUTION_LIST, 'executions', self::MODEL_EXECUTION)) - ->setModel(new BaseList('Builds List', self::MODEL_BUILD_LIST, 'builds', self::MODEL_BUILD)) // Not used anywhere yet - ->setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false)) - ->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false)) - ->setModel(new BaseList('API Keys List', self::MODEL_KEY_LIST, 'keys', self::MODEL_KEY, true, false)) - ->setModel(new BaseList('Auth Providers List', self::MODEL_AUTH_PROVIDER_LIST, 'platforms', self::MODEL_AUTH_PROVIDER, true, false)) - ->setModel(new BaseList('Platforms List', self::MODEL_PLATFORM_LIST, 'platforms', self::MODEL_PLATFORM, true, false)) - ->setModel(new BaseList('Countries List', self::MODEL_COUNTRY_LIST, 'countries', self::MODEL_COUNTRY)) - ->setModel(new BaseList('Continents List', self::MODEL_CONTINENT_LIST, 'continents', self::MODEL_CONTINENT)) - ->setModel(new BaseList('Languages List', self::MODEL_LANGUAGE_LIST, 'languages', self::MODEL_LANGUAGE)) - ->setModel(new BaseList('Currencies List', self::MODEL_CURRENCY_LIST, 'currencies', self::MODEL_CURRENCY)) - ->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE)) - ->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false)) - ->setModel(new BaseList('Variables List', self::MODEL_VARIABLE_LIST, 'variables', self::MODEL_VARIABLE)) - ->setModel(new BaseList('Status List', self::MODEL_HEALTH_STATUS_LIST, 'statuses', self::MODEL_HEALTH_STATUS)) - ->setModel(new BaseList('Rule List', self::MODEL_PROXY_RULE_LIST, 'rules', self::MODEL_PROXY_RULE)) - ->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE)) - ->setModel(new BaseList('Provider list', self::MODEL_PROVIDER_LIST, 'providers', self::MODEL_PROVIDER)) - ->setModel(new BaseList('Message list', self::MODEL_MESSAGE_LIST, 'messages', self::MODEL_MESSAGE)) - ->setModel(new BaseList('Topic list', self::MODEL_TOPIC_LIST, 'topics', self::MODEL_TOPIC)) - ->setModel(new BaseList('Subscriber list', self::MODEL_SUBSCRIBER_LIST, 'subscribers', self::MODEL_SUBSCRIBER)) - ->setModel(new BaseList('Target list', self::MODEL_TARGET_LIST, 'targets', self::MODEL_TARGET)) - ->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION)) - ->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT)) - ->setModel(new BaseList('Specifications List', self::MODEL_SPECIFICATION_LIST, 'specifications', self::MODEL_SPECIFICATION)) - ->setModel(new BaseList('VCS Content List', self::MODEL_VCS_CONTENT_LIST, 'contents', self::MODEL_VCS_CONTENT)) - // Entities - ->setModel(new Database()) - ->setModel(new Collection()) - ->setModel(new Attribute()) - ->setModel(new AttributeList()) - ->setModel(new AttributeString()) - ->setModel(new AttributeInteger()) - ->setModel(new AttributeFloat()) - ->setModel(new AttributeBoolean()) - ->setModel(new AttributeEmail()) - ->setModel(new AttributeEnum()) - ->setModel(new AttributeIP()) - ->setModel(new AttributeURL()) - ->setModel(new AttributeDatetime()) - ->setModel(new AttributeRelationship()) - ->setModel(new Index()) - ->setModel(new ModelDocument()) - ->setModel(new Log()) - ->setModel(new User()) - ->setModel(new AlgoMd5()) - ->setModel(new AlgoSha()) - ->setModel(new AlgoPhpass()) - ->setModel(new AlgoBcrypt()) - ->setModel(new AlgoScrypt()) - ->setModel(new AlgoScryptModified()) - ->setModel(new AlgoArgon2()) - ->setModel(new Account()) - ->setModel(new Preferences()) - ->setModel(new Session()) - ->setModel(new Identity()) - ->setModel(new Token()) - ->setModel(new JWT()) - ->setModel(new Locale()) - ->setModel(new LocaleCode()) - ->setModel(new File()) - ->setModel(new Bucket()) - ->setModel(new Team()) - ->setModel(new Membership()) - ->setModel(new Func()) - ->setModel(new TemplateFunction()) - ->setModel(new TemplateRuntime()) - ->setModel(new TemplateVariable()) - ->setModel(new Installation()) - ->setModel(new ProviderRepository()) - ->setModel(new Detection()) - ->setModel(new VcsContent()) - ->setModel(new Branch()) - ->setModel(new Runtime()) - ->setModel(new Deployment()) - ->setModel(new Execution()) - ->setModel(new Build()) - ->setModel(new Project()) - ->setModel(new Webhook()) - ->setModel(new Key()) - ->setModel(new MockNumber()) - ->setModel(new AuthProvider()) - ->setModel(new Platform()) - ->setModel(new Variable()) - ->setModel(new Country()) - ->setModel(new Continent()) - ->setModel(new Language()) - ->setModel(new Currency()) - ->setModel(new Phone()) - ->setModel(new HealthAntivirus()) - ->setModel(new HealthQueue()) - ->setModel(new HealthStatus()) - ->setModel(new HealthCertificate()) - ->setModel(new HealthTime()) - ->setModel(new HealthVersion()) - ->setModel(new Metric()) - ->setModel(new MetricBreakdown()) - ->setModel(new UsageDatabases()) - ->setModel(new UsageDatabase()) - ->setModel(new UsageCollection()) - ->setModel(new UsageUsers()) - ->setModel(new UsageStorage()) - ->setModel(new UsageBuckets()) - ->setModel(new UsageFunctions()) - ->setModel(new UsageFunction()) - ->setModel(new UsageProject()) - ->setModel(new Headers()) - ->setModel(new Specification()) - ->setModel(new Rule()) - ->setModel(new TemplateSMS()) - ->setModel(new TemplateEmail()) - ->setModel(new ConsoleVariables()) - ->setModel(new MFAChallenge()) - ->setModel(new MFARecoveryCodes()) - ->setModel(new MFAType()) - ->setModel(new MFAFactors()) - ->setModel(new Provider()) - ->setModel(new Message()) - ->setModel(new Topic()) - ->setModel(new Subscriber()) - ->setModel(new Target()) - ->setModel(new Migration()) - ->setModel(new MigrationReport()) - ->setModel(new MigrationFirebaseProject()) - // Tests (keep last) - ->setModel(new Mock()); - - parent::__construct($response); + parent::__construct($response->swoole); } /** @@ -495,49 +240,6 @@ class Response extends SwooleResponse public const CONTENT_TYPE_NULL = 'null'; public const CONTENT_TYPE_MULTIPART = 'multipart/form-data'; - /** - * List of defined output objects - */ - protected $models = []; - - /** - * Set Model Object - * - * @return self - */ - public function setModel(Model $instance) - { - $this->models[$instance->getType()] = $instance; - - return $this; - } - - /** - * Get Model Object - * - * @param string $key - * @return Model - * @throws Exception - */ - public function getModel(string $key): Model - { - if (!isset($this->models[$key])) { - throw new Exception('Undefined model: ' . $key); - } - - return $this->models[$key]; - } - - /** - * Get Models List - * - * @return Model[] - */ - public function getModels(): array - { - return $this->models; - } - public function applyFilters(array $data, string $model): array { foreach ($this->filters as $filter) { @@ -605,7 +307,7 @@ class Response extends SwooleResponse public function output(Document $document, string $model): array { $data = clone $document; - $model = $this->getModel($model); + $model = Models::getModel($model); $output = []; $data = $model->filter($data); @@ -640,7 +342,7 @@ class Response extends SwooleResponse if (\is_array($rule['type'])) { foreach ($rule['type'] as $type) { $condition = false; - foreach ($this->getModel($type)->conditions as $attribute => $val) { + foreach (Models::getModel($type)->conditions as $attribute => $val) { $condition = $item->getAttribute($attribute) === $val; if (!$condition) { break; @@ -655,7 +357,7 @@ class Response extends SwooleResponse $ruleType = $rule['type']; } - if (!array_key_exists($ruleType, $this->models)) { + if (!array_key_exists($ruleType, Models::getModels())) { throw new Exception('Missing model for rule: ' . $ruleType); } diff --git a/src/Appwrite/Utopia/Response/Models.php b/src/Appwrite/Utopia/Response/Models.php new file mode 100644 index 0000000000..2a0393321c --- /dev/null +++ b/src/Appwrite/Utopia/Response/Models.php @@ -0,0 +1,304 @@ +getType()] = $instance; + } + + /** + * Get Model Object + * + * @param string $key + * @return Model + * @throws Exception + */ + public static function getModel(string $key): Model + { + if (!isset(self::$models[$key])) { + throw new Exception('Undefined model: ' . $key); + } + + return self::$models[$key]; + } + + public static function getModels(): array + { + return self::$models; + } +} diff --git a/src/Appwrite/Utopia/View.php b/src/Appwrite/Utopia/View.php index e4ed8164c9..60cafb1ca8 100644 --- a/src/Appwrite/Utopia/View.php +++ b/src/Appwrite/Utopia/View.php @@ -2,7 +2,7 @@ namespace Appwrite\Utopia; -use Utopia\View as OldView; +use Utopia\View\View as OldView; class View extends OldView { diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 04f2dbd8c8..28c3227979 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -12,7 +12,7 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; -use Utopia\Validator\JSON; +use Utopia\Http\Validator\JSON; trait DatabasesBase { @@ -2869,7 +2869,7 @@ trait DatabasesBase $this->assertEquals('Invalid document structure: Attribute "floatRange" has invalid format. Value must be a valid range between 1 and 1', $badFloatRange['body']['message']); $this->assertEquals('Invalid document structure: Attribute "probability" has invalid format. Value must be a valid range between 0 and 1', $badProbability['body']['message']); $this->assertEquals('Invalid document structure: Attribute "upperBound" has invalid format. Value must be a valid range between -9,223,372,036,854,775,808 and 10', $tooHigh['body']['message']); - $this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and 9,223,372,036,854,775,807', $tooLow['body']['message']); + $this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and '.\number_format(PHP_INT_MAX), $tooLow['body']['message']); } /** diff --git a/tests/e2e/Services/Databases/DatabasesCustomClientTest.php b/tests/e2e/Services/Databases/DatabasesCustomClientTest.php index 8484996058..9b7be063db 100644 --- a/tests/e2e/Services/Databases/DatabasesCustomClientTest.php +++ b/tests/e2e/Services/Databases/DatabasesCustomClientTest.php @@ -64,9 +64,10 @@ class DatabasesCustomClientTest extends Scope 'required' => true, ]); + $this->assertEquals(202, $response['headers']['status-code']); + sleep(1); - $this->assertEquals(202, $response['headers']['status-code']); // Document aliases write to update, delete $document1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([ @@ -82,6 +83,7 @@ class DatabasesCustomClientTest extends Scope ] ]); + $this->assertEquals(201, $document1['headers']['status-code']); $this->assertNotContains(Permission::create(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']); $this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']); $this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']); diff --git a/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php b/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php index ca8753f374..4288b92613 100644 --- a/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php +++ b/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php @@ -17,6 +17,14 @@ class DatabasesPermissionsGuestTest extends Scope use SideClient; use DatabasesPermissionsScope; + protected Authorization $auth; + + public function setUp(): void + { + parent::setUp(); + $this->auth = new Authorization(); + } + public function createCollection(): array { $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ @@ -111,8 +119,8 @@ class DatabasesPermissionsGuestTest extends Scope $this->assertEquals(201, $publicResponse['headers']['status-code']); $this->assertEquals(201, $privateResponse['headers']['status-code']); - $roles = Authorization::getRoles(); - Authorization::cleanRoles(); + $roles = $this->auth->getRoles(); + $this->auth->cleanRoles(); $publicDocuments = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [ 'content-type' => 'application/json', @@ -134,7 +142,7 @@ class DatabasesPermissionsGuestTest extends Scope } foreach ($roles as $role) { - Authorization::setRole($role); + $this->auth->addRole($role); } } @@ -145,8 +153,8 @@ class DatabasesPermissionsGuestTest extends Scope $privateCollectionId = $data['privateCollectionId']; $databaseId = $data['databaseId']; - $roles = Authorization::getRoles(); - Authorization::cleanRoles(); + $roles = $this->auth->getRoles(); + $this->auth->cleanRoles(); $publicResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [ 'content-type' => 'application/json', @@ -222,7 +230,7 @@ class DatabasesPermissionsGuestTest extends Scope $this->assertEquals(401, $privateDocument['headers']['status-code']); foreach ($roles as $role) { - Authorization::setRole($role); + $this->auth->addRole($role); } } diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 2d94b9f0e3..f9898757f7 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -7,12 +7,11 @@ use Utopia\CLI\Console; trait FunctionsBase { - protected string $stdout = ''; - protected string $stderr = ''; + protected string $output = ''; protected function packageCode($folder) { - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output); } protected function awaitDeploymentIsBuilt($functionId, $deploymentId, $checkForSuccess = true): void diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 2958e6cb5f..dccbf2e544 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -10,12 +10,12 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; -use Utopia\App; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; +use Utopia\System\System; class FunctionsCustomServerTest extends Scope { @@ -476,7 +476,6 @@ class FunctionsCustomServerTest extends Scope * and ensure variable works as expected in execution. */ $this->assertEmpty($template['body']['variables']); - // Create function using settings from template. // Deployment is automatically created from template inside endpoint $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ @@ -504,7 +503,6 @@ class FunctionsCustomServerTest extends Scope $this->assertNotEmpty($function['body']['$id']); $functionId = $function['body']['$id']; - // List deployments so we can await deployment build $deployments = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments', [ 'content-type' => 'application/json', @@ -2464,7 +2462,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals($cookie, $response['body']); // Await Aggregation - sleep(App::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', 30)); + sleep(System::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', 30)); $tries = 0; while (true) { diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 735e4eced5..b77f006bf8 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -2498,6 +2498,6 @@ trait Base protected function packageCode($folder): void { - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout); } } diff --git a/tests/e2e/Services/GraphQL/StorageClientTest.php b/tests/e2e/Services/GraphQL/StorageClientTest.php index 9896598c2d..7d6d617c87 100644 --- a/tests/e2e/Services/GraphQL/StorageClientTest.php +++ b/tests/e2e/Services/GraphQL/StorageClientTest.php @@ -183,7 +183,6 @@ class StorageClientTest extends Scope /** * @depends testCreateFile * @param $file - * @return array * @throws \Exception */ public function testGetFileDownload($file) diff --git a/tests/e2e/Services/GraphQL/StorageServerTest.php b/tests/e2e/Services/GraphQL/StorageServerTest.php index 7fea895b1c..6be103629e 100644 --- a/tests/e2e/Services/GraphQL/StorageServerTest.php +++ b/tests/e2e/Services/GraphQL/StorageServerTest.php @@ -232,7 +232,6 @@ class StorageServerTest extends Scope /** * @depends testCreateFile * @param $file - * @return array * @throws \Exception */ public function testGetFileDownload($file) diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index c41d861f1d..9fc862e85b 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -1691,7 +1691,7 @@ class ProjectsConsoleClientTest extends Scope ]); $this->assertEquals(400, $response['headers']['status-code']); - $this->assertEquals('Invalid `numbers` param: Value must a valid array no longer than 10 items and Phone number must start with a \'+\' can have a maximum of fifteen digits.', $response['body']['message']); + $this->assertEquals('Invalid `numbers` param: Value must a valid array no longer than 10 items', $response['body']['message']); /** * Test for success diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index c3372b98c5..86aaee2ab0 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -1272,11 +1272,10 @@ class RealtimeCustomClientTest extends Scope $this->assertNotEmpty($function['body']['$id']); $folder = 'timeout'; - $stderr = ''; - $stdout = ''; + $output = ''; $code = realpath(__DIR__ . '/../../../resources/functions') . "/{$folder}/code.tar.gz"; - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $output); $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ 'content-type' => 'multipart/form-data', diff --git a/tests/e2e/Services/Storage/StorageCustomClientTest.php b/tests/e2e/Services/Storage/StorageCustomClientTest.php index c723fba50a..55340ab849 100644 --- a/tests/e2e/Services/Storage/StorageCustomClientTest.php +++ b/tests/e2e/Services/Storage/StorageCustomClientTest.php @@ -1089,7 +1089,7 @@ class StorageCustomClientTest extends Scope $this->assertEquals(200, $file['headers']['status-code']); - // Team 1 view success + // Team 2 view success $file = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/view', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1112,6 +1112,8 @@ class StorageCustomClientTest extends Scope 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'permissions.png'), ]); + $this->assertEquals($file['headers']['status-code'], 401); + // Team 2 create failure $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', [ 'content-type' => 'multipart/form-data', diff --git a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php index d2f132e960..54d55e5e68 100644 --- a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php +++ b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php @@ -486,11 +486,10 @@ class WebhooksCustomServerTest extends Scope /** * Test for SUCCESS */ - $stderr = ''; - $stdout = ''; + $output = ''; $folder = 'timeout'; $code = realpath(__DIR__ . '/../../../resources/functions') . "/{$folder}/code.tar.gz"; - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $output); $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/deployments', array_merge([ 'content-type' => 'multipart/form-data', diff --git a/tests/resources/docker/docker-compose.yml b/tests/resources/docker/docker-compose.yml index a34b4fcf88..8f86fb8374 100644 --- a/tests/resources/docker/docker-compose.yml +++ b/tests/resources/docker/docker-compose.yml @@ -324,7 +324,7 @@ services: - MYSQL_USER=user - MYSQL_PASSWORD=password - MARIADB_AUTO_UPGRADE=1 - command: 'mysqld --innodb-flush-method=fsync' + command: 'mysqld --innodb-flush-method=fsync --max_connections=5000' maildev: image: djfarrelly/maildev diff --git a/tests/unit/Auth/AuthTest.php b/tests/unit/Auth/AuthTest.php index 705da42879..1ff5850402 100644 --- a/tests/unit/Auth/AuthTest.php +++ b/tests/unit/Auth/AuthTest.php @@ -3,6 +3,7 @@ namespace Tests\Unit\Auth; use Appwrite\Auth\Auth; +use Appwrite\Auth\Authentication; use PHPUnit\Framework\TestCase; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -13,21 +14,31 @@ use Utopia\Database\Validator\Roles; class AuthTest extends TestCase { + protected Authorization $auth; + protected Authentication $authentication; + + public function setUp(): void + { + parent::setUp(); + $this->auth = new Authorization(); + $this->authentication = new Authentication(); + } + /** * Reset Roles */ public function tearDown(): void { - Authorization::cleanRoles(); - Authorization::setRole(Role::any()->toString()); + $this->auth->cleanRoles(); + $this->auth->addRole(Role::any()->toString()); } public function testCookieName(): void { $name = 'cookie-name'; - $this->assertEquals(Auth::setCookieName($name), $name); - $this->assertEquals(Auth::$cookieName, $name); + $this->assertEquals($this->authentication->setCookieName($name), $name); + $this->assertEquals($this->authentication->getCookieName(), $name); } public function testEncodeDecodeSession(): void @@ -347,7 +358,7 @@ class AuthTest extends TestCase '$id' => '' ]); - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $this->assertCount(1, $roles); $this->assertContains(Role::guests()->toString(), $roles); } @@ -383,7 +394,7 @@ class AuthTest extends TestCase ] ]); - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $this->assertCount(13, $roles); $this->assertContains(Role::users()->toString(), $roles); @@ -404,21 +415,21 @@ class AuthTest extends TestCase $user['emailVerification'] = false; $user['phoneVerification'] = false; - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $this->assertContains(Role::users(Roles::DIMENSION_UNVERIFIED)->toString(), $roles); $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_UNVERIFIED)->toString(), $roles); // Enable single verification type $user['emailVerification'] = true; - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $this->assertContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles); $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles); } public function testPrivilegedUserRoles(): void { - Authorization::setRole(Auth::USER_ROLE_OWNER); + $this->auth->addRole(Auth::USER_ROLE_OWNER); $user = new Document([ '$id' => ID::custom('123'), 'emailVerification' => true, @@ -444,7 +455,7 @@ class AuthTest extends TestCase ] ]); - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $this->assertCount(7, $roles); $this->assertNotContains(Role::users()->toString(), $roles); @@ -462,7 +473,7 @@ class AuthTest extends TestCase public function testAppUserRoles(): void { - Authorization::setRole(Auth::USER_ROLE_APPS); + $this->auth->addRole(Auth::USER_ROLE_APPS); $user = new Document([ '$id' => ID::custom('123'), 'memberships' => [ @@ -486,7 +497,7 @@ class AuthTest extends TestCase ] ]); - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $this->assertCount(7, $roles); $this->assertNotContains(Role::users()->toString(), $roles); diff --git a/tests/unit/Event/EventTest.php b/tests/unit/Event/EventTest.php index dd9833378f..38534b9b16 100644 --- a/tests/unit/Event/EventTest.php +++ b/tests/unit/Event/EventTest.php @@ -66,9 +66,13 @@ class EventTest extends TestCase $this->assertEquals('eventValue1', $this->object->getParam('eventKey1')); $this->assertEquals('eventValue2', $this->object->getParam('eventKey2')); $this->assertEquals(null, $this->object->getParam('eventKey3')); - global $register; - $pools = $register->get('pools'); - $client = new Client($this->object->getQueue(), $pools->get('queue')->pop()->getResource()); + + global $registry; + $pools = $registry->get('pools'); + $dsn = $pools['pools-queue-queue']['dsn']; + $queue = new Queue\Connection\Redis($dsn->getHost(), $dsn->getPort()); + + $client = new Client($this->object->getQueue(), $queue); $this->assertEquals($client->getQueueSize(), 1); } diff --git a/tests/unit/GraphQL/BuilderTest.php b/tests/unit/GraphQL/BuilderTest.php index d79a104c90..f11045f318 100644 --- a/tests/unit/GraphQL/BuilderTest.php +++ b/tests/unit/GraphQL/BuilderTest.php @@ -4,8 +4,10 @@ namespace Tests\Unit\GraphQL; use Appwrite\GraphQL\Types\Mapper; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Models; use PHPUnit\Framework\TestCase; use Swoole\Http\Response as SwooleResponse; +use Utopia\Http\Adapter\Swoole\Response as UtopiaSwooleResponse; class BuilderTest extends TestCase { @@ -13,8 +15,9 @@ class BuilderTest extends TestCase public function setUp(): void { - $this->response = new Response(new SwooleResponse()); - Mapper::init($this->response->getModels()); + Models::init(); + $this->response = new Response(new UtopiaSwooleResponse(new SwooleResponse())); + Mapper::init(Models::getModels()); } /** @@ -22,7 +25,7 @@ class BuilderTest extends TestCase */ public function testCreateTypeMapping() { - $model = $this->response->getModel(Response::MODEL_COLLECTION); + $model = Models::getModel(Response::MODEL_COLLECTION); $type = Mapper::model(\ucfirst($model->getType())); $this->assertEquals('Collection', $type->name); } diff --git a/tests/unit/Messaging/MessagingChannelsTest.php b/tests/unit/Messaging/MessagingChannelsTest.php index 8ba0374093..77b3cee7d6 100644 --- a/tests/unit/Messaging/MessagingChannelsTest.php +++ b/tests/unit/Messaging/MessagingChannelsTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; +use Utopia\Database\Validator\Authorization as ValidatorAuthorization; class MessagingChannelsTest extends TestCase { @@ -34,8 +35,12 @@ class MessagingChannelsTest extends TestCase 'functions.1', ]; + protected ValidatorAuthorization $auth; + public function setUp(): void { + $this->auth = new ValidatorAuthorization(); + /** * Setup global Counts */ @@ -66,7 +71,7 @@ class MessagingChannelsTest extends TestCase ] ]); - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId()); @@ -90,7 +95,7 @@ class MessagingChannelsTest extends TestCase '$id' => '' ]); - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId()); diff --git a/tests/unit/Migration/MigrationTest.php b/tests/unit/Migration/MigrationTest.php index 536278d55b..8043ec9f94 100644 --- a/tests/unit/Migration/MigrationTest.php +++ b/tests/unit/Migration/MigrationTest.php @@ -36,7 +36,7 @@ abstract class MigrationTest extends TestCase */ public function testMigrationVersions(): void { - require_once __DIR__ . '/../../../app/init.php'; + //require_once __DIR__ . '/../../../app/init.php'; foreach (Migration::$versions as $class) { $this->assertTrue(class_exists('Appwrite\\Migration\\Version\\' . $class)); diff --git a/tests/unit/Utopia/RequestTest.php b/tests/unit/Utopia/RequestTest.php index 73daaa88bc..6124a7a0c1 100644 --- a/tests/unit/Utopia/RequestTest.php +++ b/tests/unit/Utopia/RequestTest.php @@ -7,7 +7,8 @@ use PHPUnit\Framework\TestCase; use Swoole\Http\Request as SwooleRequest; use Tests\Unit\Utopia\Request\Filters\First; use Tests\Unit\Utopia\Request\Filters\Second; -use Utopia\Route; +use Utopia\Http\Adapter\Swoole\Request as UtopiaSwooleRequest; +use Utopia\Http\Route; class RequestTest extends TestCase { @@ -15,7 +16,7 @@ class RequestTest extends TestCase public function setUp(): void { - $this->request = new Request(new SwooleRequest()); + $this->request = new Request(new UtopiaSwooleRequest(new SwooleRequest())); } public function testFilters(): void @@ -36,7 +37,7 @@ class RequestTest extends TestCase // set test header to prevent header populaten inside the request class $this->request->addHeader('EXAMPLE', 'VALUE'); $this->request->setRoute($route); - $this->request->setQueryString([ + $this->request->setQuery([ 'initial' => true, 'first' => false ]); diff --git a/tests/unit/Utopia/ResponseTest.php b/tests/unit/Utopia/ResponseTest.php index 452119fafb..67253e48a0 100644 --- a/tests/unit/Utopia/ResponseTest.php +++ b/tests/unit/Utopia/ResponseTest.php @@ -3,12 +3,14 @@ namespace Tests\Unit\Utopia; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Models; use Exception; use PHPUnit\Framework\TestCase; use Swoole\Http\Response as SwooleResponse; use Tests\Unit\Utopia\Response\Filters\First; use Tests\Unit\Utopia\Response\Filters\Second; use Utopia\Database\Document; +use Utopia\Http\Adapter\Swoole\Response as UtopiaSwooleResponse; class ResponseTest extends TestCase { @@ -16,10 +18,10 @@ class ResponseTest extends TestCase public function setUp(): void { - $this->response = new Response(new SwooleResponse()); - $this->response->setModel(new Single()); - $this->response->setModel(new Lists()); - $this->response->setModel(new Nested()); + $this->response = new Response(new UtopiaSwooleResponse(new SwooleResponse())); + Models::setModel(new Single()); + Models::setModel(new Lists()); + Models::setModel(new Nested()); } public function testFilters(): void From 579d3d7f14fbc23ed1be09bf89500b3313479a1f Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:39:43 -0400 Subject: [PATCH 02/13] chore: lint & updates --- app/cli.php | 12 +-- app/init/constants.php | 7 ++ composer.lock | 94 ++++++++++---------- src/Appwrite/Platform/Tasks/ScheduleBase.php | 2 +- 4 files changed, 61 insertions(+), 54 deletions(-) diff --git a/app/cli.php b/app/cli.php index 51a8e86137..7c2b73e966 100644 --- a/app/cli.php +++ b/app/cli.php @@ -103,12 +103,12 @@ $logError $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()); - } + 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()}"); diff --git a/app/init/constants.php b/app/init/constants.php index f67023ae3e..3d8217e75a 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -149,10 +149,17 @@ const METRIC_MESSAGES_TYPE_PROVIDER_FAILED = METRIC_MESSAGES . '.{type}.{provid const METRIC_SESSIONS = 'sessions'; const METRIC_DATABASES = 'databases'; const METRIC_COLLECTIONS = 'collections'; +const METRIC_DATABASES_STORAGE = 'databases.storage'; +const METRIC_DATABASES_STORAGE_DISK = 'databases.storage_disk'; const METRIC_DATABASE_ID_COLLECTIONS = '{databaseInternalId}.collections'; +const METRIC_DATABASE_ID_STORAGE = '{databaseInternalId}.databases.storage'; +const METRIC_DATABASE_ID_STORAGE_DISK = '{databaseInternalId}.databases.storage_disk'; + const METRIC_DOCUMENTS = 'documents'; const METRIC_DATABASE_ID_DOCUMENTS = '{databaseInternalId}.documents'; const METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS = '{databaseInternalId}.{collectionInternalId}.documents'; +const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE = '{databaseInternalId}.{collectionInternalId}.databases.storage'; +const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE_DISK = '{databaseInternalId}.{collectionInternalId}.databases.storage_disk'; const METRIC_BUCKETS = 'buckets'; const METRIC_FILES = 'files'; const METRIC_FILES_STORAGE = 'files.storage'; diff --git a/composer.lock b/composer.lock index cff13d28a1..4603450316 100644 --- a/composer.lock +++ b/composer.lock @@ -2126,16 +2126,16 @@ }, { "name": "utopia-php/logger", - "version": "0.6.0", + "version": "0.6.1", "source": { "type": "git", "url": "https://github.com/utopia-php/logger.git", - "reference": "a2d1daeeb8f61fdec6d851950d9a021a3d05c9f9" + "reference": "7e8ff512c6f04577aba1df67c7b9628971946f9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/logger/zipball/a2d1daeeb8f61fdec6d851950d9a021a3d05c9f9", - "reference": "a2d1daeeb8f61fdec6d851950d9a021a3d05c9f9", + "url": "https://api.github.com/repos/utopia-php/logger/zipball/7e8ff512c6f04577aba1df67c7b9628971946f9c", + "reference": "7e8ff512c6f04577aba1df67c7b9628971946f9c", "shasum": "" }, "require": { @@ -2174,9 +2174,9 @@ ], "support": { "issues": "https://github.com/utopia-php/logger/issues", - "source": "https://github.com/utopia-php/logger/tree/0.6.0" + "source": "https://github.com/utopia-php/logger/tree/0.6.1" }, - "time": "2024-05-23T13:37:54+00:00" + "time": "2024-09-20T14:02:12+00:00" }, { "name": "utopia-php/messaging", @@ -3410,16 +3410,16 @@ }, { "name": "laravel/pint", - "version": "v1.17.3", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "9d77be916e145864f10788bb94531d03e1f7b482" + "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/9d77be916e145864f10788bb94531d03e1f7b482", - "reference": "9d77be916e145864f10788bb94531d03e1f7b482", + "url": "https://api.github.com/repos/laravel/pint/zipball/35c00c05ec43e6b46d295efc0f4386ceb30d50d9", + "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9", "shasum": "" }, "require": { @@ -3472,7 +3472,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-09-03T15:00:28+00:00" + "time": "2024-09-24T17:22:50+00:00" }, { "name": "matthiasmullie/minify", @@ -3660,16 +3660,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.2.0", + "version": "v5.3.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb" + "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", - "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3abf7425cd284141dc5d8d14a9ee444de3345d1a", + "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a", "shasum": "" }, "require": { @@ -3712,9 +3712,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.2.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.0" }, - "time": "2024-09-15T16:40:33+00:00" + "time": "2024-09-29T13:56:26+00:00" }, { "name": "phar-io/manifest", @@ -4281,16 +4281,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.30.1", + "version": "1.32.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "51b95ec8670af41009e2b2b56873bad96682413e" + "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/51b95ec8670af41009e2b2b56873bad96682413e", - "reference": "51b95ec8670af41009e2b2b56873bad96682413e", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6ca22b154efdd9e3c68c56f5d94670920a1c19a4", + "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4", "shasum": "" }, "require": { @@ -4322,9 +4322,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.32.0" }, - "time": "2024-09-07T20:13:05+00:00" + "time": "2024-09-26T07:23:32+00:00" }, { "name": "phpstan/phpstan", @@ -6020,16 +6020,16 @@ }, { "name": "symfony/console", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111" + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1eed7af6961d763e7832e874d7f9b21c3ea9c111", - "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111", + "url": "https://api.github.com/repos/symfony/console/zipball/0fa539d12b3ccf068a722bbbffa07ca7079af9ee", + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee", "shasum": "" }, "require": { @@ -6093,7 +6093,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.4" + "source": "https://github.com/symfony/console/tree/v7.1.5" }, "funding": [ { @@ -6109,7 +6109,7 @@ "type": "tidelift" } ], - "time": "2024-08-15T22:48:53+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/deprecation-contracts", @@ -6180,16 +6180,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.1.2", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "92a91985250c251de9b947a14bb2c9390b1a562c" + "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/92a91985250c251de9b947a14bb2c9390b1a562c", - "reference": "92a91985250c251de9b947a14bb2c9390b1a562c", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/61fe0566189bf32e8cfee78335d8776f64a66f5a", + "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a", "shasum": "" }, "require": { @@ -6226,7 +6226,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.1.2" + "source": "https://github.com/symfony/filesystem/tree/v7.1.5" }, "funding": [ { @@ -6242,7 +6242,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T10:03:55+00:00" + "time": "2024-09-17T09:16:35+00:00" }, { "name": "symfony/finder", @@ -6691,16 +6691,16 @@ }, { "name": "symfony/process", - "version": "v7.1.3", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca" + "reference": "5c03ee6369281177f07f7c68252a280beccba847" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7f2f542c668ad6c313dc4a5e9c3321f733197eca", - "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca", + "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847", + "reference": "5c03ee6369281177f07f7c68252a280beccba847", "shasum": "" }, "require": { @@ -6732,7 +6732,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.1.3" + "source": "https://github.com/symfony/process/tree/v7.1.5" }, "funding": [ { @@ -6748,7 +6748,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:44:47+00:00" + "time": "2024-09-19T21:48:23+00:00" }, { "name": "symfony/service-contracts", @@ -6835,16 +6835,16 @@ }, { "name": "symfony/string", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b" + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b", - "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b", + "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306", + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306", "shasum": "" }, "require": { @@ -6902,7 +6902,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.4" + "source": "https://github.com/symfony/string/tree/v7.1.5" }, "funding": [ { @@ -6918,7 +6918,7 @@ "type": "tidelift" } ], - "time": "2024-08-12T09:59:40+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "textalk/websocket", diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 8dc037897d..ada5e63b07 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -166,7 +166,7 @@ abstract class ScheduleBase extends Action $total = $total + $sum; foreach ($results as $document) { - $localDocument = $this->schedules[$document->getInternalId()] ?? null; + $localDocument = $this->schedules[$document->getInternalId()] ?? null; // Check if resource has been updated since last sync $org = $localDocument !== null ? \strtotime($localDocument['resourceUpdatedAt']) : null; From 5e8ef5523dcc37eae70aa5184a4409af6de13165 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 1 Oct 2024 11:08:47 -0400 Subject: [PATCH 03/13] Reapply "Fix auth injection" This reverts commit 904e4c86507a26369e409b033336adc3c8d71e09. --- app/controllers/api/avatars.php | 24 ++++++++++++------------ app/controllers/api/vcs.php | 30 +++++++++++++++--------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 54ba096c5d..2b8327e898 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -604,9 +604,9 @@ Http::get('/v1/cards/cloud') ->inject('contributors') ->inject('employees') ->inject('logger') - ->inject('auth') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $auth) use ($getUserGitHub) { - $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); + ->inject('authorization') + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) use ($getUserGitHub) { + $user = $authorization->skip(fn () => $dbForConsole->getDocument('users', $userId)); if ($user->isEmpty() && empty($mock)) { throw new Exception(Exception::USER_NOT_FOUND); @@ -617,7 +617,7 @@ Http::get('/v1/cards/cloud') $email = $user->getAttribute('email', ''); $createdAt = new \DateTime($user->getCreatedAt()); - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $auth); + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $authorization); $githubName = $gitHub['name'] ?? ''; $githubId = $gitHub['id'] ?? ''; @@ -812,9 +812,9 @@ Http::get('/v1/cards/cloud-back') ->inject('contributors') ->inject('employees') ->inject('logger') - ->inject('auth') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $auth) use ($getUserGitHub) { - $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); + ->inject('authorization') + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) use ($getUserGitHub) { + $user = $authorization->skip(fn () => $dbForConsole->getDocument('users', $userId)); if ($user->isEmpty() && empty($mock)) { throw new Exception(Exception::USER_NOT_FOUND); @@ -824,7 +824,7 @@ Http::get('/v1/cards/cloud-back') $userId = $user->getId(); $email = $user->getAttribute('email', ''); - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $auth); + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $authorization); $githubId = $gitHub['id'] ?? ''; $isHero = \array_key_exists($email, $heroes); @@ -891,9 +891,9 @@ Http::get('/v1/cards/cloud-og') ->inject('contributors') ->inject('employees') ->inject('logger') - ->inject('auth') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $auth) use ($getUserGitHub) { - $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); + ->inject('authorization') + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) use ($getUserGitHub) { + $user = $authorization->skip(fn () => $dbForConsole->getDocument('users', $userId)); if ($user->isEmpty() && empty($mock)) { throw new Exception(Exception::USER_NOT_FOUND); @@ -908,7 +908,7 @@ Http::get('/v1/cards/cloud-og') $email = $user->getAttribute('email', ''); $createdAt = new \DateTime($user->getCreatedAt()); - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $auth); + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $authorization); $githubName = $gitHub['name'] ?? ''; $githubId = $gitHub['id'] ?? ''; diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 987d84bb7e..ef325ee56b 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -900,9 +900,9 @@ Http::post('/v1/vcs/github/events') ->inject('dbForConsole') ->inject('getProjectDB') ->inject('queueForBuilds') - ->inject('auth') + ->inject('authorization') ->action( - function (GitHub $github, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $auth) use ($createGitDeployments) { + function (GitHub $github, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $authorization) use ($createGitDeployments) { $payload = $request->getRawPayload(); $signatureRemote = $request->getHeader('x-hub-signature-256', ''); $signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', ''); @@ -936,14 +936,14 @@ Http::post('/v1/vcs/github/events') $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); //find functionId from functions table - $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::limit(100), ])); // create new deployment only on push and not when branch is created if (!$providerBranchCreated) { - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $auth); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $authorization); } } elseif ($event == $github::EVENT_INSTALLATION) { if ($parsedPayload["action"] == "deleted") { @@ -956,13 +956,13 @@ Http::post('/v1/vcs/github/events') ]); foreach ($installations as $installation) { - $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('installationInternalId', [$installation->getInternalId()]), Query::limit(1000) ])); foreach ($repositories as $repository) { - $auth->skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId())); + $authorization->skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId())); } $dbForConsole->deleteDocument('installations', $installation->getId()); @@ -994,12 +994,12 @@ Http::post('/v1/vcs/github/events') $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; $providerCommitMessage = $commitDetails["commitMessage"] ?? ''; - $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') ])); - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $auth); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $authorization); } elseif ($parsedPayload["action"] == "closed") { // Allowed external contributions cleanup @@ -1008,7 +1008,7 @@ Http::post('/v1/vcs/github/events') $external = $parsedPayload["external"] ?? true; if ($external) { - $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') ])); @@ -1019,7 +1019,7 @@ Http::post('/v1/vcs/github/events') if (\in_array($providerPullRequestId, $providerPullRequestIds)) { $providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]); $repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds); - $repository = $auth->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); + $repository = $authorization->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); } } } @@ -1172,15 +1172,15 @@ Http::patch('/v1/vcs/github/installations/:installationId/repositories/:reposito ->inject('dbForConsole') ->inject('getProjectDB') ->inject('queueForBuilds') - ->inject('auth') - ->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $auth) use ($createGitDeployments) { + ->inject('authorization') + ->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $authorization) use ($createGitDeployments) { $installation = $dbForConsole->getDocument('installations', $installationId); if ($installation->isEmpty()) { throw new Exception(Exception::INSTALLATION_NOT_FOUND); } - $repository = $auth->skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [ + $repository = $authorization->skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [ Query::equal('projectInternalId', [$project->getInternalId()]) ])); @@ -1197,7 +1197,7 @@ Http::patch('/v1/vcs/github/installations/:installationId/repositories/:reposito // TODO: Delete from array when PR is closed - $repository = $auth->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); + $repository = $authorization->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); @@ -1221,7 +1221,7 @@ Http::patch('/v1/vcs/github/installations/:installationId/repositories/:reposito $providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? ''; $providerCommitHash = $pullRequestResponse['head']['sha'] ?? ''; - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $auth); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $authorization); $response->noContent(); }); From c06948b266e461b1e141338ed53fcb6126831d00 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:57:07 -0400 Subject: [PATCH 04/13] fixes: phpstan --- src/Appwrite/Platform/Tasks/Specs.php | 10 ---------- .../Services/Functions/FunctionsCustomServerTest.php | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/Specs.php b/src/Appwrite/Platform/Tasks/Specs.php index 9d361aa6c0..776410e4e4 100644 --- a/src/Appwrite/Platform/Tasks/Specs.php +++ b/src/Appwrite/Platform/Tasks/Specs.php @@ -34,16 +34,6 @@ class Specs extends Action return 'specs'; } - public function getRequest(): UtopiaRequest - { - return new AppwriteRequest(new SwooleRequest()); - } - - public function getResponse(): UtopiaResponse - { - return new AppwriteResponse(new SwooleResponse()); - } - public function __construct() { $this diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 59a3041a3f..908a1e9148 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -553,7 +553,7 @@ class FunctionsCustomServerTest extends Scope $folder = 'php-large'; $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output); $chunkSize = 5 * 1024 * 1024; $handle = @fopen($code, "rb"); From d1729404c1bb80e10a4829e21fb11b027b809421 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:29:35 -0400 Subject: [PATCH 05/13] fix: adjusting test --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 908a1e9148..f605564726 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1859,10 +1859,11 @@ class FunctionsCustomServerTest extends Scope 'x-appwrite-response-format' => '1.4.0', // Set response format for 1.4 syntax ], $this->getHeaders()), [ - 'queries' => [ 'equal("name", ["Test2"])' ] + 'queries' => [ + Query::equal('name', ['Test2'])->toString(), + ] ] ); - $this->assertEquals(200, $response['headers']['status-code']); $this->assertCount(1, $response['body']['functions']); $this->assertEquals('Test2', $response['body']['functions'][0]['name']); From 160b6c3386835e9e3c7826e732c04502e82c1bcb Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:11:39 -0400 Subject: [PATCH 06/13] chore: updates --- composer.json | 8 ++--- composer.lock | 88 ++++++++++++++++++++++++++++----------------------- 2 files changed, 52 insertions(+), 44 deletions(-) diff --git a/composer.json b/composer.json index 938b53639e..2bc63daaa2 100644 --- a/composer.json +++ b/composer.json @@ -47,13 +47,13 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.15.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "0.44.*", + "utopia-php/abuse": "0.46.*", "utopia-php/analytics": "0.13.*", - "utopia-php/audit": "0.44.*", + "utopia-php/audit": "0.46.*", "utopia-php/cache": "0.10.*", "utopia-php/cli": "0.19.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.54.*", + "utopia-php/database": "0.55.*", "utopia-php/domains": "0.6.*", "utopia-php/dsn": "0.2.*", "utopia-php/framework": "1.0.*", @@ -62,7 +62,7 @@ "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.12.*", - "utopia-php/migration": "0.5.*", + "utopia-php/migration": "0.7.*", "utopia-php/orchestration": "0.15.*", "utopia-php/platform": "0.8.*", "utopia-php/view": "0.2.*", diff --git a/composer.lock b/composer.lock index e540fc6c17..2fda8b29c4 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": "7a1cc88504a554200880c4b2ba7cd6e5", + "content-hash": "99f1d21e82fb9944a2b83284f36b88b2", "packages": [ { "name": "adhocore/jwt", @@ -65,16 +65,16 @@ }, { "name": "appwrite/appwrite", - "version": "10.1.0", + "version": "11.1.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "da579af70723cfc117b5af84375bdef117e27312" + "reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/da579af70723cfc117b5af84375bdef117e27312", - "reference": "da579af70723cfc117b5af84375bdef117e27312", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/1d043f543acdb17b9fdb440b1b2dd208e400bad3", + "reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3", "shasum": "" }, "require": { @@ -83,7 +83,8 @@ "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "3.7.35" + "mockery/mockery": "^1.6.6", + "phpunit/phpunit": "^10" }, "type": "library", "autoload": { @@ -99,10 +100,10 @@ "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/10.1.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/11.1.0", "url": "https://appwrite.io/support" }, - "time": "2023-11-20T09:56:12+00:00" + "time": "2024-06-26T07:03:23+00:00" }, { "name": "appwrite/php-clamav", @@ -1429,16 +1430,16 @@ }, { "name": "utopia-php/abuse", - "version": "0.44.0", + "version": "0.46.0", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "cd37bba23778f3a0dc54b4ba6ffb36d4d91ed8f1" + "reference": "ab09ecf6ceac5420020e6046ec651c155005d3c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/cd37bba23778f3a0dc54b4ba6ffb36d4d91ed8f1", - "reference": "cd37bba23778f3a0dc54b4ba6ffb36d4d91ed8f1", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/ab09ecf6ceac5420020e6046ec651c155005d3c3", + "reference": "ab09ecf6ceac5420020e6046ec651c155005d3c3", "shasum": "" }, "require": { @@ -1446,7 +1447,7 @@ "ext-pdo": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "0.54.*" + "utopia-php/database": "0.55.*" }, "require-dev": { "laravel/pint": "1.5.*", @@ -1474,9 +1475,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/0.44.0" + "source": "https://github.com/utopia-php/abuse/tree/0.46.0" }, - "time": "2024-09-05T16:09:32+00:00" + "time": "2024-10-07T19:09:29+00:00" }, { "name": "utopia-php/analytics", @@ -1527,21 +1528,21 @@ }, { "name": "utopia-php/audit", - "version": "0.44.0", + "version": "0.46.0", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "69eee24e4d6cb8fdae31235d80b9a46b18092139" + "reference": "660bb322ca1e6a9fa121610a85930e65863e1e5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/69eee24e4d6cb8fdae31235d80b9a46b18092139", - "reference": "69eee24e4d6cb8fdae31235d80b9a46b18092139", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/660bb322ca1e6a9fa121610a85930e65863e1e5d", + "reference": "660bb322ca1e6a9fa121610a85930e65863e1e5d", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/database": "0.54.*" + "utopia-php/database": "0.55.*" }, "require-dev": { "laravel/pint": "1.5.*", @@ -1568,9 +1569,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/0.44.0" + "source": "https://github.com/utopia-php/audit/tree/0.46.0" }, - "time": "2024-09-05T16:12:41+00:00" + "time": "2024-10-07T19:10:12+00:00" }, { "name": "utopia-php/cache", @@ -1726,16 +1727,16 @@ }, { "name": "utopia-php/database", - "version": "0.54.1", + "version": "0.55.1", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "c32d6eab5992c927cbf6fb4aad51d76fc5f64946" + "reference": "6f44079999491feb0ef85686f6b2ac7ef54db828" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/c32d6eab5992c927cbf6fb4aad51d76fc5f64946", - "reference": "c32d6eab5992c927cbf6fb4aad51d76fc5f64946", + "url": "https://api.github.com/repos/utopia-php/database/zipball/6f44079999491feb0ef85686f6b2ac7ef54db828", + "reference": "6f44079999491feb0ef85686f6b2ac7ef54db828", "shasum": "" }, "require": { @@ -1776,9 +1777,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.54.1" + "source": "https://github.com/utopia-php/database/tree/0.55.1" }, - "time": "2024-09-10T10:08:37+00:00" + "time": "2024-10-07T18:52:26+00:00" }, { "name": "utopia-php/di", @@ -2231,27 +2232,34 @@ }, { "name": "utopia-php/migration", - "version": "0.5.3", + "version": "0.7.0", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "b30e7834da69e25084b0c8e9ba29e4a7b54c6eb6" + "reference": "aa996ebfd3223fca2aa7d8b9aa31457c95344084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/b30e7834da69e25084b0c8e9ba29e4a7b54c6eb6", - "reference": "b30e7834da69e25084b0c8e9ba29e4a7b54c6eb6", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/aa996ebfd3223fca2aa7d8b9aa31457c95344084", + "reference": "aa996ebfd3223fca2aa7d8b9aa31457c95344084", "shasum": "" }, "require": { - "appwrite/appwrite": "10.1.0", - "php": "8.*" + "appwrite/appwrite": "11.1.*", + "ext-curl": "*", + "ext-openssl": "*", + "php": "8.3.*", + "utopia-php/database": "0.55.*", + "utopia-php/dsn": "0.2.*", + "utopia-php/storage": "0.19.*" }, "require-dev": { - "laravel/pint": "1.*", - "phpunit/phpunit": "9.*", - "utopia-php/cli": "^0.18.0", - "vlucas/phpdotenv": "5.*" + "ext-pdo": "*", + "laravel/pint": "1.17.*", + "phpstan/phpstan": "1.11.*", + "phpunit/phpunit": "11.2.*", + "utopia-php/cli": "0.19.*", + "vlucas/phpdotenv": "5.6.*" }, "type": "library", "autoload": { @@ -2273,9 +2281,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.5.3" + "source": "https://github.com/utopia-php/migration/tree/0.7.0" }, - "time": "2024-09-10T10:45:18+00:00" + "time": "2024-10-07T19:00:18+00:00" }, { "name": "utopia-php/mongo", From 3e9a24232c4e6945996ec056db4b60bdf3628364 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:26:01 -0400 Subject: [PATCH 07/13] fix: introducing local variable --- app/controllers/api/account.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index a76b9dbea5..007435a5df 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1379,6 +1379,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') $email = $oauth2->getUserEmail($accessToken); // Check if this identity is connected to a different user + $sessionUpgrade = false; if (!$user->isEmpty()) { $userId = $user->getId(); @@ -1672,7 +1673,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); } - if (isset($sessionUpgrade) && $sessionUpgrade) { + if ($sessionUpgrade) { foreach ($user->getAttribute('targets', []) as $target) { if ($target->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) { continue; From ab5632bfcd18f948d6de5e37ce48efeefbecd5af Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 20:17:45 -0400 Subject: [PATCH 08/13] fix: wrong document check --- app/controllers/api/teams.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 6e6ca758c9..633a95c01e 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -463,17 +463,17 @@ Http::post('/v1/teams/:teamId/memberships') $name = empty($name) ? $invitee->getAttribute('name', '') : $name; } elseif (!empty($email)) { $invitee = $dbForProject->findOne('users', [Query::equal('email', [$email])]); // Get user by email address - if (!empty($invitee) && !empty($phone) && $invitee->getAttribute('phone', '') !== $phone) { + if ($invitee->isEmpty() && !empty($phone) && $invitee->getAttribute('phone', '') !== $phone) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given email and phone doesn\'t match', 409); } } elseif (!empty($phone)) { $invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]); - if (!empty($invitee) && !empty($email) && $invitee->getAttribute('email', '') !== $email) { + if ($invitee->isEmpty() && !empty($email) && $invitee->getAttribute('email', '') !== $email) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given phone and email doesn\'t match', 409); } } - if (empty($invitee)) { // Create new user if no user with same email found + if ($invitee->isEmpty()) { // Create new user if no user with same email found $limit = $project->getAttribute('auths', [])['limit'] ?? 0; if (!$isPrivilegedUser && !$isAppUser && $limit !== 0 && $project->getId() !== 'console') { // check users limit, console invites are allways allowed. @@ -525,6 +525,7 @@ Http::post('/v1/teams/:teamId/memberships') 'memberships' => null, 'search' => implode(' ', [$userId, $email, $name]), ]))); + } catch (Duplicate $th) { throw new Exception(Exception::USER_ALREADY_EXISTS); } @@ -572,6 +573,7 @@ Http::post('/v1/teams/:teamId/memberships') $dbForProject->purgeCachedDocument('users', $invitee->getId()); } else { try { + print_r($membership); $membership = $dbForProject->createDocument('memberships', $membership); } catch (Duplicate $th) { throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS); From c34f8afa819c92de1d2c843f1aa46bba857eb0e7 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 20:34:27 -0400 Subject: [PATCH 09/13] fix: wrong document check --- app/controllers/api/account.php | 36 +++++++++---------- app/controllers/api/functions.php | 2 +- app/controllers/api/migrations.php | 10 +++--- app/controllers/api/projects.php | 20 +++++------ app/controllers/api/proxy.php | 2 +- app/controllers/api/teams.php | 2 +- app/controllers/api/users.php | 12 +++---- app/controllers/api/vcs.php | 8 ++--- app/controllers/general.php | 4 +-- .../Platform/Workers/Certificates.php | 6 ++-- src/Appwrite/Platform/Workers/Messaging.php | 4 +-- 11 files changed, 53 insertions(+), 53 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 007435a5df..ea0c61a949 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -332,7 +332,7 @@ Http::post('/v1/account') $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } @@ -395,7 +395,7 @@ Http::post('/v1/account') $existingTarget = $dbForProject->findOne('targets', [ Query::equal('identifier', [$email]), ]); - if ($existingTarget) { + if (!$existingTarget->isEmpty()) { $user->setAttribute('targets', $existingTarget, Document::SET_TYPE_APPEND); } } @@ -840,7 +840,7 @@ Http::post('/v1/account/sessions/email') Query::equal('email', [$email]), ]); - if (!$profile || empty($profile->getAttribute('passwordUpdate')) || !Auth::passwordVerify($password, $profile->getAttribute('password'), $profile->getAttribute('hash'), $profile->getAttribute('hashOptions'))) { + if ($profile->isEmpty() || empty($profile->getAttribute('passwordUpdate')) || !Auth::passwordVerify($password, $profile->getAttribute('password'), $profile->getAttribute('hash'), $profile->getAttribute('hashOptions'))) { throw new Exception(Exception::USER_INVALID_CREDENTIALS); } @@ -1387,7 +1387,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') Query::equal('providerEmail', [$email]), Query::notEqual('userInternalId', $user->getInternalId()), ]); - if (!empty($identityWithMatchingEmail)) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::USER_ALREADY_EXISTS); } @@ -1418,7 +1418,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') Query::equal('provider', [$provider]), Query::equal('providerUid', [$oauth2ID]), ]); - if ($session !== false && !$session->isEmpty()) { + if (!$session->isEmpty()) { $user->setAttributes($dbForProject->getDocument('users', $session->getAttribute('userId'))->getArrayCopy()); } } @@ -1436,7 +1436,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') $userWithEmail = $dbForProject->findOne('users', [ Query::equal('email', [$email]), ]); - if ($userWithEmail !== false && !$userWithEmail->isEmpty()) { + if (!$userWithEmail->isEmpty()) { $user->setAttributes($userWithEmail->getArrayCopy()); } @@ -1447,7 +1447,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') Query::equal('providerUid', [$oauth2ID]), ]); - if ($identity !== false && !$identity->isEmpty()) { + if (!$identity->isEmpty()) { $user = $dbForProject->getDocument('users', $identity->getAttribute('userId')); } } @@ -1467,7 +1467,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } @@ -1531,7 +1531,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') Query::equal('provider', [$provider]), Query::equal('providerUid', [$oauth2ID]), ]); - if ($identity === false || $identity->isEmpty()) { + if ($identity->isEmpty()) { // Before creating the identity, check if the email is already associated with another user $userId = $user->getId(); @@ -1816,7 +1816,7 @@ Http::post('/v1/account/tokens/magic-url') $isAppUser = Auth::isAppUser($roles); $result = $dbForProject->findOne('users', [Query::equal('email', [$email])]); - if ($result !== false && !$result->isEmpty()) { + if (!$result->isEmpty()) { $user->setAttributes($result->getArrayCopy()); } else { $limit = $project->getAttribute('auths', [])['limit'] ?? 0; @@ -1833,7 +1833,7 @@ Http::post('/v1/account/tokens/magic-url') $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -2058,7 +2058,7 @@ Http::post('/v1/account/tokens/email') $isAppUser = Auth::isAppUser($roles); $result = $dbForProject->findOne('users', [Query::equal('email', [$email])]); - if ($result !== false && !$result->isEmpty()) { + if (!$result->isEmpty()) { $user->setAttributes($result->getArrayCopy()); } else { $limit = $project->getAttribute('auths', [])['limit'] ?? 0; @@ -2075,7 +2075,7 @@ Http::post('/v1/account/tokens/email') $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } @@ -2350,7 +2350,7 @@ Http::post('/v1/account/tokens/phone') $isAppUser = Auth::isAppUser($roles); $result = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]); - if ($result !== false && !$result->isEmpty()) { + if (!$result->isEmpty()) { $user->setAttributes($result->getArrayCopy()); } else { $limit = $project->getAttribute('auths', [])['limit'] ?? 0; @@ -2674,7 +2674,7 @@ Http::patch('/v1/account/email') Query::equal('providerEmail', [$email]), Query::notEqual('userInternalId', $user->getInternalId()), ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } @@ -2695,7 +2695,7 @@ Http::patch('/v1/account/email') Query::equal('identifier', [$email]), ])); - if ($target instanceof Document && !$target->isEmpty()) { + if (!$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); } @@ -2866,7 +2866,7 @@ Http::patch('/v1/account/phone') Query::equal('identifier', [$phone]), ])); - if ($target instanceof Document && !$target->isEmpty()) { + if (!$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); } @@ -3027,7 +3027,7 @@ Http::post('/v1/account/recovery') Query::equal('email', [$email]), ]); - if (!$profile) { + if ($profile->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index d0aa60ad06..77311c8a90 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -2297,7 +2297,7 @@ Http::delete('/v1/functions/:functionId/executions/:executionId') Query::equal('active', [true]), ]); - if ($schedule && !$schedule->isEmpty()) { + if (!$schedule->isEmpty()) { $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index f7bd875e3f..5e08e197ae 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -120,7 +120,7 @@ Http::post('/v1/migrations/firebase/oauth') Query::equal('provider', ['firebase']), Query::equal('userInternalId', [$user->getInternalId()]), ]); - if ($identity === false || $identity->isEmpty()) { + if ($identity->isEmpty()) { throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } @@ -569,7 +569,7 @@ Http::get('/v1/migrations/firebase/report/oauth') Query::equal('userInternalId', [$user->getInternalId()]), ]); - if ($identity === false || $identity->isEmpty()) { + if ($identity->isEmpty()) { throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } @@ -744,7 +744,7 @@ Http::get('/v1/migrations/firebase/redirect') Query::equal('providerEmail', [$email]), ]); - if ($identity !== false && !$identity->isEmpty()) { + if (!$identity->isEmpty()) { if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -813,7 +813,7 @@ Http::get('/v1/migrations/firebase/projects') Query::equal('userInternalId', [$user->getInternalId()]), ]); - if ($identity === false || $identity->isEmpty()) { + if ($identity->isEmpty()) { throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } @@ -893,7 +893,7 @@ Http::get('/v1/migrations/firebase/deauthorize') Query::equal('userInternalId', [$user->getInternalId()]), ]); - if ($identity === false || $identity->isEmpty()) { + if ($identity->isEmpty()) { throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase'); //TODO: Replace with USER_IDENTITY_NOT_FOUND } diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index c1b04836d6..8897752f2e 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1077,7 +1077,7 @@ Http::get('/v1/projects/:projectId/webhooks/:webhookId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($webhook === false || $webhook->isEmpty()) { + if ($webhook->isEmpty()) { throw new Exception(Exception::WEBHOOK_NOT_FOUND); } @@ -1119,7 +1119,7 @@ Http::put('/v1/projects/:projectId/webhooks/:webhookId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($webhook === false || $webhook->isEmpty()) { + if ($webhook->isEmpty()) { throw new Exception(Exception::WEBHOOK_NOT_FOUND); } @@ -1168,7 +1168,7 @@ Http::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($webhook === false || $webhook->isEmpty()) { + if ($webhook->isEmpty()) { throw new Exception(Exception::WEBHOOK_NOT_FOUND); } @@ -1205,7 +1205,7 @@ Http::delete('/v1/projects/:projectId/webhooks/:webhookId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($webhook === false || $webhook->isEmpty()) { + if ($webhook->isEmpty()) { throw new Exception(Exception::WEBHOOK_NOT_FOUND); } @@ -1324,7 +1324,7 @@ Http::get('/v1/projects/:projectId/keys/:keyId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($key === false || $key->isEmpty()) { + if ($key->isEmpty()) { throw new Exception(Exception::KEY_NOT_FOUND); } @@ -1360,7 +1360,7 @@ Http::put('/v1/projects/:projectId/keys/:keyId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($key === false || $key->isEmpty()) { + if ($key->isEmpty()) { throw new Exception(Exception::KEY_NOT_FOUND); } @@ -1401,7 +1401,7 @@ Http::delete('/v1/projects/:projectId/keys/:keyId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($key === false || $key->isEmpty()) { + if ($key->isEmpty()) { throw new Exception(Exception::KEY_NOT_FOUND); } @@ -1557,7 +1557,7 @@ Http::get('/v1/projects/:projectId/platforms/:platformId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($platform === false || $platform->isEmpty()) { + if ($platform->isEmpty()) { throw new Exception(Exception::PLATFORM_NOT_FOUND); } @@ -1594,7 +1594,7 @@ Http::put('/v1/projects/:projectId/platforms/:platformId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($platform === false || $platform->isEmpty()) { + if ($platform->isEmpty()) { throw new Exception(Exception::PLATFORM_NOT_FOUND); } @@ -1637,7 +1637,7 @@ Http::delete('/v1/projects/:projectId/platforms/:platformId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($platform === false || $platform->isEmpty()) { + if ($platform->isEmpty()) { throw new Exception(Exception::PLATFORM_NOT_FOUND); } diff --git a/app/controllers/api/proxy.php b/app/controllers/api/proxy.php index 3ca019c9ce..d4283691ae 100644 --- a/app/controllers/api/proxy.php +++ b/app/controllers/api/proxy.php @@ -63,7 +63,7 @@ Http::post('/v1/proxy/rules') Query::equal('domain', [$domain]), ]); - if ($document && !$document->isEmpty()) { + if (!$document->isEmpty()) { if ($document->getAttribute('projectId') === $project->getId()) { $resourceType = $document->getAttribute('resourceType'); $resourceId = $document->getAttribute('resourceId'); diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 633a95c01e..5734e96250 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -473,7 +473,7 @@ Http::post('/v1/teams/:teamId/memberships') } } - if ($invitee->isEmpty()) { // Create new user if no user with same email found + if (!isset($invitee) || $invitee->isEmpty()) { // Create new user if no user with same email found $limit = $project->getAttribute('auths', [])['limit'] ?? 0; if (!$isPrivilegedUser && !$isAppUser && $limit !== 0 && $project->getId() !== 'console') { // check users limit, console invites are allways allowed. diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 3d080fd98e..9f33ee38db 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -63,7 +63,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } } @@ -140,7 +140,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e $existingTarget = $dbForProject->findOne('targets', [ Query::equal('identifier', [$email]), ]); - if ($existingTarget) { + if (!$existingTarget->isEmpty()) { $user->setAttribute('targets', $existingTarget, Document::SET_TYPE_APPEND); } } @@ -164,7 +164,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e $existingTarget = $dbForProject->findOne('targets', [ Query::equal('identifier', [$phone]), ]); - if ($existingTarget) { + if (!$existingTarget->isEmpty()) { $user->setAttribute('targets', $existingTarget, Document::SET_TYPE_APPEND); } } @@ -1215,7 +1215,7 @@ Http::patch('/v1/users/:userId/email') Query::equal('providerEmail', [$email]), Query::notEqual('userInternalId', $user->getInternalId()), ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -1223,7 +1223,7 @@ Http::patch('/v1/users/:userId/email') Query::equal('identifier', [$email]), ]); - if ($target instanceof Document && !$target->isEmpty()) { + if (!$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); } } @@ -1313,7 +1313,7 @@ Http::patch('/v1/users/:userId/phone') Query::equal('identifier', [$number]), ]); - if ($target instanceof Document && !$target->isEmpty()) { + if (!$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); } } diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index ef325ee56b..1f97e4f2d5 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -108,7 +108,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId Query::orderDesc('$createdAt'), ])); - if ($latestComment !== false && !$latestComment->isEmpty()) { + if (!$latestComment->isEmpty()) { $latestCommentId = $latestComment->getAttribute('providerCommentId', ''); $comment = new Comment(); $comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId)); @@ -369,7 +369,7 @@ Http::get('/v1/vcs/github/callback') $identity = $dbForConsole->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if ($identity !== false && !$identity->isEmpty()) { + if (!$identity->isEmpty()) { if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -416,7 +416,7 @@ Http::get('/v1/vcs/github/callback') Query::equal('projectInternalId', [$projectInternalId]) ]); - if ($installation === false || $installation->isEmpty()) { + if ($installation->isEmpty()) { $teamId = $project->getAttribute('teamId', ''); $installation = new Document([ @@ -724,7 +724,7 @@ Http::post('/v1/vcs/github/installations/:installationId/providerRepositories') Query::equal('provider', ['github']), Query::equal('userInternalId', [$user->getInternalId()]), ]); - if ($identity === false || $identity->isEmpty()) { + if ($identity->isEmpty()) { throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } diff --git a/app/controllers/general.php b/app/controllers/general.php index 1e403ef93a..70cf55d280 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -520,7 +520,7 @@ Http::init() $mainDomain = $envDomain; } else { $domainDocument = $dbForConsole->findOne('rules', [Query::orderAsc('$id')]); - $mainDomain = $domainDocument ? $domainDocument->getAttribute('domain') : $domain->get(); + $mainDomain = !$domainDocument->isEmpty() ? $domainDocument->getAttribute('domain') : $domain->get(); } if ($mainDomain !== $domain->get()) { @@ -530,7 +530,7 @@ Http::init() Query::equal('domain', [$domain->get()]) ]); - if (!$domainDocument) { + if ($domainDocument->isEmpty()) { $domainDocument = new Document([ 'domain' => $domain->get(), 'resourceType' => 'api', diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index 4a8d928ba2..07e1dcdaa6 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -126,7 +126,7 @@ class Certificates extends Action $certificate = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain->get()])]); // If we don't have certificate for domain yet, let's create new document. At the end we save it - if (!$certificate) { + if ($certificate->isEmpty()) { $certificate = new Document(); $certificate->setAttribute('domain', $domain->get()); } @@ -216,7 +216,7 @@ class Certificates extends Action { // Check if update or insert required $certificateDocument = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain])]); - if (!empty($certificateDocument) && !$certificateDocument->isEmpty()) { + if (!$certificateDocument->isEmpty()) { // Merge new data with current data $certificate = new Document(\array_merge($certificateDocument->getArrayCopy(), $certificate->getArrayCopy())); $certificate = $dbForConsole->updateDocument('certificates', $certificate->getId(), $certificate); @@ -478,7 +478,7 @@ class Certificates extends Action Query::equal('domain', [$domain]), ]); - if ($rule !== false && !$rule->isEmpty()) { + if (!$rule->isEmpty()) { $rule->setAttribute('certificateId', $certificateId); $rule->setAttribute('status', $success ? 'verified' : 'unverified'); $dbForConsole->updateDocument('rules', $rule->getId(), $rule); diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 510fec0431..ddb72803e3 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -178,7 +178,7 @@ class Messaging extends Action Query::equal('type', [$providerType]), ]); - if ($default === false || $default->isEmpty()) { + if ($default->isEmpty()) { $dbForProject->updateDocument('messages', $message->getId(), $message->setAttributes([ 'status' => MessageStatus::FAILED, 'deliveryErrors' => ['No enabled provider found.'] @@ -275,7 +275,7 @@ class Messaging extends Action Query::equal('identifier', [$result['recipient']]) ]); - if ($target instanceof Document && !$target->isEmpty()) { + if (!$target->isEmpty()) { $dbForProject->updateDocument( 'targets', $target->getId(), From 60e0554fdf0c4622bc3220d65d26403ae5358673 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 20:41:44 -0400 Subject: [PATCH 10/13] fix: remove leftover print_r --- app/controllers/api/teams.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 5734e96250..8f5fba30ab 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -573,7 +573,6 @@ Http::post('/v1/teams/:teamId/memberships') $dbForProject->purgeCachedDocument('users', $invitee->getId()); } else { try { - print_r($membership); $membership = $dbForProject->createDocument('memberships', $membership); } catch (Duplicate $th) { throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS); From 40266481e0232f6162b4018270fa211667fed8f3 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 20:48:55 -0400 Subject: [PATCH 11/13] chore: package update --- composer.json | 2 +- composer.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 2bc63daaa2..135822bc9e 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,7 @@ "utopia-php/cache": "0.10.*", "utopia-php/cli": "0.19.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.55.*", + "utopia-php/database": "0.55.1", "utopia-php/domains": "0.6.*", "utopia-php/dsn": "0.2.*", "utopia-php/framework": "1.0.*", diff --git a/composer.lock b/composer.lock index 2fda8b29c4..cb468c1663 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": "99f1d21e82fb9944a2b83284f36b88b2", + "content-hash": "b808f001c3ff4c0396cf51f46e8b314a", "packages": [ { "name": "adhocore/jwt", From 25650e7501b8dba8a2a3a5de96283dd4f2e32b89 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:01:59 -0400 Subject: [PATCH 12/13] chore: trigger ci From 61dba4eefb77b67e7f61befb9a0de331c5771741 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:16:31 -0400 Subject: [PATCH 13/13] revert: reverting commit 39490e9 --- .github/workflows/tests.yml | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6e7012527d..3ca3c9c833 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,22 +16,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v3 with: submodules: recursive - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v2 - name: Build Appwrite - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v3 with: context: . push: false tags: ${{ env.IMAGE }} load: true - cache-from: type=gha,scope=appwrite - cache-to: type=gha,mode=max,scope=appwrite + cache-from: type=gha + cache-to: type=gha,mode=max outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar build-args: | DEBUG=false @@ -39,13 +39,12 @@ jobs: VERSION=dev - name: Cache Docker Image - uses: actions/cache@v4 + uses: actions/cache@v3 with: key: ${{ env.CACHE_KEY }} - restore-keys: | - appwrite-dev- path: /tmp/${{ env.IMAGE }}.tar + unit_test: name: Unit Test runs-on: ubuntu-latest @@ -53,10 +52,10 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Load Cache - uses: actions/cache@v4 + uses: actions/cache@v3 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar @@ -83,10 +82,10 @@ jobs: needs: setup steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Load Cache - uses: actions/cache@v4 + uses: actions/cache@v3 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar @@ -131,10 +130,10 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Load Cache - uses: actions/cache@v4 + uses: actions/cache@v3 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar @@ -158,9 +157,9 @@ jobs: needs: setup steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Load Cache - uses: actions/cache@v4 + uses: actions/cache@v3 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar