diff --git a/Dockerfile b/Dockerfile index ecc5112cc4..ac8cff0884 100755 --- a/Dockerfile +++ b/Dockerfile @@ -77,6 +77,7 @@ RUN chmod +x /usr/local/bin/doctor && \ chmod +x /usr/local/bin/queue-count-success && \ chmod +x /usr/local/bin/worker-audits && \ chmod +x /usr/local/bin/worker-builds && \ + chmod +x /usr/local/bin/worker-screenshots && \ chmod +x /usr/local/bin/worker-certificates && \ chmod +x /usr/local/bin/worker-databases && \ chmod +x /usr/local/bin/worker-deletes && \ diff --git a/app/cli.php b/app/cli.php index 07966b2450..7493d10ab3 100644 --- a/app/cli.php +++ b/app/cli.php @@ -41,8 +41,6 @@ Config::setParam('runtimes', (new Runtimes('v5'))->getAll(supported: false)); // require controllers after overwriting runtimes require_once __DIR__ . '/controllers/general.php'; -Authorization::disable(); - CLI::setResource('register', fn () => $register); CLI::setResource('cache', function ($pools) { @@ -60,7 +58,13 @@ CLI::setResource('pools', function (Registry $register) { return $register->get('pools'); }, ['register']); -CLI::setResource('dbForPlatform', function ($pools, $cache) { +CLI::setResource('authorization', function () { + $authorization = new Authorization(); + $authorization->disable(); + return $authorization; +}, []); + +CLI::setResource('dbForPlatform', function ($pools, $cache, $authorization) { $sleep = 3; $maxAttempts = 5; $attempts = 0; @@ -74,6 +78,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) { $dbForPlatform = new Database($adapter, $cache); $dbForPlatform + ->setAuthorization($authorization) ->setNamespace('_console') ->setMetadata('host', \gethostname()) ->setMetadata('project', 'console'); @@ -99,7 +104,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) { } return $dbForPlatform; -}, ['pools', 'cache']); +}, ['pools', 'cache', 'authorization']); CLI::setResource('console', function () { return new Document(Config::getParam('console')); @@ -110,10 +115,10 @@ CLI::setResource( fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false ); -CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) { +CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) { $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools - return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases) { + return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForPlatform; } @@ -146,6 +151,7 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform $adapter = new DatabasePool($pools->get($dsn->getHost())); $database = new Database($adapter, $cache); + $databases[$dsn->getHost()] = $database; $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); @@ -162,17 +168,18 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform } $database + ->setAuthorization($authorization) ->setMetadata('host', \gethostname()) ->setMetadata('project', $project->getId()); return $database; }; -}, ['pools', 'dbForPlatform', 'cache']); +}, ['pools', 'dbForPlatform', 'cache', 'authorization']); -CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) { +CLI::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { $database = null; - return function (?Document $project = null) use ($pools, $cache, $database) { + return function (?Document $project = null) use ($pools, $cache, $database, $authorization) { if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') { $database->setTenant((int)$project->getSequence()); return $database; @@ -182,6 +189,7 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) { $database = new Database($adapter, $cache); $database + ->setAuthorization($authorization) ->setSharedTables(true) ->setNamespace('logsV1') ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_TASK) @@ -194,7 +202,7 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) { return $database; }; -}, ['pools', 'cache']); +}, ['pools', 'cache', 'authorization']); CLI::setResource('publisher', function (Group $pools) { return new BrokerPool(publisher: $pools->get('publisher')); }, ['pools']); diff --git a/app/config/services.php b/app/config/services.php index e4bbf9b6f6..531306391e 100644 --- a/app/config/services.php +++ b/app/config/services.php @@ -48,7 +48,7 @@ return [ 'name' => 'Avatars', 'subtitle' => 'The Avatars service aims to help you complete everyday tasks related to your app image, icons, and avatars.', 'description' => '/docs/services/avatars.md', - 'controller' => 'api/avatars.php', + 'controller' => '', // Uses modules 'sdk' => true, 'docs' => true, 'docsUrl' => 'https://appwrite.io/docs/client/avatars', @@ -146,7 +146,7 @@ return [ 'name' => 'Storage', 'subtitle' => 'The Storage service allows you to manage your project files.', 'description' => '/docs/services/storage.md', - 'controller' => '', + 'controller' => '', // Uses modules 'sdk' => true, 'docs' => true, 'docsUrl' => 'https://appwrite.io/docs/client/storage', diff --git a/app/config/storage/resource_limits.php b/app/config/storage/resource_limits.php index cfbcea5a47..43ed2b8b05 100644 --- a/app/config/storage/resource_limits.php +++ b/app/config/storage/resource_limits.php @@ -3,4 +3,6 @@ use Utopia\Image\Image; use Utopia\System\System; -Image::setResourceLimit('memory', intval(System::getEnv('_APP_IMAGES_RESOURCE_LIMIT_MEMORY', 1024*1024*64))); +if (\class_exists('Imagick')) { + Image::setResourceLimit('memory', intval(System::getEnv('_APP_IMAGES_RESOURCE_LIMIT_MEMORY', 1024*1024*64))); +} diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 2c481b500c..bcea3387a2 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -207,10 +207,10 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr } -$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode) { +$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, Authorization $authorization) { /** @var Appwrite\Utopia\Database\Documents\User $userFromRequest */ - $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); @@ -266,7 +266,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', [ @@ -275,7 +275,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 @@ -376,8 +376,9 @@ App::post('/v1/account') ->inject('user') ->inject('project') ->inject('dbForProject') + ->inject('authorization') ->inject('hooks') - ->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Hooks $hooks) { + ->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Hooks $hooks) { $email = \strtolower($email); if ('console' === $project->getId()) { @@ -469,9 +470,9 @@ App::post('/v1/account') ]); $user->removeAttribute('$sequence'); - $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())), @@ -497,9 +498,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()); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -976,7 +977,8 @@ App::post('/v1/account/sessions/email') ->inject('store') ->inject('proofForPassword') ->inject('proofForToken') - ->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) { + ->inject('authorization') + ->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) { $email = \strtolower($email); $protocol = $request->getProtocol(); @@ -1021,7 +1023,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') !== $proofForPassword->getHash()->getName()) { @@ -1120,7 +1122,8 @@ App::post('/v1/account/sessions/anonymous') ->inject('store') ->inject('proofForPassword') ->inject('proofForToken') - ->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) { + ->inject('authorization') + ->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) { $protocol = $request->getProtocol(); if ('console' === $project->getId()) { @@ -1165,7 +1168,7 @@ App::post('/v1/account/sessions/anonymous') 'accessedAt' => DateTime::now(), ]); $user->removeAttribute('$sequence'); - Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); // Create session token $duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG; @@ -1191,7 +1194,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())), @@ -1274,6 +1277,7 @@ App::post('/v1/account/sessions/token') ->inject('store') ->inject('proofForToken') ->inject('proofForCode') +->inject('authorization') ->action($createSession); App::get('/v1/account/sessions/oauth2/:provider') @@ -1470,7 +1474,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->inject('store') ->inject('proofForPassword') ->inject('proofForToken') - ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) use ($oauthDefaultSuccess) { + ->inject('authorization') + ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) { $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; $port = $request->getPort(); $callbackBase = $protocol . '://' . $request->getHostname(); @@ -1726,7 +1731,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ]); $user->removeAttribute('$sequence'); - $userDoc = Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + $userDoc = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); $dbForProject->createDocument('targets', new Document([ '$permissions' => [ Permission::read(Role::user($user->getId())), @@ -1744,8 +1749,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 @@ -1816,7 +1821,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']); @@ -1840,7 +1845,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', [ @@ -2077,7 +2082,8 @@ App::post('/v1/account/tokens/magic-url') ->inject('queueForMails') ->inject('proofForPassword') ->inject('platform') - ->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, array $platform) { + ->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, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); } @@ -2150,7 +2156,7 @@ App::post('/v1/account/tokens/magic-url') ]); $user->removeAttribute('$sequence'); - Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); } $proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL); @@ -2170,7 +2176,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', [ @@ -2356,7 +2362,8 @@ App::post('/v1/account/tokens/email') ->inject('queueForMails') ->inject('proofForPassword') ->inject('proofForCode') - ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, ProofsCode $proofForCode) { + ->inject('authorization') + ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); } @@ -2425,9 +2432,9 @@ App::post('/v1/account/tokens/email') ]); $user->removeAttribute('$sequence'); - $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())), @@ -2465,7 +2472,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', [ @@ -2662,10 +2669,11 @@ App::put('/v1/account/sessions/magic-url') ->inject('queueForMails') ->inject('store') ->inject('proofForCode') - ->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode) use ($createSession) { + ->inject('authorization') + ->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $authorization) use ($createSession) { $proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL); $proofForToken->setHash(new Sha()); - $createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode); + $createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $authorization); }); App::put('/v1/account/sessions/phone') @@ -2711,6 +2719,7 @@ App::put('/v1/account/sessions/phone') ->inject('store') ->inject('proofForToken') ->inject('proofForCode') + ->inject('authorization') ->action($createSession); App::post('/v1/account/tokens/phone') @@ -2754,7 +2763,8 @@ App::post('/v1/account/tokens/phone') ->inject('plan') ->inject('store') ->inject('proofForCode') - ->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Store $store, ProofsCode $proofForCode) { + ->inject('authorization') + ->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Store $store, ProofsCode $proofForCode, Authorization $authorization) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } @@ -2804,9 +2814,9 @@ App::post('/v1/account/tokens/phone') ]); $user->removeAttribute('$sequence'); - 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())), @@ -2852,7 +2862,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', [ @@ -3243,7 +3253,8 @@ App::patch('/v1/account/email') ->inject('project') ->inject('hooks') ->inject('proofForPassword') - ->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword) { + ->inject('authorization') + ->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword, Authorization $authorization) { // passwordUpdate will be empty if the user has never set a password $passwordUpdate = $user->getAttribute('passwordUpdate'); @@ -3295,7 +3306,7 @@ App::patch('/v1/account/email') ->setAttribute('passwordUpdate', DateTime::now()); } - $target = Authorization::skip(fn () => $dbForProject->findOne('targets', [ + $target = $authorization->skip(fn () => $dbForProject->findOne('targets', [ Query::equal('identifier', [$email]), ])); @@ -3311,7 +3322,7 @@ App::patch('/v1/account/email') $oldTarget = $user->find('identifier', $oldEmail, 'targets'); if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { - Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email))); + $authorization->skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email))); } $dbForProject->purgeCachedDocument('users', $user->getId()); } catch (Duplicate) { @@ -3352,8 +3363,9 @@ App::patch('/v1/account/phone') ->inject('queueForEvents') ->inject('project') ->inject('hooks') - ->inject('proofForPassword') - ->action(function (string $phone, string $password, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword) { + ->inject('proofForPassword') +->inject('authorization') + ->action(function (string $phone, string $password, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword, Authorization $authorization) { // passwordUpdate will be empty if the user has never set a password $passwordUpdate = $user->getAttribute('passwordUpdate'); @@ -3368,7 +3380,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]), ])); @@ -3399,7 +3411,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) { @@ -3535,7 +3547,9 @@ App::post('/v1/account/recovery') ->inject('queueForMails') ->inject('queueForEvents') ->inject('proofForToken') - ->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken) { + ->inject('authorization') + ->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) { + if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); } @@ -3571,7 +3585,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', [ @@ -3727,7 +3741,8 @@ App::put('/v1/account/recovery') ->inject('hooks') ->inject('proofForPassword') ->inject('proofForToken') - ->action(function (string $userId, string $secret, string $password, Response $response, User $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks, ProofsPassword $proofForPassword, ProofsToken $proofForToken) { +->inject('authorization') + ->action(function (string $userId, string $secret, string $password, Response $response, User $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) { /** @var Appwrite\Utopia\Database\Documents\User $profile */ $profile = $dbForProject->getDocument('users', $userId); @@ -3741,7 +3756,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 = $proofForPassword->hash($password); @@ -3844,7 +3859,8 @@ App::post('/v1/account/verifications/email') ->inject('queueForEvents') ->inject('queueForMails') ->inject('proofForToken') - ->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken) { + ->inject('authorization') + ->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken, Authorization $authorization) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); @@ -3873,7 +3889,7 @@ App::post('/v1/account/verifications/email') 'ip' => $request->getIP(), ]); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $verification = $dbForProject->createDocument('tokens', $verification ->setAttribute('$permissions', [ @@ -4072,9 +4088,10 @@ App::put('/v1/account/verifications/email') ->inject('dbForProject') ->inject('queueForEvents') ->inject('proofForToken') - ->action(function (string $userId, string $secret, Response $response, User $user, Database $dbForProject, Event $queueForEvents, ProofsToken $proofForToken) { + ->inject('authorization') + ->action(function (string $userId, string $secret, Response $response, User $user, Database $dbForProject, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) { /** @var Appwrite\Utopia\Database\Documents\User $profile */ - $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); @@ -4086,7 +4103,7 @@ App::put('/v1/account/verifications/email') 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)); @@ -4146,7 +4163,8 @@ App::post('/v1/account/verifications/phone') ->inject('queueForStatsUsage') ->inject('plan') ->inject('proofForCode') - ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, ProofsCode $proofForCode) { + ->inject('authorization') + ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, ProofsCode $proofForCode, Authorization $authorization) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } @@ -4185,7 +4203,7 @@ App::post('/v1/account/verifications/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', [ @@ -4291,9 +4309,10 @@ App::put('/v1/account/verifications/phone') ->inject('dbForProject') ->inject('queueForEvents') ->inject('proofForCode') - ->action(function (string $userId, string $secret, Response $response, User $user, Database $dbForProject, Event $queueForEvents, ProofsCode $proofForCode) { + ->inject('authorization') + ->action(function (string $userId, string $secret, Response $response, User $user, Database $dbForProject, Event $queueForEvents, ProofsCode $proofForCode, Authorization $authorization) { /** @var Appwrite\Utopia\Database\Documents\User $profile */ - $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); @@ -4305,7 +4324,7 @@ App::put('/v1/account/verifications/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)); @@ -4358,12 +4377,13 @@ App::post('/v1/account/targets/push') ->inject('dbForProject') ->inject('store') ->inject('proofForToken') - ->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, User $user, Request $request, Response $response, Database $dbForProject, Store $store, ProofsToken $proofForToken) { + ->inject('authorization') + ->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, User $user, Request $request, Response $response, Database $dbForProject, Store $store, ProofsToken $proofForToken, Authorization $authorization) { $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); @@ -4438,9 +4458,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); @@ -4503,8 +4524,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); diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php deleted file mode 100644 index b4f75a9ee5..0000000000 --- a/app/controllers/api/avatars.php +++ /dev/null @@ -1,1495 +0,0 @@ -crop((int) $width, (int) $height); - $output = (empty($output)) ? $type : $output; - $data = $image->output($output, $quality); - $response - ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days - ->setContentType('image/png') - ->file($data); - unset($image); -}; - -$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForPlatform, ?Logger $logger) { - try { - $user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId)); - - $sessions = $user->getAttribute('sessions', []); - - $gitHubSession = null; - foreach ($sessions as $session) { - if ($session->getAttribute('provider', '') === 'github') { - $gitHubSession = $session; - break; - } - } - - if (empty($gitHubSession)) { - throw new Exception(Exception::USER_SESSION_NOT_FOUND, 'GitHub session not found.'); - } - - $provider = $gitHubSession->getAttribute('provider', ''); - $accessToken = $gitHubSession->getAttribute('providerAccessToken'); - $accessTokenExpiry = $gitHubSession->getAttribute('providerAccessTokenExpiry'); - $refreshToken = $gitHubSession->getAttribute('providerRefreshToken'); - - $appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? ''; - $appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}'; - - $oAuthProviders = Config::getParam('oAuthProviders'); - $className = $oAuthProviders[$provider]['class']; - if (!\class_exists($className)) { - throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED); - } - - $oauth2 = new $className($appId, $appSecret, '', [], []); - - $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); - if ($isExpired) { - try { - $oauth2->refreshTokens($refreshToken); - - $accessToken = $oauth2->getAccessToken(''); - $refreshToken = $oauth2->getRefreshToken(''); - - $verificationId = $oauth2->getUserID($accessToken); - - if (empty($verificationId)) { - throw new \Exception("Locked tokens."); // Race codition, handeled in catch - } - - $gitHubSession - ->setAttribute('providerAccessToken', $accessToken) - ->setAttribute('providerRefreshToken', $refreshToken) - ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry(''))); - - Authorization::skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession)); - - $dbForProject->purgeCachedDocument('users', $user->getId()); - } catch (Throwable $err) { - $index = 0; - do { - $previousAccessToken = $gitHubSession->getAttribute('providerAccessToken'); - - $user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId)); - $sessions = $user->getAttribute('sessions', []); - - $gitHubSession = new Document(); - foreach ($sessions as $session) { - if ($session->getAttribute('provider', '') === 'github') { - $gitHubSession = $session; - break; - } - } - - $accessToken = $gitHubSession->getAttribute('providerAccessToken'); - - if ($accessToken !== $previousAccessToken) { - break; - } - - $index++; - \usleep(500000); - } while ($index < 10); - } - } - - $oauth2 = new $className($appId, $appSecret, '', [], []); - $githubUser = $oauth2->getUserSlug($accessToken); - $githubId = $oauth2->getUserID($accessToken); - - return [ - 'name' => $githubUser, - 'id' => $githubId - ]; - } catch (Exception $error) { - return []; - } -}; - -App::get('/v1/avatars/credit-cards/:code') - ->desc('Get credit card icon') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resource', 'avatar/credit-card') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getCreditCard', - description: '/docs/references/avatars/get-credit-card.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE_PNG - )) - ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-credit-cards'))), 'Credit Card Code. Possible values: ' . \implode(', ', \array_keys(Config::getParam('avatar-credit-cards'))) . '.') - ->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) - ->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') - ->desc('Get browser icon') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resource', 'avatar/browser') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getBrowser', - description: '/docs/references/avatars/get-browser.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE_PNG - )) - ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-browsers'))), 'Browser Code.') - ->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) - ->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') - ->desc('Get country flag') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resource', 'avatar/flag') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getFlag', - description: '/docs/references/avatars/get-flag.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE_PNG - )) - ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-flags'))), 'Country Code. ISO Alpha-2 country code format.') - ->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) - ->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') - ->desc('Get image from URL') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resource', 'avatar/image') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getImage', - description: '/docs/references/avatars/get-image.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE - )) - ->param('url', '', new URL(['http', 'https']), 'Image URL which you want to crop.') - ->param('width', 400, new Range(0, 2000), 'Resize preview image width, Pass an integer between 0 to 2000. Defaults to 400.', true) - ->param('height', 400, new Range(0, 2000), 'Resize preview image height, Pass an integer between 0 to 2000. Defaults to 400.', true) - ->inject('response') - ->action(function (string $url, int $width, int $height, Response $response) { - - $quality = 80; - $output = 'png'; - $type = 'png'; - - if (!\extension_loaded('imagick')) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); - } - - $domain = new Domain(\parse_url($url, PHP_URL_HOST)); - - if (!$domain->isKnown()) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); - } - - $client = new Client(); - try { - $res = $client - ->setAllowRedirects(false) - ->fetch($url); - } catch (\Throwable) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); - } - - if ($res->getStatusCode() !== 200) { - throw new Exception(Exception::AVATAR_IMAGE_NOT_FOUND); - } - - try { - $image = new Image($res->getBody()); - } catch (\Throwable $exception) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unable to parse image'); - } - - $image->crop((int) $width, (int) $height); - $output = (empty($output)) ? $type : $output; - $data = $image->output($output, $quality); - - $response - ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days - ->setContentType('image/png') - ->file($data); - unset($image); - }); - -App::get('/v1/avatars/favicon') - ->desc('Get favicon') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resource', 'avatar/favicon') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getFavicon', - description: '/docs/references/avatars/get-favicon.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE - )) - ->param('url', '', new URL(['http', 'https']), 'Website URL which you want to fetch the favicon from.') - ->inject('response') - ->action(function (string $url, Response $response) { - - $width = 56; - $height = 56; - $quality = 80; - $output = 'png'; - $type = 'png'; - - if (!\extension_loaded('imagick')) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); - } - - $domain = new Domain(\parse_url($url, PHP_URL_HOST)); - - if (!$domain->isKnown()) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); - } - - $client = new Client(); - try { - $res = $client - ->setAllowRedirects(true) - ->setMaxRedirects(5) - ->setUserAgent(\sprintf( - APP_USERAGENT, - System::getEnv('_APP_VERSION', 'UNKNOWN'), - System::getEnv('_APP_EMAIL_SECURITY', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)) - )) - ->fetch($url); - } catch (\Throwable) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); - } - - $doc = new DOMDocument(); - $doc->strictErrorChecking = false; - @$doc->loadHTML($res->getBody()); - - $links = $doc->getElementsByTagName('link') ?? []; - $outputHref = ''; - $outputExt = ''; - $space = 0; - - foreach ($links as $link) { /* @var $link DOMElement */ - $href = $link->getAttribute('href'); - $rel = $link->getAttribute('rel'); - $sizes = $link->getAttribute('sizes'); - $absolute = URLParse::unparse(\array_merge(\parse_url($url), \parse_url($href))); - - switch (\strtolower($rel)) { - case 'icon': - case 'shortcut icon': - //case 'apple-touch-icon': - $ext = \pathinfo(\parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION); - - switch ($ext) { - case 'svg': - // SVG icons are prioritized by assigning the maximum possible value. - $space = PHP_INT_MAX; - $outputHref = $absolute; - $outputExt = $ext; - break; - case 'ico': - case 'png': - case 'jpg': - case 'jpeg': - $size = \explode('x', \strtolower($sizes)); - - $sizeWidth = (int) ($size[0] ?? 0); - $sizeHeight = (int) ($size[1] ?? 0); - - if (($sizeWidth * $sizeHeight) >= $space) { - $space = $sizeWidth * $sizeHeight; - $outputHref = $absolute; - $outputExt = $ext; - } - - break; - } - - break; - } - } - - if (empty($outputHref) || empty($outputExt)) { - $default = \parse_url($url); - - $outputHref = $default['scheme'] . '://' . $default['host'] . '/favicon.ico'; - $outputExt = 'ico'; - } - - $domain = new Domain(\parse_url($outputHref, PHP_URL_HOST)); - - if (!$domain->isKnown()) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); - } - - $client = new Client(); - try { - $res = $client - ->setAllowRedirects(true) - ->setMaxRedirects(5) - ->fetch($outputHref); - } catch (\Throwable) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); - } - - if ($res->getStatusCode() !== 200) { - throw new Exception(Exception::AVATAR_ICON_NOT_FOUND); - } - - $data = $res->getBody(); - - if ('ico' === $outputExt) { // Skip crop, Imagick isn\'t supporting icon files - if ( - empty($data) || - stripos($data, 'addHeader('Cache-Control', 'private, max-age=2592000') // 30 days - ->setContentType('image/x-icon') - ->file($data); - return; - } - - if ('svg' === $outputExt) { // Skip crop, Imagick isn\'t supporting svg files - $sanitizer = new SvgSanitizer(); - $sanitizer->minify(true); - $cleanSvg = $sanitizer->sanitize($data); - if ($cleanSvg === false) { - throw new Exception(Exception::AVATAR_SVG_SANITIZATION_FAILED); - } - $response - ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days - ->setContentType('image/svg+xml') - ->file($cleanSvg); - return; - } - - $image = new Image($data); - $image->crop((int) $width, (int) $height); - $output = (empty($output)) ? $type : $output; - $data = $image->output($output, $quality); - - $response - ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days - ->setContentType('image/png') - ->file($data); - unset($image); - }); - -App::get('/v1/avatars/qr') - ->desc('Get QR code') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getQR', - description: '/docs/references/avatars/get-qr.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE_PNG - )) - ->param('text', '', new Text(512), 'Plain text to be converted to QR code image.') - ->param('size', 400, new Range(1, 1000), 'QR code size. Pass an integer between 1 to 1000. Defaults to 400.', true) - ->param('margin', 1, new Range(0, 10), 'Margin from edge. Pass an integer between 0 to 10. Defaults to 1.', true) - ->param('download', false, new Boolean(true), 'Return resulting image with \'Content-Disposition: attachment \' headers for the browser to start downloading it. Pass 0 for no header, or 1 for otherwise. Default value is set to 0.', true) - ->inject('response') - ->action(function (string $text, int $size, int $margin, bool $download, Response $response) { - - $download = ($download === '1' || $download === 'true' || $download === 1 || $download === true); - $options = new QROptions([ - 'addQuietzone' => true, - 'quietzoneSize' => $margin, - 'outputType' => QRCode::OUTPUT_IMAGICK, - 'scale' => 15, - ]); - - $qrcode = new QRCode($options); - - if ($download) { - $response->addHeader('Content-Disposition', 'attachment; filename="qr.png"'); - } - - $image = new Image($qrcode->render($text)); - $image->crop((int) $size, (int) $size); - - $response - ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days - ->setContentType('image/png') - ->send($image->output('png', 90)); - }); - -App::get('/v1/avatars/initials') - ->desc('Get user initials') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache.resource', 'avatar/initials') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getInitials', - description: '/docs/references/avatars/get-initials.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE_PNG - )) - ->param('name', '', new Text(128), 'Full Name. When empty, current user name or email will be used. Max length: 128 chars.', true) - ->param('width', 500, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('height', 500, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('background', '', new HexColor(), 'Changes background color. By default a random color will be picked and stay will persistent to the given name.', true) - ->inject('response') - ->inject('user') - ->action(function (string $name, int $width, int $height, string $background, Response $response, Document $user) { - - $themes = [ - ['background' => '#FD366E'], // Default (Pink) - ['background' => '#FE9567'], // Orange - ['background' => '#7C67FE'], // Purple - ['background' => '#68A3FE'], // Blue - ['background' => '#85DBD8'], // Mint - ]; - - $name = (!empty($name)) ? $name : $user->getAttribute('name', $user->getAttribute('email', '')); - $words = \explode(' ', \strtoupper($name)); - // if there is no space, try to split by `_` underscore - $words = (count($words) == 1) ? \explode('_', \strtoupper($name)) : $words; - - $initials = ''; - $code = 0; - - foreach ($words as $key => $w) { - if (ctype_alnum($w[0] ?? '')) { - $initials .= $w[0]; - $code += ord($w[0]); - - if ($key == 1) { - break; - } - } - } - - $rand = \substr($code, -1); - - // Wrap rand value to avoid out of range - $rand = ($rand > \count($themes) - 1) ? $rand % \count($themes) : $rand; - - $background = (!empty($background)) ? '#' . $background : $themes[$rand]['background']; - - $image = new \Imagick(); - $punch = new \Imagick(); - $draw = new \ImagickDraw(); - $fontSize = \min($width, $height) / 2; - - $punch->newImage($width, $height, 'transparent'); - - $draw->setFont(__DIR__ . "/../../assets/fonts/inter-v8-latin-regular.woff2"); - $image->setFont(__DIR__ . "/../../assets/fonts/inter-v8-latin-regular.woff2"); - - $draw->setFillColor(new ImagickPixel('black')); - $draw->setFontSize($fontSize); - - $draw->setTextAlignment(\Imagick::ALIGN_CENTER); - $draw->annotation($width / 1.97, ($height / 2) + ($fontSize / 3), $initials); - - $punch->drawImage($draw); - $punch->negateImage(true, Imagick::CHANNEL_ALPHA); - - $image->newImage($width, $height, $background); - $image->setImageFormat("png"); - $image->compositeImage($punch, Imagick::COMPOSITE_COPYOPACITY, 0, 0); - - $response - ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days - ->setContentType('image/png') - ->file($image->getImageBlob()); - }); - -App::get('/v1/avatars/screenshots') - ->desc('Get webpage screenshot') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('usage.metric', METRIC_AVATARS_SCREENSHOTS_GENERATED) - ->label('abuse-limit', 60) - ->label('cache', true) - ->label('cache.resourceType', 'avatar/screenshot') - ->label('cache.resource', 'screenshot/{request.url}/{request.width}/{request.height}/{request.scale}/{request.theme}/{request.userAgent}/{request.fullpage}/{request.locale}/{request.timezone}/{request.latitude}/{request.longitude}/{request.accuracy}/{request.touch}/{request.permissions}/{request.sleep}/{request.quality}/{request.output}') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getScreenshot', - description: '/docs/references/avatars/get-screenshot.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE_PNG - )) - ->param('url', '', new URL(['http', 'https']), 'Website URL which you want to capture.', example: 'https://example.com') - ->param('headers', [], new Assoc(), 'HTTP headers to send with the browser request. Defaults to empty.', true, example: '{"Authorization":"Bearer token123","X-Custom-Header":"value"}') - ->param('viewportWidth', 1280, new Range(1, 1920), 'Browser viewport width. Pass an integer between 1 to 1920. Defaults to 1280.', true, example: '1920') - ->param('viewportHeight', 720, new Range(1, 1080), 'Browser viewport height. Pass an integer between 1 to 1080. Defaults to 720.', true, example: '1080') - ->param('scale', 1, new Range(0.1, 3, Range::TYPE_FLOAT), 'Browser scale factor. Pass a number between 0.1 to 3. Defaults to 1.', true, example: '2') - ->param('theme', 'light', new WhiteList(['light', 'dark']), 'Browser theme. Pass "light" or "dark". Defaults to "light".', true, example: 'dark') - ->param('userAgent', '', new Text(512), 'Custom user agent string. Defaults to browser default.', true, example: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15') - ->param('fullpage', false, new Boolean(true), 'Capture full page scroll. Pass 0 for viewport only, or 1 for full page. Defaults to 0.', true, example: 'true') - ->param('locale', '', new Text(10), 'Browser locale (e.g., "en-US", "fr-FR"). Defaults to browser default.', true, example: 'en-US') - ->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'america/new_york') - ->param('latitude', 0, new Range(-90, 90, Range::TYPE_FLOAT), 'Geolocation latitude. Pass a number between -90 to 90. Defaults to 0.', true, example: '37.7749') - ->param('longitude', 0, new Range(-180, 180, Range::TYPE_FLOAT), 'Geolocation longitude. Pass a number between -180 to 180. Defaults to 0.', true, example: '-122.4194') - ->param('accuracy', 0, new Range(0, 100000, Range::TYPE_FLOAT), 'Geolocation accuracy in meters. Pass a number between 0 to 100000. Defaults to 0.', true, example: '100') - ->param('touch', false, new Boolean(true), 'Enable touch support. Pass 0 for no touch, or 1 for touch enabled. Defaults to 0.', true, example: 'true') - ->param('permissions', [], new ArrayList(new WhiteList(['geolocation', 'camera', 'microphone', 'notifications', 'midi', 'push', 'clipboard-read', 'clipboard-write', 'payment-handler', 'usb', 'bluetooth', 'accelerometer', 'gyroscope', 'magnetometer', 'ambient-light-sensor', 'background-sync', 'persistent-storage', 'screen-wake-lock', 'web-share', 'xr-spatial-tracking'])), 'Browser permissions to grant. Pass an array of permission names like ["geolocation", "camera", "microphone"]. Defaults to empty.', true, example: '["geolocation","notifications"]') - ->param('sleep', 0, new Range(0, 10), 'Wait time in seconds before taking the screenshot. Pass an integer between 0 to 10. Defaults to 0.', true, example: '3') - ->param('width', 0, new Range(0, 2000), 'Output image width. Pass 0 to use original width, or an integer between 1 to 2000. Defaults to 0 (original width).', true, example: '800') - ->param('height', 0, new Range(0, 2000), 'Output image height. Pass 0 to use original height, or an integer between 1 to 2000. Defaults to 0 (original height).', true, example: '600') - ->param('quality', -1, new Range(-1, 100), 'Screenshot quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true, example: '85') - ->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true, example: 'jpeg') - ->inject('response') - ->inject('queueForStatsUsage') - ->action(function (string $url, array $headers, int $viewportWidth, int $viewportHeight, float $scale, string $theme, string $userAgent, bool $fullpage, string $locale, string $timezone, float $latitude, float $longitude, float $accuracy, bool $touch, array $permissions, int $sleep, int $width, int $height, int $quality, string $output, Response $response, StatsUsage $queueForStatsUsage) { - - if (!\extension_loaded('imagick')) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); - } - - $domain = new Domain(\parse_url($url, PHP_URL_HOST)); - - if (!$domain->isKnown()) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); - } - - $client = new Client(); - $client->setTimeout(30 * 1000); // 30 seconds - $client->addHeader('content-type', Client::CONTENT_TYPE_APPLICATION_JSON); - - // Convert indexed array to empty array (should not happen due to Assoc validator) - if (is_array($headers) && count($headers) > 0 && array_keys($headers) === range(0, count($headers) - 1)) { - $headers = []; - } - - // Create a new object to ensure proper JSON serialization - $headersObject = new \stdClass(); - foreach ($headers as $key => $value) { - $headersObject->$key = $value; - } - - // Create the config with headers as an object - // The custom browser service accepts: url, theme, headers, sleep, viewport, userAgent, fullPage, locale, timezoneId, geolocation, hasTouch, scale - $config = [ - 'url' => $url, - 'theme' => $theme, - 'headers' => $headersObject, - 'sleep' => $sleep * 1000, // Convert seconds to milliseconds - 'waitUntil' => 'load', - 'viewport' => [ - 'width' => $viewportWidth, - 'height' => $viewportHeight - ] - ]; - - // Add scale if not default - if ($scale != 1) { - $config['deviceScaleFactor'] = $scale; - } - - // Add optional parameters that were set, preserving arrays as arrays - if (!empty($userAgent)) { - $config['userAgent'] = $userAgent; - } - - if ($fullpage) { - $config['fullPage'] = true; - } - - if (!empty($locale)) { - $config['locale'] = $locale; - } - - if (!empty($timezone)) { - $config['timezoneId'] = $timezone; - } - - // Add geolocation if any coordinates are provided - if ($latitude != 0 || $longitude != 0) { - $config['geolocation'] = [ - 'latitude' => $latitude, - 'longitude' => $longitude, - 'accuracy' => $accuracy - ]; - } - - if ($touch) { - $config['hasTouch'] = true; - } - - // Add permissions if provided (preserve as array) - if (!empty($permissions)) { - $config['permissions'] = $permissions; // Keep as array - } - - try { - $browserEndpoint = System::getEnv('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1'); - - $fetchResponse = $client->fetch( - url: $browserEndpoint . '/screenshots', - method: 'POST', - body: $config - ); - - if ($fetchResponse->getStatusCode() >= 400) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot service failed: ' . $fetchResponse->getBody()); - } - - $screenshot = $fetchResponse->getBody(); - - if (empty($screenshot)) { - throw new Exception(Exception::AVATAR_IMAGE_NOT_FOUND, 'Screenshot not generated'); - } - - // Determine if image processing is needed - $needsProcessing = ($width > 0 || $height > 0) || $quality !== -1 || !empty($output); - - if ($needsProcessing) { - // Process image with cropping, quality adjustment, or format conversion - $image = new Image($screenshot); - - $image->crop($width, $height); - - $output = $output ?: 'png'; // Default to PNG if not specified - $resizedScreenshot = $image->output($output, $quality); - unset($image); - } else { - // Return original screenshot without processing - $resizedScreenshot = $screenshot; - $output = 'png'; // Screenshots are typically PNG by default - } - - // Set content type based on output format - $outputs = Config::getParam('storage-outputs'); - $contentType = $outputs[$output] ?? $outputs['png']; - - $queueForStatsUsage->addMetric(METRIC_AVATARS_SCREENSHOTS_GENERATED, 1); - - $response - ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days - ->setContentType($contentType) - ->file($resizedScreenshot); - - - } catch (\Throwable $th) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot generation failed: ' . $th->getMessage()); - } - }); - -App::get('/v1/cards/cloud') - ->desc('Get front Of Cloud Card') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resourceType', 'cards/cloud') - ->label('cache.resource', 'card/{request.userId}') - ->label('docs', false) - ->label('origin', '*') - ->param('userId', '', new UID(), 'User ID.', true) - ->param('mock', '', new WhiteList(['employee', 'employee-2digit', 'hero', 'contributor', 'normal', 'platinum', 'normal-no-github', 'normal-long']), 'Mocking behaviour.', true) - ->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true) - ->param('height', 0, new Range(0, 320), 'Resize image height, Pass an integer between 0 to 320.', true) - ->inject('user') - ->inject('project') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->inject('response') - ->inject('heroes') - ->inject('contributors') - ->inject('employees') - ->inject('logger') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { - $user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId)); - - if ($user->isEmpty() && empty($mock)) { - throw new Exception(Exception::USER_NOT_FOUND); - } - - if (!$mock) { - $name = $user->getAttribute('name', 'Anonymous'); - $email = $user->getAttribute('email', ''); - $createdAt = new \DateTime($user->getCreatedAt()); - - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger); - $githubName = $gitHub['name'] ?? ''; - $githubId = $gitHub['id'] ?? ''; - - $isHero = \array_key_exists($email, $heroes); - $isContributor = \in_array($githubId, $contributors); - $isEmployee = \array_key_exists($email, $employees); - $employeeNumber = $isEmployee ? $employees[$email]['spot'] : ''; - - if ($isHero) { - $createdAt = new \DateTime($heroes[$email]['memberSince'] ?? ''); - } elseif ($isEmployee) { - $createdAt = new \DateTime($employees[$email]['memberSince'] ?? ''); - } - - if (!$isEmployee && !empty($githubName)) { - $employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees)); - if (!empty($employeeGitHub)) { - $isEmployee = true; - $employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : ''; - $createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? ''); - } - } - - $isPlatinum = $user->getSequence() % 100 === 0; - } else { - $name = $mock === 'normal-long' ? 'Sir First Walter O\'Brian Junior' : 'Walter O\'Brian'; - $createdAt = new \DateTime('now'); - $githubName = $mock === 'normal-no-github' ? '' : ($mock === 'normal-long' ? 'sir-first-walterobrian-junior' : 'walterobrian'); - $isHero = $mock === 'hero'; - $isContributor = $mock === 'contributor'; - $isEmployee = \str_starts_with($mock, 'employee'); - $employeeNumber = match ($mock) { - 'employee' => '1', - 'employee-2digit' => '18', - default => '' - }; - - $isPlatinum = $mock === 'platinum'; - } - - if ($isEmployee) { - $isContributor = false; - $isHero = false; - } - - if ($isHero) { - $isContributor = false; - $isEmployee = false; - } - - if ($isContributor) { - $isHero = false; - $isEmployee = false; - } - - $isGolden = $isEmployee || $isHero || $isContributor; - $isPlatinum = $isGolden ? false : $isPlatinum; - $memberSince = \strtoupper('Member since ' . $createdAt->format('M') . ' ' . $createdAt->format('d') . ', ' . $createdAt->format('o')); - - $imagePath = $isGolden ? 'front-golden.png' : ($isPlatinum ? 'front-platinum.png' : 'front.png'); - - $baseImage = new \Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $imagePath); - - if ($isEmployee) { - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/employee.png'); - $image->setGravity(Imagick::GRAVITY_CENTER); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 35); - - $text = new \ImagickDraw(); - $text->setTextAlignment(Imagick::ALIGN_CENTER); - $text->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); - $text->setFillColor(new \ImagickPixel('#FFFADF')); - $text->setFontSize(\strlen($employeeNumber) <= 2 ? 54 : 48); - $text->setFontWeight(700); - $metricsText = $baseImage->queryFontMetrics($text, $employeeNumber); - - $hashtag = new \ImagickDraw(); - $hashtag->setTextAlignment(Imagick::ALIGN_CENTER); - $hashtag->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); - $hashtag->setFillColor(new \ImagickPixel('#FFFADF')); - $hashtag->setFontSize(28); - $hashtag->setFontWeight(700); - $metricsHashtag = $baseImage->queryFontMetrics($hashtag, '#'); - - $startX = 898; - $totalWidth = $metricsHashtag['textWidth'] + 12 + $metricsText['textWidth']; - - $hashtagX = ($metricsHashtag['textWidth'] / 2); - $textX = $hashtagX + 12 + ($metricsText['textWidth'] / 2); - - $hashtagX -= $totalWidth / 2; - $textX -= $totalWidth / 2; - - $hashtagX += $startX; - $textX += $startX; - - $baseImage->annotateImage($hashtag, $hashtagX, 150, 0, '#'); - $baseImage->annotateImage($text, $textX, 150, 0, $employeeNumber); - } - - if ($isContributor) { - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/contributor.png'); - $image->setGravity(Imagick::GRAVITY_CENTER); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 34); - } - - if ($isHero) { - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/hero.png'); - $image->setGravity(Imagick::GRAVITY_CENTER); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 34); - } - - setlocale(LC_ALL, "en_US.utf8"); - // $name = \iconv("utf-8", "ascii//TRANSLIT", $name); - // $memberSince = \iconv("utf-8", "ascii//TRANSLIT", $memberSince); - // $githubName = \iconv("utf-8", "ascii//TRANSLIT", $githubName); - - $text = new \ImagickDraw(); - $text->setTextAlignment(Imagick::ALIGN_CENTER); - $text->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); - $text->setFillColor(new \ImagickPixel('#FFFFFF')); - - if (\strlen($name) > 32) { - $name = \substr($name, 0, 32); - } - - if (\strlen($name) <= 23) { - $text->setFontSize(80); - $scalingDown = false; - } else { - $text->setFontSize(54); - $scalingDown = true; - } - $text->setFontWeight(700); - $baseImage->annotateImage($text, 512, 477, 0, $name); - - $text = new \ImagickDraw(); - $text->setTextAlignment(Imagick::ALIGN_CENTER); - $text->setFont(__DIR__ . '/../../../public/fonts/Inter-SemiBold.ttf'); - $text->setFillColor(new \ImagickPixel($isGolden || $isPlatinum ? '#FFFFFF' : '#FFB9CC')); - $text->setFontSize(27); - $text->setFontWeight(600); - $text->setTextKerning(1.08); - $baseImage->annotateImage($text, 512, 541, 0, \strtoupper($memberSince)); - - if (!empty($githubName)) { - $text = new \ImagickDraw(); - $text->setTextAlignment(Imagick::ALIGN_CENTER); - $text->setFont(__DIR__ . '/../../../public/fonts/Inter-Regular.ttf'); - $text->setFillColor(new \ImagickPixel('#FFFFFF')); - $text->setFontSize($scalingDown ? 28 : 32); - $text->setFontWeight(400); - $metrics = $baseImage->queryFontMetrics($text, $githubName); - - $baseImage->annotateImage($text, 512 + 20 + 4, 373 + ($scalingDown ? 2 : 0), 0, $githubName); - - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github.png'); - $image->setGravity(Imagick::GRAVITY_CENTER); - $precisionFix = 5; - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 512 - ($metrics['textWidth'] / 2) - 20 - 4, 373 - ($metrics['textHeight'] - $precisionFix)); - } - - if (!empty($width) || !empty($height)) { - $baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1); - } - - $response - ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days - ->setContentType('image/png') - ->file($baseImage->getImageBlob()); - }); - -App::get('/v1/cards/cloud-back') - ->desc('Get back Of Cloud Card') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resourceType', 'cards/cloud-back') - ->label('cache.resource', 'card-back/{request.userId}') - ->label('docs', false) - ->label('origin', '*') - ->param('userId', '', new UID(), 'User ID.', true) - ->param('mock', '', new WhiteList(['golden', 'normal', 'platinum']), 'Mocking behaviour.', true) - ->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true) - ->param('height', 0, new Range(0, 320), 'Resize image height, Pass an integer between 0 to 320.', true) - ->inject('user') - ->inject('project') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->inject('response') - ->inject('heroes') - ->inject('contributors') - ->inject('employees') - ->inject('logger') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { - $user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId)); - - if ($user->isEmpty() && empty($mock)) { - throw new Exception(Exception::USER_NOT_FOUND); - } - - if (!$mock) { - $userId = $user->getId(); - $email = $user->getAttribute('email', ''); - - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger); - $githubId = $gitHub['id'] ?? ''; - - $isHero = \array_key_exists($email, $heroes); - $isContributor = \in_array($githubId, $contributors); - $isEmployee = \array_key_exists($email, $employees); - - $isGolden = $isEmployee || $isHero || $isContributor; - $isPlatinum = $user->getSequence() % 100 === 0; - } else { - $userId = '63e0bcf3c3eb803ba530'; - - $isGolden = $mock === 'golden'; - $isPlatinum = $mock === 'platinum'; - } - - $userId = 'UID ' . $userId; - - $isPlatinum = $isGolden ? false : $isPlatinum; - - $imagePath = $isGolden ? 'back-golden.png' : ($isPlatinum ? 'back-platinum.png' : 'back.png'); - - $baseImage = new \Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $imagePath); - - setlocale(LC_ALL, "en_US.utf8"); - // $userId = \iconv("utf-8", "ascii//TRANSLIT", $userId); - - $text = new \ImagickDraw(); - $text->setTextAlignment(Imagick::ALIGN_CENTER); - $text->setFont(__DIR__ . '/../../../public/fonts/SourceCodePro-Regular.ttf'); - $text->setFillColor(new \ImagickPixel($isGolden ? '#664A1E' : ($isPlatinum ? '#555555' : '#E8E9F0'))); - $text->setFontSize(28); - $text->setFontWeight(400); - $baseImage->annotateImage($text, 512, 596, 0, $userId); - - if (!empty($width) || !empty($height)) { - $baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1); - } - - $response - ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days - ->setContentType('image/png') - ->file($baseImage->getImageBlob()); - }); - -App::get('/v1/cards/cloud-og') - ->desc('Get OG image From Cloud Card') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resourceType', 'cards/cloud-og') - ->label('cache.resource', 'card-og/{request.userId}') - ->label('docs', false) - ->label('origin', '*') - ->param('userId', '', new UID(), 'User ID.', true) - ->param('mock', '', new WhiteList(['employee', 'employee-2digit', 'hero', 'contributor', 'normal', 'platinum', 'normal-no-github', 'normal-long', 'normal-long-right', 'normal-long-middle', 'normal-bg2', 'normal-bg3', 'normal-right', 'normal-middle', 'platinum-right', 'platinum-middle', 'hero-middle', 'hero-right', 'contributor-right', 'employee-right', 'contributor-middle', 'employee-middle', 'employee-2digit-middle', 'employee-2digit-right']), 'Mocking behaviour.', true) - ->param('width', 0, new Range(0, 1024), 'Resize image card width, Pass an integer between 0 to 1024.', true) - ->param('height', 0, new Range(0, 1024), 'Resize image card height, Pass an integer between 0 to 1024.', true) - ->inject('user') - ->inject('project') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->inject('response') - ->inject('heroes') - ->inject('contributors') - ->inject('employees') - ->inject('logger') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { - $user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId)); - - if ($user->isEmpty() && empty($mock)) { - throw new Exception(Exception::USER_NOT_FOUND); - } - - if (!$mock) { - $sequence = $user->getSequence(); - $bgVariation = $sequence % 3 === 0 ? '1' : ($sequence % 3 === 1 ? '2' : '3'); - $cardVariation = $sequence % 3 === 0 ? '1' : ($sequence % 3 === 1 ? '2' : '3'); - - $name = $user->getAttribute('name', 'Anonymous'); - $email = $user->getAttribute('email', ''); - $createdAt = new \DateTime($user->getCreatedAt()); - - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger); - $githubName = $gitHub['name'] ?? ''; - $githubId = $gitHub['id'] ?? ''; - - $isHero = \array_key_exists($email, $heroes); - $isContributor = \in_array($githubId, $contributors); - $isEmployee = \array_key_exists($email, $employees); - $employeeNumber = $isEmployee ? $employees[$email]['spot'] : ''; - - if ($isHero) { - $createdAt = new \DateTime($heroes[$email]['memberSince'] ?? ''); - } elseif ($isEmployee) { - $createdAt = new \DateTime($employees[$email]['memberSince'] ?? ''); - } - - if (!$isEmployee && !empty($githubName)) { - $employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees)); - if (!empty($employeeGitHub)) { - $isEmployee = true; - $employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : ''; - $createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? ''); - } - } - - $isPlatinum = $user->getSequence() % 100 === 0; - } else { - $bgVariation = \str_ends_with($mock, '-bg2') ? '2' : (\str_ends_with($mock, '-bg3') ? '3' : '1'); - $cardVariation = \str_ends_with($mock, '-right') ? '2' : (\str_ends_with($mock, '-middle') ? '3' : '1'); - $name = \str_starts_with($mock, 'normal-long') ? 'Sir First Walter O\'Brian Junior' : 'Walter O\'Brian'; - $createdAt = new \DateTime('now'); - $githubName = $mock === 'normal-no-github' ? '' : (\str_starts_with($mock, 'normal-long') ? 'sir-first-walterobrian-junior' : 'walterobrian'); - $isHero = \str_starts_with($mock, 'hero'); - $isContributor = \str_starts_with($mock, 'contributor'); - $isEmployee = \str_starts_with($mock, 'employee'); - $employeeNumber = match ($mock) { - 'employee' => '1', - 'employee-right' => '1', - 'employee-middle' => '1', - 'employee-2digit' => '18', - 'employee-2digit-right' => '18', - 'employee-2digit-middle' => '18', - default => '' - }; - - $isPlatinum = \str_starts_with($mock, 'platinum'); - } - - if ($isEmployee) { - $isContributor = false; - $isHero = false; - } - - if ($isHero) { - $isContributor = false; - $isEmployee = false; - } - - if ($isContributor) { - $isHero = false; - $isEmployee = false; - } - - $isGolden = $isEmployee || $isHero || $isContributor; - $isPlatinum = $isGolden ? false : $isPlatinum; - $memberSince = \strtoupper('Member since ' . $createdAt->format('M') . ' ' . $createdAt->format('d') . ', ' . $createdAt->format('o')); - - $baseImage = new \Imagick(__DIR__ . "/../../../public/images/cards/cloud/og-background{$bgVariation}.png"); - - $cardType = $isGolden ? '-golden' : ($isPlatinum ? '-platinum' : ''); - - $image = new Imagick(__DIR__ . "/../../../public/images/cards/cloud/og-card{$cardType}{$cardVariation}.png"); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 1008 / 2 - $image->getImageWidth() / 2, 1008 / 2 - $image->getImageHeight() / 2); - - $imageLogo = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/og-background-logo.png'); - $imageShadow = new Imagick(__DIR__ . "/../../../public/images/cards/cloud/og-shadow{$cardType}.png"); - if ($cardVariation === '1') { - $baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 32, 1008 - $imageLogo->getImageHeight() - 32); - $baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -450, 700); - } elseif ($cardVariation === '2') { - $baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 1008 - $imageLogo->getImageWidth() - 32, 1008 - $imageLogo->getImageHeight() - 32); - $baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -20, 710); - } else { - $baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 1008 - $imageLogo->getImageWidth() - 32, 1008 - $imageLogo->getImageHeight() - 32); - $baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -135, 710); - } - - if ($isEmployee) { - $file = $cardVariation === '3' ? 'employee-skew.png' : 'employee.png'; - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $file); - $image->setGravity(Imagick::GRAVITY_CENTER); - - $hashtag = new \ImagickDraw(); - $hashtag->setTextAlignment(Imagick::ALIGN_LEFT); - $hashtag->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); - $hashtag->setFillColor(new \ImagickPixel('#FFFADF')); - $hashtag->setFontSize(20); - $hashtag->setFontWeight(700); - - $text = new \ImagickDraw(); - $text->setTextAlignment(Imagick::ALIGN_LEFT); - $text->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); - $text->setFillColor(new \ImagickPixel('#FFFADF')); - $text->setFontSize(\strlen($employeeNumber) <= 1 ? 36 : 28); - $text->setFontWeight(700); - - if ($cardVariation === '3') { - $hashtag->setFontSize(16); - $text->setFontSize(\strlen($employeeNumber) <= 1 ? 30 : 26); - - $hashtag->skewY(20); - $hashtag->skewX(20); - $text->skewY(20); - $text->skewX(20); - } - - $metricsHashtag = $baseImage->queryFontMetrics($hashtag, '#'); - $metricsText = $baseImage->queryFontMetrics($text, $employeeNumber); - - $group = new Imagick(); - $groupWidth = $metricsHashtag['textWidth'] + 6 + $metricsText['textWidth']; - - if ($cardVariation === '1') { - $group->newImage($groupWidth, $metricsText['textHeight'], '#00000000'); - $group->annotateImage($hashtag, 0, $metricsText['textHeight'], 0, '#'); - $group->annotateImage($text, $metricsHashtag['textWidth'] + 6, $metricsText['textHeight'], 0, $employeeNumber); - - $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); - $image->rotateImage(new ImagickPixel('#00000000'), -20); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203); - - $group->rotateImage(new ImagickPixel('#00000000'), -22); - - if (\strlen($employeeNumber) <= 1) { - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 660, 245); - } else { - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 655, 247); - } - } elseif ($cardVariation === '2') { - $group->newImage($groupWidth, $metricsText['textHeight'], '#00000000'); - $group->annotateImage($hashtag, 0, $metricsText['textHeight'], 0, '#'); - $group->annotateImage($text, $metricsHashtag['textWidth'] + 6, $metricsText['textHeight'], 0, $employeeNumber); - - $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); - $image->rotateImage(new ImagickPixel('#00000000'), 30); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425); - - $group->rotateImage(new ImagickPixel('#00000000'), 32); - - if (\strlen($employeeNumber) <= 1) { - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 775, 465); - } else { - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 767, 470); - } - } else { - $group->newImage(300, 300, '#00000000'); - - $hashtag->annotation(0, $metricsText['textHeight'], '#'); - $text->annotation($metricsHashtag['textWidth'] + 2, $metricsText['textHeight'], $employeeNumber); - - $group->drawImage($hashtag); - $group->drawImage($text); - - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293); - - if (\strlen($employeeNumber) <= 1) { - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 670, 317); - } else { - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 663, 322); - } - } - } - - if ($isContributor) { - $file = $cardVariation === '3' ? 'contributor-skew.png' : 'contributor.png'; - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $file); - $image->setGravity(Imagick::GRAVITY_CENTER); - - if ($cardVariation === '1') { - $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); - $image->rotateImage(new ImagickPixel('#00000000'), -20); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203); - } elseif ($cardVariation === '2') { - $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); - $image->rotateImage(new ImagickPixel('#00000000'), 30); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425); - } else { - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293); - } - } - - if ($isHero) { - $file = $cardVariation === '3' ? 'hero-skew.png' : 'hero.png'; - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $file); - $image->setGravity(Imagick::GRAVITY_CENTER); - - if ($cardVariation === '1') { - $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); - $image->rotateImage(new ImagickPixel('#00000000'), -20); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203); - } elseif ($cardVariation === '2') { - $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); - $image->rotateImage(new ImagickPixel('#00000000'), 30); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425); - } else { - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293); - } - } - - setlocale(LC_ALL, "en_US.utf8"); - // $name = \iconv("utf-8", "ascii//TRANSLIT", $name); - // $memberSince = \iconv("utf-8", "ascii//TRANSLIT", $memberSince); - // $githubName = \iconv("utf-8", "ascii//TRANSLIT", $githubName); - - $textName = new \ImagickDraw(); - $textName->setTextAlignment(Imagick::ALIGN_CENTER); - $textName->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); - $textName->setFillColor(new \ImagickPixel('#FFFFFF')); - - if (\strlen($name) > 32) { - $name = \substr($name, 0, 32); - } - - if ($cardVariation === '1') { - if (\strlen($name) <= 23) { - $scalingDown = false; - $textName->setFontSize(54); - } else { - $scalingDown = true; - $textName->setFontSize(36); - } - } elseif ($cardVariation === '2') { - if (\strlen($name) <= 23) { - $scalingDown = false; - $textName->setFontSize(50); - } else { - $scalingDown = true; - $textName->setFontSize(34); - } - } else { - if (\strlen($name) <= 23) { - $scalingDown = false; - $textName->setFontSize(44); - } else { - $scalingDown = true; - $textName->setFontSize(32); - } - } - - $textName->setFontWeight(700); - - $textMember = new \ImagickDraw(); - $textMember->setTextAlignment(Imagick::ALIGN_CENTER); - $textMember->setFont(__DIR__ . '/../../../public/fonts/Inter-Medium.ttf'); - $textMember->setFillColor(new \ImagickPixel($isGolden || $isPlatinum ? '#FFFFFF' : '#FFB9CC')); - $textMember->setFontWeight(500); - $textMember->setTextKerning(1.12); - - if ($cardVariation === '1') { - $textMember->setFontSize(21); - - $baseImage->annotateImage($textName, 550, 600, -22, $name); - $baseImage->annotateImage($textMember, 585, 635, -22, $memberSince); - } elseif ($cardVariation === '2') { - $textMember->setFontSize(20); - - $baseImage->annotateImage($textName, 435, 590, 31.37, $name); - $baseImage->annotateImage($textMember, 412, 628, 31.37, $memberSince); - } else { - $textMember->setFontSize(16); - - $textName->skewY(20); - $textName->skewX(20); - $textName->annotation(320, 700, $name); - - $textMember->skewY(20); - $textMember->skewX(20); - $textMember->annotation(330, 735, $memberSince); - - $baseImage->drawImage($textName); - $baseImage->drawImage($textMember); - } - - if (!empty($githubName)) { - $text = new \ImagickDraw(); - $text->setTextAlignment(Imagick::ALIGN_LEFT); - $text->setFont(__DIR__ . '/../../../public/fonts/Inter-Regular.ttf'); - $text->setFillColor(new \ImagickPixel('#FFFFFF')); - $text->setFontSize($scalingDown ? 16 : 20); - $text->setFontWeight(400); - - if ($cardVariation === '1') { - $metrics = $baseImage->queryFontMetrics($text, $githubName); - - $group = new Imagick(); - $groupWidth = $metrics['textWidth'] + 32 + 4; - $group->newImage($groupWidth, $metrics['textHeight'] + 10, '#00000000'); - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github.png'); - $image->setGravity(Imagick::GRAVITY_CENTER); - $image->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1); - $precisionFix = -1; - - $group->compositeImage($image, Imagick::COMPOSITE_OVER, 0, 0); - $group->annotateImage($text, 32 + 4, $metrics['textHeight'] - $precisionFix, 0, $githubName); - - $group->rotateImage(new ImagickPixel('#00000000'), -22); - $x = 510 - $group->getImageWidth() / 2; - $y = 530 - $group->getImageHeight() / 2; - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, $x, $y); - } elseif ($cardVariation === '2') { - $metrics = $baseImage->queryFontMetrics($text, $githubName); - - $group = new Imagick(); - $groupWidth = $metrics['textWidth'] + 32 + 4; - $group->newImage($groupWidth, $metrics['textHeight'] + 10, '#00000000'); - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github.png'); - $image->setGravity(Imagick::GRAVITY_CENTER); - $image->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1); - $precisionFix = -1; - - $group->compositeImage($image, Imagick::COMPOSITE_OVER, 0, 0); - $group->annotateImage($text, 32 + 4, $metrics['textHeight'] - $precisionFix, 0, $githubName); - - $group->rotateImage(new ImagickPixel('#00000000'), 31.11); - $x = 485 - $group->getImageWidth() / 2; - $y = 530 - $group->getImageHeight() / 2; - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, $x, $y); - } else { - $text->skewY(20); - $text->skewX(20); - $text->setTextAlignment(\Imagick::ALIGN_CENTER); - - $text->annotation(320 + 15 + 2, 640, $githubName); - $metrics = $baseImage->queryFontMetrics($text, $githubName); - - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github-skew.png'); - $image->setGravity(Imagick::GRAVITY_CENTER); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 512 - ($metrics['textWidth'] / 2), 518 + \strlen($githubName) * 1.3); - - $baseImage->drawImage($text); - } - } - - if (!empty($width) || !empty($height)) { - $baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1); - } - - $response - ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days - ->setContentType('image/png') - ->file($baseImage->getImageBlob()); - }); diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index baf0ba1512..e0cc4181db 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -28,11 +28,12 @@ use Utopia\Validator\Text; App::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'] - && !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles())) + && !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index 97ddf8391c..d6388185d3 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -11,6 +11,7 @@ use Appwrite\Event\Func; use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Event\Migration; +use Appwrite\Event\Screenshot; use Appwrite\Event\StatsResources; use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; @@ -26,6 +27,7 @@ use Utopia\Cache\Adapter\Pool as CachePool; use Utopia\Config\Config; use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Domains\Validator\PublicDomain; use Utopia\Pools\Group; use Utopia\Registry\Registry; @@ -100,7 +102,8 @@ App::get('/v1/health/db') )) ->inject('response') ->inject('pools') - ->action(function (Response $response, Group $pools) { + ->inject('authorization') + ->action(action: function (Response $response, Group $pools, Authorization $authorization) { $output = []; $failures = []; @@ -113,14 +116,14 @@ App::get('/v1/health/db') foreach ($config as $database) { try { $adapter = new DatabasePool($pools->get($database)); - + $adapter->setAuthorization($authorization); $checkStart = \microtime(true); if ($adapter->ping()) { $output[] = new Document([ 'name' => $key . " ($database)", 'status' => 'pass', - 'ping' => \round((\microtime(true) - $checkStart) / 1000) + 'ping' => \round((\microtime(true) - $checkStart) * 1000) ]); } else { $failures[] = $database; @@ -131,6 +134,8 @@ App::get('/v1/health/db') } } + // Only throw error if ALL databases failed (no successful pings) + // This allows partial failures in environments where not all DBs are ready if (!empty($failures)) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'DB failure on: ' . implode(", ", $failures)); } @@ -180,7 +185,7 @@ App::get('/v1/health/cache') $output[] = new Document([ 'name' => $key . " ($cache)", 'status' => 'pass', - 'ping' => \round((\microtime(true) - $checkStart) / 1000) + 'ping' => \round((\microtime(true) - $checkStart) * 1000) ]); } else { $failures[] = $cache; @@ -240,7 +245,7 @@ App::get('/v1/health/pubsub') $output[] = new Document([ 'name' => $key . " ($pubsub)", 'status' => 'pass', - 'ping' => \round((\microtime(true) - $checkStart) / 1000) + 'ping' => \round((\microtime(true) - $checkStart) * 1000) ]); } else { $failures[] = $pubsub; @@ -822,7 +827,7 @@ App::get('/v1/health/storage/local') $output = [ 'status' => 'pass', - 'ping' => \round((\microtime(true) - $checkStart) / 1000) + 'ping' => \round((\microtime(true) - $checkStart) * 1000) ]; $response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); @@ -874,7 +879,7 @@ App::get('/v1/health/storage') $output = [ 'status' => 'pass', - 'ping' => \round((\microtime(true) - $checkStart) / 1000) + 'ping' => \round((\microtime(true) - $checkStart) * 1000) ]; $response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); @@ -955,6 +960,7 @@ App::get('/v1/health/queue/failed/:name') System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME), System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME), System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME), + System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME), System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME), System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) ]), 'The name of the queue') @@ -972,6 +978,7 @@ App::get('/v1/health/queue/failed/:name') ->inject('queueForBuilds') ->inject('queueForMessaging') ->inject('queueForMigrations') + ->inject('queueForScreenshots') ->action(function ( string $name, int|string $threshold, @@ -987,7 +994,8 @@ App::get('/v1/health/queue/failed/:name') Certificate $queueForCertificates, Build $queueForBuilds, Messaging $queueForMessaging, - Migration $queueForMigrations + Migration $queueForMigrations, + Screenshot $queueForScreenshots, ) { $threshold = \intval($threshold); @@ -1003,6 +1011,7 @@ App::get('/v1/health/queue/failed/:name') System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME) => $queueForWebhooks, System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $queueForCertificates, System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME) => $queueForBuilds, + System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $queueForScreenshots, System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $queueForMessaging, System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $queueForMigrations, }; diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 0b6a314dc5..6ac36fe3c0 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -36,6 +36,7 @@ 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\Cursor; @@ -1073,8 +1074,9 @@ App::get('/v1/messaging/providers') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('dbForProject') + ->inject('authorization') ->inject('response') - ->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) { + ->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -1100,7 +1102,7 @@ App::get('/v1/messaging/providers') } $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."); @@ -2481,8 +2483,9 @@ App::get('/v1/messaging/topics') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('dbForProject') + ->inject('authorization') ->inject('response') - ->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) { + ->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -2508,7 +2511,7 @@ App::get('/v1/messaging/topics') } $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."); @@ -2782,29 +2785,27 @@ App::post('/v1/messaging/topics/:topicId/subscribers') ->param('targetId', '', new UID(), 'Target ID. The target ID to link to the specified Topic ID.') ->inject('queueForEvents') ->inject('dbForProject') + ->inject('authorization') ->inject('response') - ->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Authorization $authorization, Response $response) { $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, @@ -2837,7 +2838,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, @@ -2882,8 +2883,9 @@ App::get('/v1/messaging/topics/:topicId/subscribers') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('dbForProject') + ->inject('authorization') ->inject('response') - ->action(function (string $topicId, array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) { + ->action(function (string $topicId, array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -2894,7 +2896,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); @@ -2917,7 +2919,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers') } $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."); @@ -2931,10 +2933,10 @@ App::get('/v1/messaging/topics/:topicId/subscribers') throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); } - $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) @@ -3067,9 +3069,10 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.') ->param('subscriberId', '', new UID(), 'Subscriber ID.') ->inject('dbForProject') + ->inject('authorization') ->inject('response') - ->action(function (string $topicId, string $subscriberId, Database $dbForProject, Response $response) { - $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + ->action(function (string $topicId, string $subscriberId, Database $dbForProject, Authorization $authorization, Response $response) { + $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($topic->isEmpty()) { throw new Exception(Exception::TOPIC_NOT_FOUND); @@ -3081,8 +3084,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) @@ -3118,9 +3121,10 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->param('subscriberId', '', new UID(), 'Subscriber ID.') ->inject('queueForEvents') ->inject('dbForProject') + ->inject('authorization') ->inject('response') - ->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Response $response) { - $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + ->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Authorization $authorization, Response $response) { + $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($topic->isEmpty()) { throw new Exception(Exception::TOPIC_NOT_FOUND); @@ -3143,7 +3147,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, @@ -3702,8 +3706,9 @@ App::get('/v1/messaging/messages') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('dbForProject') + ->inject('authorization') ->inject('response') - ->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) { + ->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -3729,7 +3734,7 @@ App::get('/v1/messaging/messages') } $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."); diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 3989ad3298..1a17853577 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -342,6 +342,7 @@ App::post('/v1/migrations/csv/imports') ->inject('response') ->inject('dbForProject') ->inject('dbForPlatform') + ->inject('authorization') ->inject('project') ->inject('platform') ->inject('deviceForFiles') @@ -356,6 +357,7 @@ App::post('/v1/migrations/csv/imports') Response $response, Database $dbForProject, Database $dbForPlatform, + Authorization $authorization, Document $project, array $platform, Device $deviceForFiles, @@ -363,7 +365,7 @@ App::post('/v1/migrations/csv/imports') Event $queueForEvents, Migration $queueForMigrations ) { - $bucket = Authorization::skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) { + $bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) { if ($internalFile) { return $dbForPlatform->getDocument('buckets', 'default'); } @@ -374,7 +376,7 @@ App::post('/v1/migrations/csv/imports') throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } - $file = Authorization::skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); + $file = $authorization->skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); if ($file->isEmpty()) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } @@ -491,6 +493,7 @@ App::post('/v1/migrations/csv/exports') ->inject('response') ->inject('dbForProject') ->inject('dbForPlatform') + ->inject('authorization') ->inject('project') ->inject('platform') ->inject('queueForEvents') @@ -509,6 +512,7 @@ App::post('/v1/migrations/csv/exports') Response $response, Database $dbForProject, Database $dbForPlatform, + Authorization $authorization, Document $project, array $platform, Event $queueForEvents, @@ -520,7 +524,7 @@ App::post('/v1/migrations/csv/exports') throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } - $bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'default')); + $bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'default')); if ($bucket->isEmpty()) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } @@ -533,12 +537,12 @@ App::post('/v1/migrations/csv/exports') throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $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); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } diff --git a/app/controllers/api/project.php b/app/controllers/api/project.php index a57675d3e8..cda03f923a 100644 --- a/app/controllers/api/project.php +++ b/app/controllers/api/project.php @@ -45,9 +45,10 @@ App::get('/v1/project/usage') ->inject('response') ->inject('project') ->inject('dbForProject') + ->inject('authorization') ->inject('getLogsDB') ->inject('smsRates') - ->action(function (string $startDate, string $endDate, string $period, Response $response, Document $project, Database $dbForProject, callable $getLogsDB, array $smsRates) { + ->action(function (string $startDate, string $endDate, string $period, Response $response, Document $project, Database $dbForProject, Authorization $authorization, callable $getLogsDB, array $smsRates) { $stats = $total = $usage = []; $format = 'Y-m-d 00:00:00'; $firstDay = (new DateTime($startDate))->format($format); @@ -102,7 +103,7 @@ App::get('/v1/project/usage') '1d' => 'Y-m-d\T00:00:00.000P', }; - Authorization::skip(function () use ($dbForProject, $dbForLogs, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) { + $authorization->skip(function () use ($dbForProject, $dbForLogs, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) { foreach ($metrics['total'] as $metric) { $db = ($metric === METRIC_FILES_IMAGES_TRANSFORMED) ? $dbForLogs : $dbForProject; @@ -286,7 +287,7 @@ App::get('/v1/project/usage') }, $dbForProject->find('functions')); // This total is includes free and paid SMS usage - $authPhoneTotal = Authorization::skip(fn () => $dbForProject->sum('stats', 'value', [ + $authPhoneTotal = $authorization->skip(fn () => $dbForProject->sum('stats', 'value', [ Query::equal('metric', [METRIC_AUTH_METHOD_PHONE]), Query::equal('period', ['1d']), Query::greaterThanEqual('time', $firstDay), @@ -294,7 +295,7 @@ App::get('/v1/project/usage') ])); // This estimate is only for paid SMS usage - $authPhoneMetrics = Authorization::skip(fn () => $dbForProject->find('stats', [ + $authPhoneMetrics = $authorization->skip(fn () => $dbForProject->find('stats', [ Query::startsWith('metric', METRIC_AUTH_METHOD_PHONE . '.'), Query::equal('period', ['1d']), Query::greaterThanEqual('time', $firstDay), diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 1f8555b6cd..aa67a90885 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -86,16 +86,17 @@ App::post('/v1/teams') ->inject('response') ->inject('user') ->inject('dbForProject') + ->inject('authorization') ->inject('queueForEvents') - ->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { + ->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Authorization $authorization, Event $queueForEvents) { - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); - $isAppUser = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); + $isAppUser = User::isApp($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)), @@ -491,6 +492,7 @@ App::post('/v1/teams/:teamId/memberships') ->inject('project') ->inject('user') ->inject('dbForProject') + ->inject('authorization') ->inject('locale') ->inject('queueForMails') ->inject('queueForMessaging') @@ -500,9 +502,9 @@ App::post('/v1/teams/:teamId/memberships') ->inject('plan') ->inject('proofForPassword') ->inject('proofForToken') - ->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, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Password $proofForPassword, Token $proofForToken) { - $isAppUser = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Authorization $authorization, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Password $proofForPassword, Token $proofForToken) { + $isAppUser = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); $url = htmlentities($url); if (empty($url)) { @@ -619,13 +621,13 @@ App::post('/v1/teams/:teamId/memberships') ]); try { - $invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', $userDocument)); + $invitee = $authorization->skip(fn () => $dbForProject->createDocument('users', $userDocument)); } catch (Duplicate $th) { throw new Exception(Exception::USER_ALREADY_EXISTS); } } - $isOwner = Authorization::isRole('team:' . $team->getId() . '/owner'); + $isOwner = $authorization->hasRole('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'); @@ -661,11 +663,11 @@ App::post('/v1/teams/:teamId/memberships') ]); $membership = ($isPrivilegedUser || $isAppUser) ? - Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)) : + $authorization->skip(fn () => $dbForProject->createDocument('memberships', $membership)) : $dbForProject->createDocument('memberships', $membership); if ($isPrivilegedUser || $isAppUser) { - Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); + $authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); } } elseif ($membership->getAttribute('confirm') === false) { $membership->setAttribute('secret', $proofForToken->hash($secret)); @@ -677,7 +679,7 @@ App::post('/v1/teams/:teamId/memberships') } $membership = ($isPrivilegedUser || $isAppUser) ? - Authorization::skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) : + $authorization->skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) : $dbForProject->updateDocument('memberships', $membership->getId(), $membership); } else { throw new Exception(Exception::MEMBERSHIP_ALREADY_CONFIRMED); @@ -863,7 +865,8 @@ App::get('/v1/teams/:teamId/memberships') ->inject('response') ->inject('project') ->inject('dbForProject') - ->action(function (string $teamId, array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $teamId, array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject, Authorization $authorization) { $team = $dbForProject->getDocument('teams', $teamId); if ($team->isEmpty()) { @@ -933,7 +936,7 @@ App::get('/v1/teams/:teamId/memberships') 'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true, ]; - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = User::isPrivileged($roles); $isAppUser = User::isApp($roles); @@ -1004,7 +1007,8 @@ App::get('/v1/teams/:teamId/memberships/:membershipId') ->inject('response') ->inject('project') ->inject('dbForProject') - ->action(function (string $teamId, string $membershipId, Response $response, Document $project, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $teamId, string $membershipId, Response $response, Document $project, Database $dbForProject, Authorization $authorization) { $team = $dbForProject->getDocument('teams', $teamId); @@ -1024,7 +1028,7 @@ App::get('/v1/teams/:teamId/memberships/:membershipId') 'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true, ]; - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = User::isPrivileged($roles); $isAppUser = User::isApp($roles); @@ -1103,8 +1107,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') ->inject('user') ->inject('project') ->inject('dbForProject') + ->inject('authorization') ->inject('queueForEvents') - ->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) { + ->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Event $queueForEvents) { $team = $dbForProject->getDocument('teams', $teamId); if ($team->isEmpty()) { @@ -1121,9 +1126,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') throw new Exception(Exception::USER_NOT_FOUND); } - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); - $isAppUser = User::isApp(Authorization::getRoles()); - $isOwner = Authorization::isRole('team:' . $team->getId() . '/owner'); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); + $isAppUser = User::isApp($authorization->getRoles()); + $isOwner = $authorization->hasRole('team:' . $team->getId() . '/owner'); if ($project->getId() === 'console') { // Quick check: fetch up to 2 owners to determine if only one exists @@ -1204,12 +1209,13 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->inject('response') ->inject('user') ->inject('dbForProject') + ->inject('authorization') ->inject('project') ->inject('geodb') ->inject('queueForEvents') ->inject('store') ->inject('proofForToken') - ->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) { + ->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) { $protocol = $request->getProtocol(); $membership = $dbForProject->getDocument('memberships', $membershipId); @@ -1218,7 +1224,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); @@ -1254,11 +1260,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))); // Create session for the user if not logged in if (!$hasSession) { - 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()); @@ -1286,7 +1292,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') $session = $dbForProject->createDocument('sessions', $session); - Authorization::setRole(Role::user($userId)->toString()); + $authorization->addRole(Role::user($userId)->toString()); $encoded = $store ->setProperty('id', $user->getId()) @@ -1324,7 +1330,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') $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()) @@ -1368,8 +1374,9 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') ->inject('project') ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->inject('queueForEvents') - ->action(function (string $teamId, string $membershipId, Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents) { + ->action(function (string $teamId, string $membershipId, Document $user, Document $project, Response $response, Database $dbForProject, Authorization $authorization, Event $queueForEvents) { $membership = $dbForProject->getDocument('memberships', $membershipId); @@ -1427,7 +1434,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') $dbForProject->purgeCachedDocument('users', $profile->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 diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index bbe1d8a84a..a963284538 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -2678,8 +2678,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 = []; @@ -2689,7 +2689,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 4249dbfd48..2270f4fd89 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -76,7 +76,7 @@ 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 $dbForPlatform, Build $queueForBuilds, callable $getProjectDB, array $platform) { +$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 $dbForPlatform, Authorization $authorization, Build $queueForBuilds, callable $getProjectDB, Request $request, array $platform) { $errors = []; foreach ($repositories as $repository) { try { @@ -87,12 +87,12 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId } $projectId = $repository->getAttribute('projectId'); - $project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); + $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); $dbForProject = $getProjectDB($project); $resourceCollection = $resourceType === "function" ? 'functions' : 'sites'; $resourceId = $repository->getAttribute('resourceId'); - $resource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId)); + $resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId)); $resourceInternalId = $resource->getSequence(); $deploymentId = ID::unique(); @@ -141,7 +141,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $latestCommentId = ''; if (!empty($providerPullRequestId) && $resource->getAttribute('providerSilentMode', false) === false) { - $latestComment = Authorization::skip(fn () => $dbForPlatform->findOne('vcsComments', [ + $latestComment = $authorization->skip(fn () => $dbForPlatform->findOne('vcsComments', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::equal('providerPullRequestId', [$providerPullRequestId]), Query::orderDesc('$createdAt'), @@ -180,7 +180,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment())); } finally { - Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId)); + $authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId)); } } } else { @@ -191,7 +191,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId if (!empty($latestCommentId)) { $teamId = $project->getAttribute('teamId', ''); - $latestComment = Authorization::skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([ + $latestComment = $authorization->skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([ '$id' => ID::unique(), '$permissions' => [ Permission::read(Role::team(ID::custom($teamId))), @@ -212,7 +212,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId } } } elseif (!empty($providerBranch)) { - $latestComments = Authorization::skip(fn () => $dbForPlatform->find('vcsComments', [ + $latestComments = $authorization->skip(fn () => $dbForPlatform->find('vcsComments', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::equal('providerBranch', [$providerBranch]), Query::orderDesc('$createdAt'), @@ -251,7 +251,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment())); } finally { - Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId)); + $authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId)); } } } @@ -294,7 +294,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $commands[] = $resource->getAttribute('commands', ''); } - $deployment = Authorization::skip(fn () => $dbForProject->createDocument('deployments', new Document([ + $deployment = $authorization->skip(fn () => $dbForProject->createDocument('deployments', new Document([ '$id' => $deploymentId, '$permissions' => [ Permission::read(Role::any()), @@ -334,7 +334,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - Authorization::skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource)); + $authorization->skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource)); if ($resource->getCollection() === 'sites') { $projectId = $project->getId(); @@ -344,7 +344,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); $previewRuleId = $ruleId; - Authorization::skip( + $authorization->skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), @@ -377,7 +377,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $domain = "branch-{$branchPrefix}-{$resourceProjectHash}.{$sitesDomain}"; $ruleId = md5($domain); try { - Authorization::skip( + $authorization->skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), @@ -408,7 +408,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $domain = "commit-" . substr($providerCommitHash, 0, 16) . ".{$sitesDomain}"; $ruleId = md5($domain); try { - Authorization::skip( + $authorization->skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), @@ -460,7 +460,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId if ($lockAcquired) { // Wrap in try/finally to ensure lock file gets deleted try { - $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId)); + $rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId)); $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; $previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : ''; @@ -472,7 +472,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()); } } finally { - Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId)); + $authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId)); } } } @@ -1476,11 +1476,12 @@ App::post('/v1/vcs/github/events') ->inject('request') ->inject('response') ->inject('dbForPlatform') + ->inject('authorization') ->inject('getProjectDB') ->inject('queueForBuilds') ->inject('platform') ->action( - function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) { + function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) { $payload = $request->getRawPayload(); $signatureRemote = $request->getHeader('x-hub-signature-256', ''); $signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', ''); @@ -1516,14 +1517,14 @@ App::post('/v1/vcs/github/events') $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); //find resourceId from relevant resources table - $repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [ + $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::limit(100), ])); // create new deployment only on push (not committed by us) and not when branch is created or deleted if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) { - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform); } } elseif ($event == $github::EVENT_INSTALLATION) { if ($parsedPayload["action"] == "deleted") { @@ -1536,16 +1537,16 @@ App::post('/v1/vcs/github/events') ]); foreach ($installations as $installation) { - $repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [ + $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ Query::equal('installationInternalId', [$installation->getSequence()]), Query::limit(1000) ])); foreach ($repositories as $repository) { - Authorization::skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId())); + $authorization->skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId())); } - Authorization::skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId())); + $authorization->skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId())); } } } elseif ($event == $github::EVENT_PULL_REQUEST) { @@ -1574,12 +1575,12 @@ App::post('/v1/vcs/github/events') $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; $providerCommitMessage = $commitDetails["commitMessage"] ?? ''; - $repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [ + $repositories = $authorization->skip(fn () => $dbForPlatform->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, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform); } elseif ($parsedPayload["action"] == "closed") { // Allowed external contributions cleanup @@ -1588,7 +1589,7 @@ App::post('/v1/vcs/github/events') $external = $parsedPayload["external"] ?? true; if ($external) { - $repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [ + $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') ])); @@ -1599,7 +1600,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 () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository)); + $repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository)); } } } @@ -1786,17 +1787,18 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor ->inject('response') ->inject('project') ->inject('dbForPlatform') + ->inject('authorization') ->inject('getProjectDB') ->inject('queueForBuilds') ->inject('platform') - ->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Response $response, Document $project, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) { + ->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) { $installation = $dbForPlatform->getDocument('installations', $installationId); if ($installation->isEmpty()) { throw new Exception(Exception::INSTALLATION_NOT_FOUND); } - $repository = Authorization::skip(fn () => $dbForPlatform->findOne('repositories', [ + $repository = $authorization->skip(fn () => $dbForPlatform->findOne('repositories', [ Query::equal('$id', [$repositoryId]), Query::equal('projectInternalId', [$project->getSequence()]) ])); @@ -1814,7 +1816,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor // TODO: Delete from array when PR is closed - $repository = Authorization::skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository)); + $repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository)); $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); @@ -1846,7 +1848,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor $providerCommitMessage = $pullRequestResponse['title'] ?? ''; $providerCommitUrl = $pullRequestResponse['html_url'] ?? ''; - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, '', '', '', '', $providerCommitHash, '', '', '', '', $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform); $response->noContent(); }); diff --git a/app/controllers/general.php b/app/controllers/general.php index ec8cfef775..671c948e93 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -59,7 +59,7 @@ Config::setParam('domainVerification', false); Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); -function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey) +function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey) { $host = $request->getHostname() ?? ''; if (!empty($previewHostname)) { @@ -67,16 +67,16 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } // TODO: (@Meldiron) Remove after 1.7.x migration - $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $rule = Authorization::skip(function () use ($dbForPlatform, $host, $isMd5) { - if ($isMd5) { - return $dbForPlatform->getDocument('rules', md5($host)); - } - - return $dbForPlatform->findOne('rules', [ - Query::equal('domain', [$host]), - ]) ?? new Document(); - }); + if (System::getEnv('_APP_RULES_FORMAT') === 'md5') { + $rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($host))); + } else { + $rule = $authorization->skip( + fn () => $dbForPlatform->find('rules', [ + Query::equal('domain', [$host]), + Query::limit(1) + ]) + )[0] ?? new Document(); + } $errorView = __DIR__ . '/../views/general/error.phtml'; $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; @@ -111,7 +111,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } $projectId = $rule->getAttribute('projectId'); - $project = Authorization::skip( + $project = $authorization->skip( fn () => $dbForPlatform->getDocument('projects', $projectId) ); @@ -119,7 +119,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $accessedAt = $project->getAttribute('accessedAt', 0); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { $project->setAttribute('accessedAt', DateTime::now()); - Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project)); } /** @@ -158,7 +158,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw /** @var Document $deployment */ if (!empty($rule->getAttribute('deploymentId', ''))) { - $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId'))); + $deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId'))); } else { // 1.6.x DB schema compatibility // TODO: Make sure deploymentId is never empty, and remove this code @@ -172,15 +172,15 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw // Document of site or function $resource = $resourceType === 'function' ? - Authorization::skip(fn () => $dbForProject->getDocument('functions', $resourceId)) : - Authorization::skip(fn () => $dbForProject->getDocument('sites', $resourceId)); + $authorization->skip(fn () => $dbForProject->getDocument('functions', $resourceId)) : + $authorization->skip(fn () => $dbForProject->getDocument('sites', $resourceId)); // ID of active deployments // Attempts to use attribute from both schemas (1.6 and 1.7) $activeDeploymentId = $resource->getAttribute('deploymentId', $resource->getAttribute('deployment', '')); // Get deployment document, as intended originally - $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId)); + $deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId)); } if ($deployment->getAttribute('resourceType', '') === 'functions') { @@ -199,8 +199,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } $resource = $type === 'function' ? - Authorization::skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) : - Authorization::skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', ''))); + $authorization->skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) : + $authorization->skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', ''))); $isPreview = $type === 'function' ? false : ($rule->getAttribute('trigger', '') !== 'manual'); @@ -242,7 +242,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $userExists = false; $userId = $payload['userId'] ?? ''; if (!empty($userId)) { - $user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId)); + $user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId)); if (!$user->isEmpty() && $user->getAttribute('status', false)) { $userExists = true; } @@ -255,7 +255,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } $membershipExists = false; - $project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); + $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); if (!$project->isEmpty() && isset($user)) { $teamId = $project->getAttribute('teamId', ''); $membership = $user->find('teamId', $teamId, 'memberships'); @@ -862,15 +862,16 @@ App::init() ->inject('devKey') ->inject('apiKey') ->inject('cors') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors) { + ->inject('authorization') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization) { /* * Appwrite Router */ $hostname = $request->getHostname() ?? ''; $platformHostnames = $platform['hostnames'] ?? []; // Only run Router when external domain - if (!in_array($hostname, $platformHostnames) || !empty($previewHostname)) { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) { + if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey)) { $utopia->getRoute()?->label('router', true); } } @@ -1033,7 +1034,8 @@ App::init() ->inject('dbForPlatform') ->inject('queueForCertificates') ->inject('platform') - ->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform) { + ->inject('authorization') + ->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization) { $hostname = $request->getHostname(); $cache = Config::getParam('hostnames', []); $platformHostnames = $platform['hostnames'] ?? []; @@ -1061,64 +1063,64 @@ App::init() } // 4. Check/create rule (requires DB access) - Authorization::disable(); - try { - // TODO: (@Meldiron) Remove after 1.7.x migration - $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $document = $isMd5 - ? $dbForPlatform->getDocument('rules', md5($domain->get())) - : $dbForPlatform->findOne('rules', [ - Query::equal('domain', [$domain->get()]), + $authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, &$cache) { + try { + // TODO: (@Meldiron) Remove after 1.7.x migration + $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; + $document = $isMd5 + ? $dbForPlatform->getDocument('rules', md5($domain->get())) + : $dbForPlatform->findOne('rules', [ + Query::equal('domain', [$domain->get()]), + ]); + + if (!$document->isEmpty()) { + return; + } + + // 5. Create new rule + $owner = ''; + $fallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''); + $funcDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + $siteDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + + if (!empty($fallback) && \str_ends_with($domain->get(), $fallback)) { + $funcDomain = $fallback; + } + + if ( + (!empty($funcDomain) && \str_ends_with($domain->get(), $funcDomain)) || + (!empty($siteDomain) && \str_ends_with($domain->get(), $siteDomain)) + ) { + $owner = 'Appwrite'; + } + + $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); + $document = new Document([ + '$id' => $ruleId, + 'domain' => $domain->get(), + 'type' => 'api', + 'status' => 'verifying', + 'projectId' => $console->getId(), + 'projectInternalId' => $console->getSequence(), + 'search' => implode(' ', [$ruleId, $domain->get()]), + 'owner' => $owner, + 'region' => $console->getAttribute('region') ]); - if (!$document->isEmpty()) { - return; + $dbForPlatform->createDocument('rules', $document); + + Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...'); + $queueForCertificates + ->setDomain($document) + ->setSkipRenewCheck(true) + ->trigger(); + } catch (Duplicate $e) { + Console::info('Certificate already exists'); + } finally { + $cache[$domain->get()] = true; + Config::setParam('hostnames', $cache); } - - // 5. Create new rule - $owner = ''; - $fallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''); - $funcDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - $siteDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - - if (!empty($fallback) && \str_ends_with($domain->get(), $fallback)) { - $funcDomain = $fallback; - } - - if ( - (!empty($funcDomain) && \str_ends_with($domain->get(), $funcDomain)) || - (!empty($siteDomain) && \str_ends_with($domain->get(), $siteDomain)) - ) { - $owner = 'Appwrite'; - } - - $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); - $document = new Document([ - '$id' => $ruleId, - 'domain' => $domain->get(), - 'type' => 'api', - 'status' => 'verifying', - 'projectId' => $console->getId(), - 'projectInternalId' => $console->getSequence(), - 'search' => implode(' ', [$ruleId, $domain->get()]), - 'owner' => $owner, - 'region' => $console->getAttribute('region') - ]); - - $dbForPlatform->createDocument('rules', $document); - - Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...'); - $queueForCertificates - ->setDomain($document) - ->setSkipRenewCheck(true) - ->trigger(); - } catch (Duplicate $e) { - Console::info('Certificate already exists'); - } finally { - $cache[$domain->get()] = true; - Config::setParam('hostnames', $cache); - Authorization::reset(); - } + }); }); App::options() @@ -1141,7 +1143,8 @@ App::options() ->inject('devKey') ->inject('apiKey') ->inject('cors') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors) { + ->inject('authorization') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization) { /* * Appwrite Router */ @@ -1182,7 +1185,8 @@ App::error() ->inject('log') ->inject('queueForStatsUsage') ->inject('devKey') - ->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage) { + ->inject('authorization') + ->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage, Document $devKey, Authorization $authorization) { $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); $route = $utopia->getRoute(); $class = \get_class($error); @@ -1264,7 +1268,7 @@ App::error() * If not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php */ if (!$publish && $project->getId() !== 'console') { - if (!DBUser::isPrivileged(Authorization::getRoles())) { + if (!DBUser::isPrivileged($authorization->getRoles())) { $fileSize = 0; $file = $request->getFiles('file'); if (!empty($file)) { @@ -1326,7 +1330,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()); try { /* add queries to log */ @@ -1530,13 +1534,14 @@ App::get('/robots.txt') ->inject('platform') ->inject('previewHostname') ->inject('apiKey') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey) { + ->inject('authorization') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization) { $platformHostnames = $platform['hostnames'] ?? []; if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) { $template = new View(__DIR__ . '/../views/general/robots.phtml'); $response->text($template->render(false)); } else { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey)) { $utopia->getRoute()?->label('router', true); } } @@ -1562,13 +1567,14 @@ App::get('/humans.txt') ->inject('platform') ->inject('previewHostname') ->inject('apiKey') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey) { + ->inject('authorization') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization) { $platformHostnames = $platform['hostnames'] ?? []; if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) { $template = new View(__DIR__ . '/../views/general/humans.phtml'); $response->text($template->render(false)); } else { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey)) { $utopia->getRoute()?->label('router', true); } } @@ -1652,7 +1658,8 @@ App::get('/v1/ping') ->inject('project') ->inject('dbForPlatform') ->inject('queueForEvents') - ->action(function (Response $response, Document $project, Database $dbForPlatform, Event $queueForEvents) { + ->inject('authorization') + ->action(function (Response $response, Document $project, Database $dbForPlatform, Event $queueForEvents, Authorization $authorization) { if ($project->isEmpty() || $project->getId() === 'console') { throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND); } @@ -1664,7 +1671,7 @@ App::get('/v1/ping') ->setAttribute('pingCount', $pingCount) ->setAttribute('pingedAt', $pingedAt); - Authorization::skip(function () use ($dbForPlatform, $project) { + $authorization->skip(function () use ($dbForPlatform, $project) { $dbForPlatform->updateDocument('projects', $project->getId(), $project); }); diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 05c08a2231..23bbb12183 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -30,6 +30,7 @@ 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\Queue\Publisher; use Utopia\System\System; use Utopia\Telemetry\Adapter as Telemetry; @@ -233,7 +234,8 @@ App::init() ->inject('mode') ->inject('team') ->inject('apiKey') - ->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) { + ->inject('authorization') + ->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) { $route = $utopia->getRoute(); /** @@ -318,7 +320,7 @@ App::init() // Handle special app role case if ($apiKey->getRole() === User::ROLE_APPS) { // Disable authorization checks for API keys - Authorization::setDefaultStatus(false); + $authorization->setDefaultStatus(false); $user = new User([ '$id' => '', @@ -392,14 +394,14 @@ App::init() $scopes = \array_merge($scopes, $roles[$role]['scopes']); } - Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users. + $authorization->setDefaultStatus(false); // Cancel security segmentation for admin users. } $scopes = \array_unique($scopes); - Authorization::setRole($role); - foreach ($user->getRoles() as $authRole) { - Authorization::setRole($authRole); + $authorization->addRole($role); + foreach ($user->getRoles($authorization) as $authRole) { + $authorization->addRole($authRole); } // Step 6: Update project and user last activity @@ -407,7 +409,7 @@ App::init() $accessedAt = $project->getAttribute('accessedAt', 0); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { $project->setAttribute('accessedAt', DateTime::now()); - Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project)); } } @@ -442,7 +444,7 @@ App::init() if ( array_key_exists($namespace, $project->getAttribute('services', [])) && !$project->getAttribute('services', [])[$namespace] - && !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles())) + && !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles())) ) { throw new Exception(Exception::GENERAL_SERVICE_DISABLED); } @@ -509,14 +511,15 @@ App::init() ->inject('devKey') ->inject('telemetry') ->inject('platform') - ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Mail $queueForMails, Migration $queueForMigrations, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform) use ($usageDatabaseListener, $eventDatabaseListener) { + ->inject('authorization') + ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Mail $queueForMails, Migration $queueForMigrations, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) use ($usageDatabaseListener, $eventDatabaseListener) { $route = $utopia->getRoute(); if ( array_key_exists('rest', $project->getAttribute('apis', [])) && !$project->getAttribute('apis', [])['rest'] - && !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles())) + && !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } @@ -546,7 +549,7 @@ App::init() $closestLimit = null; - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = User::isPrivileged($roles); $isAppUser = User::isApp($roles); @@ -657,10 +660,10 @@ App::init() if ($useCache) { $route = $utopia->match($request); $isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview'; - $isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !User::isPrivileged(Authorization::getRoles()); + $isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !User::isPrivileged($authorization->getRoles()); $key = $request->cacheIdentifier(); - $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()) ); @@ -677,10 +680,10 @@ App::init() if ($type === 'bucket' && (!$isImageTransformation || !$isDisabled)) { $bucketId = $parts[1] ?? null; - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence(); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); @@ -691,8 +694,7 @@ App::init() } $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 && !$isToken) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -703,7 +705,7 @@ App::init() if ($fileSecurity && !$valid && !$isToken) { $file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId); } else { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); } if (!$resourceToken->isEmpty() && $resourceToken->getAttribute('fileInternalId') !== $file->getSequence()) { @@ -714,11 +716,11 @@ App::init() throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } //Do not update transformedAt if it's a console user - if (!User::isPrivileged(Authorization::getRoles())) { + if (!User::isPrivileged($authorization->getRoles())) { $transformedAt = $file->getAttribute('transformedAt', ''); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) { $file->setAttribute('transformedAt', DateTime::now()); - Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file)); + $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file)); } } } @@ -814,8 +816,9 @@ App::shutdown() ->inject('queueForWebhooks') ->inject('queueForRealtime') ->inject('dbForProject') + ->inject('authorization') ->inject('timelimit') - ->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, callable $timelimit) use ($parseLabel) { + ->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit) use ($parseLabel) { $responsePayload = $response->getPayload(); @@ -976,11 +979,11 @@ App::shutdown() $key = $request->cacheIdentifier(); $signature = md5($data['payload']); - $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); + $cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key)); $accessedAt = $cacheLog->getAttribute('accessedAt', 0); $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, @@ -990,7 +993,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)); // Overwrite the file every APP_CACHE_UPDATE seconds to update the file modified time that is used in the TTL checks in cache->load() $cache->save($key, $data['payload']); } @@ -1002,7 +1005,7 @@ App::shutdown() } if ($project->getId() !== 'console') { - if (!User::isPrivileged(Authorization::getRoles())) { + if (!User::isPrivileged($authorization->getRoles())) { $fileSize = 0; $file = $request->getFiles('file'); if (!empty($file)) { diff --git a/app/controllers/shared/api/auth.php b/app/controllers/shared/api/auth.php index efa733fc34..c0f7494125 100644 --- a/app/controllers/shared/api/auth.php +++ b/app/controllers/shared/api/auth.php @@ -36,7 +36,8 @@ App::init() ->inject('request') ->inject('project') ->inject('geodb') - ->action(function (App $utopia, Request $request, Document $project, Reader $geodb) { + ->inject('authorization') + ->action(function (App $utopia, Request $request, Document $project, Reader $geodb, Authorization $authorization) { $denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', ''); if (!empty($denylist && $project->getId() === 'console')) { $countries = explode(',', $denylist); @@ -49,8 +50,8 @@ App::init() $route = $utopia->match($request); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); - $isAppUser = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); + $isAppUser = User::isApp($authorization->getRoles()); if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs return; diff --git a/app/http.php b/app/http.php index b7f857da48..5d08c53eee 100644 --- a/app/http.php +++ b/app/http.php @@ -27,7 +27,6 @@ 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\Logger\Log; use Utopia\Logger\Log\User; use Utopia\Pools\Group; @@ -261,7 +260,9 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg createDatabase($app, 'getLogsDB', 'logs', $collections['logs'], $pools); // create appwrite database, `dbForPlatform` is a direct access call. - createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections) { + createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $app) { + $authorization = $app->getResource('authorization'); + if ($dbForPlatform->getCollection(AuditAdapterSQL::COLLECTION)->isEmpty()) { $adapter = new AdapterDatabase($dbForPlatform); $audit = new Audit($adapter); @@ -321,9 +322,9 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg $dbForPlatform->createCollection('bucket_' . $bucket->getSequence(), $attributes, $indexes); } - if (Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty())) { + if ($authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty())) { Console::info(" └── Creating screenshots bucket..."); - Authorization::skip(fn () => $dbForPlatform->createDocument('buckets', new Document([ + $authorization->skip(fn () => $dbForPlatform->createDocument('buckets', new Document([ '$id' => ID::custom('screenshots'), '$collection' => ID::custom('buckets'), 'name' => 'Screenshots', @@ -338,7 +339,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg 'search' => 'buckets Screenshots', ]))); - $bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); + $bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); Console::info(" └── Creating files collection for screenshots bucket..."); $files = $collections['buckets']['files'] ?? []; @@ -366,7 +367,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg 'orders' => $index['orders'], ]), $files['indexes']); - Authorization::skip(fn () => $dbForPlatform->createCollection('bucket_' . $bucket->getSequence(), $attributes, $indexes)); + $authorization->skip(fn () => $dbForPlatform->createCollection('bucket_' . $bucket->getSequence(), $attributes, $indexes)); } }); @@ -458,8 +459,12 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool App::setResource('pools', fn () => $pools); try { - Authorization::cleanRoles(); - Authorization::setRole(Role::any()->toString()); + $authorization = $app->getResource('authorization'); + + $request->setAuthorization($authorization); + $response->setAuthorization($authorization); + $authorization->cleanRoles(); + $authorization->addRole(Role::any()->toString()); $app->run($request, $response); } catch (\Throwable $th) { @@ -501,7 +506,7 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool $log->addExtra('file', $th->getFile()); $log->addExtra('line', $th->getLine()); $log->addExtra('trace', $th->getTraceAsString()); - $log->addExtra('roles', Authorization::getRoles()); + $log->addExtra('roles', isset($authorization) ? $authorization->getRoles() : []); $sdk = $route->getLabel("sdk", false); @@ -560,7 +565,7 @@ $http->on(Constant::EVENT_TASK, function () use ($register, $domains) { /** @var Utopia\Database\Database $dbForPlatform */ $dbForPlatform = $app->getResource('dbForPlatform'); - Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $domains, &$lastSyncUpdate) { + Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $domains, &$lastSyncUpdate, $app) { try { $time = DateTime::now(); $limit = 1000; @@ -577,7 +582,8 @@ $http->on(Constant::EVENT_TASK, function () use ($register, $domains) { } $results = []; try { - $results = Authorization::skip(fn () => $dbForPlatform->find('rules', $queries)); + $authorization = $app->getResource('authorization'); + $results = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries)); } catch (Throwable $th) { Console::error($th->getMessage()); } diff --git a/app/init/database/filters.php b/app/init/database/filters.php index c9ad3fce03..2b2e17b6a9 100644 --- a/app/init/database/filters.php +++ b/app/init/database/filters.php @@ -4,7 +4,6 @@ use Appwrite\OpenSSL\OpenSSL; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization; use Utopia\System\System; Database::addFilter( @@ -70,11 +69,11 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - $attributes = $database->find('attributes', [ + $attributes = $database->getAuthorization()->skip(fn () => $database->find('attributes', [ Query::equal('collectionInternalId', [$document->getSequence()]), Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), Query::limit($database->getLimitForAttributes()), - ]); + ])); foreach ($attributes as $attribute) { $attributeType = $attribute->getAttribute('type'); @@ -105,12 +104,12 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - return $database + return $database->getAuthorization()->skip(fn () => $database ->find('indexes', [ Query::equal('collectionInternalId', [$document->getSequence()]), Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), Query::limit($database->getLimitForIndexes()), - ]); + ])); } ); @@ -120,11 +119,11 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - return $database + return $database->getAuthorization()->skip(fn () => $database ->find('platforms', [ Query::equal('projectInternalId', [$document->getSequence()]), Query::limit(APP_LIMIT_SUBQUERY), - ]); + ])); } ); @@ -134,12 +133,12 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - return $database + return $database->getAuthorization()->skip(fn () => $database ->find('keys', [ Query::equal('resourceType', ['projects']), Query::equal('resourceInternalId', [$document->getSequence()]), Query::limit(APP_LIMIT_SUBQUERY), - ]); + ])); } ); @@ -149,11 +148,11 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - return $database + return $database->getAuthorization()->skip(fn () => $database ->find('devKeys', [ Query::equal('projectInternalId', [$document->getSequence()]), Query::limit(APP_LIMIT_SUBQUERY), - ]); + ])); } ); @@ -163,11 +162,11 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - return $database + return $database->getAuthorization()->skip(fn () => $database ->find('webhooks', [ Query::equal('projectInternalId', [$document->getSequence()]), Query::limit(APP_LIMIT_SUBQUERY), - ]); + ])); } ); @@ -177,7 +176,7 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - return Authorization::skip(fn () => $database->find('sessions', [ + return $database->getAuthorization()->skip(fn () => $database->find('sessions', [ Query::equal('userInternalId', [$document->getSequence()]), Query::limit(APP_LIMIT_SUBQUERY), ])); @@ -190,7 +189,7 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - return Authorization::skip(fn () => $database + return $database->getAuthorization()->skip(fn () => $database ->find('tokens', [ Query::equal('userInternalId', [$document->getSequence()]), Query::limit(APP_LIMIT_SUBQUERY), @@ -204,7 +203,7 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - return Authorization::skip(fn () => $database + return $database->getAuthorization()->skip(fn () => $database ->find('challenges', [ Query::equal('userInternalId', [$document->getSequence()]), Query::limit(APP_LIMIT_SUBQUERY), @@ -218,7 +217,7 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - return Authorization::skip(fn () => $database + return $database->getAuthorization()->skip(fn () => $database ->find('authenticators', [ Query::equal('userInternalId', [$document->getSequence()]), Query::limit(APP_LIMIT_SUBQUERY), @@ -232,7 +231,7 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - return Authorization::skip(fn () => $database + return $database->getAuthorization()->skip(fn () => $database ->find('memberships', [ Query::equal('userInternalId', [$document->getSequence()]), Query::limit(APP_LIMIT_SUBQUERY), @@ -252,14 +251,14 @@ Database::addFilter( default => ['function', 'site'] }; - return $database + return $database->getAuthorization()->skip(fn () => $database ->find('variables', [ Query::equal('resourceInternalId', [$document->getSequence()]), Query::equal('resourceType', $resourceType), Query::orderAsc('resourceType'), Query::orderAsc(), Query::limit(APP_LIMIT_SUBQUERY), - ]); + ])); } ); @@ -295,11 +294,11 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - return $database + return $database->getAuthorization()->skip(fn () => $database ->find('variables', [ Query::equal('resourceType', ['project']), Query::limit(APP_LIMIT_SUBQUERY) - ]); + ])); } ); @@ -332,7 +331,7 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - return Authorization::skip(fn () => $database + return $database->getAuthorization()->skip(fn () => $database ->find('targets', [ Query::equal('userInternalId', [$document->getSequence()]), Query::limit(APP_LIMIT_SUBQUERY) @@ -346,7 +345,7 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - $targetIds = Authorization::skip(fn () => \array_map( + $targetIds = $database->getAuthorization()->skip(fn () => \array_map( fn ($document) => $document->getAttribute('targetInternalId'), $database->find('subscribers', [ Query::equal('topicInternalId', [$document->getSequence()]), diff --git a/app/init/resources.php b/app/init/resources.php index d56354c14b..371609da97 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -15,6 +15,7 @@ use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Event\Migration; use Appwrite\Event\Realtime; +use Appwrite\Event\Screenshot; use Appwrite\Event\StatsResources; use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; @@ -129,6 +130,9 @@ App::setResource('queueForMails', function (Publisher $publisher) { App::setResource('queueForBuilds', function (Publisher $publisher) { return new Build($publisher); }, ['publisher']); +App::setResource('queueForScreenshots', function (Publisher $publisher) { + return new Screenshot($publisher); +}, ['publisher']); App::setResource('queueForDatabase', function (Publisher $publisher) { return new EventDatabase($publisher); }, ['publisher']); @@ -226,7 +230,7 @@ App::setResource('allowedSchemes', function (Document $project) { /** * Rule associated with a request origin. */ -App::setResource('rule', function (Request $request, Database $dbForPlatform, Document $project) { +App::setResource('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) { $domain = \parse_url($request->getOrigin(), PHP_URL_HOST); if (empty($domain)) { return new Document(); @@ -234,7 +238,7 @@ App::setResource('rule', function (Request $request, Database $dbForPlatform, Do // TODO: (@Meldiron) Remove after 1.7.x migration $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $rule = Authorization::skip(function () use ($dbForPlatform, $domain, $isMd5) { + $rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) { if ($isMd5) { return $dbForPlatform->getDocument('rules', md5($domain)); } @@ -249,7 +253,7 @@ App::setResource('rule', function (Request $request, Database $dbForPlatform, Do } return $rule; -}, ['request', 'dbForPlatform', 'project']); +}, ['request', 'dbForPlatform', 'project', 'authorization']); /** * CORS service @@ -317,7 +321,7 @@ App::setResource('redirectValidator', function (Document $devKey, array $allowed return new Redirect($allowedHostnames, $allowedSchemes); }, ['devKey', 'allowedHostnames', 'allowedSchemes']); -App::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken) { +App::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) { /** * Handles user authentication and session validation. * @@ -337,7 +341,7 @@ App::setResource('user', function (string $mode, Document $project, Document $co * overwriting the previous value. */ - Authorization::setDefaultStatus(true); + $authorization->setDefaultStatus(true); $store->setKey('a_session_' . $project->getId()); @@ -404,7 +408,7 @@ App::setResource('user', function (string $mode, Document $project, Document $co } // if (APP_MODE_ADMIN === $mode) { // if ($user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships')) { - // Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users. + // $authorization->setDefaultStatus(false); // Cancel security segmentation for admin users. // } else { // $user = new Document([]); // } @@ -436,9 +440,9 @@ App::setResource('user', function (string $mode, Document $project, Document $co $dbForPlatform->setMetadata('user', $user->getId()); return $user; -}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken']); +}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']); -App::setResource('project', function ($dbForPlatform, $request, $console) { +App::setResource('project', function ($dbForPlatform, $request, $console, $authorization) { /** @var Appwrite\Utopia\Request $request */ /** @var Utopia\Database\Database $dbForPlatform */ /** @var Utopia\Database\Document $console */ @@ -449,10 +453,10 @@ App::setResource('project', function ($dbForPlatform, $request, $console) { return $console; } - $project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); + $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); return $project; -}, ['dbForPlatform', 'request', 'console']); +}, ['dbForPlatform', 'request', 'console', 'authorization']); App::setResource('session', function (User $user, Store $store, Token $proofForToken) { if ($user->isEmpty()) { @@ -475,10 +479,6 @@ App::setResource('session', function (User $user, Store $store, Token $proofForT return; }, ['user', 'store', 'proofForToken']); -App::setResource('console', function () { - return new Document(Config::getParam('console')); -}, []); - App::setResource('store', function (): Store { return new Store(); }); @@ -509,7 +509,15 @@ App::setResource('proofForCode', function (): Code { return $code; }); -App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project) { +App::setResource('console', function () { + return new Document(Config::getParam('console')); +}, []); + +App::setResource('authorization', function () { + return new Authorization(); +}, []); + +App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Authorization $authorization) { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForPlatform; } @@ -525,6 +533,7 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform $database = new Database($adapter, $cache); $database + ->setAuthorization($authorization) ->setMetadata('host', \gethostname()) ->setMetadata('project', $project->getId()) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) @@ -546,13 +555,15 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform } return $database; -}, ['pools', 'dbForPlatform', 'cache', 'project']); +}, ['pools', 'dbForPlatform', 'cache', 'project', 'authorization']); + +App::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) { -App::setResource('dbForPlatform', function (Group $pools, Cache $cache) { $adapter = new DatabasePool($pools->get('console')); $database = new Database($adapter, $cache); $database + ->setAuthorization($authorization) ->setNamespace('_console') ->setMetadata('host', \gethostname()) ->setMetadata('project', 'console') @@ -562,12 +573,12 @@ App::setResource('dbForPlatform', function (Group $pools, Cache $cache) { $database->setDocumentType('users', User::class); return $database; -}, ['pools', 'cache']); +}, ['pools', 'cache', 'authorization']); -App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) { +App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) { $databases = []; - return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases) { + return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForPlatform; } @@ -579,13 +590,15 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform $dsn = new DSN('mysql://' . $project->getAttribute('database')); } - $configure = (function (Database $database) use ($project, $dsn) { + $configure = (function (Database $database) use ($project, $dsn, $authorization) { $database + ->setAuthorization($authorization) ->setMetadata('host', \gethostname()) ->setMetadata('project', $project->getId()) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) - ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); - $database->setDocumentType('users', User::class); + ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES) + ->setDocumentType('users', User::class) + ; $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); @@ -615,12 +628,12 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform return $database; }; -}, ['pools', 'dbForPlatform', 'cache']); +}, ['pools', 'dbForPlatform', 'cache', 'authorization']); -App::setResource('getLogsDB', function (Group $pools, Cache $cache) { +App::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { $database = null; - return function (?Document $project = null) use ($pools, $cache, &$database) { + return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) { if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') { $database->setTenant((int) $project->getSequence()); return $database; @@ -630,6 +643,7 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) { $database = new Database($adapter, $cache); $database + ->setAuthorization($authorization) ->setSharedTables(true) ->setNamespace('logsV1') ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) @@ -642,7 +656,7 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) { return $database; }; -}, ['pools', 'cache']); +}, ['pools', 'cache', 'authorization']); App::setResource('audit', function ($dbForProject) { $adapter = new AdapterDatabase($dbForProject); @@ -841,7 +855,7 @@ App::setResource('promiseAdapter', function ($register) { return $register->get('promiseAdapter'); }, ['register']); -App::setResource('schema', function ($utopia, $dbForProject) { +App::setResource('schema', function ($utopia, $dbForProject, $authorization) { $complexity = function (int $complexity, array $args) { $queries = Query::parseQueries($args['queries'] ?? []); @@ -851,8 +865,8 @@ App::setResource('schema', function ($utopia, $dbForProject) { return $complexity * $limit; }; - $attributes = function (int $limit, int $offset) use ($dbForProject) { - $attrs = Authorization::skip(fn () => $dbForProject->find('attributes', [ + $attributes = function (int $limit, int $offset) use ($dbForProject, $authorization) { + $attrs = $authorization->skip(fn () => $dbForProject->find('attributes', [ Query::limit($limit), Query::offset($offset), ])); @@ -926,7 +940,7 @@ App::setResource('schema', function ($utopia, $dbForProject) { $urls, $params, ); -}, ['utopia', 'dbForProject']); +}, ['utopia', 'dbForProject', 'authorization']); App::setResource('gitHub', function (Cache $cache) { return new VcsGitHub($cache); @@ -954,7 +968,7 @@ App::setResource('smsRates', function () { return []; }); -App::setResource('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform) { +App::setResource('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) { $devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', '')); // Check if given key match project's development keys @@ -973,7 +987,7 @@ App::setResource('devKey', function (Request $request, Document $project, array $accessedAt = $key->getAttribute('accessedAt', 0); if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) { $key->setAttribute('accessedAt', DatabaseDateTime::now()); - Authorization::skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key)); $dbForPlatform->purgeCachedDocument('projects', $project->getId()); } @@ -990,15 +1004,15 @@ App::setResource('devKey', function (Request $request, Document $project, array /** Update access time as well */ $key->setAttribute('accessedAt', DatabaseDateTime::now()); - $key = Authorization::skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key)); + $key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key)); $dbForPlatform->purgeCachedDocument('projects', $project->getId()); } } return $key; -}, ['request', 'project', 'servers', 'dbForPlatform']); +}, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']); -App::setResource('team', function (Document $project, Database $dbForPlatform, App $utopia, Request $request) { +App::setResource('team', function (Document $project, Database $dbForPlatform, App $utopia, Request $request, Authorization $authorization) { $teamInternalId = ''; if ($project->getId() !== 'console') { $teamInternalId = $project->getAttribute('teamInternalId', ''); @@ -1008,7 +1022,7 @@ App::setResource('team', function (Document $project, Database $dbForPlatform, A if (str_starts_with($path, '/v1/projects/:projectId')) { $uri = $request->getURI(); $pid = explode('/', $uri)[3]; - $p = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $pid)); + $p = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $pid)); $teamInternalId = $p->getAttribute('teamInternalId', ''); } elseif ($path === '/v1/projects') { $teamId = $request->getParam('teamId', ''); @@ -1017,7 +1031,7 @@ App::setResource('team', function (Document $project, Database $dbForPlatform, A return new Document([]); } - $team = Authorization::skip(fn () => $dbForPlatform->getDocument('teams', $teamId)); + $team = $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $teamId)); return $team; } } @@ -1026,14 +1040,14 @@ App::setResource('team', function (Document $project, Database $dbForPlatform, A return new Document([]); } - $team = Authorization::skip(function () use ($dbForPlatform, $teamInternalId) { + $team = $authorization->skip(function () use ($dbForPlatform, $teamInternalId) { return $dbForPlatform->findOne('teams', [ Query::equal('$sequence', [$teamInternalId]), ]); }); return $team; -}, ['project', 'dbForPlatform', 'utopia', 'request']); +}, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']); App::setResource( 'isResourceBlocked', @@ -1071,7 +1085,7 @@ App::setResource('apiKey', function (Request $request, Document $project): ?Key App::setResource('executor', fn () => new Executor()); -App::setResource('resourceToken', function ($project, $dbForProject, $request) { +App::setResource('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) { $tokenJWT = $request->getParam('token'); if (!empty($tokenJWT) && !$project->isEmpty()) { // JWT authentication @@ -1089,7 +1103,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) { return new Document([]); } - $token = Authorization::skip(fn () => $dbForProject->getDocument('resourceTokens', $tokenId)); + $token = $authorization->skip(fn () => $dbForProject->getDocument('resourceTokens', $tokenId)); if ($token->isEmpty()) { return new Document([]); @@ -1107,7 +1121,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) { } return match ($token->getAttribute('resourceType')) { - TOKENS_RESOURCE_TYPE_FILES => (function () use ($token, $dbForProject) { + TOKENS_RESOURCE_TYPE_FILES => (function () use ($token, $dbForProject, $authorization) { $sequences = explode(':', $token->getAttribute('resourceInternalId')); $ids = explode(':', $token->getAttribute('resourceId')); @@ -1118,7 +1132,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) { $accessedAt = $token->getAttribute('accessedAt', 0); if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) { $token->setAttribute('accessedAt', DatabaseDateTime::now()); - Authorization::skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token)); + $authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token)); } return new Document([ @@ -1133,8 +1147,8 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) { }; } return new Document([]); -}, ['project', 'dbForProject', 'request']); +}, ['project', 'dbForProject', 'request', 'authorization']); -App::setResource('transactionState', function (Database $dbForProject) { - return new TransactionState($dbForProject); -}, ['dbForProject']); +App::setResource('transactionState', function (Database $dbForProject, Authorization $authorization) { + return new TransactionState($dbForProject, $authorization); +}, ['dbForProject', 'authorization']); diff --git a/app/realtime.php b/app/realtime.php index fab0ce7561..31e6015d92 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -32,7 +32,6 @@ 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\Logger\Log; use Utopia\Pools\Group; @@ -309,7 +308,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume 'value' => '{}' ]); - $statsDocument = Authorization::skip(fn () => $database->createDocument('realtime', $document)); + $statsDocument = $database->getAuthorization()->skip(fn () => $database->createDocument('realtime', $document)); break; } catch (Throwable) { Console::warning("Collection not ready. Retrying connection ({$attempts})..."); @@ -339,7 +338,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume ->setAttribute('timestamp', DateTime::now()) ->setAttribute('value', json_encode($payload)); - Authorization::skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument)); + $database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument)); } catch (Throwable $th) { $logError($th, "updateWorkerDocument"); } @@ -370,7 +369,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $payload = []; - $list = Authorization::skip(fn () => $database->find('realtime', [ + $list = $database->getAuthorization()->skip(fn () => $database->find('realtime', [ Query::greaterThan('timestamp', DateTime::addSeconds(new \DateTime(), -15)), ])); @@ -464,13 +463,13 @@ $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)); + $project = $consoleDatabase->getAuthorization()->skip(fn () => $consoleDatabase->getDocument('projects', $projectId)); $database = getProjectDB($project); /** @var Appwrite\Utopia\Database\Documents\User $user */ $user = $database->getDocument('users', $userId); - $roles = $user->getRoles(); + $roles = $user->getRoles($database->getAuthorization()); $channels = $realtime->connections[$connection]['channels']; $realtime->unsubscribe($connection); @@ -526,6 +525,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, try { /** @var Document $project */ $project = $app->getResource('project'); + $authorization = $app->getResource('authorization'); /* * Project Check @@ -537,7 +537,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, if ( array_key_exists('realtime', $project->getAttribute('apis', [])) && !$project->getAttribute('apis', [])['realtime'] - && !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles())) + && !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } @@ -573,7 +573,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription()); } - $roles = $user->getRoles(); + $roles = $user->getRoles($authorization); $channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId()); @@ -586,6 +586,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $realtime->subscribe($project->getId(), $connection, $roles, $channels); + $realtime->connections[$connection]['authorization'] = $authorization; + $user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT); $server->send([$connection], json_encode([ @@ -614,6 +616,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $code = 500; } + $message = $th->getMessage(); // sanitize 0 && 5xx errors @@ -643,12 +646,19 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) { try { $response = new Response(new SwooleResponse()); - $projectId = $realtime->connections[$connection]['projectId']; + $projectId = $realtime->connections[$connection]['projectId'] ?? null; + + // Get authorization from connection (stored during onOpen) + $authorization = $realtime->connections[$connection]['authorization'] ?? null; + $database = getConsoleDB(); + $database->setAuthorization($authorization); if ($projectId !== 'console') { - $project = Authorization::skip(fn () => $database->getDocument('projects', $projectId)); + $project = $authorization->skip(fn () => $database->getDocument('projects', $projectId)); + $database = getProjectDB($project); + $database->setAuthorization($authorization); } else { $project = null; } @@ -712,10 +722,19 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.'); } - $roles = $user->getRoles(); + $roles = $user->getRoles($database->getAuthorization()); $channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId()); + + // Preserve authorization before subscribe overwrites the connection array + $authorization = $realtime->connections[$connection]['authorization'] ?? null; + $realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels); + // Restore authorization after subscribe + if ($authorization !== null) { + $realtime->connections[$connection]['authorization'] = $authorization; + } + $user = $response->output($user, Response::MODEL_ACCOUNT); $server->send([$connection], json_encode([ 'type' => 'response', diff --git a/app/worker.php b/app/worker.php index 094d2b993e..d31e63fc8b 100644 --- a/app/worker.php +++ b/app/worker.php @@ -14,6 +14,7 @@ use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Event\Migration; use Appwrite\Event\Realtime; +use Appwrite\Event\Screenshot; use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; use Appwrite\Platform\Appwrite; @@ -48,19 +49,30 @@ use Utopia\System\System; use Utopia\Telemetry\Adapter as Telemetry; use Utopia\Telemetry\Adapter\None as NoTelemetry; -Authorization::disable(); Runtime::enableCoroutine(); Server::setResource('register', fn () => $register); -Server::setResource('dbForPlatform', function (Cache $cache, Registry $register) { +Server::setResource('authorization', function () { + $authorization = new Authorization(); + $authorization->disable(); + return $authorization; +}, []); + +Server::setResource('dbForPlatform', function (Cache $cache, Registry $register, Authorization $authorization) { $pools = $register->get('pools'); $adapter = new DatabasePool($pools->get('console')); $dbForPlatform = new Database($adapter, $cache); - $dbForPlatform->setNamespace('_console'); - $dbForPlatform->setDocumentType('users', User::class); + + $dbForPlatform + ->setAuthorization($authorization) + ->setNamespace('_console') + ->setDocumentType('users', User::class) + ; + + return $dbForPlatform; -}, ['cache', 'register']); +}, ['cache', 'register', 'authorization']); Server::setResource('project', function (Message $message, Database $dbForPlatform) { $payload = $message->getPayload() ?? []; @@ -73,7 +85,7 @@ Server::setResource('project', function (Message $message, Database $dbForPlatfo return $dbForPlatform->getDocument('projects', $project->getId()); }, ['message', 'dbForPlatform']); -Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform) { +Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform, Authorization $authorization) { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForPlatform; } @@ -105,15 +117,17 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register, ->setNamespace('_' . $project->getSequence()); } - $database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER); + $database + ->setAuthorization($authorization) + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER); return $database; -}, ['cache', 'register', 'message', 'project', 'dbForPlatform']); +}, ['cache', 'register', 'message', 'project', 'dbForPlatform', 'authorization']); -Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) { +Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) { $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools - return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases): Database { + return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForPlatform; } @@ -127,7 +141,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf if (isset($databases[$dsn->getHost()])) { $database = $databases[$dsn->getHost()]; - + $database->setAuthorization($authorization); $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); if (\in_array($dsn->getHost(), $sharedTables)) { @@ -164,15 +178,17 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf ->setNamespace('_' . $project->getSequence()); } - $database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER); + $database + ->setAuthorization($authorization) + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER); return $database; }; -}, ['pools', 'dbForPlatform', 'cache']); +}, ['pools', 'dbForPlatform', 'cache', 'authorization']); -Server::setResource('getLogsDB', function (Group $pools, Cache $cache) { +Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { $database = null; - return function (?Document $project = null) use ($pools, $cache, $database) { + return function (?Document $project = null) use ($pools, $cache, $database, $authorization) { if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') { $database->setTenant((int)$project->getSequence()); return $database; @@ -182,6 +198,7 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache) { $database = new Database($adapter, $cache); $database + ->setAuthorization($authorization) ->setSharedTables(true) ->setNamespace('logsV1') ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER) @@ -194,7 +211,7 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache) { return $database; }; -}, ['pools', 'cache']); +}, ['pools', 'cache', 'authorization']); Server::setResource('abuseRetention', function () { return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day @@ -307,6 +324,10 @@ Server::setResource('queueForBuilds', function (Publisher $publisher) { return new Build($publisher); }, ['publisher']); +Server::setResource('queueForScreenshots', function (Publisher $publisher) { + return new Screenshot($publisher); +}, ['publisher']); + Server::setResource('queueForDeletes', function (Publisher $publisher) { return new Delete($publisher); }, ['publisher']); @@ -509,7 +530,8 @@ $worker ->inject('log') ->inject('pools') ->inject('project') - ->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project) use ($worker, $queueName) { + ->inject('authorization') + ->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($worker, $queueName) { $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); if ($logger) { @@ -525,7 +547,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); diff --git a/bin/worker-screenshots b/bin/worker-screenshots new file mode 100644 index 0000000000..0252556075 --- /dev/null +++ b/bin/worker-screenshots @@ -0,0 +1,3 @@ +#!/bin/sh + +exec php /usr/src/code/app/worker.php screenshots "$@" \ No newline at end of file diff --git a/composer.json b/composer.json index 131455d206..e4a268f546 100644 --- a/composer.json +++ b/composer.json @@ -45,14 +45,14 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.19.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "1.*.*", + "utopia-php/abuse": "1.*", "utopia-php/analytics": "0.10.*", - "utopia-php/audit": "2.0.2-rc3", + "utopia-php/audit": "2.*", "utopia-php/auth": "0.5.*", "utopia-php/cache": "0.13.*", "utopia-php/cli": "0.15.*", - "utopia-php/config": "1.*.*", - "utopia-php/database": "3.*.*", + "utopia-php/config": "1.*", + "utopia-php/database": "4.*", "utopia-php/detector": "0.2.*", "utopia-php/domains": "0.9.*", "utopia-php/emails": "0.6.*", @@ -64,7 +64,7 @@ "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", - "utopia-php/migration": "1.3.*", + "utopia-php/migration": "1.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.8.*", diff --git a/composer.lock b/composer.lock index c047bf2e03..8b68b2a1ba 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": "ff3172688b600aa3c560131c9d9c5588", + "content-hash": "ad3d3cc3b265daf8657cb43836fc9879", "packages": [ { "name": "adhocore/jwt", @@ -756,16 +756,16 @@ }, { "name": "google/protobuf", - "version": "v4.33.2", + "version": "v4.33.4", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318" + "reference": "22d28025cda0d223a2e48c2e16c5284ecc9f5402" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318", - "reference": "fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/22d28025cda0d223a2e48c2e16c5284ecc9f5402", + "reference": "22d28025cda0d223a2e48c2e16c5284ecc9f5402", "shasum": "" }, "require": { @@ -794,9 +794,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.2" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.4" }, - "time": "2025-12-05T22:12:22+00:00" + "time": "2026-01-12T17:58:43+00:00" }, { "name": "league/csv", @@ -3455,25 +3455,24 @@ }, { "name": "utopia-php/abuse", - "version": "1.2.0", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "3339d057c6bb1fa3e5ac5b2598923f6938425ec2" + "reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/3339d057c6bb1fa3e5ac5b2598923f6938425ec2", - "reference": "3339d057c6bb1fa3e5ac5b2598923f6938425ec2", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/611fa66a97e87c0dbbc133a717d970da7a5ca828", + "reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828", "shasum": "" }, "require": { - "appwrite/appwrite": "19.*.*", "ext-curl": "*", "ext-pdo": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "3.*.*" + "utopia-php/database": "*" }, "require-dev": { "laravel/pint": "1.*", @@ -3501,9 +3500,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/1.2.0" + "source": "https://github.com/utopia-php/abuse/tree/1.0.2" }, - "time": "2026-01-05T21:29:10+00:00" + "time": "2025-10-20T07:18:33+00:00" }, { "name": "utopia-php/analytics", @@ -3553,21 +3552,21 @@ }, { "name": "utopia-php/audit", - "version": "2.0.2-rc3", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "f60a298b516300f56a328403b334b7d62a96e7e7" + "reference": "27d66630f528473cb563bbcf362d7d9a711b384e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/f60a298b516300f56a328403b334b7d62a96e7e7", - "reference": "f60a298b516300f56a328403b334b7d62a96e7e7", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/27d66630f528473cb563bbcf362d7d9a711b384e", + "reference": "27d66630f528473cb563bbcf362d7d9a711b384e", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/database": "3.*", + "utopia-php/database": "4.*", "utopia-php/fetch": "0.5.*", "utopia-php/validators": "0.1.*" }, @@ -3596,9 +3595,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/2.0.2-rc3" + "source": "https://github.com/utopia-php/audit/tree/2.0.2" }, - "time": "2026-01-06T15:32:52+00:00" + "time": "2026-01-07T07:01:25+00:00" }, { "name": "utopia-php/auth", @@ -3899,16 +3898,16 @@ }, { "name": "utopia-php/database", - "version": "3.6.1", + "version": "4.4.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "c8c1b2f5770245dd4006e2680681e3efbe8b1fa7" + "reference": "783193d5cdc723b3784e8fb399068b17d4228d53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/c8c1b2f5770245dd4006e2680681e3efbe8b1fa7", - "reference": "c8c1b2f5770245dd4006e2680681e3efbe8b1fa7", + "url": "https://api.github.com/repos/utopia-php/database/zipball/783193d5cdc723b3784e8fb399068b17d4228d53", + "reference": "783193d5cdc723b3784e8fb399068b17d4228d53", "shasum": "" }, "require": { @@ -3951,9 +3950,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/3.6.1" + "source": "https://github.com/utopia-php/database/tree/4.4.0" }, - "time": "2025-12-16T09:55:41+00:00" + "time": "2026-01-08T04:54:39+00:00" }, { "name": "utopia-php/detector", @@ -4267,16 +4266,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.35", + "version": "0.33.36", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "82b139fb04f30045db51b0d322224f222da32313" + "reference": "fd835ed77e1cdf327067ce4e650cce86304e7098" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/82b139fb04f30045db51b0d322224f222da32313", - "reference": "82b139fb04f30045db51b0d322224f222da32313", + "url": "https://api.github.com/repos/utopia-php/http/zipball/fd835ed77e1cdf327067ce4e650cce86304e7098", + "reference": "fd835ed77e1cdf327067ce4e650cce86304e7098", "shasum": "" }, "require": { @@ -4309,9 +4308,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.35" + "source": "https://github.com/utopia-php/http/tree/0.33.36" }, - "time": "2025-12-12T08:33:52+00:00" + "time": "2026-01-12T07:32:29+00:00" }, { "name": "utopia-php/image", @@ -4516,16 +4515,16 @@ }, { "name": "utopia-php/migration", - "version": "1.3.13", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "c5e3f5e970e62e8f7db97b5b90baae2af800a715" + "reference": "4cb7a0e65a36058d153ef5643090414c6525e4a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/c5e3f5e970e62e8f7db97b5b90baae2af800a715", - "reference": "c5e3f5e970e62e8f7db97b5b90baae2af800a715", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/4cb7a0e65a36058d153ef5643090414c6525e4a2", + "reference": "4cb7a0e65a36058d153ef5643090414c6525e4a2", "shasum": "" }, "require": { @@ -4534,7 +4533,7 @@ "ext-openssl": "*", "php": ">=8.1", "utopia-php/console": "0.0.*", - "utopia-php/database": "3.*", + "utopia-php/database": "4.*", "utopia-php/dsn": "0.2.*", "utopia-php/storage": "0.18.*" }, @@ -4565,9 +4564,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.3.13" + "source": "https://github.com/utopia-php/migration/tree/1.4.2" }, - "time": "2026-01-07T14:48:05+00:00" + "time": "2026-01-08T04:46:18+00:00" }, { "name": "utopia-php/mongo", @@ -5014,22 +5013,22 @@ }, { "name": "utopia-php/swoole", - "version": "0.8.5", + "version": "0.8.6", "source": { "type": "git", "url": "https://github.com/utopia-php/swoole.git", - "reference": "e42b6b8e44c457a7b35d8a857d7af1d67d667c58" + "reference": "14b00277c35a258cb263706fd4e05c50368feb4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/swoole/zipball/e42b6b8e44c457a7b35d8a857d7af1d67d667c58", - "reference": "e42b6b8e44c457a7b35d8a857d7af1d67d667c58", + "url": "https://api.github.com/repos/utopia-php/swoole/zipball/14b00277c35a258cb263706fd4e05c50368feb4f", + "reference": "14b00277c35a258cb263706fd4e05c50368feb4f", "shasum": "" }, "require": { "ext-swoole": "*", "php": ">=8.0", - "utopia-php/framework": "0.33.35" + "utopia-php/framework": "0.33.36" }, "require-dev": { "laravel/pint": "1.2.*", @@ -5059,9 +5058,9 @@ ], "support": { "issues": "https://github.com/utopia-php/swoole/issues", - "source": "https://github.com/utopia-php/swoole/tree/0.8.5" + "source": "https://github.com/utopia-php/swoole/tree/0.8.6" }, - "time": "2025-12-15T14:03:23+00:00" + "time": "2026-01-12T07:57:35+00:00" }, { "name": "utopia-php/system", @@ -5439,16 +5438,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.8.9", + "version": "1.8.11", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "5fc210f7403f9ecfa068cd2a74210ec6e2a3cec1" + "reference": "936404bbcbf4cd692bac102f2912b6c97ac87215" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/5fc210f7403f9ecfa068cd2a74210ec6e2a3cec1", - "reference": "5fc210f7403f9ecfa068cd2a74210ec6e2a3cec1", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/936404bbcbf4cd692bac102f2912b6c97ac87215", + "reference": "936404bbcbf4cd692bac102f2912b6c97ac87215", "shasum": "" }, "require": { @@ -5484,9 +5483,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.8.9" + "source": "https://github.com/appwrite/sdk-generator/tree/1.8.11" }, - "time": "2026-01-02T12:09:51+00:00" + "time": "2026-01-12T08:41:56+00:00" }, { "name": "doctrine/annotations", @@ -8945,9 +8944,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "utopia-php/audit": 5 - }, + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -8971,5 +8968,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/docker-compose.yml b/docker-compose.yml index 14591db926..20c0ad8f79 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -466,14 +466,12 @@ services: - appwrite-functions:/storage/functions:rw - appwrite-sites:/storage/sites:rw - appwrite-builds:/storage/builds:rw - - appwrite-uploads:/storage/uploads:rw - ./app:/usr/src/code/app - ./src:/usr/src/code/src depends_on: - redis - mariadb environment: - - _APP_BROWSER_HOST - _APP_ENV - _APP_WORKER_PER_CORE - _APP_OPENSSL_KEY_V1 @@ -529,6 +527,65 @@ services: extra_hosts: - "host.docker.internal:host-gateway" + appwrite-worker-screenshots: + entrypoint: worker-screenshots + <<: *x-logging + container_name: appwrite-worker-screenshots + image: appwrite-dev + networks: + - appwrite + volumes: + - appwrite-uploads:/storage/uploads:rw + - ./app:/usr/src/code/app + - ./src:/usr/src/code/src + depends_on: + - redis + - mariadb + environment: + # Specific + - _APP_BROWSER_HOST + # Basic + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_LOGGING_CONFIG + # Database + - _APP_OPENSSL_KEY_V1 + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_DATABASE_SHARED_TABLES + # Storage + - _APP_STORAGE_DEVICE + - _APP_STORAGE_S3_ACCESS_KEY + - _APP_STORAGE_S3_SECRET + - _APP_STORAGE_S3_REGION + - _APP_STORAGE_S3_BUCKET + - _APP_STORAGE_S3_ENDPOINT + - _APP_STORAGE_DO_SPACES_ACCESS_KEY + - _APP_STORAGE_DO_SPACES_SECRET + - _APP_STORAGE_DO_SPACES_REGION + - _APP_STORAGE_DO_SPACES_BUCKET + - _APP_STORAGE_BACKBLAZE_ACCESS_KEY + - _APP_STORAGE_BACKBLAZE_SECRET + - _APP_STORAGE_BACKBLAZE_REGION + - _APP_STORAGE_BACKBLAZE_BUCKET + - _APP_STORAGE_LINODE_ACCESS_KEY + - _APP_STORAGE_LINODE_SECRET + - _APP_STORAGE_LINODE_REGION + - _APP_STORAGE_LINODE_BUCKET + - _APP_STORAGE_WASABI_ACCESS_KEY + - _APP_STORAGE_WASABI_SECRET + - _APP_STORAGE_WASABI_REGION + - _APP_STORAGE_WASABI_BUCKET + extra_hosts: + - "host.docker.internal:host-gateway" + appwrite-worker-certificates: entrypoint: worker-certificates <<: *x-logging diff --git a/src/Appwrite/Databases/TransactionState.php b/src/Appwrite/Databases/TransactionState.php index 23dc6fc2e9..8e098774e6 100644 --- a/src/Appwrite/Databases/TransactionState.php +++ b/src/Appwrite/Databases/TransactionState.php @@ -20,10 +20,12 @@ use Utopia\Database\Validator\Authorization; class TransactionState { private Database $dbForProject; - - public function __construct(Database $dbForProject) + private Authorization $authorization; + /** @var Authorization $authorization */ + public function __construct(Database $dbForProject, Authorization $authorization) { $this->dbForProject = $dbForProject; + $this->authorization = $authorization; } @@ -342,12 +344,12 @@ class TransactionState */ private function getTransactionState(string $transactionId): array { - $transaction = Authorization::skip(fn () => $this->dbForProject->getDocument('transactions', $transactionId)); + $transaction = $this->authorization->skip(fn () => $this->dbForProject->getDocument('transactions', $transactionId)); if ($transaction->isEmpty() || $transaction->getAttribute('status') !== 'pending') { return []; } - $operations = Authorization::skip(fn () => $this->dbForProject->find('transactionLogs', [ + $operations = $this->authorization->skip(fn () => $this->dbForProject->find('transactionLogs', [ Query::equal('transactionInternalId', [$transaction->getSequence()]), Query::orderAsc(), Query::limit(PHP_INT_MAX) diff --git a/src/Appwrite/Deletes/Targets.php b/src/Appwrite/Deletes/Targets.php index 794ab0b87a..5ba667d823 100644 --- a/src/Appwrite/Deletes/Targets.php +++ b/src/Appwrite/Deletes/Targets.php @@ -3,8 +3,10 @@ namespace Appwrite\Deletes; use Appwrite\Extend\Exception; +use Utopia\Console; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Query; class Targets @@ -42,12 +44,17 @@ class Targets MESSAGE_TYPE_PUSH => 'pushTotal', default => throw new Exception('Invalid target provider type'), }; - $database->decreaseDocumentAttribute( - 'topics', - $topicId, - $totalAttribute, - min: 0 - ); + + try { + $database->decreaseDocumentAttribute( + 'topics', + $topicId, + $totalAttribute, + min: 0 + ); + } catch (LimitException $e) { + Console::error("Delete subscribers decreaseDocumentAttribute (topicId={$topicId}): {$e->getMessage()}"); + } } } ); diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index c7bb22f715..9805ab7830 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -39,6 +39,9 @@ class Event public const BUILDS_QUEUE_NAME = 'v1-builds'; public const BUILDS_CLASS_NAME = 'BuildsV1'; + public const SCREENSHOTS_QUEUE_NAME = 'v1-screenshots'; + public const SCREENSHOTS_CLASS_NAME = 'ScreenshotsV1'; + public const MESSAGING_QUEUE_NAME = 'v1-messaging'; public const MESSAGING_CLASS_NAME = 'MessagingV1'; diff --git a/src/Appwrite/Event/Screenshot.php b/src/Appwrite/Event/Screenshot.php new file mode 100644 index 0000000000..acacf2b872 --- /dev/null +++ b/src/Appwrite/Event/Screenshot.php @@ -0,0 +1,50 @@ +setQueue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME)) + ->setClass(System::getEnv('_APP_SCREENSHOTS_CLASS_NAME', Event::SCREENSHOTS_CLASS_NAME)); + } + + public function setDeploymentId(string $deploymentId): self + { + $this->deploymentId = $deploymentId; + + return $this; + } + + protected function preparePayload(): array + { + $platform = $this->platform; + if (empty($platform)) { + $platform = Config::getParam('platform', []); + } + + return [ + 'project' => $this->project, + 'deploymentId' => $this->deploymentId, + 'platform' => $platform, + ]; + } + + public function reset(): self + { + $this->deploymentId = ''; + parent::reset(); + + return $this; + } +} diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index bc37924db6..ea51225ba6 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -100,8 +100,6 @@ abstract class Migration public function __construct() { - Authorization::disable(); - Authorization::setDefaultStatus(false); $this->collections = Config::getParam('collections', []); @@ -129,6 +127,7 @@ abstract class Migration Document $project, Database $dbForProject, Database $dbForPlatform, + Authorization $authorization, ?callable $getProjectDB = null ): self { $this->project = $project; @@ -136,6 +135,9 @@ abstract class Migration $this->dbForPlatform = $dbForPlatform; $this->getProjectDB = $getProjectDB; + $authorization->disable(); + $authorization->setDefaultStatus(false); + return $this; } diff --git a/src/Appwrite/Platform/Appwrite.php b/src/Appwrite/Platform/Appwrite.php index a34c79308a..2b929765f2 100644 --- a/src/Appwrite/Platform/Appwrite.php +++ b/src/Appwrite/Platform/Appwrite.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform; use Appwrite\Platform\Modules\Account; +use Appwrite\Platform\Modules\Avatars; use Appwrite\Platform\Modules\Console; use Appwrite\Platform\Modules\Core; use Appwrite\Platform\Modules\Databases; @@ -20,6 +21,7 @@ class Appwrite extends Platform { parent::__construct(new Core()); $this->addModule(new Account\Module()); + $this->addModule(new Avatars\Module()); $this->addModule(new Databases\Module()); $this->addModule(new Projects\Module()); $this->addModule(new Functions\Module()); diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Action.php b/src/Appwrite/Platform/Modules/Avatars/Http/Action.php new file mode 100644 index 0000000000..bf7d01764f --- /dev/null +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Action.php @@ -0,0 +1,158 @@ +crop((int) $width, (int) $height); + $output = (empty($output)) ? $type : $output; + $data = $image->output($output, $quality); + $response + ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days + ->setContentType('image/png') + ->file($data); + unset($image); + } + + protected function getUserGitHub(string $userId, Document $project, Database $dbForProject, Database $dbForPlatform, ?Logger $logger, Authorization $authorization): array + { + try { + $user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId)); + + $sessions = $user->getAttribute('sessions', []); + + $gitHubSession = null; + foreach ($sessions as $session) { + if ($session->getAttribute('provider', '') === 'github') { + $gitHubSession = $session; + break; + } + } + + if (empty($gitHubSession)) { + throw new Exception(Exception::USER_SESSION_NOT_FOUND, 'GitHub session not found.'); + } + + $provider = $gitHubSession->getAttribute('provider', ''); + $accessToken = $gitHubSession->getAttribute('providerAccessToken'); + $accessTokenExpiry = $gitHubSession->getAttribute('providerAccessTokenExpiry'); + $refreshToken = $gitHubSession->getAttribute('providerRefreshToken'); + + $appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? ''; + $appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}'; + + $oAuthProviders = Config::getParam('oAuthProviders'); + $className = $oAuthProviders[$provider]['class']; + if (!\class_exists($className)) { + throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED); + } + + $oauth2 = new $className($appId, $appSecret, '', [], []); + + $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); + if ($isExpired) { + try { + $oauth2->refreshTokens($refreshToken); + + $accessToken = $oauth2->getAccessToken(''); + $refreshToken = $oauth2->getRefreshToken(''); + + $verificationId = $oauth2->getUserID($accessToken); + + if (empty($verificationId)) { + throw new \Exception("Locked tokens."); // Race codition, handeled in catch + } + + $gitHubSession + ->setAttribute('providerAccessToken', $accessToken) + ->setAttribute('providerRefreshToken', $refreshToken) + ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry(''))); + + $authorization->skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession)); + + $dbForProject->purgeCachedDocument('users', $user->getId()); + } catch (Throwable $err) { + $index = 0; + do { + $previousAccessToken = $gitHubSession->getAttribute('providerAccessToken'); + + $user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId)); + $sessions = $user->getAttribute('sessions', []); + + $gitHubSession = new Document(); + foreach ($sessions as $session) { + if ($session->getAttribute('provider', '') === 'github') { + $gitHubSession = $session; + break; + } + } + + $accessToken = $gitHubSession->getAttribute('providerAccessToken'); + + if ($accessToken !== $previousAccessToken) { + break; + } + + $index++; + \usleep(500000); + } while ($index < 10); + } + } + + $oauth2 = new $className($appId, $appSecret, '', [], []); + $githubUser = $oauth2->getUserSlug($accessToken); + $githubId = $oauth2->getUserID($accessToken); + + return [ + 'name' => $githubUser, + 'id' => $githubId + ]; + } catch (Exception $error) { + return []; + } + } +} diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Browsers/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Browsers/Get.php new file mode 100644 index 0000000000..637ea647ef --- /dev/null +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Browsers/Get.php @@ -0,0 +1,64 @@ +setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/avatars/browsers/:code') + ->desc('Get browser icon') + ->groups(['api', 'avatars']) + ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resource', 'avatar/browser') + ->label('sdk', new Method( + namespace: 'avatars', + group: null, + name: 'getBrowser', + description: '/docs/references/avatars/get-browser.md', + auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], + type: MethodType::LOCATION, + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_NONE, + ) + ], + contentType: ContentType::IMAGE_PNG + )) + ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-browsers'))), 'Browser Code.') + ->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) + ->inject('response') + ->callback($this->action(...)); + } + + public function action(string $code, int $width, int $height, int $quality, Response $response) + { + $this->avatar('browsers', $code, $width, $height, $quality, $response); + } +} diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Back/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Back/Get.php new file mode 100644 index 0000000000..a6a013ef21 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Back/Get.php @@ -0,0 +1,116 @@ +setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/cards/cloud-back') + ->desc('Get back Of Cloud Card') + ->groups(['api', 'avatars']) + ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resourceType', 'cards/cloud-back') + ->label('cache.resource', 'card-back/{request.userId}') + ->label('docs', false) + ->label('origin', '*') + ->param('userId', '', new UID(), 'User ID.', true) + ->param('mock', '', new WhiteList(['golden', 'normal', 'platinum']), 'Mocking behaviour.', true) + ->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true) + ->param('height', 0, new Range(0, 320), 'Resize image height, Pass an integer between 0 to 320.', true) + ->inject('user') + ->inject('project') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->inject('response') + ->inject('heroes') + ->inject('contributors') + ->inject('employees') + ->inject('logger') + ->inject('authorization') + ->callback($this->action(...)); + } + + public function action(string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) + { + $user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId)); + + if ($user->isEmpty() && empty($mock)) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + if (!$mock) { + $userId = $user->getId(); + $email = $user->getAttribute('email', ''); + + $gitHub = $this->getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger, $authorization); + $githubId = $gitHub['id'] ?? ''; + + $isHero = \array_key_exists($email, $heroes); + $isContributor = \in_array($githubId, $contributors); + $isEmployee = \array_key_exists($email, $employees); + + $isGolden = $isEmployee || $isHero || $isContributor; + $isPlatinum = $user->getSequence() % 100 === 0; + } else { + $userId = '63e0bcf3c3eb803ba530'; + + $isGolden = $mock === 'golden'; + $isPlatinum = $mock === 'platinum'; + } + + $userId = 'UID ' . $userId; + + $isPlatinum = $isGolden ? false : $isPlatinum; + + $imagePath = $isGolden ? 'back-golden.png' : ($isPlatinum ? 'back-platinum.png' : 'back.png'); + + $baseImage = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/' . $imagePath); + + setlocale(LC_ALL, "en_US.utf8"); + // $userId = \iconv("utf-8", "ascii//TRANSLIT", $userId); + + $text = new ImagickDraw(); + $text->setTextAlignment(Imagick::ALIGN_CENTER); + $text->setFont($this->getAppRoot() . '/public/fonts/SourceCodePro-Regular.ttf'); + $text->setFillColor(new ImagickPixel($isGolden ? '#664A1E' : ($isPlatinum ? '#555555' : '#E8E9F0'))); + $text->setFontSize(28); + $text->setFontWeight(400); + $baseImage->annotateImage($text, 512, 596, 0, $userId); + + if (!empty($width) || !empty($height)) { + $baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1); + } + + $response + ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days + ->setContentType('image/png') + ->file($baseImage->getImageBlob()); + } +} diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Front/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Front/Get.php new file mode 100644 index 0000000000..f8e7a35b05 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Front/Get.php @@ -0,0 +1,245 @@ +setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/cards/cloud') + ->desc('Get front Of Cloud Card') + ->groups(['api', 'avatars']) + ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resourceType', 'cards/cloud') + ->label('cache.resource', 'card/{request.userId}') + ->label('docs', false) + ->label('origin', '*') + ->param('userId', '', new UID(), 'User ID.', true) + ->param('mock', '', new WhiteList(['employee', 'employee-2digit', 'hero', 'contributor', 'normal', 'platinum', 'normal-no-github', 'normal-long']), 'Mocking behaviour.', true) + ->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true) + ->param('height', 0, new Range(0, 320), 'Resize image height, Pass an integer between 0 to 320.', true) + ->inject('user') + ->inject('project') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->inject('response') + ->inject('heroes') + ->inject('contributors') + ->inject('employees') + ->inject('logger') + ->inject('authorization') + ->callback($this->action(...)); + } + + public function action(string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) + { + $user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId)); + + if ($user->isEmpty() && empty($mock)) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + if (!$mock) { + $name = $user->getAttribute('name', 'Anonymous'); + $email = $user->getAttribute('email', ''); + $createdAt = new \DateTime($user->getCreatedAt()); + + $gitHub = $this->getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger, $authorization); + $githubName = $gitHub['name'] ?? ''; + $githubId = $gitHub['id'] ?? ''; + + $isHero = \array_key_exists($email, $heroes); + $isContributor = \in_array($githubId, $contributors); + $isEmployee = \array_key_exists($email, $employees); + $employeeNumber = $isEmployee ? $employees[$email]['spot'] : ''; + + if ($isHero) { + $createdAt = new \DateTime($heroes[$email]['memberSince'] ?? ''); + } elseif ($isEmployee) { + $createdAt = new \DateTime($employees[$email]['memberSince'] ?? ''); + } + + if (!$isEmployee && !empty($githubName)) { + $employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees)); + if (!empty($employeeGitHub)) { + $isEmployee = true; + $employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : ''; + $createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? ''); + } + } + + $isPlatinum = $user->getSequence() % 100 === 0; + } else { + $name = $mock === 'normal-long' ? 'Sir First Walter O\'Brian Junior' : 'Walter O\'Brian'; + $createdAt = new \DateTime('now'); + $githubName = $mock === 'normal-no-github' ? '' : ($mock === 'normal-long' ? 'sir-first-walterobrian-junior' : 'walterobrian'); + $isHero = $mock === 'hero'; + $isContributor = $mock === 'contributor'; + $isEmployee = \str_starts_with($mock, 'employee'); + $employeeNumber = match ($mock) { + 'employee' => '1', + 'employee-2digit' => '18', + default => '' + }; + + $isPlatinum = $mock === 'platinum'; + } + + if ($isEmployee) { + $isContributor = false; + $isHero = false; + } + + if ($isHero) { + $isContributor = false; + $isEmployee = false; + } + + if ($isContributor) { + $isHero = false; + $isEmployee = false; + } + + $isGolden = $isEmployee || $isHero || $isContributor; + $isPlatinum = $isGolden ? false : $isPlatinum; + $memberSince = \strtoupper('Member since ' . $createdAt->format('M') . ' ' . $createdAt->format('d') . ', ' . $createdAt->format('o')); + + $imagePath = $isGolden ? 'front-golden.png' : ($isPlatinum ? 'front-platinum.png' : 'front.png'); + + $baseImage = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/' . $imagePath); + + if ($isEmployee) { + $image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/employee.png'); + $image->setGravity(Imagick::GRAVITY_CENTER); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 35); + + $text = new ImagickDraw(); + $text->setTextAlignment(Imagick::ALIGN_CENTER); + $text->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf'); + $text->setFillColor(new ImagickPixel('#FFFADF')); + $text->setFontSize(\strlen($employeeNumber) <= 2 ? 54 : 48); + $text->setFontWeight(700); + $metricsText = $baseImage->queryFontMetrics($text, $employeeNumber); + + $hashtag = new ImagickDraw(); + $hashtag->setTextAlignment(Imagick::ALIGN_CENTER); + $hashtag->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf'); + $hashtag->setFillColor(new ImagickPixel('#FFFADF')); + $hashtag->setFontSize(28); + $hashtag->setFontWeight(700); + $metricsHashtag = $baseImage->queryFontMetrics($hashtag, '#'); + + $startX = 898; + $totalWidth = $metricsHashtag['textWidth'] + 12 + $metricsText['textWidth']; + + $hashtagX = ($metricsHashtag['textWidth'] / 2); + $textX = $hashtagX + 12 + ($metricsText['textWidth'] / 2); + + $hashtagX -= $totalWidth / 2; + $textX -= $totalWidth / 2; + + $hashtagX += $startX; + $textX += $startX; + + $baseImage->annotateImage($hashtag, $hashtagX, 150, 0, '#'); + $baseImage->annotateImage($text, $textX, 150, 0, $employeeNumber); + } + + if ($isContributor) { + $image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/contributor.png'); + $image->setGravity(Imagick::GRAVITY_CENTER); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 34); + } + + if ($isHero) { + $image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/hero.png'); + $image->setGravity(Imagick::GRAVITY_CENTER); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 34); + } + + setlocale(LC_ALL, "en_US.utf8"); + // $name = \iconv("utf-8", "ascii//TRANSLIT", $name); + // $memberSince = \iconv("utf-8", "ascii//TRANSLIT", $memberSince); + // $githubName = \iconv("utf-8", "ascii//TRANSLIT", $githubName); + + $text = new ImagickDraw(); + $text->setTextAlignment(Imagick::ALIGN_CENTER); + $text->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf'); + $text->setFillColor(new ImagickPixel('#FFFFFF')); + + if (\strlen($name) > 32) { + $name = \substr($name, 0, 32); + } + + if (\strlen($name) <= 23) { + $text->setFontSize(80); + $scalingDown = false; + } else { + $text->setFontSize(54); + $scalingDown = true; + } + $text->setFontWeight(700); + $baseImage->annotateImage($text, 512, 477, 0, $name); + + $text = new ImagickDraw(); + $text->setTextAlignment(Imagick::ALIGN_CENTER); + $text->setFont($this->getAppRoot() . '/public/fonts/Inter-SemiBold.ttf'); + $text->setFillColor(new ImagickPixel($isGolden || $isPlatinum ? '#FFFFFF' : '#FFB9CC')); + $text->setFontSize(27); + $text->setFontWeight(600); + $text->setTextKerning(1.08); + $baseImage->annotateImage($text, 512, 541, 0, \strtoupper($memberSince)); + + if (!empty($githubName)) { + $text = new ImagickDraw(); + $text->setTextAlignment(Imagick::ALIGN_CENTER); + $text->setFont($this->getAppRoot() . '/public/fonts/Inter-Regular.ttf'); + $text->setFillColor(new ImagickPixel('#FFFFFF')); + $text->setFontSize($scalingDown ? 28 : 32); + $text->setFontWeight(400); + $metrics = $baseImage->queryFontMetrics($text, $githubName); + + $baseImage->annotateImage($text, 512 + 20 + 4, 373 + ($scalingDown ? 2 : 0), 0, $githubName); + + $image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/github.png'); + $image->setGravity(Imagick::GRAVITY_CENTER); + $precisionFix = 5; + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 512 - ($metrics['textWidth'] / 2) - 20 - 4, 373 - ($metrics['textHeight'] - $precisionFix)); + } + + if (!empty($width) || !empty($height)) { + $baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1); + } + + $response + ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days + ->setContentType('image/png') + ->file($baseImage->getImageBlob()); + } +} diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/OG/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/OG/Get.php new file mode 100644 index 0000000000..37776a3466 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/OG/Get.php @@ -0,0 +1,428 @@ +setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/cards/cloud-og') + ->desc('Get OG image From Cloud Card') + ->groups(['api', 'avatars']) + ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resourceType', 'cards/cloud-og') + ->label('cache.resource', 'card-og/{request.userId}') + ->label('docs', false) + ->label('origin', '*') + ->param('userId', '', new UID(), 'User ID.', true) + ->param('mock', '', new WhiteList(['employee', 'employee-2digit', 'hero', 'contributor', 'normal', 'platinum', 'normal-no-github', 'normal-long', 'normal-long-right', 'normal-long-middle', 'normal-bg2', 'normal-bg3', 'normal-right', 'normal-middle', 'platinum-right', 'platinum-middle', 'hero-middle', 'hero-right', 'contributor-right', 'employee-right', 'contributor-middle', 'employee-middle', 'employee-2digit-middle', 'employee-2digit-right']), 'Mocking behaviour.', true) + ->param('width', 0, new Range(0, 1024), 'Resize image card width, Pass an integer between 0 to 1024.', true) + ->param('height', 0, new Range(0, 1024), 'Resize image card height, Pass an integer between 0 to 1024.', true) + ->inject('user') + ->inject('project') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->inject('response') + ->inject('heroes') + ->inject('contributors') + ->inject('employees') + ->inject('logger') + ->inject('authorization') + ->callback($this->action(...)); + } + + public function action(string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) + { + $user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId)); + + if ($user->isEmpty() && empty($mock)) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + if (!$mock) { + $sequence = $user->getSequence(); + $bgVariation = $sequence % 3 === 0 ? '1' : ($sequence % 3 === 1 ? '2' : '3'); + $cardVariation = $sequence % 3 === 0 ? '1' : ($sequence % 3 === 1 ? '2' : '3'); + + $name = $user->getAttribute('name', 'Anonymous'); + $email = $user->getAttribute('email', ''); + $createdAt = new \DateTime($user->getCreatedAt()); + + $gitHub = $this->getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger, $authorization); + $githubName = $gitHub['name'] ?? ''; + $githubId = $gitHub['id'] ?? ''; + + $isHero = \array_key_exists($email, $heroes); + $isContributor = \in_array($githubId, $contributors); + $isEmployee = \array_key_exists($email, $employees); + $employeeNumber = $isEmployee ? $employees[$email]['spot'] : ''; + + if ($isHero) { + $createdAt = new \DateTime($heroes[$email]['memberSince'] ?? ''); + } elseif ($isEmployee) { + $createdAt = new \DateTime($employees[$email]['memberSince'] ?? ''); + } + + if (!$isEmployee && !empty($githubName)) { + $employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees)); + if (!empty($employeeGitHub)) { + $isEmployee = true; + $employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : ''; + $createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? ''); + } + } + + $isPlatinum = $user->getSequence() % 100 === 0; + } else { + $bgVariation = \str_ends_with($mock, '-bg2') ? '2' : (\str_ends_with($mock, '-bg3') ? '3' : '1'); + $cardVariation = \str_ends_with($mock, '-right') ? '2' : (\str_ends_with($mock, '-middle') ? '3' : '1'); + $name = \str_starts_with($mock, 'normal-long') ? 'Sir First Walter O\'Brian Junior' : 'Walter O\'Brian'; + $createdAt = new \DateTime('now'); + $githubName = $mock === 'normal-no-github' ? '' : (\str_starts_with($mock, 'normal-long') ? 'sir-first-walterobrian-junior' : 'walterobrian'); + $isHero = \str_starts_with($mock, 'hero'); + $isContributor = \str_starts_with($mock, 'contributor'); + $isEmployee = \str_starts_with($mock, 'employee'); + $employeeNumber = match ($mock) { + 'employee' => '1', + 'employee-right' => '1', + 'employee-middle' => '1', + 'employee-2digit' => '18', + 'employee-2digit-right' => '18', + 'employee-2digit-middle' => '18', + default => '' + }; + + $isPlatinum = \str_starts_with($mock, 'platinum'); + } + + if ($isEmployee) { + $isContributor = false; + $isHero = false; + } + + if ($isHero) { + $isContributor = false; + $isEmployee = false; + } + + if ($isContributor) { + $isHero = false; + $isEmployee = false; + } + + $isGolden = $isEmployee || $isHero || $isContributor; + $isPlatinum = $isGolden ? false : $isPlatinum; + $memberSince = \strtoupper('Member since ' . $createdAt->format('M') . ' ' . $createdAt->format('d') . ', ' . $createdAt->format('o')); + + $baseImage = new Imagick($this->getAppRoot() . "/public/images/cards/cloud/og-background{$bgVariation}.png"); + + $cardType = $isGolden ? '-golden' : ($isPlatinum ? '-platinum' : ''); + + $image = new Imagick($this->getAppRoot() . "/public/images/cards/cloud/og-card{$cardType}{$cardVariation}.png"); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 1008 / 2 - $image->getImageWidth() / 2, 1008 / 2 - $image->getImageHeight() / 2); + + $imageLogo = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/og-background-logo.png'); + $imageShadow = new Imagick($this->getAppRoot() . "/public/images/cards/cloud/og-shadow{$cardType}.png"); + if ($cardVariation === '1') { + $baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 32, 1008 - $imageLogo->getImageHeight() - 32); + $baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -450, 700); + } elseif ($cardVariation === '2') { + $baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 1008 - $imageLogo->getImageWidth() - 32, 1008 - $imageLogo->getImageHeight() - 32); + $baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -20, 710); + } else { + $baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 1008 - $imageLogo->getImageWidth() - 32, 1008 - $imageLogo->getImageHeight() - 32); + $baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -135, 710); + } + + if ($isEmployee) { + $file = $cardVariation === '3' ? 'employee-skew.png' : 'employee.png'; + $image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/' . $file); + $image->setGravity(Imagick::GRAVITY_CENTER); + + $hashtag = new ImagickDraw(); + $hashtag->setTextAlignment(Imagick::ALIGN_LEFT); + $hashtag->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf'); + $hashtag->setFillColor(new ImagickPixel('#FFFADF')); + $hashtag->setFontSize(20); + $hashtag->setFontWeight(700); + + $text = new ImagickDraw(); + $text->setTextAlignment(Imagick::ALIGN_LEFT); + $text->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf'); + $text->setFillColor(new ImagickPixel('#FFFADF')); + $text->setFontSize(\strlen($employeeNumber) <= 1 ? 36 : 28); + $text->setFontWeight(700); + + if ($cardVariation === '3') { + $hashtag->setFontSize(16); + $text->setFontSize(\strlen($employeeNumber) <= 1 ? 30 : 26); + + $hashtag->skewY(20); + $hashtag->skewX(20); + $text->skewY(20); + $text->skewX(20); + } + + $metricsHashtag = $baseImage->queryFontMetrics($hashtag, '#'); + $metricsText = $baseImage->queryFontMetrics($text, $employeeNumber); + + $group = new Imagick(); + $groupWidth = $metricsHashtag['textWidth'] + 6 + $metricsText['textWidth']; + + if ($cardVariation === '1') { + $group->newImage($groupWidth, $metricsText['textHeight'], '#00000000'); + $group->annotateImage($hashtag, 0, $metricsText['textHeight'], 0, '#'); + $group->annotateImage($text, $metricsHashtag['textWidth'] + 6, $metricsText['textHeight'], 0, $employeeNumber); + + $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); + $image->rotateImage(new ImagickPixel('#00000000'), -20); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203); + + $group->rotateImage(new ImagickPixel('#00000000'), -22); + + if (\strlen($employeeNumber) <= 1) { + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 660, 245); + } else { + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 655, 247); + } + } elseif ($cardVariation === '2') { + $group->newImage($groupWidth, $metricsText['textHeight'], '#00000000'); + $group->annotateImage($hashtag, 0, $metricsText['textHeight'], 0, '#'); + $group->annotateImage($text, $metricsHashtag['textWidth'] + 6, $metricsText['textHeight'], 0, $employeeNumber); + + $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); + $image->rotateImage(new ImagickPixel('#00000000'), 30); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425); + + $group->rotateImage(new ImagickPixel('#00000000'), 32); + + if (\strlen($employeeNumber) <= 1) { + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 775, 465); + } else { + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 767, 470); + } + } else { + $group->newImage(300, 300, '#00000000'); + + $hashtag->annotation(0, $metricsText['textHeight'], '#'); + $text->annotation($metricsHashtag['textWidth'] + 2, $metricsText['textHeight'], $employeeNumber); + + $group->drawImage($hashtag); + $group->drawImage($text); + + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293); + + if (\strlen($employeeNumber) <= 1) { + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 670, 317); + } else { + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 663, 322); + } + } + } + + if ($isContributor) { + $file = $cardVariation === '3' ? 'contributor-skew.png' : 'contributor.png'; + $image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/' . $file); + $image->setGravity(Imagick::GRAVITY_CENTER); + + if ($cardVariation === '1') { + $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); + $image->rotateImage(new ImagickPixel('#00000000'), -20); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203); + } elseif ($cardVariation === '2') { + $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); + $image->rotateImage(new ImagickPixel('#00000000'), 30); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425); + } else { + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293); + } + } + + if ($isHero) { + $file = $cardVariation === '3' ? 'hero-skew.png' : 'hero.png'; + $image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/' . $file); + $image->setGravity(Imagick::GRAVITY_CENTER); + + if ($cardVariation === '1') { + $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); + $image->rotateImage(new ImagickPixel('#00000000'), -20); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203); + } elseif ($cardVariation === '2') { + $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); + $image->rotateImage(new ImagickPixel('#00000000'), 30); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425); + } else { + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293); + } + } + + setlocale(LC_ALL, "en_US.utf8"); + // $name = \iconv("utf-8", "ascii//TRANSLIT", $name); + // $memberSince = \iconv("utf-8", "ascii//TRANSLIT", $memberSince); + // $githubName = \iconv("utf-8", "ascii//TRANSLIT", $githubName); + + $textName = new ImagickDraw(); + $textName->setTextAlignment(Imagick::ALIGN_CENTER); + $textName->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf'); + $textName->setFillColor(new ImagickPixel('#FFFFFF')); + + if (\strlen($name) > 32) { + $name = \substr($name, 0, 32); + } + + if ($cardVariation === '1') { + if (\strlen($name) <= 23) { + $scalingDown = false; + $textName->setFontSize(54); + } else { + $scalingDown = true; + $textName->setFontSize(36); + } + } elseif ($cardVariation === '2') { + if (\strlen($name) <= 23) { + $scalingDown = false; + $textName->setFontSize(50); + } else { + $scalingDown = true; + $textName->setFontSize(34); + } + } else { + if (\strlen($name) <= 23) { + $scalingDown = false; + $textName->setFontSize(44); + } else { + $scalingDown = true; + $textName->setFontSize(32); + } + } + + $textName->setFontWeight(700); + + $textMember = new ImagickDraw(); + $textMember->setTextAlignment(Imagick::ALIGN_CENTER); + $textMember->setFont($this->getAppRoot() . '/public/fonts/Inter-Medium.ttf'); + $textMember->setFillColor(new ImagickPixel($isGolden || $isPlatinum ? '#FFFFFF' : '#FFB9CC')); + $textMember->setFontWeight(500); + $textMember->setTextKerning(1.12); + + if ($cardVariation === '1') { + $textMember->setFontSize(21); + + $baseImage->annotateImage($textName, 550, 600, -22, $name); + $baseImage->annotateImage($textMember, 585, 635, -22, $memberSince); + } elseif ($cardVariation === '2') { + $textMember->setFontSize(20); + + $baseImage->annotateImage($textName, 435, 590, 31.37, $name); + $baseImage->annotateImage($textMember, 412, 628, 31.37, $memberSince); + } else { + $textMember->setFontSize(16); + + $textName->skewY(20); + $textName->skewX(20); + $textName->annotation(320, 700, $name); + + $textMember->skewY(20); + $textMember->skewX(20); + $textMember->annotation(330, 735, $memberSince); + + $baseImage->drawImage($textName); + $baseImage->drawImage($textMember); + } + + if (!empty($githubName)) { + $text = new ImagickDraw(); + $text->setTextAlignment(Imagick::ALIGN_LEFT); + $text->setFont($this->getAppRoot() . '/public/fonts/Inter-Regular.ttf'); + $text->setFillColor(new ImagickPixel('#FFFFFF')); + $text->setFontSize($scalingDown ? 16 : 20); + $text->setFontWeight(400); + + if ($cardVariation === '1') { + $metrics = $baseImage->queryFontMetrics($text, $githubName); + + $group = new Imagick(); + $groupWidth = $metrics['textWidth'] + 32 + 4; + $group->newImage($groupWidth, $metrics['textHeight'] + 10, '#00000000'); + $image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/github.png'); + $image->setGravity(Imagick::GRAVITY_CENTER); + $image->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1); + $precisionFix = -1; + + $group->compositeImage($image, Imagick::COMPOSITE_OVER, 0, 0); + $group->annotateImage($text, 32 + 4, $metrics['textHeight'] - $precisionFix, 0, $githubName); + + $group->rotateImage(new ImagickPixel('#00000000'), -22); + $x = 510 - $group->getImageWidth() / 2; + $y = 530 - $group->getImageHeight() / 2; + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, $x, $y); + } elseif ($cardVariation === '2') { + $metrics = $baseImage->queryFontMetrics($text, $githubName); + + $group = new Imagick(); + $groupWidth = $metrics['textWidth'] + 32 + 4; + $group->newImage($groupWidth, $metrics['textHeight'] + 10, '#00000000'); + $image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/github.png'); + $image->setGravity(Imagick::GRAVITY_CENTER); + $image->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1); + $precisionFix = -1; + + $group->compositeImage($image, Imagick::COMPOSITE_OVER, 0, 0); + $group->annotateImage($text, 32 + 4, $metrics['textHeight'] - $precisionFix, 0, $githubName); + + $group->rotateImage(new ImagickPixel('#00000000'), 31.11); + $x = 485 - $group->getImageWidth() / 2; + $y = 530 - $group->getImageHeight() / 2; + $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, $x, $y); + } else { + $text->skewY(20); + $text->skewX(20); + $text->setTextAlignment(Imagick::ALIGN_CENTER); + + $text->annotation(320 + 15 + 2, 640, $githubName); + $metrics = $baseImage->queryFontMetrics($text, $githubName); + + $image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/github-skew.png'); + $image->setGravity(Imagick::GRAVITY_CENTER); + $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 512 - ($metrics['textWidth'] / 2), 518 + \strlen($githubName) * 1.3); + + $baseImage->drawImage($text); + } + } + + if (!empty($width) || !empty($height)) { + $baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1); + } + + $response + ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days + ->setContentType('image/png') + ->file($baseImage->getImageBlob()); + } +} diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/CreditCards/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/CreditCards/Get.php new file mode 100644 index 0000000000..87357f14c7 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Avatars/Http/CreditCards/Get.php @@ -0,0 +1,64 @@ +setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/avatars/credit-cards/:code') + ->desc('Get credit card icon') + ->groups(['api', 'avatars']) + ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resource', 'avatar/credit-card') + ->label('sdk', new Method( + namespace: 'avatars', + group: null, + name: 'getCreditCard', + description: '/docs/references/avatars/get-credit-card.md', + auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], + type: MethodType::LOCATION, + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_NONE, + ) + ], + contentType: ContentType::IMAGE_PNG + )) + ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-credit-cards'))), 'Credit Card Code. Possible values: ' . \implode(', ', \array_keys(Config::getParam('avatar-credit-cards'))) . '.') + ->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) + ->inject('response') + ->callback($this->action(...)); + } + + public function action(string $code, int $width, int $height, int $quality, Response $response) + { + $this->avatar('credit-cards', $code, $width, $height, $quality, $response); + } +} diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php new file mode 100644 index 0000000000..0a4d652d0e --- /dev/null +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php @@ -0,0 +1,216 @@ +setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/avatars/favicon') + ->desc('Get favicon') + ->groups(['api', 'avatars']) + ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resource', 'avatar/favicon') + ->label('sdk', new Method( + namespace: 'avatars', + group: null, + name: 'getFavicon', + description: '/docs/references/avatars/get-favicon.md', + auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], + type: MethodType::LOCATION, + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_NONE, + ) + ], + contentType: ContentType::IMAGE + )) + ->param('url', '', new URL(['http', 'https']), 'Website URL which you want to fetch the favicon from.') + ->inject('response') + ->callback($this->action(...)); + } + + public function action(string $url, Response $response) + { + $width = 56; + $height = 56; + $quality = 80; + $output = 'png'; + $type = 'png'; + + if (!\extension_loaded('imagick')) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); + } + + $domain = new Domain(\parse_url($url, PHP_URL_HOST)); + + if (!$domain->isKnown()) { + throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); + } + + $client = new Client(); + try { + $res = $client + ->setAllowRedirects(true) + ->setMaxRedirects(5) + ->setUserAgent(\sprintf( + APP_USERAGENT, + System::getEnv('_APP_VERSION', 'UNKNOWN'), + System::getEnv('_APP_EMAIL_SECURITY', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)) + )) + ->fetch($url); + } catch (\Throwable) { + throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); + } + + $doc = new DOMDocument(); + $doc->strictErrorChecking = false; + @$doc->loadHTML($res->getBody()); + + $links = $doc->getElementsByTagName('link') ?? []; + $outputHref = ''; + $outputExt = ''; + $space = 0; + + foreach ($links as $link) { /* @var $link DOMElement */ + $href = $link->getAttribute('href'); + $rel = $link->getAttribute('rel'); + $sizes = $link->getAttribute('sizes'); + $absolute = URLParse::unparse(\array_merge(\parse_url($url), \parse_url($href))); + + switch (\strtolower($rel)) { + case 'icon': + case 'shortcut icon': + //case 'apple-touch-icon': + $ext = \pathinfo(\parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION); + + switch ($ext) { + case 'svg': + // SVG icons are prioritized by assigning the maximum possible value. + $space = PHP_INT_MAX; + $outputHref = $absolute; + $outputExt = $ext; + break; + case 'ico': + case 'png': + case 'jpg': + case 'jpeg': + $size = \explode('x', \strtolower($sizes)); + + $sizeWidth = (int) ($size[0] ?? 0); + $sizeHeight = (int) ($size[1] ?? 0); + + if (($sizeWidth * $sizeHeight) >= $space) { + $space = $sizeWidth * $sizeHeight; + $outputHref = $absolute; + $outputExt = $ext; + } + + break; + } + + break; + } + } + + if (empty($outputHref) || empty($outputExt)) { + $default = \parse_url($url); + + $outputHref = $default['scheme'] . '://' . $default['host'] . '/favicon.ico'; + $outputExt = 'ico'; + } + + $domain = new Domain(\parse_url($outputHref, PHP_URL_HOST)); + + if (!$domain->isKnown()) { + throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); + } + + $client = new Client(); + try { + $res = $client + ->setAllowRedirects(true) + ->setMaxRedirects(5) + ->fetch($outputHref); + } catch (\Throwable) { + throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); + } + + if ($res->getStatusCode() !== 200) { + throw new Exception(Exception::AVATAR_ICON_NOT_FOUND); + } + + $data = $res->getBody(); + + if ('ico' === $outputExt) { // Skip crop, Imagick isn\'t supporting icon files + if ( + empty($data) || + stripos($data, 'addHeader('Cache-Control', 'private, max-age=2592000') // 30 days + ->setContentType('image/x-icon') + ->file($data); + return; + } + + if ('svg' === $outputExt) { // Skip crop, Imagick isn\'t supporting svg files + $sanitizer = new SvgSanitizer(); + $sanitizer->minify(true); + $cleanSvg = $sanitizer->sanitize($data); + if ($cleanSvg === false) { + throw new Exception(Exception::AVATAR_SVG_SANITIZATION_FAILED); + } + $response + ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days + ->setContentType('image/svg+xml') + ->file($cleanSvg); + return; + } + + $image = new Image($data); + $image->crop((int) $width, (int) $height); + $output = (empty($output)) ? $type : $output; + $data = $image->output($output, $quality); + + $response + ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days + ->setContentType('image/png') + ->file($data); + unset($image); + } +} diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Flags/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Flags/Get.php new file mode 100644 index 0000000000..8230b15f50 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Flags/Get.php @@ -0,0 +1,64 @@ +setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/avatars/flags/:code') + ->desc('Get country flag') + ->groups(['api', 'avatars']) + ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resource', 'avatar/flag') + ->label('sdk', new Method( + namespace: 'avatars', + group: null, + name: 'getFlag', + description: '/docs/references/avatars/get-flag.md', + auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], + type: MethodType::LOCATION, + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_NONE, + ) + ], + contentType: ContentType::IMAGE_PNG + )) + ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-flags'))), 'Country Code. ISO Alpha-2 country code format.') + ->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) + ->inject('response') + ->callback($this->action(...)); + } + + public function action(string $code, int $width, int $height, int $quality, Response $response) + { + $this->avatar('flags', $code, $width, $height, $quality, $response); + } +} diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php new file mode 100644 index 0000000000..eb56ddf0b2 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php @@ -0,0 +1,107 @@ +setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/avatars/image') + ->desc('Get image from URL') + ->groups(['api', 'avatars']) + ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resource', 'avatar/image') + ->label('sdk', new Method( + namespace: 'avatars', + group: null, + name: 'getImage', + description: '/docs/references/avatars/get-image.md', + auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], + type: MethodType::LOCATION, + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_NONE, + ) + ], + contentType: ContentType::IMAGE + )) + ->param('url', '', new URL(['http', 'https']), 'Image URL which you want to crop.') + ->param('width', 400, new Range(0, 2000), 'Resize preview image width, Pass an integer between 0 to 2000. Defaults to 400.', true) + ->param('height', 400, new Range(0, 2000), 'Resize preview image height, Pass an integer between 0 to 2000. Defaults to 400.', true) + ->inject('response') + ->callback($this->action(...)); + } + + public function action(string $url, int $width, int $height, Response $response) + { + $quality = 80; + $output = 'png'; + $type = 'png'; + + if (!\extension_loaded('imagick')) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); + } + + $domain = new Domain(\parse_url($url, PHP_URL_HOST)); + + if (!$domain->isKnown()) { + throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); + } + + $client = new Client(); + try { + $res = $client + ->setAllowRedirects(false) + ->fetch($url); + } catch (\Throwable) { + throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); + } + + if ($res->getStatusCode() !== 200) { + throw new Exception(Exception::AVATAR_IMAGE_NOT_FOUND); + } + + try { + $image = new Image($res->getBody()); + } catch (\Throwable $exception) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unable to parse image'); + } + + $image->crop((int) $width, (int) $height); + $output = (empty($output)) ? $type : $output; + $data = $image->output($output, $quality); + + $response + ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days + ->setContentType('image/png') + ->file($data); + unset($image); + } +} diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php new file mode 100644 index 0000000000..8278a43ea3 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php @@ -0,0 +1,127 @@ +setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/avatars/initials') + ->desc('Get user initials') + ->groups(['api', 'avatars']) + ->label('scope', 'avatars.read') + ->label('cache.resource', 'avatar/initials') + ->label('sdk', new Method( + namespace: 'avatars', + group: null, + name: 'getInitials', + description: '/docs/references/avatars/get-initials.md', + auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], + type: MethodType::LOCATION, + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_NONE, + ) + ], + contentType: ContentType::IMAGE_PNG + )) + ->param('name', '', new Text(128), 'Full Name. When empty, current user name or email will be used. Max length: 128 chars.', true) + ->param('width', 500, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('height', 500, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('background', '', new HexColor(), 'Changes background color. By default a random color will be picked and stay will persistent to the given name.', true) + ->inject('response') + ->inject('user') + ->callback($this->action(...)); + } + + public function action(string $name, int $width, int $height, string $background, Response $response, Document $user) + { + $themes = [ + ['background' => '#FD366E'], // Default (Pink) + ['background' => '#FE9567'], // Orange + ['background' => '#7C67FE'], // Purple + ['background' => '#68A3FE'], // Blue + ['background' => '#85DBD8'], // Mint + ]; + + $name = (!empty($name)) ? $name : $user->getAttribute('name', $user->getAttribute('email', '')); + $words = \explode(' ', \strtoupper($name)); + // if there is no space, try to split by `_` underscore + $words = (count($words) == 1) ? \explode('_', \strtoupper($name)) : $words; + + $initials = ''; + $code = 0; + + foreach ($words as $key => $w) { + if (ctype_alnum($w[0] ?? '')) { + $initials .= $w[0]; + $code += ord($w[0]); + + if ($key == 1) { + break; + } + } + } + + $rand = \substr($code, -1); + + $rand = ($rand > \count($themes) - 1) ? $rand % \count($themes) : $rand; + + $background = (!empty($background)) ? '#' . $background : $themes[$rand]['background']; + + $image = new Imagick(); + $punch = new Imagick(); + $draw = new ImagickDraw(); + $fontSize = \min($width, $height) / 2; + + $punch->newImage($width, $height, 'transparent'); + + $draw->setFont($this->getAppRoot() . '/app/assets/fonts/inter-v8-latin-regular.woff2'); + $image->setFont($this->getAppRoot() . '/app/assets/fonts/inter-v8-latin-regular.woff2'); + + $draw->setFillColor(new ImagickPixel('black')); + $draw->setFontSize($fontSize); + + $draw->setTextAlignment(Imagick::ALIGN_CENTER); + $draw->annotation($width / 1.97, ($height / 2) + ($fontSize / 3), $initials); + + $punch->drawImage($draw); + $punch->negateImage(true, Imagick::CHANNEL_ALPHA); + + $image->newImage($width, $height, $background); + $image->setImageFormat("png"); + $image->compositeImage($punch, Imagick::COMPOSITE_COPYOPACITY, 0, 0); + + $response + ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days + ->setContentType('image/png') + ->file($image->getImageBlob()); + } +} diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/QR/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/QR/Get.php new file mode 100644 index 0000000000..27fd8708d9 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Avatars/Http/QR/Get.php @@ -0,0 +1,85 @@ +setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/avatars/qr') + ->desc('Get QR code') + ->groups(['api', 'avatars']) + ->label('scope', 'avatars.read') + ->label('sdk', new Method( + namespace: 'avatars', + group: null, + name: 'getQR', + description: '/docs/references/avatars/get-qr.md', + auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], + type: MethodType::LOCATION, + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_NONE, + ) + ], + contentType: ContentType::IMAGE_PNG + )) + ->param('text', '', new Text(512), 'Plain text to be converted to QR code image.') + ->param('size', 400, new Range(1, 1000), 'QR code size. Pass an integer between 1 to 1000. Defaults to 400.', true) + ->param('margin', 1, new Range(0, 10), 'Margin from edge. Pass an integer between 0 to 10. Defaults to 1.', true) + ->param('download', false, new Boolean(true), 'Return resulting image with \'Content-Disposition: attachment \' headers for the browser to start downloading it. Pass 0 for no header, or 1 for otherwise. Default value is set to 0.', true) + ->inject('response') + ->callback($this->action(...)); + } + + public function action(string $text, int $size, int $margin, bool $download, Response $response) + { + $download = ($download === '1' || $download === 'true' || $download === 1 || $download === true); + $options = new QROptions([ + 'addQuietzone' => true, + 'quietzoneSize' => $margin, + 'outputType' => QRCode::OUTPUT_IMAGICK, + 'scale' => 15, + ]); + + $qrcode = new QRCode($options); + + if ($download) { + $response->addHeader('Content-Disposition', 'attachment; filename="qr.png"'); + } + + $image = new Image($qrcode->render($text)); + $image->crop((int) $size, (int) $size); + + $response + ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days + ->setContentType('image/png') + ->send($image->output('png', 90)); + } +} diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php new file mode 100644 index 0000000000..b6fd354ee3 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php @@ -0,0 +1,225 @@ +setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/avatars/screenshots') + ->desc('Get webpage screenshot') + ->groups(['api', 'avatars']) + ->label('scope', 'avatars.read') + ->label('usage.metric', METRIC_AVATARS_SCREENSHOTS_GENERATED) + ->label('abuse-limit', 60) + ->label('cache', true) + ->label('cache.resourceType', 'avatar/screenshot') + ->label('cache.resource', 'screenshot/{request.url}/{request.width}/{request.height}/{request.scale}/{request.theme}/{request.userAgent}/{request.fullpage}/{request.locale}/{request.timezone}/{request.latitude}/{request.longitude}/{request.accuracy}/{request.touch}/{request.permissions}/{request.sleep}/{request.quality}/{request.output}') + ->label('sdk', new Method( + namespace: 'avatars', + group: null, + name: 'getScreenshot', + description: '/docs/references/avatars/get-screenshot.md', + auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], + type: MethodType::LOCATION, + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_NONE, + ) + ], + contentType: ContentType::IMAGE_PNG + )) + ->param('url', '', new URL(['http', 'https']), 'Website URL which you want to capture.', example: 'https://example.com') + ->param('headers', [], new Assoc(), 'HTTP headers to send with the browser request. Defaults to empty.', true, example: '{"Authorization":"Bearer token123","X-Custom-Header":"value"}') + ->param('viewportWidth', 1280, new Range(1, 1920), 'Browser viewport width. Pass an integer between 1 to 1920. Defaults to 1280.', true, example: '1920') + ->param('viewportHeight', 720, new Range(1, 1080), 'Browser viewport height. Pass an integer between 1 to 1080. Defaults to 720.', true, example: '1080') + ->param('scale', 1, new Range(0.1, 3, Range::TYPE_FLOAT), 'Browser scale factor. Pass a number between 0.1 to 3. Defaults to 1.', true, example: '2') + ->param('theme', 'light', new WhiteList(['light', 'dark']), 'Browser theme. Pass "light" or "dark". Defaults to "light".', true, example: 'dark') + ->param('userAgent', '', new Text(512), 'Custom user agent string. Defaults to browser default.', true, example: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15') + ->param('fullpage', false, new Boolean(true), 'Capture full page scroll. Pass 0 for viewport only, or 1 for full page. Defaults to 0.', true, example: 'true') + ->param('locale', '', new Text(10), 'Browser locale (e.g., "en-US", "fr-FR"). Defaults to browser default.', true, example: 'en-US') + ->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'america/new_york') + ->param('latitude', 0, new Range(-90, 90, Range::TYPE_FLOAT), 'Geolocation latitude. Pass a number between -90 to 90. Defaults to 0.', true, example: '37.7749') + ->param('longitude', 0, new Range(-180, 180, Range::TYPE_FLOAT), 'Geolocation longitude. Pass a number between -180 to 180. Defaults to 0.', true, example: '-122.4194') + ->param('accuracy', 0, new Range(0, 100000, Range::TYPE_FLOAT), 'Geolocation accuracy in meters. Pass a number between 0 to 100000. Defaults to 0.', true, example: '100') + ->param('touch', false, new Boolean(true), 'Enable touch support. Pass 0 for no touch, or 1 for touch enabled. Defaults to 0.', true, example: 'true') + ->param('permissions', [], new ArrayList(new WhiteList(['geolocation', 'camera', 'microphone', 'notifications', 'midi', 'push', 'clipboard-read', 'clipboard-write', 'payment-handler', 'usb', 'bluetooth', 'accelerometer', 'gyroscope', 'magnetometer', 'ambient-light-sensor', 'background-sync', 'persistent-storage', 'screen-wake-lock', 'web-share', 'xr-spatial-tracking'])), 'Browser permissions to grant. Pass an array of permission names like ["geolocation", "camera", "microphone"]. Defaults to empty.', true, example: '["geolocation","notifications"]') + ->param('sleep', 0, new Range(0, 10), 'Wait time in seconds before taking the screenshot. Pass an integer between 0 to 10. Defaults to 0.', true, example: '3') + ->param('width', 0, new Range(0, 2000), 'Output image width. Pass 0 to use original width, or an integer between 1 to 2000. Defaults to 0 (original width).', true, example: '800') + ->param('height', 0, new Range(0, 2000), 'Output image height. Pass 0 to use original height, or an integer between 1 to 2000. Defaults to 0 (original height).', true, example: '600') + ->param('quality', -1, new Range(-1, 100), 'Screenshot quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true, example: '85') + ->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true, example: 'jpeg') + ->inject('response') + ->inject('queueForStatsUsage') + ->callback($this->action(...)); + } + + public function action(string $url, array $headers, int $viewportWidth, int $viewportHeight, float $scale, string $theme, string $userAgent, bool $fullpage, string $locale, string $timezone, float $latitude, float $longitude, float $accuracy, bool $touch, array $permissions, int $sleep, int $width, int $height, int $quality, string $output, Response $response, StatsUsage $queueForStatsUsage) + { + if (!\extension_loaded('imagick')) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); + } + + $domain = new Domain(\parse_url($url, PHP_URL_HOST)); + + if (!$domain->isKnown()) { + throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); + } + + $client = new Client(); + $client->setTimeout(30 * 1000); // 30 seconds + $client->addHeader('content-type', Client::CONTENT_TYPE_APPLICATION_JSON); + + // Convert indexed array to empty array (should not happen due to Assoc validator) + if (is_array($headers) && count($headers) > 0 && array_keys($headers) === range(0, count($headers) - 1)) { + $headers = []; + } + + // Create a new object to ensure proper JSON serialization + $headersObject = new \stdClass(); + foreach ($headers as $key => $value) { + $headersObject->$key = $value; + } + + // Create the config with headers as an object + // The custom browser service accepts: url, theme, headers, sleep, viewport, userAgent, fullPage, locale, timezoneId, geolocation, hasTouch, scale + $config = [ + 'url' => $url, + 'theme' => $theme, + 'headers' => $headersObject, + 'sleep' => $sleep * 1000, // Convert seconds to milliseconds + 'waitUntil' => 'load', + 'viewport' => [ + 'width' => $viewportWidth, + 'height' => $viewportHeight + ] + ]; + + // Add scale if not default + if ($scale != 1) { + $config['deviceScaleFactor'] = $scale; + } + + // Add optional parameters that were set, preserving arrays as arrays + if (!empty($userAgent)) { + $config['userAgent'] = $userAgent; + } + + if ($fullpage) { + $config['fullPage'] = true; + } + + if (!empty($locale)) { + $config['locale'] = $locale; + } + + if (!empty($timezone)) { + $config['timezoneId'] = $timezone; + } + + // Add geolocation if any coordinates are provided + if ($latitude != 0 || $longitude != 0) { + $config['geolocation'] = [ + 'latitude' => $latitude, + 'longitude' => $longitude, + 'accuracy' => $accuracy + ]; + } + + if ($touch) { + $config['hasTouch'] = true; + } + + // Add permissions if provided (preserve as array) + if (!empty($permissions)) { + $config['permissions'] = $permissions; // Keep as array + } + + try { + $browserEndpoint = System::getEnv('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1'); + + $fetchResponse = $client->fetch( + url: $browserEndpoint . '/screenshots', + method: 'POST', + body: $config + ); + + if ($fetchResponse->getStatusCode() >= 400) { + throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot service failed: ' . $fetchResponse->getBody()); + } + + $screenshot = $fetchResponse->getBody(); + + if (empty($screenshot)) { + throw new Exception(Exception::AVATAR_IMAGE_NOT_FOUND, 'Screenshot not generated'); + } + + // Determine if image processing is needed + $needsProcessing = ($width > 0 || $height > 0) || $quality !== -1 || !empty($output); + + if ($needsProcessing) { + // Process image with cropping, quality adjustment, or format conversion + $image = new Image($screenshot); + + $image->crop($width, $height); + + $output = $output ?: 'png'; // Default to PNG if not specified + $resizedScreenshot = $image->output($output, $quality); + unset($image); + } else { + // Return original screenshot without processing + $resizedScreenshot = $screenshot; + $output = 'png'; // Screenshots are typically PNG by default + } + + // Set content type based on output format + $outputs = Config::getParam('storage-outputs'); + $contentType = $outputs[$output] ?? $outputs['png']; + + $queueForStatsUsage->addMetric(METRIC_AVATARS_SCREENSHOTS_GENERATED, 1); + + $response + ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days + ->setContentType($contentType) + ->file($resizedScreenshot); + + + } catch (\Throwable $th) { + throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot generation failed: ' . $th->getMessage()); + } + } +} diff --git a/src/Appwrite/Platform/Modules/Avatars/Module.php b/src/Appwrite/Platform/Modules/Avatars/Module.php new file mode 100644 index 0000000000..187bd96905 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Avatars/Module.php @@ -0,0 +1,14 @@ +addService('http', new Http()); + } +} diff --git a/src/Appwrite/Platform/Modules/Avatars/Services/Http.php b/src/Appwrite/Platform/Modules/Avatars/Services/Http.php new file mode 100644 index 0000000000..c52edb6a05 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Avatars/Services/Http.php @@ -0,0 +1,36 @@ +type = Service::TYPE_HTTP; + + $this->addAction(GetCreditCard::getName(), new GetCreditCard()); + $this->addAction(GetBrowser::getName(), new GetBrowser()); + $this->addAction(GetFlag::getName(), new GetFlag()); + $this->addAction(GetImage::getName(), new GetImage()); + $this->addAction(GetFavicon::getName(), new GetFavicon()); + $this->addAction(GetQR::getName(), new GetQR()); + $this->addAction(GetInitials::getName(), new GetInitials()); + $this->addAction(GetScreenshot::getName(), new GetScreenshot()); + $this->addAction(GetCloudCard::getName(), new GetCloudCard()); + $this->addAction(GetCloudCardBack::getName(), new GetCloudCardBack()); + $this->addAction(GetCloudCardOG::getName(), new GetCloudCardOG()); + } +} diff --git a/src/Appwrite/Platform/Modules/Compute/Base.php b/src/Appwrite/Platform/Modules/Compute/Base.php index 47afc90986..33b69dd589 100644 --- a/src/Appwrite/Platform/Modules/Compute/Base.php +++ b/src/Appwrite/Platform/Modules/Compute/Base.php @@ -13,6 +13,7 @@ use Utopia\Database\Exception\Duplicate; 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\Swoole\Request; use Utopia\System\System; @@ -142,7 +143,7 @@ class Base extends Action return $deployment; } - public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForPlatform, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, string $referenceType = 'branch', string $reference = ''): Document + public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForPlatform, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, Authorization $authorization, string $referenceType = 'branch', string $reference = ''): Document { $deploymentId = ID::unique(); $providerInstallationId = $installation->getAttribute('providerInstallationId', ''); @@ -239,7 +240,7 @@ class Base extends Action $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; $ruleId = $isMd5 ? md5($domain) : ID::unique(); - Authorization::skip( + $authorization->skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), @@ -265,7 +266,7 @@ class Base extends Action $domain = "commit-" . substr($commitDetails['commitHash'], 0, 16) . ".{$sitesDomain}"; $ruleId = md5($domain); try { - Authorization::skip( + $authorization->skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), @@ -302,7 +303,7 @@ class Base extends Action $domain = "branch-{$branchPrefix}-{$resourceProjectHash}.{$sitesDomain}"; $ruleId = md5($domain); try { - Authorization::skip( + $authorization->skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), @@ -328,6 +329,8 @@ class Base extends Action } } + $this->updateEmptyManualRule($project, $site, $deployment, $dbForPlatform, $authorization); + $queueForBuilds ->setType(BUILD_TYPE_DEPLOYMENT) ->setResource($site) @@ -336,4 +339,34 @@ class Base extends Action return $deployment; } + + /** + * Update empty manual rule for deployment. + * In case of first deployment, deployment ID will be empty in the rules, so we need to update it here. + * + * @param \Utopia\Database\Document $project + * @param \Utopia\Database\Document $resource + * @param \Utopia\Database\Document $deployment + * @param \Utopia\Database\Database $dbForPlatform + * @return void + */ + public static function updateEmptyManualRule(Document $project, Document $resource, Document $deployment, Database $dbForPlatform, Authorization $authorization) + { + $resourceType = $resource->getCollection() === 'sites' ? 'site' : 'function'; + + $queries = [ + Query::equal('projectInternalId', [$project->getSequence()]), + Query::equal('deploymentResourceInternalId', [$resource->getSequence()]), + Query::equal('deploymentResourceType', [$resourceType]), + Query::equal('deploymentId', ['']), + Query::equal('type', ['deployment']), + Query::equal('trigger', ['manual']), + ]; + $dbForPlatform->forEach('rules', function (Document $rule) use ($deployment, $dbForPlatform, $authorization) { + $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + 'deploymentId' => $deployment->getId(), + 'deploymentInternalId' => $deployment->getSequence(), + ]))); + }, $queries); + } } diff --git a/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php b/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php index aa43b12125..1468bf71ac 100644 --- a/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php +++ b/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php @@ -60,6 +60,7 @@ class Get extends Action ->inject('response') ->inject('dbForPlatform') ->inject('platform') + ->inject('authorization') ->callback($this->action(...)); } @@ -68,7 +69,8 @@ class Get extends Action string $type, Response $response, Database $dbForPlatform, - array $platform + array $platform, + Authorization $authorization, ) { $domains = $platform['hostnames'] ?? []; if ($type === 'rules') { @@ -121,7 +123,7 @@ class Get extends Action throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); } - $document = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ + $document = $authorization->skip(fn () => $dbForPlatform->findOne('rules', [ Query::equal('domain', [$value]), ])); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php index 83a401a35e..e2df5d92e6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php @@ -292,7 +292,7 @@ abstract class Action extends UtopiaAction }; } - protected function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): Document + protected 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', ''); @@ -310,7 +310,7 @@ abstract class Action extends UtopiaAction throw new Exception($this->getSpatialTypeNotSupportedException(), params: [$type]); } - $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, params: [$databaseId]); @@ -371,7 +371,7 @@ abstract class Action extends UtopiaAction \in_array($attribute->getAttribute('type'), Database::SPATIAL_TYPES) && $attribute->getAttribute('required') ) { - $hasData = !Authorization::skip(fn () => $dbForProject + $hasData = !$authorization->skip(fn () => $dbForProject ->findOne('database_' . $db->getSequence() . '_collection_' . $collection->getSequence())) ->isEmpty(); @@ -472,9 +472,9 @@ abstract class Action extends UtopiaAction return $attribute; } - protected function updateAttribute(string $databaseId, string $collectionId, string $key, Database $dbForProject, Event $queueForEvents, string $type, int $size = null, string $filter = null, string|bool|int|float|array $default = null, bool $required = null, int|float|null $min = null, int|float|null $max = null, array $elements = null, array $options = [], string $newKey = null): Document + protected function updateAttribute(string $databaseId, string $collectionId, string $key, Database $dbForProject, Event $queueForEvents, Authorization $authorization, string $type, int $size = null, string $filter = null, string|bool|int|float|array $default = null, bool $required = null, int|float|null $min = null, int|float|null $max = null, array $elements = null, array $options = [], string $newKey = null): 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, params: [$databaseId]); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php index f04532aeee..442461fdd3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -69,10 +70,11 @@ class Create extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -81,7 +83,7 @@ class Create extends Action 'required' => $required, 'default' => $default, 'array' => $array, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Update.php index 003b4227c9..92324aae70 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Update.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -68,10 +69,11 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->updateAttribute( databaseId: $databaseId, @@ -79,6 +81,7 @@ class Update extends Action key: $key, dbForProject: $dbForProject, queueForEvents: $queueForEvents, + authorization: $authorization, type: Database::VAR_BOOLEAN, default: $default, required: $required, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php index c2982445a4..bd3108a871 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; @@ -70,10 +71,11 @@ class Create extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->createAttribute( $databaseId, @@ -90,7 +92,8 @@ class Create extends Action $response, $dbForProject, $queueForDatabase, - $queueForEvents + $queueForEvents, + $authorization ); $response diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Update.php index 984d4b0245..2518875424 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Update.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; @@ -69,10 +70,11 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->updateAttribute( databaseId: $databaseId, @@ -80,6 +82,7 @@ class Update extends Action key: $key, dbForProject: $dbForProject, queueForEvents: $queueForEvents, + authorization: $authorization, type: Database::VAR_DATETIME, default: $default, required: $required, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php index 649cde10aa..37ae2a7bfe 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php @@ -67,12 +67,13 @@ class Delete extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void { - $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, params: [$databaseId]); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php index b36072eb75..a36e264e50 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php @@ -13,6 +13,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -70,10 +71,11 @@ class Create extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->createAttribute( $databaseId, @@ -90,7 +92,8 @@ class Create extends Action $response, $dbForProject, $queueForDatabase, - $queueForEvents + $queueForEvents, + $authorization ); $response diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Update.php index 382f16b469..609a337625 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Update.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -69,10 +70,11 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->updateAttribute( databaseId: $databaseId, @@ -80,6 +82,7 @@ class Update extends Action key: $key, dbForProject: $dbForProject, queueForEvents: $queueForEvents, + authorization: $authorization, type: Database::VAR_STRING, filter: APP_DATABASE_ATTRIBUTE_EMAIL, default: $default, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php index 9145191b0c..3c47d1fdfe 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php @@ -13,6 +13,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -73,10 +74,11 @@ class Create extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void { if (!is_null($default) && !\in_array($default, $elements, true)) { throw new Exception($this->getInvalidValueException(), 'Default value not found in elements'); @@ -98,7 +100,8 @@ class Create extends Action $response, $dbForProject, $queueForDatabase, - $queueForEvents + $queueForEvents, + $authorization ); $response diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Update.php index 2f47eb0cc6..5bea5230c0 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Update.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -71,10 +72,11 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?array $elements, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?array $elements, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->updateAttribute( databaseId: $databaseId, @@ -82,6 +84,7 @@ class Update extends Action key: $key, dbForProject: $dbForProject, queueForEvents: $queueForEvents, + authorization: $authorization, type: Database::VAR_STRING, filter: APP_DATABASE_ATTRIBUTE_ENUM, default: $default, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php index 56d8874794..0dc11bd76c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php @@ -13,6 +13,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -74,10 +75,11 @@ class Create extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void { $min ??= -PHP_FLOAT_MAX; $max ??= PHP_FLOAT_MAX; @@ -100,7 +102,7 @@ class Create extends Action 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, 'formatOptions' => ['min' => $min, 'max' => $max], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $formatOptions = $attribute->getAttribute('formatOptions', []); if (!empty($formatOptions)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Update.php index 330c649f27..20b5c0767d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Update.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -71,10 +72,11 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->updateAttribute( databaseId: $databaseId, @@ -82,6 +84,7 @@ class Update extends Action key: $key, dbForProject: $dbForProject, queueForEvents: $queueForEvents, + authorization: $authorization, type: Database::VAR_FLOAT, default: $default, required: $required, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Get.php index 3a8eece531..436b22c6c9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Get.php @@ -68,12 +68,13 @@ class Get extends Action ->param('key', '', new Key(), 'Attribute Key.') ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject): void + public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, Authorization $authorization): void { - $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, params: [$databaseId]); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php index 2340d1d55d..2adf3977f4 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -70,10 +71,11 @@ class Create extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->createAttribute( $databaseId, @@ -90,7 +92,8 @@ class Create extends Action $response, $dbForProject, $queueForDatabase, - $queueForEvents + $queueForEvents, + $authorization ); $response diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Update.php index 236dbf7f83..eccf18b005 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Update.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -69,10 +70,11 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->updateAttribute( databaseId: $databaseId, @@ -80,6 +82,7 @@ class Update extends Action key: $key, dbForProject: $dbForProject, queueForEvents: $queueForEvents, + authorization: $authorization, type: Database::VAR_STRING, filter: APP_DATABASE_ATTRIBUTE_IP, default: $default, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php index 30f58097ce..58ded9b78a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php @@ -13,6 +13,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -74,10 +75,11 @@ class Create extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void { $min ??= \PHP_INT_MIN; $max ??= \PHP_INT_MAX; @@ -102,7 +104,7 @@ class Create extends Action 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_INT_RANGE, 'formatOptions' => ['min' => $min, 'max' => $max], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $formatOptions = $attribute->getAttribute('formatOptions', []); if (!empty($formatOptions)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Update.php index 67c371c69d..84a43018d1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Update.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -71,10 +72,11 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->updateAttribute( databaseId: $databaseId, @@ -82,6 +84,7 @@ class Update extends Action key: $key, dbForProject: $dbForProject, queueForEvents: $queueForEvents, + authorization: $authorization, type: Database::VAR_INTEGER, default: $default, required: $required, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php index f0fd728902..fc846957b0 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Spatial; use Utopia\Database\Validator\UID; @@ -69,17 +70,18 @@ class Create extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_LINESTRING, 'required' => $required, 'default' => $default - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Update.php index 3407da2b34..8fff545921 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Update.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Spatial; use Utopia\Database\Validator\UID; @@ -69,10 +70,11 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->updateAttribute( databaseId: $databaseId, @@ -80,6 +82,7 @@ class Update extends Action key: $key, dbForProject: $dbForProject, queueForEvents: $queueForEvents, + authorization: $authorization, type: Database::VAR_LINESTRING, default: $default, required: $required, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php index f2e4d19267..a89c21581d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Spatial; use Utopia\Database\Validator\UID; @@ -69,17 +70,18 @@ class Create extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_POINT, 'required' => $required, 'default' => $default, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Update.php index 86e78e56e3..9561fe6b96 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Update.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Spatial; use Utopia\Database\Validator\UID; @@ -69,10 +70,11 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->updateAttribute( databaseId: $databaseId, @@ -80,6 +82,7 @@ class Update extends Action key: $key, dbForProject: $dbForProject, queueForEvents: $queueForEvents, + authorization: $authorization, type: Database::VAR_POINT, default: $default, required: $required, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php index 4c49b21050..54da3ac604 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Spatial; use Utopia\Database\Validator\UID; @@ -69,17 +70,18 @@ class Create extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_POLYGON, 'required' => $required, 'default' => $default, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Update.php index 0dbb117cec..b82a3d4be0 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Update.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Spatial; use Utopia\Database\Validator\UID; @@ -69,10 +70,11 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->updateAttribute( databaseId: $databaseId, @@ -80,6 +82,7 @@ class Update extends Action key: $key, dbForProject: $dbForProject, queueForEvents: $queueForEvents, + authorization: $authorization, type: Database::VAR_POLYGON, default: $default, required: $required, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php index b43568a968..615e64dfd7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php @@ -83,16 +83,17 @@ class Create extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $relatedCollectionId, string $type, bool $twoWay, ?string $key, ?string $twoWayKey, string $onDelete, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $relatedCollectionId, string $type, bool $twoWay, ?string $key, ?string $twoWayKey, string $onDelete, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void { $key ??= $relatedCollectionId; $twoWayKeyWasProvided = $twoWayKey !== null; $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, params: [$databaseId]); } @@ -154,7 +155,7 @@ class Create extends Action 'twoWayKey' => $twoWayKey, 'onDelete' => $onDelete, ] - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); foreach ($attribute->getAttribute('options', []) as $k => $option) { $attribute->setAttribute($k, $option); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Update.php index feed58a4ff..d180131a44 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Update.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -71,6 +72,7 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } @@ -82,7 +84,8 @@ class Update extends Action ?string $newKey, UtopiaResponse $response, Database $dbForProject, - Event $queueForEvents + Event $queueForEvents, + Authorization $authorization ): void { $attribute = $this->updateAttribute( databaseId: $databaseId, @@ -90,6 +93,7 @@ class Update extends Action key: $key, dbForProject: $dbForProject, queueForEvents: $queueForEvents, + authorization: $authorization, type: Database::VAR_RELATIONSHIP, required: false, options: [ diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php index b42558f063..b3fe03cace 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php @@ -14,6 +14,7 @@ use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\App; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -77,6 +78,7 @@ class Create extends Action ->inject('queueForDatabase') ->inject('queueForEvents') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } @@ -93,7 +95,8 @@ class Create extends Action Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, - array $plan + array $plan, + Authorization $authorization ): void { if (!App::isDevelopment() && $encrypt && !empty($plan) && !($plan['databasesAllowEncrypt'] ?? false)) { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Encrypted string ' . $this->getSDKGroup() . ' are not available on your plan. Please upgrade to create encrypted string ' . $this->getSDKGroup() . '.'); @@ -132,7 +135,8 @@ class Create extends Action $response, $dbForProject, $queueForDatabase, - $queueForEvents + $queueForEvents, + $authorization ); $attribute->setAttribute('encrypt', $encrypt); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Update.php index 53ea2a0e03..37547f3da8 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Update.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -72,6 +73,7 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } @@ -85,7 +87,8 @@ class Update extends Action ?string $newKey, UtopiaResponse $response, Database $dbForProject, - Event $queueForEvents + Event $queueForEvents, + Authorization $authorization ): void { $attribute = $this->updateAttribute( databaseId: $databaseId, @@ -93,6 +96,7 @@ class Update extends Action key: $key, dbForProject: $dbForProject, queueForEvents: $queueForEvents, + authorization: $authorization, type: Database::VAR_STRING, size: $size, default: $default, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php index 7529845016..ed1a23acf5 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -70,6 +71,7 @@ class Create extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } @@ -83,7 +85,8 @@ class Create extends Action UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, - Event $queueForEvents + Event $queueForEvents, + Authorization $authorization ): void { $attribute = $this->createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -93,7 +96,7 @@ class Create extends Action 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_URL, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Update.php index 9ba8ebb859..08f7a26fd9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Update.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -69,6 +70,7 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } @@ -81,7 +83,8 @@ class Update extends Action ?string $newKey, UtopiaResponse $response, Database $dbForProject, - Event $queueForEvents + Event $queueForEvents, + Authorization $authorization ): void { $attribute = $this->updateAttribute( $databaseId, @@ -89,6 +92,7 @@ class Update extends Action $key, $dbForProject, $queueForEvents, + $authorization, type: Database::VAR_STRING, filter: APP_DATABASE_ATTRIBUTE_URL, default: $default, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/XList.php index 6bfe5f8913..61c5b295cf 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/XList.php @@ -64,12 +64,13 @@ class XList extends Action ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, array $queries, bool $includeTotal, UtopiaResponse $response, Database $dbForProject): void + public function action(string $databaseId, string $collectionId, array $queries, bool $includeTotal, UtopiaResponse $response, Database $dbForProject, Authorization $authorization): void { - $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, params: [$databaseId]); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php index 724f40f00e..89cc14056a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php @@ -85,12 +85,13 @@ class Create extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, array $attributes, array $indexes, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, array $attributes, array $indexes, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void { - $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, params: [$databaseId]); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php index af36649061..fd2c419954 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php @@ -64,12 +64,13 @@ class Delete extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void { - $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, params: [$databaseId]); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index f16d00998d..ec65135a05 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -258,9 +258,9 @@ abstract class Action extends DatabasesAction Document $collection, Document $document, Database $dbForProject, - /* options */ array &$collectionsCache, + Authorization $authorization, ?int &$operations = null, ): bool { @@ -297,7 +297,7 @@ abstract class Action extends DatabasesAction $relatedCollectionId = $relationship->getAttribute('relatedCollection'); if (!isset($collectionsCache[$relatedCollectionId])) { - $relatedCollectionDoc = Authorization::skip( + $relatedCollectionDoc = $authorization->skip( fn () => $dbForProject->getDocument( 'database_' . $database->getSequence(), $relatedCollectionId @@ -323,7 +323,8 @@ abstract class Action extends DatabasesAction document: $relation, dbForProject: $dbForProject, collectionsCache: $collectionsCache, - operations: $operations + operations: $operations, + authorization: $authorization ); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php index 53831f0fc5..16b7bd1b25 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php @@ -85,20 +85,21 @@ class Decrement extends Action ->inject('queueForEvents') ->inject('queueForStatsUsage') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void + public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan, Authorization $authorization): void { - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); - $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, params: [$databaseId]); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); if ($collection->isEmpty()) { throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); } @@ -106,7 +107,7 @@ class Decrement extends Action // Handle transaction staging if ($transactionId !== null) { $transaction = ($isAPIKey || $isPrivilegedUser) - ? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) + ? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) : $dbForProject->getDocument('transactions', $transactionId); if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php index ea680db3b1..7adae7633b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php @@ -85,20 +85,21 @@ class Increment extends Action ->inject('queueForEvents') ->inject('queueForStatsUsage') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void + public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan, Authorization $authorization): void { - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); - $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, params: [$databaseId]); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); if ($collection->isEmpty()) { throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); } @@ -106,7 +107,7 @@ class Increment extends Action // Handle transaction staging if ($transactionId !== null) { $transaction = ($isAPIKey || $isPrivilegedUser) - ? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) + ? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) : $dbForProject->getDocument('transactions', $transactionId); if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 6ec06f5c8a..bbc63da499 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -24,6 +24,7 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -132,9 +133,10 @@ class Create extends Action ->inject('queueForFunctions') ->inject('queueForWebhooks') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void + public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization): void { $data = \is_string($data) ? \json_decode($data, true) @@ -178,19 +180,19 @@ class Create extends Action $documents = [$data]; } - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); if ($isBulk && !$isAPIKey && !$isPrivilegedUser) { throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE); } - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); } @@ -204,7 +206,7 @@ class Create extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSDKNamespace() .' with relationship ' . $this->getStructureContext()); } - $setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk) { + $setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $dbForProject, $authorization) { $allowedPermissions = [ Database::PERMISSION_READ, Database::PERMISSION_UPDATE, @@ -247,8 +249,8 @@ class Create extends Action $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->hasRole($role)) { + throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $authorization->getRoles()) . ')'); } } } @@ -259,21 +261,25 @@ class Create extends Action $operations = 0; - $checkPermissions = function (Document $collection, Document $document, string $permission) use ($isAPIKey, $isPrivilegedUser, &$checkPermissions, $dbForProject, $database, &$operations) { + $checkPermissions = function (Document $collection, Document $document, string $permission) use ($isAPIKey, $isPrivilegedUser, &$checkPermissions, $dbForProject, $database, &$operations, $authorization) { $operations++; $documentSecurity = $collection->getAttribute('documentSecurity', false); - $validator = new Authorization($permission); - $valid = $validator->isValid($collection->getPermissionsByType($permission)); - if (($permission === Database::PERMISSION_UPDATE && !$documentSecurity) || !$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); + $validCollection = $authorization->isValid( + new Input($permission, $collection->getPermissionsByType($permission)) + ); + if (($permission === Database::PERMISSION_UPDATE && !$documentSecurity) || !$validCollection) { + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } if ($permission === Database::PERMISSION_UPDATE) { - $valid = $valid || $validator->isValid($document->getUpdate()); + $validDocument = $authorization->isValid( + new Input($permission, $document->getUpdate()) + ); + $valid = $validCollection || $validDocument; if ($documentSecurity && !$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } } @@ -298,7 +304,7 @@ class Create extends Action } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( + $relatedCollection = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId) ); @@ -314,7 +320,7 @@ class Create extends Action if ($relation instanceof Document) { $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); - $current = Authorization::skip( + $current = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId()) ); @@ -369,7 +375,7 @@ class Create extends Action // Handle transaction staging if ($transactionId !== null) { $transaction = ($isAPIKey || $isPrivilegedUser) - ? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) + ? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) : $dbForProject->getDocument('transactions', $transactionId); if ($transaction->isEmpty()) { throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]); @@ -468,6 +474,7 @@ class Create extends Action document: $document, dbForProject: $dbForProject, collectionsCache: $collectionsCache, + authorization: $authorization ); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php index faae638c88..7acf8e386e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php @@ -83,6 +83,7 @@ class Delete extends Action ->inject('queueForStatsUsage') ->inject('transactionState') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } @@ -97,18 +98,19 @@ class Delete extends Action Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionState $transactionState, - array $plan + array $plan, + Authorization $authorization ): void { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); @@ -121,7 +123,7 @@ class Delete extends Action // Use transaction-aware document retrieval to see changes from same transaction $document = $transactionState->getDocument($collectionTableId, $documentId, $transactionId); } else { - $document = Authorization::skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId)); + $document = $authorization->skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId)); } if ($document->isEmpty()) { @@ -131,7 +133,7 @@ class Delete extends Action // Handle transaction staging if ($transactionId !== null) { $transaction = ($isAPIKey || $isPrivilegedUser) - ? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) + ? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) : $dbForProject->getDocument('transactions', $transactionId); if ($transaction->isEmpty()) { throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]); @@ -205,6 +207,7 @@ class Delete extends Action document: $document, dbForProject: $dbForProject, collectionsCache: $collectionsCache, + authorization: $authorization ); $queueForStatsUsage diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php index f560267d4b..cb8b0dd42e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php @@ -70,20 +70,21 @@ class Get extends Action ->inject('dbForProject') ->inject('queueForStatsUsage') ->inject('transactionState') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState): void + public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState, Authorization $authorization): void { - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); @@ -125,6 +126,7 @@ class Get extends Action document: $document, dbForProject: $dbForProject, collectionsCache: $collectionsCache, + authorization: $authorization, operations: $operations ); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php index a4dd38ef67..2f5579f0ca 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php @@ -72,13 +72,14 @@ class XList extends Action ->inject('dbForProject') ->inject('locale') ->inject('geodb') + ->inject('authorization') ->inject('audit') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit): void + public function action(string $databaseId, string $collectionId, string $documentId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization, Audit $audit): void { - $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, params: [$databaseId]); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php index 707857347a..a92d8ec180 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php @@ -87,10 +87,11 @@ class Update extends Action ->inject('queueForStatsUsage') ->inject('transactionState') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionState $transactionState, array $plan): void + public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionState $transactionState, array $plan, Authorization $authorization): void { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -98,16 +99,16 @@ class Update extends Action throw new Exception($this->getMissingPayloadException()); } - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); @@ -125,7 +126,7 @@ class Update extends Action // Use transaction-aware document retrieval to see changes from same transaction $document = $transactionState->getDocument($collectionTableId, $documentId, $transactionId); } else { - $document = Authorization::skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId)); + $document = $authorization->skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId)); } if ($document->isEmpty()) { @@ -140,7 +141,7 @@ class Update extends Action ]); // 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) { @@ -153,7 +154,7 @@ class Update extends Action $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!Authorization::isRole($role)) { + if (!$authorization->hasRole($role)) { throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); } } @@ -171,7 +172,7 @@ class Update extends Action $operations = 0; - $setCollection = (function (Document $collection, Document $document) use ($isAPIKey, $isPrivilegedUser, &$setCollection, $dbForProject, $database, &$operations) { + $setCollection = (function (Document $collection, Document $document) use ($isAPIKey, $isPrivilegedUser, &$setCollection, $dbForProject, $database, &$operations, $authorization) { $operations++; $relationships = \array_filter( @@ -195,7 +196,7 @@ class Update extends Action } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( + $relatedCollection = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId) ); @@ -212,7 +213,7 @@ class Update extends Action if ($relation instanceof Document) { $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); - $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument( + $oldDocument = $authorization->skip(fn () => $dbForProject->getDocument( 'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId() )); @@ -249,7 +250,7 @@ class Update extends Action // Handle transaction staging if ($transactionId !== null) { $transaction = ($isAPIKey || $isPrivilegedUser) - ? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) + ? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) : $dbForProject->getDocument('transactions', $transactionId); if ($transaction->isEmpty()) { throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]); @@ -340,6 +341,7 @@ class Update extends Action document: $document, dbForProject: $dbForProject, collectionsCache: $collectionsCache, + authorization: $authorization, ); $response->dynamic($document, $this->getResponseModel()); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php index b32871add2..62e59dd010 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php @@ -91,10 +91,11 @@ class Upsert extends Action ->inject('queueForStatsUsage') ->inject('transactionState') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionState $transactionState, array $plan): void + public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionState $transactionState, array $plan, Authorization $authorization): void { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -106,15 +107,15 @@ class Upsert extends Action throw new Exception($this->getMissingPayloadException()); } - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); } @@ -139,7 +140,7 @@ class Upsert extends Action // Use transaction-aware document retrieval to see changes from same transaction $oldDocument = $transactionState->getDocument($collectionTableId, $documentId, $transactionId); } else { - $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId)); + $oldDocument = $authorization->skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId)); } if ($oldDocument->isEmpty()) { if (!empty($user->getId())) { @@ -155,7 +156,7 @@ class Upsert extends Action } // 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) { @@ -168,7 +169,7 @@ class Upsert extends Action $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!Authorization::isRole($role)) { + if (!$authorization->hasRole($role)) { throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); } } @@ -181,7 +182,7 @@ class Upsert extends Action $newDocument = new Document($data); $operations = 0; - $setCollection = (function (Document $collection, Document $document) use ($isAPIKey, $isPrivilegedUser, &$setCollection, $dbForProject, $database, &$operations) { + $setCollection = (function (Document $collection, Document $document) use ($isAPIKey, $isPrivilegedUser, &$setCollection, $dbForProject, $database, &$operations, $authorization) { $operations++; $relationships = \array_filter( @@ -205,7 +206,7 @@ class Upsert extends Action } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( + $relatedCollection = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId) ); @@ -222,7 +223,7 @@ class Upsert extends Action if ($relation instanceof Document) { $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); - $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument( + $oldDocument = $authorization->skip(fn () => $dbForProject->getDocument( 'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId() )); @@ -259,7 +260,7 @@ class Upsert extends Action // Handle transaction staging if ($transactionId !== null) { $transaction = ($isAPIKey || $isPrivilegedUser) - ? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) + ? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) : $dbForProject->getDocument('transactions', $transactionId); if ($transaction->isEmpty()) { throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]); @@ -361,6 +362,7 @@ class Upsert extends Action document: $document, dbForProject: $dbForProject, collectionsCache: $collectionsCache, + authorization: $authorization ); $relationships = \array_map( diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php index 8b770284c3..ff94e67b02 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php @@ -74,20 +74,21 @@ class XList extends Action ->inject('dbForProject') ->inject('queueForStatsUsage') ->inject('transactionState') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState): void + public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState, Authorization $authorization): void { - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); } @@ -115,7 +116,7 @@ class XList extends Action $documentId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId)); + $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId)); if ($cursorDocument->isEmpty()) { $type = ucfirst($this->getContext()); @@ -161,7 +162,8 @@ class XList extends Action document: $document, dbForProject: $dbForProject, collectionsCache: $collectionsCache, - operations: $operations, + authorization: $authorization, + operations: $operations ); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Get.php index e7909772a5..d8df8f1f8c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Get.php @@ -57,12 +57,13 @@ class Get extends Action ->param('collectionId', '', new UID(), 'Collection ID.') ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject): void + public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject, Authorization $authorization): void { - $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, params: [$databaseId]); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php index 872b7348fe..5b035a8688 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php @@ -79,12 +79,13 @@ class Create extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, array $lengths, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, array $lengths, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void { - $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, params: [$databaseId]); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php index 27b28e866c..d9f9f66504 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php @@ -70,12 +70,13 @@ class Delete extends Action ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void { - $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, params: [$databaseId]); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Get.php index d66bf8f38f..661f259910 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Get.php @@ -59,12 +59,13 @@ class Get extends Action ->param('key', null, new Key(), 'Index Key.') ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject): void + public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, Authorization $authorization): void { - $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, params: [$databaseId]); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php index abbdefb4d5..90826ffbe3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php @@ -66,13 +66,14 @@ class XList extends Action ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, array $queries, bool $includeTotal, UtopiaResponse $response, Database $dbForProject): void + public function action(string $databaseId, string $collectionId, array $queries, bool $includeTotal, UtopiaResponse $response, Database $dbForProject, Authorization $authorization): void { /** @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, params: [$databaseId]); @@ -112,7 +113,7 @@ class XList extends Action } $indexId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->find('indexes', [ + $cursorDocument = $authorization->skip(fn () => $dbForProject->find('indexes', [ Query::equal('collectionInternalId', [$collection->getSequence()]), Query::equal('databaseInternalId', [$database->getSequence()]), Query::equal('key', [$indexId]), diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php index 0f5a57c6e9..0b6e47a798 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php @@ -71,13 +71,14 @@ class XList extends Action ->inject('dbForProject') ->inject('locale') ->inject('geodb') + ->inject('authorization') ->inject('audit') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit): void + public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization, Audit $audit): void { - $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, params: [$databaseId]); @@ -112,9 +113,9 @@ class XList extends Action $detector = new Detector($log['userAgent']); $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - $os = $detector->getOS(); - $client = $detector->getClient(); - $device = $detector->getDevice(); + $os = $detector->getOS() ?: []; + $client = $detector->getClient() ?: []; + $device = $detector->getDevice() ?: []; $output[$i] = new Document([ 'event' => $log['event'], @@ -122,20 +123,20 @@ class XList extends Action 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, - 'ip' => $log['ip'], - 'time' => $log['time'], - 'osCode' => $os['osCode'], - 'osName' => $os['osName'], - 'osVersion' => $os['osVersion'], - 'clientType' => $client['clientType'], - 'clientCode' => $client['clientCode'], - 'clientName' => $client['clientName'], - 'clientVersion' => $client['clientVersion'], - 'clientEngine' => $client['clientEngine'], - 'clientEngineVersion' => $client['clientEngineVersion'], - 'deviceName' => $device['deviceName'], - 'deviceBrand' => $device['deviceBrand'], - 'deviceModel' => $device['deviceModel'] + 'ip' => $log['ip'] ?? null, + 'time' => $log['time'] ?? null, + 'osCode' => $os['osCode'] ?? null, + 'osName' => $os['osName'] ?? null, + 'osVersion' => $os['osVersion'] ?? null, + 'clientType' => $client['clientType'] ?? null, + 'clientCode' => $client['clientCode'] ?? null, + 'clientName' => $client['clientName'] ?? null, + 'clientVersion' => $client['clientVersion'] ?? null, + 'clientEngine' => $client['clientEngine'] ?? null, + 'clientEngineVersion' => $client['clientEngineVersion'] ?? null, + 'deviceName' => $device['deviceName'] ?? null, + 'deviceBrand' => $device['deviceBrand'] ?? null, + 'deviceModel' => $device['deviceModel'] ?? null ]); $record = $geodb->get($log['ip']); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php index e319a33e67..304ce5c88e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php @@ -71,12 +71,13 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void + public function action(string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void { - $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, params: [$databaseId]); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Usage/Get.php index c4a46650c9..0552a31509 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Usage/Get.php @@ -63,10 +63,11 @@ class Get extends Action ->param('collectionId', '', new UID(), 'Collection ID.') ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $range, string $collectionId, UtopiaResponse $response, Database $dbForProject): void + public function action(string $databaseId, string $range, string $collectionId, UtopiaResponse $response, Database $dbForProject, Authorization $authorization): void { $database = $dbForProject->getDocument('databases', $databaseId); $collectionDocument = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId); @@ -83,7 +84,7 @@ class Get extends Action str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getSequence(), $collectionDocument->getSequence()], 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/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/XList.php index b0b0385bf5..c23286f3cd 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/XList.php @@ -67,12 +67,13 @@ class XList extends Action ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, array $queries, string $search, bool $includeTotal, UtopiaResponse $response, Database $dbForProject): void + public function action(string $databaseId, array $queries, string $search, bool $includeTotal, UtopiaResponse $response, Database $dbForProject, Authorization $authorization): void { - $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, params: [$databaseId]); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php index 20c71223c6..4ca20f8414 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php @@ -55,10 +55,11 @@ class Create extends Action ->inject('response') ->inject('dbForProject') ->inject('user') + ->inject('authorization') ->callback($this->action(...)); } - public function action(int $ttl, UtopiaResponse $response, Database $dbForProject, Document $user): void + public function action(int $ttl, UtopiaResponse $response, Database $dbForProject, Document $user, Authorization $authorization): void { $permissions = []; if (!empty($user->getId())) { @@ -73,7 +74,7 @@ class Create extends Action } } - $transaction = Authorization::skip(fn () => $dbForProject->createDocument('transactions', new Document([ + $transaction = $authorization->skip(fn () => $dbForProject->createDocument('transactions', new Document([ '$id' => ID::unique(), '$permissions' => $permissions, 'status' => 'pending', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php index 5a2568db0c..f09ed2bc27 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php @@ -18,6 +18,7 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; use Utopia\Validator\ArrayList; @@ -63,21 +64,22 @@ class Create extends Action ->inject('dbForProject') ->inject('transactionState') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $transactionId, array $operations, UtopiaResponse $response, Database $dbForProject, TransactionState $transactionState, array $plan): void + public function action(string $transactionId, array $operations, UtopiaResponse $response, Database $dbForProject, TransactionState $transactionState, array $plan, Authorization $authorization): void { if (empty($operations)) { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Operations array cannot be empty'); } - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); // API keys and admins can read any transaction, regular users need permissions $transaction = ($isAPIKey || $isPrivilegedUser) - ? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) + ? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) : $dbForProject->getDocument('transactions', $transactionId); if ($transaction->isEmpty()) { throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]); @@ -113,13 +115,13 @@ class Create extends Action throw new Exception(Exception::USER_UNAUTHORIZED); } - $database = $databases[$operation['databaseId']] ??= Authorization::skip(fn () => $dbForProject->getDocument('databases', $operation['databaseId'])); + $database = $databases[$operation['databaseId']] ??= $authorization->skip(fn () => $dbForProject->getDocument('databases', $operation['databaseId'])); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$operation['databaseId']]); } $collection = $collections[$operation[$this->getGroupId()]] ??= - Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $operation[$this->getGroupId()])); + $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $operation[$this->getGroupId()])); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND, params: [$operation[$this->getGroupId()]]); @@ -165,14 +167,20 @@ class Create extends Action // For individual operations, enforce permissions unless using API key/admin if (!$isAPIKey && !$isPrivilegedUser) { $documentSecurity = $collection->getAttribute('documentSecurity', false); - $validator = new Authorization($permissionType); - $collectionValid = $validator->isValid($collection->getPermissionsByType($permissionType)); + + $collectionValid = $authorization->isValid( + new Input($permissionType, $collection->getPermissionsByType($permissionType)) + ); $documentValid = false; if ($document !== null && !$document->isEmpty() && $documentSecurity) { if ($permissionType === Database::PERMISSION_UPDATE) { - $documentValid = $validator->isValid($document->getUpdate()); + $documentValid = $authorization->isValid( + new Input(Database::PERMISSION_UPDATE, $document->getUpdate()) + ); } elseif ($permissionType === Database::PERMISSION_DELETE) { - $documentValid = $validator->isValid($document->getDelete()); + $documentValid = $authorization->isValid( + new Input(Database::PERMISSION_DELETE, $document->getDelete()) + ); } } @@ -189,7 +197,7 @@ class Create extends Action // Users can only set permissions for roles they have if (isset($operation['data']['$permissions'])) { $permissions = $operation['data']['$permissions']; - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { $permission = Permission::parse($permission); @@ -201,7 +209,7 @@ class Create extends Action $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!Authorization::isRole($role)) { + if (!$authorization->hasRole($role)) { throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); } } @@ -230,7 +238,7 @@ class Create extends Action } } - $transaction = Authorization::skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) { + $transaction = $authorization->skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) { $dbForProject->createDocuments('transactionLogs', $staged); return $dbForProject->increaseDocumentAttribute( 'transactions', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 9235c81b8e..e4f1051464 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -76,6 +76,7 @@ class Update extends Action ->inject('queueForRealtime') ->inject('queueForFunctions') ->inject('queueForWebhooks') + ->inject('authorization') ->callback($this->action(...)); } @@ -102,7 +103,7 @@ class Update extends Action * @throws Structure * @throws \Utopia\Exception */ - public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, Document $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks): void + public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, Document $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization): void { if (!$commit && !$rollback) { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true'); @@ -111,11 +112,11 @@ class Update extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Cannot commit and rollback at the same time'); } - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); $transaction = ($isAPIKey || $isPrivilegedUser) - ? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) + ? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) : $dbForProject->getDocument('transactions', $transactionId); if ($transaction->isEmpty()) { throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]); @@ -138,12 +139,12 @@ class Update extends Action $currentDocumentId = null; try { - $dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $queueForStatsUsage, $queueForRealtime, $queueForFunctions, $queueForWebhooks) { - Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ + $dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $queueForStatsUsage, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) { + $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'committing', ]))); - $operations = Authorization::skip(fn () => $dbForProject->find('transactionLogs', [ + $operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [ Query::equal('transactionInternalId', [$transaction->getSequence()]), Query::orderAsc(), Query::limit(PHP_INT_MAX), @@ -167,7 +168,7 @@ class Update extends Action } if (!isset($collections[$collectionId])) { - $collections[$collectionId] = Authorization::skip( + $collections[$collectionId] = $authorization->skip( fn () => $dbForProject->getCollection($collectionId) ); } @@ -232,7 +233,7 @@ class Update extends Action } } - $transaction = Authorization::skip(fn () => $dbForProject->updateDocument( + $transaction = $authorization->skip(fn () => $dbForProject->updateDocument( 'transactions', $transactionId, new Document(['status' => 'committed']) @@ -243,33 +244,33 @@ class Update extends Action ->setDocument($transaction); }); } catch (NotFoundException $e) { - Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ + $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ]))); throw new Exception(Exception::DOCUMENT_NOT_FOUND, previous: $e, params: [$currentDocumentId ?? 'unknown']); } catch (DuplicateException | ConflictException $e) { - Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ + $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ]))); throw new Exception(Exception::TRANSACTION_CONFLICT, previous: $e); } catch (StructureException $e) { - Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ + $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ]))); throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); } catch (LimitException $e) { - Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ + $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ]))); throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, $e->getMessage()); } catch (TransactionException $e) { - Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ + $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ]))); throw new Exception(Exception::TRANSACTION_FAILED, $e->getMessage()); } catch (QueryException $e) { - Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ + $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ]))); throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); @@ -297,11 +298,11 @@ class Update extends Action $data = $data->getArrayCopy(); } - $database = Authorization::skip(fn () => $dbForProject->findOne('databases', [ + $database = $authorization->skip(fn () => $dbForProject->findOne('databases', [ Query::equal('$sequence', [$databaseInternalId]) ])); - $collection = Authorization::skip(fn () => $dbForProject->findOne('database_' . $databaseInternalId, [ + $collection = $authorization->skip(fn () => $dbForProject->findOne('database_' . $databaseInternalId, [ Query::equal('$sequence', [$collectionInternalId]) ])); @@ -393,7 +394,7 @@ class Update extends Action } if ($rollback) { - $transaction = Authorization::skip(fn () => $dbForProject->updateDocument( + $transaction = $authorization->skip(fn () => $dbForProject->updateDocument( 'transactions', $transactionId, new Document(['status' => 'failed']) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php index a717b00ae4..a1aa7a70b8 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php @@ -59,10 +59,11 @@ class Get extends Action ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $range, UtopiaResponse $response, Database $dbForProject): void + public function action(string $databaseId, string $range, UtopiaResponse $response, Database $dbForProject, Authorization $authorization): void { $database = $dbForProject->getDocument('databases', $databaseId); @@ -81,7 +82,7 @@ class Get extends Action str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES) ]; - 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/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php index c13149cfc7..757f845c68 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php @@ -56,10 +56,11 @@ class XList extends Action ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $range, UtopiaResponse $response, Database $dbForProject): void + public function action(string $range, UtopiaResponse $response, Database $dbForProject, Authorization $authorization): void { $periods = Config::getParam('usage', []); @@ -74,7 +75,7 @@ class XList extends Action METRIC_DATABASES_OPERATIONS_WRITES, ]; - 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/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php index c0d502d10a..eede1b221b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php @@ -60,6 +60,7 @@ class Create extends BooleanCreate ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Update.php index c5939b6974..cd8d392cfc 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Update.php @@ -61,6 +61,7 @@ class Update extends BooleanUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php index 63693abb67..79722efee1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php @@ -62,6 +62,7 @@ class Create extends DatetimeCreate ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Update.php index b022d0ed85..c39681a743 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Update.php @@ -63,6 +63,7 @@ class Update extends DatetimeUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php index 8a691a6e98..da63b0cef7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php @@ -58,6 +58,7 @@ class Delete extends AttributesDelete ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php index 6d19f99b7b..51e7f295a1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php @@ -61,6 +61,7 @@ class Create extends EmailCreate ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Update.php index 48a04304bd..daca13d587 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Update.php @@ -62,6 +62,7 @@ class Update extends EmailUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php index bd280a2910..4d5881c81e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php @@ -64,6 +64,7 @@ class Create extends EnumCreate ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Update.php index ac5c1cf907..122671adc5 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Update.php @@ -65,6 +65,7 @@ class Update extends EnumUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php index 8293d66992..cd898fa0bf 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php @@ -63,6 +63,7 @@ class Create extends FloatCreate ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Update.php index bf2815db45..ee9c5f6cb1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Update.php @@ -64,6 +64,7 @@ class Update extends FloatUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Get.php index ee88ac8683..39dafbd1a6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Get.php @@ -61,6 +61,7 @@ class Get extends AttributesGet ->param('key', '', new Key(), 'Column Key.') ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php index 9b38cd9dfd..80c764b4c5 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php @@ -61,6 +61,7 @@ class Create extends IPCreate ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Update.php index 7db8625ebf..54ed029c71 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Update.php @@ -62,6 +62,7 @@ class Update extends IPUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php index e0ed059681..45e0cc6f60 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php @@ -63,6 +63,7 @@ class Create extends IntegerCreate ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Update.php index 7afc239201..f1f4ebb4a9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Update.php @@ -64,6 +64,7 @@ class Update extends IntegerUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php index 6110d6ee07..227fece7de 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php @@ -61,6 +61,7 @@ class Create extends LineCreate ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Update.php index afd0098152..b0e433da5f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Update.php @@ -63,6 +63,7 @@ class Update extends LineUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php index 084adca860..3fc5865905 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php @@ -61,6 +61,7 @@ class Create extends PointCreate ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Update.php index 632be85871..040b8171d7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Update.php @@ -63,6 +63,7 @@ class Update extends PointUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php index 723940af58..630340ba7b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php @@ -61,6 +61,7 @@ class Create extends PolygonCreate ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Update.php index 91b55f74b4..43b4a4e6a4 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Update.php @@ -63,6 +63,7 @@ class Update extends PolygonUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php index f3933160c0..7f28a3cdb7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php @@ -73,6 +73,7 @@ class Create extends RelationshipCreate ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Update.php index eb87713457..fd7fdab8de 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Update.php @@ -65,6 +65,7 @@ class Update extends RelationshipUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php index 9279409e88..ff50313a7c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php @@ -66,6 +66,7 @@ class Create extends StringCreate ->inject('queueForDatabase') ->inject('queueForEvents') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Update.php index 9fffa71b33..6ad1be124b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Update.php @@ -65,6 +65,7 @@ class Update extends StringUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php index 50f5ea5d5b..b19d6e80a2 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php @@ -61,6 +61,7 @@ class Create extends URLCreate ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Update.php index b52ea66ce1..dce11964e8 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Update.php @@ -62,6 +62,7 @@ class Update extends URLUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/XList.php index 39551e5113..13ebe14682 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/XList.php @@ -52,6 +52,7 @@ class XList extends AttributesXList ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Create.php index 7287c2cb3e..bd08ad5617 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Create.php @@ -67,6 +67,7 @@ class Create extends CollectionCreate ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php index d4af8b3508..925a7b2494 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php @@ -55,6 +55,7 @@ class Delete extends CollectionDelete ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Get.php index 4286ee07ca..ad83291815 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Get.php @@ -50,6 +50,7 @@ class Get extends CollectionGet ->param('tableId', '', new UID(), 'Table ID.') ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php index 727334b6da..09720f4d71 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php @@ -66,6 +66,8 @@ class Create extends IndexCreate ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } + } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php index 7d187ab5a1..7fa8073d1e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php @@ -61,6 +61,7 @@ class Delete extends IndexDelete ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Get.php index 75ee507aa8..246d569825 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Get.php @@ -52,6 +52,7 @@ class Get extends IndexGet ->param('key', null, new Key(), 'Index Key.') ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/XList.php index bf5f27e388..1dc2d3ea43 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/XList.php @@ -54,6 +54,7 @@ class XList extends IndexXList ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Logs/XList.php index 5eab050b7e..79691436e4 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Logs/XList.php @@ -50,6 +50,7 @@ class XList extends CollectionLogXList ->inject('dbForProject') ->inject('locale') ->inject('geodb') + ->inject('authorization') ->inject('audit') ->callback($this->action(...)); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php index accb0392fe..b9896d282d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php @@ -66,6 +66,7 @@ class Delete extends DocumentsDelete ->inject('queueForFunctions') ->inject('queueForWebhooks') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php index fea59b8b13..f4ccea1698 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php @@ -68,6 +68,7 @@ class Update extends DocumentsUpdate ->inject('queueForFunctions') ->inject('queueForWebhooks') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php index 492af25e9f..69a687d92f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php @@ -68,6 +68,7 @@ class Upsert extends DocumentsUpsert ->inject('queueForFunctions') ->inject('queueForWebhooks') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Decrement.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Decrement.php index 42f2919ce1..a660b008e1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Decrement.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Decrement.php @@ -67,6 +67,7 @@ class Decrement extends DecrementDocumentAttribute ->inject('queueForEvents') ->inject('queueForStatsUsage') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Increment.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Increment.php index 3d04d71c26..c2b69429ce 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Increment.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Increment.php @@ -67,6 +67,7 @@ class Increment extends IncrementDocumentAttribute ->inject('queueForEvents') ->inject('queueForStatsUsage') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php index b5491a593b..c70ed71378 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php @@ -111,6 +111,7 @@ class Create extends DocumentCreate ->inject('queueForFunctions') ->inject('queueForWebhooks') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Delete.php index bcd8682a48..1763491c19 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Delete.php @@ -70,6 +70,7 @@ class Delete extends DocumentDelete ->inject('queueForStatsUsage') ->inject('transactionState') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Get.php index 450fb4d746..bb24e93de0 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Get.php @@ -58,6 +58,7 @@ class Get extends DocumentGet ->inject('dbForProject') ->inject('queueForStatsUsage') ->inject('transactionState') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Logs/XList.php index 27bd82195d..86bfcfec85 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Logs/XList.php @@ -51,6 +51,7 @@ class XList extends DocumentLogXList ->inject('dbForProject') ->inject('locale') ->inject('geodb') + ->inject('authorization') ->inject('audit') ->callback($this->action(...)); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Update.php index fe4ffc4995..0879055a78 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Update.php @@ -69,6 +69,7 @@ class Update extends DocumentUpdate ->inject('queueForStatsUsage') ->inject('transactionState') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php index 0fbaa921cb..99e0487c93 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php @@ -72,6 +72,7 @@ class Upsert extends DocumentUpsert ->inject('queueForStatsUsage') ->inject('transactionState') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/XList.php index c51017fa75..230d391110 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/XList.php @@ -59,6 +59,7 @@ class XList extends DocumentXList ->inject('dbForProject') ->inject('queueForStatsUsage') ->inject('transactionState') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Update.php index 03316783cd..0d3bc9afc1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Update.php @@ -62,6 +62,7 @@ class Update extends CollectionUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Usage/Get.php index 0fb44ee94a..b8be7edd56 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Usage/Get.php @@ -52,6 +52,7 @@ class Get extends CollectionUsageGet ->param('tableId', '', new UID(), 'Table ID.') ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/XList.php index e0c590379b..5532203d0a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/XList.php @@ -55,6 +55,7 @@ class XList extends CollectionXList ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Create.php index 27454664f4..e7e5f0132f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Create.php @@ -50,6 +50,7 @@ class Create extends TransactionsCreate ->inject('response') ->inject('dbForProject') ->inject('user') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php index 4668ae2d15..1228c83e30 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php @@ -54,6 +54,7 @@ class Create extends OperationsCreate ->inject('dbForProject') ->inject('transactionState') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php index 4337a8d28d..8be28ce9f7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php @@ -60,6 +60,7 @@ class Update extends TransactionsUpdate ->inject('queueForRealtime') ->inject('queueForFunctions') ->inject('queueForWebhooks') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/Get.php index 89b9fbd8c2..87be8a9eab 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/Get.php @@ -48,6 +48,7 @@ class Get extends DatabaseUsageGet ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/XList.php index 0bd96fc40a..2cde337f5f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/XList.php @@ -46,6 +46,7 @@ class XList extends DatabaseUsageXList ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php index e7e34d4c5b..c5ae08728d 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php @@ -17,6 +17,7 @@ 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\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -88,6 +89,7 @@ class Create extends Action ->inject('deviceForLocal') ->inject('queueForBuilds') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } @@ -105,7 +107,8 @@ class Create extends Action Device $deviceForFunctions, Device $deviceForLocal, Build $queueForBuilds, - array $plan + array $plan, + Authorization $authorization ) { $activate = \strval($activate) === 'true' || \strval($activate) === '1'; diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php index 0aaea3bd4a..acfaa965ac 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php @@ -15,6 +15,7 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -77,6 +78,7 @@ class Create extends Base ->inject('project') ->inject('queueForBuilds') ->inject('gitHub') + ->inject('authorization') ->callback($this->action(...)); } @@ -95,7 +97,8 @@ class Create extends Base Event $queueForEvents, Document $project, Build $queueForBuilds, - GitHub $github + GitHub $github, + Authorization $authorization ) { $function = $dbForProject->getDocument('functions', $functionId); @@ -127,7 +130,9 @@ class Create extends Base queueForBuilds: $queueForBuilds, template: $template, github: $github, - activate: $activate + activate: $activate, + referenceType: $type, + reference: $reference ); $queueForEvents @@ -170,6 +175,9 @@ class Create extends Base ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); $dbForProject->updateDocument('functions', $function->getId(), $function); + + $this->updateEmptyManualRule($project, $function, $deployment, $dbForPlatform, $authorization); + $queueForBuilds ->setType(BUILD_TYPE_DEPLOYMENT) ->setResource($function) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Vcs/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Vcs/Create.php index 69594c3d86..25dce63b38 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Vcs/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Vcs/Create.php @@ -87,7 +87,7 @@ class Create extends Base Document $project, Event $queueForEvents, Build $queueForBuilds, - GitHub $github + GitHub $github, ) { $function = $dbForProject->getDocument('functions', $functionId); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index 81f55ba829..1a265298d3 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -29,6 +29,7 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; @@ -99,6 +100,7 @@ class Create extends Base ->inject('proofForToken') ->inject('executor') ->inject('platform') + ->inject('authorization') ->callback($this->action(...)); } @@ -123,7 +125,8 @@ class Create extends Base Store $store, Token $proofForToken, Executor $executor, - array $platform + array $platform, + Authorization $authorization, ) { $async = \strval($async) === 'true' || \strval($async) === '1'; @@ -161,10 +164,10 @@ class Create extends Base throw new Exception($validator->getDescription(), 400); } - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); @@ -180,7 +183,7 @@ class Create extends Base throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); } - $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deploymentId', ''))); + $deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deploymentId', ''))); 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'); @@ -194,10 +197,8 @@ class Create extends Base 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 @@ -295,7 +296,7 @@ class Create extends Base 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) @@ -336,7 +337,7 @@ class Create extends Base ->setAttribute('scheduleInternalId', $schedule->getSequence()) ->setAttribute('scheduledAt', $scheduledAt); - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); } return $response @@ -488,7 +489,7 @@ class Create extends Base ->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) ; - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); } $executionResponse['headers']['x-appwrite-execution-id'] = $execution->getId(); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php index 9a93e5a342..c7a9a6d330 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php @@ -61,6 +61,7 @@ class Delete extends Base ->inject('dbForProject') ->inject('dbForPlatform') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } @@ -70,7 +71,8 @@ class Delete extends Base Response $response, Database $dbForProject, Database $dbForPlatform, - Event $queueForEvents + Event $queueForEvents, + Authorization $authorization ) { $function = $dbForProject->getDocument('functions', $functionId); @@ -108,7 +110,7 @@ class Delete extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php index 6bd0a3675e..c5eebe139e 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php @@ -52,6 +52,7 @@ class Get extends Base ->param('executionId', '', new UID(), 'Execution ID.') ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } @@ -59,12 +60,13 @@ class Get extends Base string $functionId, string $executionId, Response $response, - Database $dbForProject + Database $dbForProject, + Authorization $authorization ) { - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php index 20680e87ff..ff381e1f3d 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php @@ -60,6 +60,7 @@ class XList extends Base ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } @@ -68,12 +69,13 @@ class XList extends Base array $queries, bool $includeTotal, Response $response, - Database $dbForProject + Database $dbForProject, + Authorization $authorization ) { - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index 5c226c5925..6ad488283e 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -115,6 +115,7 @@ class Create extends Base ->inject('dbForPlatform') ->inject('request') ->inject('gitHub') + ->inject('authorization') ->callback($this->action(...)); } @@ -152,7 +153,8 @@ class Create extends Base Func $queueForFunctions, Database $dbForPlatform, Request $request, - GitHub $github + GitHub $github, + Authorization $authorization ) { // Temporary abuse check @@ -237,7 +239,7 @@ class Create extends Base throw new Exception(Exception::FUNCTION_ALREADY_EXISTS); } - $schedule = Authorization::skip( + $schedule = $authorization->skip( fn () => $dbForPlatform->createDocument('schedules', new Document([ 'region' => $project->getAttribute('region'), 'resourceType' => SCHEDULE_RESOURCE_TYPE_FUNCTION, @@ -315,6 +317,7 @@ class Create extends Base template: $template, github: $github, activate: true, + authorization: $authorization, reference: $providerBranch, referenceType: 'branch' ); @@ -366,7 +369,7 @@ class Create extends Base $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; $ruleId = $isMd5 ? md5($domain) : ID::unique(); - $rule = Authorization::skip( + $rule = $authorization->skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php index dfa6636554..9cafc17bbe 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php @@ -61,6 +61,7 @@ class Delete extends Base ->inject('queueForDeletes') ->inject('queueForEvents') ->inject('dbForPlatform') + ->inject('authorization') ->callback($this->action(...)); } @@ -70,7 +71,8 @@ class Delete extends Base Database $dbForProject, DeleteEvent $queueForDeletes, Event $queueForEvents, - Database $dbForPlatform + Database $dbForPlatform, + Authorization $authorization ) { $function = $dbForProject->getDocument('functions', $functionId); @@ -87,7 +89,7 @@ class Delete extends Base $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php index b6dcfd6cf8..aeccf98a02 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php @@ -62,6 +62,7 @@ class Update extends Base ->inject('dbForProject') ->inject('queueForEvents') ->inject('dbForPlatform') + ->inject('authorization') ->callback($this->action(...)); } @@ -72,7 +73,8 @@ class Update extends Base Response $response, Database $dbForProject, Event $queueForEvents, - Database $dbForPlatform + Database $dbForPlatform, + Authorization $authorization ) { $function = $dbForProject->getDocument('functions', $functionId); $deployment = $dbForProject->getDocument('deployments', $deploymentId); @@ -101,7 +103,7 @@ class Update extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId'))); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); $queries = [ Query::equal('trigger', ['manual']), @@ -112,12 +114,12 @@ class Update extends Base Query::equal('projectInternalId', [$project->getSequence()]) ]; - Authorization::skip(fn () => $dbForPlatform->foreach('rules', function (Document $rule) use ($dbForPlatform, $deployment) { + $authorization->skip(fn () => $dbForPlatform->foreach('rules', function (Document $rule) use ($dbForPlatform, $deployment, $authorization) { $rule = $rule ->setAttribute('deploymentId', $deployment->getId()) ->setAttribute('deploymentInternalId', $deployment->getSequence()); - Authorization::skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), $rule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), $rule)); }, $queries)); $queueForEvents diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index adb29bc533..55c5b30418 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -104,6 +104,7 @@ class Update extends Base ->inject('dbForPlatform') ->inject('gitHub') ->inject('executor') + ->inject('authorization') ->callback($this->action(...)); } @@ -134,7 +135,8 @@ class Update extends Base Build $queueForBuilds, Database $dbForPlatform, GitHub $github, - Executor $executor + Executor $executor, + Authorization $authorization ) { // TODO: If only branch changes, re-deploy $function = $dbForProject->getDocument('functions', $functionId); @@ -282,7 +284,7 @@ class Update extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId'))); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); $queueForEvents->setParam('functionId', $function->getId()); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Usage/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Usage/Get.php index acb6995d6f..1fa65d0cc9 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Usage/Get.php @@ -55,10 +55,11 @@ class Get extends Base ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $functionId, string $range, Response $response, Database $dbForProject) + public function action(string $functionId, string $range, Response $response, Database $dbForProject, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); @@ -83,7 +84,7 @@ class Get extends Base str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getSequence()], METRIC_RESOURCE_TYPE_ID_BUILDS_FAILED), ]; - 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/src/Appwrite/Platform/Modules/Functions/Http/Usage/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Usage/XList.php index 6a4ded4db7..38a95d4469 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Usage/XList.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Usage/XList.php @@ -52,10 +52,11 @@ class XList extends Base ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $range, Response $response, Database $dbForProject) + public function action(string $range, Response $response, Database $dbForProject, Authorization $authorization) { $periods = Config::getParam('usage', []); $stats = $usage = []; @@ -75,7 +76,7 @@ class XList extends Base str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_BUILDS_FAILED), ]; - 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/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php index 815f1bd8fc..5438479d40 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php @@ -65,6 +65,7 @@ class Create extends Base ->inject('dbForProject') ->inject('dbForPlatform') ->inject('project') + ->inject('authorization') ->callback($this->action(...)); } @@ -76,7 +77,8 @@ class Create extends Base Response $response, Database $dbForProject, Database $dbForPlatform, - Document $project + Document $project, + Authorization $authorization ) { $function = $dbForProject->getDocument('functions', $functionId); @@ -119,7 +121,7 @@ class Create extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId'))); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); $response ->setStatusCode(Response::STATUS_CODE_CREATED) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php index 50c1de4232..161eed3112 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php @@ -57,6 +57,7 @@ class Delete extends Base ->inject('response') ->inject('dbForProject') ->inject('dbForPlatform') + ->inject('authorization') ->callback($this->action(...)); } @@ -65,7 +66,8 @@ class Delete extends Base string $variableId, Response $response, Database $dbForProject, - Database $dbForPlatform + Database $dbForPlatform, + Authorization $authorization ) { $function = $dbForProject->getDocument('functions', $functionId); @@ -92,7 +94,7 @@ class Delete extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId'))); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); $response->noContent(); } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php index 5c1f5809cd..6af5ac90c2 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php @@ -62,6 +62,7 @@ class Update extends Base ->inject('response') ->inject('dbForProject') ->inject('dbForPlatform') + ->inject('authorization') ->callback($this->action(...)); } @@ -73,7 +74,8 @@ class Update extends Base ?bool $secret, Response $response, Database $dbForProject, - Database $dbForPlatform + Database $dbForPlatform, + Authorization $authorization ) { $function = $dbForProject->getDocument('functions', $functionId); @@ -110,7 +112,7 @@ class Update extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId'))); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); $response->dynamic($variable, Response::MODEL_VARIABLE); } diff --git a/src/Appwrite/Platform/Modules/Functions/Services/Workers.php b/src/Appwrite/Platform/Modules/Functions/Services/Workers.php index 61256b6bf9..2acb31ad8a 100644 --- a/src/Appwrite/Platform/Modules/Functions/Services/Workers.php +++ b/src/Appwrite/Platform/Modules/Functions/Services/Workers.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Functions\Services; use Appwrite\Platform\Modules\Functions\Workers\Builds; +use Appwrite\Platform\Modules\Functions\Workers\Screenshots; use Utopia\Platform\Service; class Workers extends Service @@ -11,5 +12,6 @@ class Workers extends Service { $this->type = Service::TYPE_WORKER; $this->addAction(Builds::getName(), new Builds()); + $this->addAction(Screenshots::getName(), new Screenshots()); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index e38a56bd2b..ac1fee6bad 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -6,10 +6,9 @@ use Ahc\Jwt\JWT; use Appwrite\Event\Event; use Appwrite\Event\Func; use Appwrite\Event\Realtime; +use Appwrite\Event\Screenshot; use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; -use Appwrite\Permission; -use Appwrite\Role; use Appwrite\Utopia\Response\Model\Deployment; use Appwrite\Vcs\Comment; use Exception; @@ -25,24 +24,18 @@ use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; -use Utopia\Database\Helpers\ID; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization; use Utopia\Detector\Detection\Rendering\SSR; use Utopia\Detector\Detection\Rendering\XStatic; use Utopia\Detector\Detector\Rendering; -use Utopia\Fetch\Client as FetchClient; use Utopia\Logger\Log; use Utopia\Platform\Action; use Utopia\Queue\Message; -use Utopia\Storage\Compression\Compression; use Utopia\Storage\Device; use Utopia\Storage\Device\Local; use Utopia\System\System; use Utopia\VCS\Adapter\Git\GitHub; -use function Swoole\Coroutine\batch; - class Builds extends Action { public static function getName(): string @@ -62,6 +55,7 @@ class Builds extends Action ->inject('project') ->inject('dbForPlatform') ->inject('queueForEvents') + ->inject('queueForScreenshots') ->inject('queueForWebhooks') ->inject('queueForFunctions') ->inject('queueForRealtime') @@ -83,6 +77,7 @@ class Builds extends Action * @param Document $project * @param Database $dbForPlatform * @param Event $queueForEvents + * @param Screenshot $queueForScreenshots * @param Webhook $queueForWebhooks * @param Func $queueForFunctions * @param Realtime $queueForRealtime @@ -103,6 +98,7 @@ class Builds extends Action Document $project, Database $dbForPlatform, Event $queueForEvents, + Screenshot $queueForScreenshots, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, @@ -143,6 +139,7 @@ class Builds extends Action $deviceForFunctions, $deviceForSites, $deviceForFiles, + $queueForScreenshots, $queueForWebhooks, $queueForFunctions, $queueForRealtime, @@ -172,6 +169,7 @@ class Builds extends Action * @param Device $deviceForFunctions * @param Device $deviceForSites * @param Device $deviceForFiles + * @param Screenshot $queueForScreenshots * @param Webhook $queueForWebhooks * @param Func $queueForFunctions * @param Realtime $queueForRealtime @@ -196,6 +194,7 @@ class Builds extends Action Device $deviceForFunctions, Device $deviceForSites, Device $deviceForFiles, + Screenshot $queueForScreenshots, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, @@ -913,187 +912,6 @@ class Builds extends Action Console::log('Build details stored'); $this->afterBuildSuccess($queueForRealtime, $dbForProject, $deployment, $runtime, $adapter); - $logs = $deployment->getAttribute('buildLogs', ''); - - /** Screenshot site */ - if ($resource->getCollection() === 'sites') { - Console::log('Site screenshot started'); - - $date = \date('H:i:s'); - $logs .= "[$date] [appwrite] Screenshot capturing started. \n"; - $deployment->setAttribute('buildLogs', $logs); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); - $queueForRealtime - ->setPayload($deployment->getArrayCopy()) - ->trigger(); - - try { - $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ - Query::equal("projectInternalId", [$project->getSequence()]), - Query::equal("type", ["deployment"]), - Query::equal('deploymentInternalId', [$deployment->getSequence()]), - ])); - - if ($rule->isEmpty()) { - throw new \Exception("Rule for build not found"); - } - - $client = new FetchClient(); - $client->setTimeout(\intval($resource->getAttribute('timeout', '15')) * 1000); - $client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON); - - $bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); - - $configs = [ - 'screenshotLight' => [ - 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], - 'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=light', - 'theme' => 'light' - ], - 'screenshotDark' => [ - 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], - 'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=dark', - 'theme' => 'dark' - ], - ]; - - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0); - $apiKey = $jwtObj->encode([ - 'hostnameOverride' => true, - 'disabledMetrics' => [ - METRIC_EXECUTIONS, - METRIC_EXECUTIONS_COMPUTE, - METRIC_EXECUTIONS_MB_SECONDS, - METRIC_NETWORK_REQUESTS, - METRIC_NETWORK_INBOUND, - METRIC_NETWORK_OUTBOUND, - str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS), - str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE), - str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS), - str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS), - str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE), - str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), - ], - 'bannerDisabled' => true, - 'projectCheckDisabled' => true, - 'previewAuthDisabled' => true, - 'deploymentStatusIgnored' => true - ]); - - $screenshotError = null; - $screenshots = batch(\array_map(function ($key) use ($configs, $apiKey, $resource, $client, &$screenshotError) { - return function () use ($key, $configs, $apiKey, $resource, $client, &$screenshotError) { - try { - $config = $configs[$key]; - - $config['headers'] = \array_merge($config['headers'] ?? [], [ - 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey - ]); - $config['sleep'] = 3000; - - $frameworks = Config::getParam('frameworks', []); - $framework = $frameworks[$resource->getAttribute('framework', '')] ?? null; - if (!is_null($framework)) { - $config['sleep'] = $framework['screenshotSleep']; - } - - $browserEndpoint = System::getEnv('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1'); - $fetchResponse = $client->fetch( - url: $browserEndpoint . '/screenshots', - method: 'POST', - body: $config - ); - - if ($fetchResponse->getStatusCode() >= 400) { - throw new \Exception($fetchResponse->getBody()); - } - - $screenshot = $fetchResponse->getBody(); - - return ['key' => $key, 'screenshot' => $screenshot]; - } catch (\Throwable $th) { - $screenshotError = $th->getMessage(); - return; - } - }; - }, \array_keys($configs))); - - if (!\is_null($screenshotError)) { - throw new \Exception($screenshotError); - } - - $mimeType = "image/png"; - - foreach ($screenshots as $data) { - $key = $data['key']; - $screenshot = $data['screenshot']; - - $fileId = ID::unique(); - $fileName = $fileId . '.png'; - $path = $deviceForFiles->getPath($fileName); - $path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root - $success = $deviceForFiles->write($path, $screenshot, $mimeType); - - if (!$success) { - throw new \Exception("Screenshot failed to save"); - } - - $teamId = $project->getAttribute('teamId', ''); - $file = new Document([ - '$id' => $fileId, - '$permissions' => [ - Permission::read(Role::team(ID::custom($teamId))), - ], - 'bucketId' => $bucket->getId(), - 'bucketInternalId' => $bucket->getSequence(), - 'name' => $fileName, - 'path' => $path, - 'signature' => $deviceForFiles->getFileHash($path), - 'mimeType' => $mimeType, - 'sizeOriginal' => \strlen($screenshot), - 'sizeActual' => $deviceForFiles->getFileSize($path), - 'algorithm' => Compression::NONE, - 'comment' => '', - 'chunksTotal' => 1, - 'chunksUploaded' => 1, - 'openSSLVersion' => null, - 'openSSLCipher' => null, - 'openSSLTag' => null, - 'openSSLIV' => null, - 'search' => implode(' ', [$fileId, $fileName]), - 'metadata' => ['content_type' => $mimeType], - ]); - - Authorization::skip(fn () => $dbForPlatform->createDocument('bucket_' . $bucket->getSequence(), $file)); - - $deployment->setAttribute($key, $fileId); - } - - $logs = $deployment->getAttribute('buildLogs', ''); - $date = \date('H:i:s'); - $logs .= "[$date] [appwrite] Screenshot capturing finished. \n"; - - $deployment->setAttribute('buildLogs', $logs); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); - - $queueForRealtime - ->setPayload($deployment->getArrayCopy()) - ->trigger(); - } catch (\Throwable $th) { - Console::warning("Screenshot failed to generate:"); - Console::warning($th->getMessage()); - Console::warning($th->getTraceAsString()); - - $logs = $deployment->getAttribute('buildLogs', ''); - $date = \date('H:i:s'); - $logs .= "[$date] [appwrite] Screenshot capturing failed. Deployment will continue. \n"; - - $deployment->setAttribute('buildLogs', $logs); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); - } - - Console::log('Site screenshot finished'); - } $logs = $deployment->getAttribute('buildLogs', ''); $date = \date('H:i:s'); @@ -1114,6 +932,16 @@ class Builds extends Action $this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform); } + /** Screenshot site */ + if ($resource->getCollection() === 'sites') { + $queueForScreenshots + ->setDeploymentId($deployment->getId()) + ->setProject($project) + ->trigger(); + + Console::log('Site screenshot queued'); + } + /** Set auto deploy */ $activateBuild = false; if ($deployment->getAttribute('activate') === true) { @@ -1171,8 +999,6 @@ class Builds extends Action 'live' => true, 'deploymentId' => $deployment->getId(), 'deploymentInternalId' => $deployment->getSequence(), - 'deploymentScreenshotDark' => $deployment->getAttribute('screenshotDark', ''), - 'deploymentScreenshotLight' => $deployment->getAttribute('screenshotLight', ''), 'deploymentCreatedAt' => $deployment->getCreatedAt(), ])); $queries = [ @@ -1265,9 +1091,10 @@ class Builds extends Action $endTime = DateTime::now(); $durationEnd = \microtime(true); - $deployment->setAttribute('buildEndedAt', $endTime); - $deployment->setAttribute('buildDuration', \intval(\ceil($durationEnd - $durationStart))); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildEndedAt' => $endTime, + 'buildDuration' => \intval(\ceil($durationEnd - $durationStart)), + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) ->trigger(); @@ -1288,7 +1115,7 @@ class Builds extends Action ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $resource->getAttribute('schedule')) ->setAttribute('active', !empty($resource->getAttribute('schedule')) && !empty($resource->getAttribute('deploymentId'))); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule); } Console::info('Deployment action finished'); @@ -1497,7 +1324,6 @@ class Builds extends Action * @return void * @throws Structure * @throws \Utopia\Database\Exception - * @throws Authorization * @throws Conflict * @throws Restricted */ @@ -1586,11 +1412,11 @@ class Builds extends Action default => throw new \Exception('Invalid resource type') }; - $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ + $rule = $dbForPlatform->findOne('rules', [ Query::equal("projectInternalId", [$project->getSequence()]), Query::equal("type", ["deployment"]), Query::equal("deploymentInternalId", [$deployment->getSequence()]), - ])); + ]); $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; $previewUrl = match($resource->getCollection()) { diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php new file mode 100644 index 0000000000..ca89532307 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php @@ -0,0 +1,281 @@ +desc('Screenshots worker') + ->groups(['screenshots']) + ->inject('message') + ->inject('queueForRealtime') + ->inject('dbForPlatform') + ->inject('dbForProject') + ->inject('project') + ->inject('deviceForFiles') + ->callback($this->action(...)); + } + + public function action( + Message $message, + Realtime $queueForRealtime, + Database $dbForPlatform, + Database $dbForProject, + Document $project, + Device $deviceForFiles + ): void { + Console::log('Screenshot action started'); + + $payload = $message->getPayload() ?? []; + + if (empty($payload)) { + throw new \Exception('Missing payload'); + } + + Console::log('Site screenshot started'); + + $deploymentId = $payload['deploymentId'] ?? null; + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + if ($deployment->isEmpty()) { + throw new \Exception('Deployment not found'); + } + + $siteId = $deployment->getAttribute('resourceId'); + $site = $dbForProject->getDocument('sites', $siteId); + + if ($site->isEmpty()) { + throw new \Exception('Site not found'); + } + + // Realtime preparation + $event = "sites.[siteId].deployments.[deploymentId].update"; + $queueForRealtime + ->setSubscribers(['console']) + ->setProject($project) + ->setEvent($event) + ->setParam('siteId', $site->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $date = \date('H:i:s'); + $this->appendToLogs($dbForProject, $deployment->getId(), $queueForRealtime, "[$date] [appwrite] Screenshot capturing started. \n"); + + try { + $rule = $dbForPlatform->findOne('rules', [ + Query::equal("projectInternalId", [$project->getSequence()]), + Query::equal("type", ["deployment"]), + Query::equal('deploymentInternalId', [$deployment->getSequence()]), + ]); + + if ($rule->isEmpty()) { + throw new \Exception("Rule for deployment not found"); + } + + $client = new FetchClient(); + $client->setTimeout(\intval($site->getAttribute('timeout', '15')) * 1000); + $client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON); + + $bucket = $dbForPlatform->getDocument('buckets', 'screenshots'); + + if ($bucket->isEmpty()) { + throw new \Exception('Bucket not found'); + } + + $configs = [ + 'screenshotLight' => [ + 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], + 'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=light', + 'theme' => 'light' + ], + 'screenshotDark' => [ + 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], + 'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=dark', + 'theme' => 'dark' + ], + ]; + + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0); + $apiKey = $jwtObj->encode([ + 'hostnameOverride' => true, + 'disabledMetrics' => [ + METRIC_EXECUTIONS, + METRIC_EXECUTIONS_COMPUTE, + METRIC_EXECUTIONS_MB_SECONDS, + METRIC_NETWORK_REQUESTS, + METRIC_NETWORK_INBOUND, + METRIC_NETWORK_OUTBOUND, + str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS), + str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE), + str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS), + str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $site->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS), + str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $site->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE), + str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $site->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), + ], + 'bannerDisabled' => true, + 'projectCheckDisabled' => true, + 'previewAuthDisabled' => true, + 'deploymentStatusIgnored' => true + ]); + + $screenshotError = null; + $screenshots = batch(\array_map(function ($key) use ($configs, $apiKey, $site, $client, &$screenshotError) { + return function () use ($key, $configs, $apiKey, $site, $client, &$screenshotError) { + try { + $config = $configs[$key]; + + $config['headers'] = \array_merge($config['headers'] ?? [], [ + 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey + ]); + $config['sleep'] = 3000; + + $frameworks = Config::getParam('frameworks', []); + $framework = $frameworks[$site->getAttribute('framework', '')] ?? null; + if (!is_null($framework)) { + $config['sleep'] = $framework['screenshotSleep']; + } + + $browserEndpoint = System::getEnv('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1'); + $fetchResponse = $client->fetch( + url: $browserEndpoint . '/screenshots', + method: 'POST', + body: $config + ); + + if ($fetchResponse->getStatusCode() >= 400) { + throw new \Exception($fetchResponse->getBody()); + } + + $screenshot = $fetchResponse->getBody(); + + return ['key' => $key, 'screenshot' => $screenshot]; + } catch (\Throwable $th) { + $screenshotError = $th->getMessage(); + return; + } + }; + }, \array_keys($configs))); + + if (!\is_null($screenshotError)) { + throw new \Exception($screenshotError); + } + + $mimeType = "image/png"; + $updates = new Document([]); + + foreach ($screenshots as $data) { + $key = $data['key']; + $screenshot = $data['screenshot']; + + $fileId = ID::unique(); + $fileName = $fileId . '.png'; + $path = $deviceForFiles->getPath($fileName); + $path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root + $success = $deviceForFiles->write($path, $screenshot, $mimeType); + + if (!$success) { + throw new \Exception("Screenshot failed to save"); + } + + $teamId = $project->getAttribute('teamId', ''); + $file = new Document([ + '$id' => $fileId, + '$permissions' => [ + Permission::read(Role::team(ID::custom($teamId))), + ], + 'bucketId' => $bucket->getId(), + 'bucketInternalId' => $bucket->getSequence(), + 'name' => $fileName, + 'path' => $path, + 'signature' => $deviceForFiles->getFileHash($path), + 'mimeType' => $mimeType, + 'sizeOriginal' => \strlen($screenshot), + 'sizeActual' => $deviceForFiles->getFileSize($path), + 'algorithm' => Compression::NONE, + 'comment' => '', + 'chunksTotal' => 1, + 'chunksUploaded' => 1, + 'openSSLVersion' => null, + 'openSSLCipher' => null, + 'openSSLTag' => null, + 'openSSLIV' => null, + 'search' => implode(' ', [$fileId, $fileName]), + 'metadata' => ['content_type' => $mimeType], + ]); + + $dbForPlatform->createDocument('bucket_' . $bucket->getSequence(), $file); + + $updates->setAttribute($key, $fileId); + } + + $date = \date('H:i:s'); + $this->appendToLogs($dbForProject, $deployment->getId(), $queueForRealtime, "[$date] [appwrite] Screenshot capturing finished. \n"); + + // Apply screenshot properties + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $updates); + + $queueForRealtime + ->setPayload($deployment->getArrayCopy()) + ->trigger(); + + $site = $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'deploymentScreenshotDark' => $deployment->getAttribute('screenshotDark', ''), + 'deploymentScreenshotLight' => $deployment->getAttribute('screenshotLight', ''), + ])); + } catch (\Throwable $th) { + Console::warning("Screenshot failed to generate:"); + Console::warning($th->getMessage()); + Console::warning($th->getTraceAsString()); + + $date = \date('H:i:s'); + $this->appendToLogs($dbForProject, $deployment->getId(), $queueForRealtime, "[$date] [appwrite] Screenshot capturing failed. Deployment will continue. \n"); + + throw $th; + } + } + + protected function appendToLogs(Database $dbForProject, string $deploymentId, Realtime $queueForRealtime, string $logs) + { + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + $buildLogs = $deployment->getAttribute('buildLogs', ''); + $buildLogs .= $logs; + + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildLogs' => $buildLogs + ])); + + $queueForRealtime + ->setPayload($deployment->getArrayCopy()) + ->trigger(); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php index 4ba51bca37..3de0322d6e 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php @@ -87,6 +87,7 @@ class Create extends Action ->inject('deviceForLocal') ->inject('queueForBuilds') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } @@ -106,7 +107,8 @@ class Create extends Action Device $deviceForSites, Device $deviceForLocal, Build $queueForBuilds, - array $plan + array $plan, + Authorization $authorization ) { $activate = \strval($activate) === 'true' || \strval($activate) === '1'; @@ -276,7 +278,7 @@ class Create extends Action $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; $ruleId = $isMd5 ? md5($domain) : ID::unique(); - Authorization::skip( + $authorization->skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), @@ -341,7 +343,7 @@ class Create extends Action $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); - Authorization::skip( + $authorization->skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), @@ -366,6 +368,8 @@ class Create extends Action } } + + $metadata = null; $queueForEvents diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php index 2f9b1bdfde..9554e2aa14 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php @@ -65,6 +65,7 @@ class Create extends Action ->inject('queueForEvents') ->inject('queueForBuilds') ->inject('deviceForSites') + ->inject('authorization') ->callback($this->action(...)); } @@ -78,7 +79,8 @@ class Create extends Action Database $dbForPlatform, Event $queueForEvents, Build $queueForBuilds, - Device $deviceForSites + Device $deviceForSites, + Authorization $authorization ) { $site = $dbForProject->getDocument('sites', $siteId); @@ -147,7 +149,7 @@ class Create extends Action $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; $ruleId = $isMd5 ? md5($domain) : ID::unique(); - Authorization::skip( + $authorization->skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php index 5f1d446809..30d5e779c1 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -79,6 +79,7 @@ class Create extends Base ->inject('queueForEvents') ->inject('queueForBuilds') ->inject('gitHub') + ->inject('authorization') ->callback($this->action(...)); } @@ -97,7 +98,8 @@ class Create extends Base Document $project, Event $queueForEvents, Build $queueForBuilds, - GitHub $github + GitHub $github, + Authorization $authorization ) { $site = $dbForProject->getDocument('sites', $siteId); @@ -130,6 +132,7 @@ class Create extends Base template: $template, github: $github, activate: $activate, + authorization: $authorization, ); $queueForEvents @@ -189,7 +192,7 @@ class Create extends Base $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; $ruleId = $isMd5 ? md5($domain) : ID::unique(); - Authorization::skip( + $authorization->skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), @@ -209,6 +212,8 @@ class Create extends Base ])) ); + $this->updateEmptyManualRule($project, $site, $deployment, $dbForPlatform, $authorization); + $queueForBuilds ->setType(BUILD_TYPE_DEPLOYMENT) ->setResource($site) diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Vcs/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Vcs/Create.php index 915e3c5c9f..feff28427e 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Vcs/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Vcs/Create.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -72,6 +73,7 @@ class Create extends Base ->inject('queueForEvents') ->inject('queueForBuilds') ->inject('gitHub') + ->inject('authorization') ->callback($this->action(...)); } @@ -87,7 +89,8 @@ class Create extends Base Document $project, Event $queueForEvents, Build $queueForBuilds, - GitHub $github + GitHub $github, + Authorization $authorization ) { $site = $dbForProject->getDocument('sites', $siteId); @@ -110,6 +113,7 @@ class Create extends Base template: $template, github: $github, activate: $activate, + authorization: $authorization, reference: $reference, referenceType: $type ); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php index f962d0118d..b5d956128b 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php @@ -60,6 +60,7 @@ class Update extends Base ->inject('dbForProject') ->inject('queueForEvents') ->inject('dbForPlatform') + ->inject('authorization') ->callback($this->action(...)); } @@ -70,7 +71,8 @@ class Update extends Base Response $response, Database $dbForProject, Event $queueForEvents, - Database $dbForPlatform + Database $dbForPlatform, + Authorization $authorization ) { $site = $dbForProject->getDocument('sites', $siteId); $deployment = $dbForProject->getDocument('deployments', $deploymentId); @@ -104,12 +106,12 @@ class Update extends Base Query::equal('projectInternalId', [$project->getSequence()]) ]; - Authorization::skip(fn () => $dbForPlatform->foreach('rules', function (Document $rule) use ($dbForPlatform, $deployment) { + $authorization->skip(fn () => $dbForPlatform->foreach('rules', function (Document $rule) use ($dbForPlatform, $deployment, $authorization) { $rule = $rule ->setAttribute('deploymentId', $deployment->getId()) ->setAttribute('deploymentInternalId', $deployment->getSequence()); - Authorization::skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), $rule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), $rule)); }, $queries)); $queueForEvents diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Usage/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Usage/Get.php index af96c10457..5c274d6a20 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Usage/Get.php @@ -55,6 +55,7 @@ class Get extends Base ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } @@ -62,7 +63,8 @@ class Get extends Base string $siteId, string $range, Response $response, - Database $dbForProject + Database $dbForProject, + Authorization $authorization ) { $site = $dbForProject->getDocument('sites', $siteId); @@ -91,7 +93,7 @@ class Get extends Base ]; - 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/src/Appwrite/Platform/Modules/Sites/Http/Usage/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Usage/XList.php index d36cc56ae5..a90cb0cab9 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Usage/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Usage/XList.php @@ -52,10 +52,11 @@ class XList extends Base ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $range, Response $response, Database $dbForProject) + public function action(string $range, Response $response, Database $dbForProject, Authorization $authorization) { $periods = Config::getParam('usage', []); $stats = $usage = []; @@ -78,7 +79,7 @@ class XList extends Base METRIC_SITES_OUTBOUND, ]; - 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/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php index ed5c23b6c1..4757461a98 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php @@ -22,6 +22,7 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; @@ -90,6 +91,7 @@ class Create extends Action ->inject('mode') ->inject('deviceForFiles') ->inject('deviceForLocal') + ->inject('authorization') ->callback($this->action(...)); } @@ -105,26 +107,26 @@ class Create extends Action Event $queueForEvents, string $mode, Device $deviceForFiles, - Device $deviceForLocal + Device $deviceForLocal, + Authorization $authorization ) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } - $validator = new Authorization(\Utopia\Database\Database::PERMISSION_CREATE); - if (!$validator->isValid($bucket->getCreate())) { - throw new Exception(Exception::USER_UNAUTHORIZED); + if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) { + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } $allowedPermissions = [ - \Utopia\Database\Database::PERMISSION_READ, - \Utopia\Database\Database::PERMISSION_UPDATE, - \Utopia\Database\Database::PERMISSION_DELETE, + Database::PERMISSION_READ, + Database::PERMISSION_UPDATE, + Database::PERMISSION_DELETE, ]; // Map aggregate permissions to into the set of individual permissions they represent. @@ -141,7 +143,7 @@ class Create extends Action } // Users can only manage their own roles, API keys and Admin users can manage any - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); if (!$isAPIKey && !$isPrivilegedUser) { foreach (\Utopia\Database\Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { @@ -154,7 +156,7 @@ class Create extends Action $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!Authorization::isRole($role)) { + if (!$authorization->hasRole($role)) { throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); } } @@ -379,11 +381,10 @@ class Create extends Action * 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(\Utopia\Database\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->getSequence(), $fileId, $file)); + $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file)); } // Trigger after create success hook @@ -427,13 +428,12 @@ class Create extends Action * 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(\Utopia\Database\Database::PERMISSION_CREATE); - if (!$validator->isValid($bucket->getCreate())) { + if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) { throw new Exception(Exception::USER_UNAUTHORIZED); } try { - $file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file)); + $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file)); } catch (NotFoundException) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php index eccacaafd2..ca376842e2 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php @@ -14,6 +14,7 @@ use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -64,6 +65,7 @@ class Delete extends Action ->inject('queueForEvents') ->inject('deviceForFiles') ->inject('queueForDeletes') + ->inject('authorization') ->callback($this->action(...)); } @@ -75,33 +77,33 @@ class Delete extends Action Event $queueForEvents, Device $deviceForFiles, DeleteEvent $queueForDeletes, + Authorization $authorization ) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($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); + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } // Read permission should not be required for delete - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $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())) { - throw new Exception(Exception::USER_UNAUTHORIZED); + if ($fileSecurity && !$valid && !$authorization->isValid(new Input(Database::PERMISSION_DELETE, $file->getDelete()))) { + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } $deviceDeleted = false; @@ -125,7 +127,7 @@ class Delete extends Action if ($fileSecurity && !$valid) { $deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getSequence(), $fileId); } else { - $deleted = Authorization::skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getSequence(), $fileId)); + $deleted = $authorization->skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getSequence(), $fileId)); } } catch (NotFoundException) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Download/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Download/Get.php index 45e3b83375..bbceff51ec 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Download/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Download/Get.php @@ -14,6 +14,7 @@ use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -68,6 +69,7 @@ class Get extends Action ->inject('mode') ->inject('resourceToken') ->inject('deviceForFiles') + ->inject('authorization') ->callback($this->action(...)); } @@ -80,13 +82,14 @@ class Get extends Action Database $dbForProject, string $mode, Document $resourceToken, - Device $deviceForFiles + Device $deviceForFiles, + Authorization $authorization, ) { /* @type Document $bucket */ - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); @@ -94,17 +97,16 @@ class Get extends Action $isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence(); $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 && !$isToken) { - throw new Exception(Exception::USER_UNAUTHORIZED); + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } if ($fileSecurity && !$valid && !$isToken) { $file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId); } else { /* @type Document $file */ - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); } if (!$resourceToken->isEmpty() && $resourceToken->getAttribute('fileInternalId') !== $file->getSequence()) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Get.php index 77f163e5fb..caaab29efc 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Get.php @@ -10,6 +10,7 @@ use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -49,6 +50,7 @@ class Get extends Action ->param('fileId', '', new UID(), 'File ID.') ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } @@ -57,27 +59,27 @@ class Get extends Action string $fileId, Response $response, Database $dbForProject, + Authorization $authorization ) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($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); + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId); } else { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); } if ($file->isEmpty()) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php index 9c4e49d0bb..7ab3e713bc 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php @@ -17,6 +17,7 @@ use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\UID; use Utopia\Image\Image; use Utopia\Platform\Action; @@ -90,6 +91,7 @@ class Get extends Action ->inject('deviceForFiles') ->inject('deviceForLocal') ->inject('project') + ->inject('authorization') ->callback($this->action(...)); } @@ -114,7 +116,8 @@ class Get extends Action Document $resourceToken, Device $deviceForFiles, Device $deviceForLocal, - Document $project + Document $project, + Authorization $authorization ) { if (!\extension_loaded('imagick')) { @@ -122,10 +125,10 @@ class Get extends Action } /* @type Document $bucket */ - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); @@ -137,17 +140,16 @@ class Get extends Action $isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence(); $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 && !$isToken) { - throw new Exception(Exception::USER_UNAUTHORIZED); + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } if ($fileSecurity && !$valid && !$isToken) { $file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId); } else { /* @type Document $file */ - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); } if (!$resourceToken->isEmpty() && $resourceToken->getAttribute('fileInternalId') !== $file->getSequence()) { @@ -269,11 +271,11 @@ class Get extends Action $contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg']; //Do not update transformedAt if it's a console user - if (!User::isPrivileged(Authorization::getRoles())) { + if (!User::isPrivileged($authorization->getRoles())) { $transformedAt = $file->getAttribute('transformedAt', ''); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) { $file->setAttribute('transformedAt', DateTime::now()); - Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file)); + $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file)); } } diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Push/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Push/Get.php index 67372435b1..516343e23f 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Push/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Push/Get.php @@ -51,6 +51,7 @@ class Get extends Action ->inject('project') ->inject('mode') ->inject('deviceForFiles') + ->inject('authorization') ->callback($this->action(...)); } @@ -64,7 +65,8 @@ class Get extends Action Database $dbForPlatform, Document $project, string $mode, - Device $deviceForFiles + Device $deviceForFiles, + Authorization $authorization ) { $decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); @@ -86,15 +88,15 @@ class Get extends Action $disposition = $decoded['disposition'] ?? 'inline'; $dbForProject = $isInternal ? $dbForPlatform : $dbForProject; - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); if ($file->isEmpty()) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Update.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Update.php index be78cc358b..57856c1564 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Update.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Update.php @@ -14,6 +14,7 @@ use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; @@ -62,6 +63,7 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } @@ -72,26 +74,26 @@ class Update extends Action ?array $permissions, Response $response, Database $dbForProject, - Event $queueForEvents + Event $queueForEvents, + Authorization $authorization ) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($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); + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } // Read permission should not be required for update - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); if ($file->isEmpty()) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); @@ -105,7 +107,7 @@ class Update extends Action ]); // Users can only manage their own roles, API keys and Admin users can manage any - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); if (!User::isApp($roles) && !User::isPrivileged($roles) && !\is_null($permissions)) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { @@ -118,7 +120,7 @@ class Update extends Action $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!Authorization::isRole($role)) { + if (!$authorization->hasRole($role)) { throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); } } @@ -139,7 +141,7 @@ class Update extends Action if ($fileSecurity && !$valid) { $file = $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file); } else { - $file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file)); + $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file)); } } catch (NotFoundException) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/View/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/View/Get.php index 41ee95b165..3874fedacf 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/View/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/View/Get.php @@ -15,6 +15,7 @@ use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -69,6 +70,7 @@ class Get extends Action ->inject('mode') ->inject('resourceToken') ->inject('deviceForFiles') + ->inject('authorization') ->callback($this->action(...)); } @@ -81,13 +83,14 @@ class Get extends Action Database $dbForProject, string $mode, Document $resourceToken, - Device $deviceForFiles + Device $deviceForFiles, + Authorization $authorization ) { /* @type Document $bucket */ - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); @@ -95,17 +98,16 @@ class Get extends Action $isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence(); $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 && !$isToken) { - throw new Exception(Exception::USER_UNAUTHORIZED); + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } if ($fileSecurity && !$valid && !$isToken) { $file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId); } else { /* @type Document $file */ - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); } if (!$resourceToken->isEmpty() && $resourceToken->getAttribute('fileInternalId') !== $file->getSequence()) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php index e46fdb2a0a..3663b56fab 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php @@ -16,6 +16,7 @@ use Utopia\Database\Exception\Order as OrderException; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; @@ -61,6 +62,7 @@ class XList extends Action ->inject('response') ->inject('dbForProject') ->inject('mode') + ->inject('authorization') ->callback($this->action(...)); } @@ -71,22 +73,22 @@ class XList extends Action bool $includeTotal, Response $response, Database $dbForProject, - string $mode + string $mode, + Authorization $authorization ) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($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(\Utopia\Database\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); + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } try { @@ -119,7 +121,7 @@ class XList extends Action if ($fileSecurity && !$valid) { $cursorDocument = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId); } else { - $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); + $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); } if ($cursorDocument->isEmpty()) { @@ -136,8 +138,8 @@ class XList extends Action $files = $dbForProject->find('bucket_' . $bucket->getSequence(), $queries); $total = $includeTotal ? $dbForProject->count('bucket_' . $bucket->getSequence(), $filterQueries, APP_LIMIT_COUNT) : 0; } else { - $files = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getSequence(), $queries)); - $total = $includeTotal ? Authorization::skip(fn () => $dbForProject->count('bucket_' . $bucket->getSequence(), $filterQueries, APP_LIMIT_COUNT)) : 0; + $files = $authorization->skip(fn () => $dbForProject->find('bucket_' . $bucket->getSequence(), $queries)); + $total = $includeTotal ? $authorization->skip(fn () => $dbForProject->count('bucket_' . $bucket->getSequence(), $filterQueries, APP_LIMIT_COUNT)) : 0; } } catch (NotFoundException) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php index 5c3515122b..4e75de27c8 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php @@ -51,6 +51,7 @@ class Get extends Action ->inject('dbForProject') ->inject('project') ->inject('getLogsDB') + ->inject('authorization') ->callback($this->action(...)); } @@ -59,7 +60,8 @@ class Get extends Action Response $response, Database $dbForProject, Document $project, - callable $getLogsDB + callable $getLogsDB, + Authorization $authorization, ): void { $bucket = $dbForProject->getDocument('buckets', $bucketId); @@ -75,19 +77,20 @@ class Get extends Action $statsDocId = md5('_inf_' . $metric); - $dbForLogs = call_user_func($getLogsDB, $project); - $storageStats = Authorization::skip( - fn () => $dbForLogs->getDocument( + $totalSize = 0; + + try { + $dbForLogs = $getLogsDB($project); + $storageStats = $authorization->skip(fn () => $dbForLogs->getDocument( 'stats', $statsDocId, [Query::select(['value'])] - ) - ); + )); - /** - * The value can be 0 if stats were not aggregated when this request was made! - */ - $totalSize = $storageStats->isEmpty() ? 0 : $storageStats->getAttribute('value', 0); + $totalSize = $storageStats->getAttribute('value', 0); + } catch (\Throwable) { + // Stats may not be available, default to 0 + } $bucket->setAttribute('totalSize', $totalSize); diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/XList.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/XList.php index a2c880ce08..601d9b5321 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/XList.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/XList.php @@ -58,6 +58,7 @@ class XList extends Action ->inject('dbForProject') ->inject('project') ->inject('getLogsDB') + ->inject('authorization') ->callback($this->action(...)); } @@ -68,7 +69,8 @@ class XList extends Action Response $response, Database $dbForProject, Document $project, - callable $getLogsDB + callable $getLogsDB, + Authorization $authorization ) { try { $queries = Query::parseQueries($queries); @@ -117,7 +119,6 @@ class XList extends Action if (!empty($buckets)) { $bucketByStatsId = []; - $dbForLogs = call_user_func($getLogsDB, $project); foreach ($buckets as $bucket) { $metric = str_replace( @@ -134,22 +135,28 @@ class XList extends Action $bucket->setAttribute('totalSize', 0); } - /* @type Document[] $stats */ - $stats = Authorization::skip(function () use ($dbForLogs, $bucketByStatsId) { - $statsIds = array_keys($bucketByStatsId); + try { + $dbForLogs = $getLogsDB($project); - return $dbForLogs->find('stats', [ - Query::equal('$id', $statsIds), - Query::select(['value']), - ]); - }); + /* @var array $stats */ + $stats = $authorization->skip(function () use ($dbForLogs, $bucketByStatsId) { + $statsIds = array_keys($bucketByStatsId); - foreach ($stats as $stat) { - $bucket = $bucketByStatsId[$stat->getId()]; + return $dbForLogs->find('stats', [ + Query::equal('$id', $statsIds), + Query::select(['value']), + ]); + }); - if ($bucket) { - $bucket->setAttribute('totalSize', $stat->getAttribute('value', 0)); + foreach ($stats as $stat) { + $bucket = $bucketByStatsId[$stat->getId()]; + + if ($bucket) { + $bucket->setAttribute('totalSize', $stat->getAttribute('value', 0)); + } } + } catch (\Throwable) { + // Stats may not be available, default to 0 } } diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Usage/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Usage/Get.php index b816e83f72..a7bda355da 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Usage/Get.php @@ -54,10 +54,11 @@ class Get extends Action ->inject('project') ->inject('dbForProject') ->inject('getLogsDB') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $bucketId, string $range, Response $response, Document $project, Database $dbForProject, callable $getLogsDB) + public function action(string $bucketId, string $range, Response $response, Document $project, Database $dbForProject, callable $getLogsDB, Authorization $authorization) { $dbForLogs = call_user_func($getLogsDB, $project); $bucket = $dbForProject->getDocument('buckets', $bucketId); @@ -75,7 +76,7 @@ class Get extends Action str_replace('{bucketInternalId}', $bucket->getSequence(), METRIC_BUCKET_ID_FILES_IMAGES_TRANSFORMED), ]; - Authorization::skip(function () use ($dbForProject, $dbForLogs, $bucket, $days, $metrics, &$stats) { + $authorization->skip(function () use ($dbForProject, $dbForLogs, $bucket, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $db = ($metric === str_replace('{bucketInternalId}', $bucket->getSequence(), METRIC_BUCKET_ID_FILES_IMAGES_TRANSFORMED)) ? $dbForLogs diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Usage/XList.php b/src/Appwrite/Platform/Modules/Storage/Http/Usage/XList.php index d29fa7c1b4..44fdd54e8c 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Usage/XList.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Usage/XList.php @@ -49,10 +49,11 @@ class XList extends Action ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $range, Response $response, Database $dbForProject) + public function action(string $range, Response $response, Database $dbForProject, Authorization $authorization) { $periods = Config::getParam('usage', []); $stats = $usage = []; @@ -63,7 +64,7 @@ class XList extends Action METRIC_FILES_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]), diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php index f79dece530..5f1bd55788 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php @@ -6,32 +6,31 @@ use Appwrite\Extend\Exception; use Appwrite\Utopia\Database\Documents\User; use Utopia\Database\Database; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Platform\Action as UtopiaAction; class Action extends UtopiaAction { - protected function getFileAndBucket(Database $dbForProject, string $bucketId, string $fileId): array + protected function getFileAndBucket(Database $dbForProject, Authorization $authorization, string $bucketId, string $fileId): array { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = User::isApp(Authorization::getRoles()); - $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAPIKey = User::isApp($authorization->getRoles()); + $isPrivilegedUser = User::isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } - $validator = new Authorization(Database::PERMISSION_READ); - $valid = $validator->isValid($bucket->getRead()); - if (!$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); + if (!$authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead()))) { + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); if ($fileSecurity) { $file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId); } else { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); } if ($file->isEmpty()) { diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php index 3d1f6eef38..6cbaeaa915 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php @@ -14,6 +14,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; @@ -65,23 +66,23 @@ class Create extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $bucketId, string $fileId, ?string $expire, Response $response, Database $dbForProject, Event $queueForEvents): void + public function action(string $bucketId, string $fileId, ?string $expire, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void { /** * @var Document $bucket * @var Document $file */ - ['bucket' => $bucket, 'file' => $file] = $this->getFileAndBucket($dbForProject, $bucketId, $fileId); + ['bucket' => $bucket, 'file' => $file] = $this->getFileAndBucket($dbForProject, $authorization, $bucketId, $fileId); $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_UPDATE); - $bucketPermission = $validator->isValid($bucket->getUpdate()); + $bucketPermission = $authorization->isValid(new Input(Database::PERMISSION_UPDATE, $bucket->getUpdate())); if ($fileSecurity) { - $filePermission = $validator->isValid($file->getUpdate()); + $filePermission = $authorization->isValid(new Input(Database::PERMISSION_UPDATE, $file->getUpdate())); if (!$bucketPermission && !$filePermission) { throw new Exception(Exception::USER_UNAUTHORIZED); } diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/XList.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/XList.php index 8a9301713b..13da92cbc6 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/XList.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/XList.php @@ -13,6 +13,7 @@ use Exception; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Boolean; @@ -57,12 +58,13 @@ class XList extends Action ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('dbForProject') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $bucketId, string $fileId, array $queries, bool $includeTotal, Response $response, Database $dbForProject) + public function action(string $bucketId, string $fileId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Authorization $authorization) { - ['bucket' => $bucket, 'file' => $file] = $this->getFileAndBucket($dbForProject, $bucketId, $fileId); + ['bucket' => $bucket, 'file' => $file] = $this->getFileAndBucket($dbForProject, $authorization, $bucketId, $fileId); $queries = Query::parseQueries($queries); $queries[] = Query::equal('resourceType', [TOKENS_RESOURCE_TYPE_FILES]); diff --git a/src/Appwrite/Platform/Tasks/Migrate.php b/src/Appwrite/Platform/Tasks/Migrate.php index 3e35c1c1fa..cc6981fa1b 100644 --- a/src/Appwrite/Platform/Tasks/Migrate.php +++ b/src/Appwrite/Platform/Tasks/Migrate.php @@ -31,6 +31,7 @@ class Migrate extends Action ->inject('dbForPlatform') ->inject('getProjectDB') ->inject('register') + ->inject('authorisation') ->callback($this->action(...)); } @@ -47,8 +48,8 @@ class Migrate extends Action Database $dbForPlatform, callable $getProjectDB, Registry $register, + Authorization $authorization ): void { - Authorization::disable(); if (!\array_key_exists($version, Migration::$versions)) { Console::error("No migration found for version $version."); @@ -66,14 +67,14 @@ class Migrate extends Action $count = 0; $total = $dbForPlatform->count('projects') + 1; - $dbForPlatform->foreach('projects', function (Document $project) use ($dbForPlatform, $getProjectDB, $register, $migration, &$count, $total) { + $dbForPlatform->foreach('projects', function (Document $project) use ($dbForPlatform, $getProjectDB, $register, $migration, &$count, $total, $authorization) { /** @var Database $dbForProject */ $dbForProject = $getProjectDB($project); $dbForProject->disableValidation(); try { $migration - ->setProject($project, $dbForProject, $dbForPlatform, $getProjectDB) + ->setProject($project, $dbForProject, $dbForPlatform, $authorization, $getProjectDB) ->setPDO($register->get('db', true)) ->execute(); } catch (\Throwable $th) { @@ -88,7 +89,7 @@ class Migrate extends Action try { $migration - ->setProject($console, $getProjectDB($console), $dbForPlatform, $getProjectDB) + ->setProject($console, $getProjectDB($console), $dbForPlatform, $authorization, $getProjectDB) ->setPDO($register->get('db', true)) ->execute(); } catch (\Throwable $th) { diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 9698fe9034..19ed3bc099 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -8,7 +8,6 @@ use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization; use Utopia\Platform\Action; use Utopia\Queue\Broker\Pool as BrokerPool; use Utopia\System\System; @@ -61,7 +60,7 @@ abstract class ScheduleBase extends Action $accessedAt = $project->getAttribute('accessedAt', 0); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { $project->setAttribute('accessedAt', DateTime::now()); - Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project)); + $dbForPlatform->updateDocument('projects', $project->getId(), $project); } } } diff --git a/src/Appwrite/Platform/Tasks/StatsResources.php b/src/Appwrite/Platform/Tasks/StatsResources.php index b64dd61f86..6d04d2109a 100644 --- a/src/Appwrite/Platform/Tasks/StatsResources.php +++ b/src/Appwrite/Platform/Tasks/StatsResources.php @@ -8,7 +8,6 @@ use Utopia\CLI\Console; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization; use Utopia\System\System; /** @@ -61,9 +60,7 @@ class StatsResources extends Action $interval = (int) System::getEnv('_APP_STATS_RESOURCES_INTERVAL', '3600'); - Console::loop(function () use ($queue) { - Authorization::disable(); - Authorization::setDefaultStatus(false); + Console::loop(function () use ($queue, $dbForPlatform) { $last24Hours = (new \DateTime())->sub(\DateInterval::createFromDateString('24 hours')); /** diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index 5132687279..33ebd39092 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -21,6 +21,7 @@ use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception\Authorization; use Utopia\Database\Exception\Conflict; +use Utopia\Database\Exception\NotFound; use Utopia\Database\Exception\Structure; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; @@ -58,6 +59,7 @@ class Certificates extends Action ->inject('log') ->inject('certificates') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } @@ -72,6 +74,8 @@ class Certificates extends Action * @param Certificate $queueForCertificates * @param Log $log * @param CertificatesAdapter $certificates + * @param array $plan + * @param ValidatorAuthorization $authorization * @return void * @throws Throwable * @throws \Utopia\Database\Exception @@ -87,7 +91,8 @@ class Certificates extends Action Certificate $queueForCertificates, Log $log, CertificatesAdapter $certificates, - array $plan + array $plan, + ValidatorAuthorization $authorization, ): void { $payload = $message->getPayload() ?? []; @@ -106,11 +111,11 @@ class Certificates extends Action switch ($action) { case Certificate::ACTION_DOMAIN_VERIFICATION: - $this->handleDomainVerificationAction($domain, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $queueForCertificates, $log, $validationDomain); + $this->handleDomainVerificationAction($domain, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $queueForCertificates, $log, $authorization, $validationDomain); break; case Certificate::ACTION_GENERATION: - $this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $queueForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $skipRenewCheck, $plan, $validationDomain); + $this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $queueForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $plan, $validationDomain); break; default: @@ -127,10 +132,12 @@ class Certificates extends Action * @param Realtime $queueForRealtime * @param Certificate $queueForCertificates * @param Log $log + * @param ValidatorAuthorization $authorization * @param string|null $validationDomain * @return void - * @throws Throwable * @throws \Utopia\Database\Exception + * @throws NotFound + * @throws \Utopia\Database\Exception\Query */ private function handleDomainVerificationAction( Domain $domain, @@ -141,12 +148,13 @@ class Certificates extends Action Realtime $queueForRealtime, Certificate $queueForCertificates, Log $log, + ValidatorAuthorization $authorization, ?string $validationDomain = null ): void { // Get rule $rule = System::getEnv('_APP_RULES_FORMAT') === 'md5' - ? ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($domain->get()))) - : ValidatorAuthorization::skip(fn () => $dbForPlatform->findOne('rules', [ + ? $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($domain->get()))) + : $authorization->skip(fn () => $dbForPlatform->findOne('rules', [ Query::equal('domain', [$domain->get()]), Query::limit(1), ])); @@ -195,15 +203,23 @@ class Certificates extends Action * @param Database $dbForPlatform * @param Mail $queueForMails * @param Event $queueForEvents + * @param Webhook $queueForWebhooks * @param Func $queueForFunctions * @param Realtime $queueForRealtime + * @param Log $log * @param CertificatesAdapter $certificates + * @param ValidatorAuthorization $authorization * @param bool $skipRenewCheck * @param array $plan * @param string|null $validationDomain * @return void + * @throws Authorization + * @throws Conflict + * @throws NotFound + * @throws Structure * @throws Throwable * @throws \Utopia\Database\Exception + * @throws \Utopia\Database\Exception\Query */ private function handleCertificateGenerationAction( Domain $domain, @@ -216,6 +232,7 @@ class Certificates extends Action Realtime $queueForRealtime, Log $log, CertificatesAdapter $certificates, + ValidatorAuthorization $authorization, bool $skipRenewCheck = false, array $plan = [], ?string $validationDomain = null @@ -252,8 +269,8 @@ class Certificates extends Action // Get rule document for domain // TODO: (@Meldiron) Remove after 1.7.x migration $rule = System::getEnv('_APP_RULES_FORMAT') === 'md5' - ? ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($domain->get()))) - : ValidatorAuthorization::skip(fn () => $dbForPlatform->findOne('rules', [ + ? $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($domain->get()))) + : $authorization->skip(fn () => $dbForPlatform->findOne('rules', [ Query::equal('domain', [$domain->get()]), Query::limit(1), ])); diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 0b2f7c75ae..9687f4f4bb 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -19,12 +19,10 @@ use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception as DatabaseException; -use Utopia\Database\Exception\Authorization; 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; @@ -203,7 +201,6 @@ class Deletes extends Action * @param string $datetime * @param Document|null $document * @return void - * @throws Authorization * @throws Conflict * @throws Restricted * @throws Structure @@ -1002,14 +999,14 @@ class Deletes extends Action } Console::info("Deleting screenshots for deployment " . $deployment->getId()); - $bucket = ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); + $bucket = $dbForPlatform->getDocument('buckets', 'screenshots'); if ($bucket->isEmpty()) { Console::error('Failed to get bucket for deployment screenshots'); return; } foreach ($screenshotIds as $id) { - $file = ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $id)); + $file = $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $id); if ($file->isEmpty()) { Console::error('Failed to get deployment screenshot: ' . $id); diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index fba5154079..a54b982634 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -15,7 +15,6 @@ use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Exception\Authorization; use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Structure; use Utopia\Database\Helpers\ID; @@ -337,7 +336,6 @@ class Functions extends Action * @param string|null $eventData * @param string|null $executionId * @return void - * @throws Authorization * @throws Structure * @throws \Utopia\Database\Exception * @throws Conflict diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index e1039510f4..6ef2f1899c 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -80,6 +80,7 @@ class Migrations extends Action ->inject('deviceForFiles') ->inject('queueForMails') ->inject('plan') + ->inject('authorization') ->callback($this->action(...)); } @@ -97,6 +98,7 @@ class Migrations extends Action Device $deviceForFiles, Mail $queueForMails, array $plan, + Authorization $authorization, ): void { $payload = $message->getPayload() ?? []; $this->deviceForMigrations = $deviceForMigrations; @@ -134,7 +136,13 @@ class Migrations extends Action } try { - $this->processMigration($migration, $queueForRealtime, $queueForMails, $platform); + $this->processMigration( + $migration, + $queueForRealtime, + $queueForMails, + $platform, + $authorization + ); } finally { $this->dbForProject = null; $this->dbForPlatform = null; @@ -145,7 +153,7 @@ class Migrations extends Action $this->plan = []; $this->sourceReport = []; - gc_collect_cycles(); + \gc_collect_cycles(); } } @@ -319,6 +327,7 @@ class Migrations extends Action Realtime $queueForRealtime, Mail $queueForMails, array $platform, + Authorization $authorization, ): void { $project = $this->project; @@ -435,14 +444,14 @@ class Migrations extends Action $destination?->success(); $source?->success(); - // todo: Move to CSV hook + // TODO: Move to CSV hook if ($migration->getAttribute('destination') === DestinationCSV::getName()) { - $this->handleCSVExportComplete($project, $migration, $queueForMails, $queueForRealtime, $platform); + $this->handleCSVExportComplete($project, $migration, $queueForMails, $queueForRealtime, $platform, $authorization); } } } finally { - $source?->cleanUp(); - $destination?->cleanUp(); + $source?->cleanup(); + $destination?->cleanup(); $transfer = null; $source = null; @@ -457,11 +466,10 @@ class Migrations extends Action * @param Document $project * @param Document $migration * @param Mail $queueForMails + * @param Realtime $queueForRealtime + * @param array $platform + * @param Authorization $authorization * @return void - * @throws AuthorizationException - * @throws Structure - * @throws \Utopia\Database\Exception - * @throws Exception */ protected function handleCSVExportComplete( Document $project, @@ -469,6 +477,7 @@ class Migrations extends Action Mail $queueForMails, Realtime $queueForRealtime, array $platform, + Authorization $authorization, ): void { $options = $migration->getAttribute('options', []); $bucketId = 'default'; // Always use platform default bucket @@ -482,7 +491,7 @@ class Migrations extends Action throw new \Exception('User ' . $userInternalId . ' not found'); } - $bucket = Authorization::skip(fn () => $this->dbForPlatform->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $this->dbForPlatform->getDocument('buckets', $bucketId)); if ($bucket->isEmpty()) { throw new \Exception('Bucket not found'); } diff --git a/src/Appwrite/Utopia/Database/Documents/User.php b/src/Appwrite/Utopia/Database/Documents/User.php index a85b0a897c..cbd22aaee5 100644 --- a/src/Appwrite/Utopia/Database/Documents/User.php +++ b/src/Appwrite/Utopia/Database/Documents/User.php @@ -7,7 +7,6 @@ use Utopia\Auth\Proofs\Token; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\Role; -use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Roles; class User extends Document @@ -36,11 +35,11 @@ class User extends Document * * @return array */ - public function getRoles(): array + public function getRoles($authorization): array { $roles = []; - if (!$this->isPrivileged(Authorization::getRoles()) && !$this->isApp(Authorization::getRoles())) { + if (!$this->isPrivileged($authorization->getRoles()) && !$this->isApp($authorization->getRoles())) { if ($this->getId()) { $roles[] = Role::user($this->getId())->toString(); $roles[] = Role::users()->toString(); diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index cb449e6ffa..c87279f126 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -214,7 +214,7 @@ class Request extends UtopiaRequest { $forwardedUserAgent = $this->getHeader('x-forwarded-user-agent'); if (!empty($forwardedUserAgent)) { - $roles = Authorization::getRoles(); + $roles = $this->authorization->getRoles(); $isAppUser = User::isApp($roles); if ($isAppUser) { @@ -237,4 +237,11 @@ class Request extends UtopiaRequest ksort($params); return md5($this->getURI() . '*' . serialize($params) . '*' . APP_CACHE_BUSTER); } + + private ?Authorization $authorization = null; + + public function setAuthorization(Authorization $authorization): void + { + $this->authorization = $authorization; + } } diff --git a/src/Appwrite/Utopia/Request/Filter.php b/src/Appwrite/Utopia/Request/Filter.php index 56fed746d9..6d47d4d150 100644 --- a/src/Appwrite/Utopia/Request/Filter.php +++ b/src/Appwrite/Utopia/Request/Filter.php @@ -10,7 +10,7 @@ abstract class Filter private array $params; private ?Database $dbForProject; - public function __construct(Database $dbForProject = null, array $params = []) + public function __construct(?Database $dbForProject = null, array $params = []) { $this->params = $params; $this->dbForProject = $dbForProject; diff --git a/src/Appwrite/Utopia/Request/Filters/V20.php b/src/Appwrite/Utopia/Request/Filters/V20.php index 69e7da6b7a..e3d5fe2f79 100644 --- a/src/Appwrite/Utopia/Request/Filters/V20.php +++ b/src/Appwrite/Utopia/Request/Filters/V20.php @@ -7,7 +7,6 @@ use Appwrite\Utopia\Request\Filter; use Utopia\Database\Database; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization; class V20 extends Filter { @@ -138,7 +137,7 @@ class V20 extends Filter } try { - $database = Authorization::skip(fn () => $dbForProject->getDocument( + $database = $dbForProject->getAuthorization()->skip(fn () => $dbForProject->getDocument( 'databases', $databaseId )); @@ -150,7 +149,7 @@ class V20 extends Filter } try { - $collection = Authorization::skip(fn () => $dbForProject->getDocument( + $collection = $database = $dbForProject->getAuthorization()->skip(fn () => $dbForProject->getDocument( 'database_' . $database->getSequence(), $collectionId )); diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 1dfaa1a41f..f2ac486f82 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -483,7 +483,7 @@ class Response extends SwooleResponse } if ($rule['sensitive']) { - $roles = Authorization::getRoles(); + $roles = $this->authorization->getRoles(); $isPrivilegedUser = DBUser::isPrivileged($roles); $isAppUser = DBUser::isApp($roles); @@ -651,4 +651,11 @@ class Response extends SwooleResponse self::$showSensitive = false; } } + + private ?Authorization $authorization = null; + + public function setAuthorization(Authorization $authorization): void + { + $this->authorization = $authorization; + } } diff --git a/tests/e2e/Services/Databases/Legacy/Permissions/DatabasesPermissionsGuestTest.php b/tests/e2e/Services/Databases/Legacy/Permissions/DatabasesPermissionsGuestTest.php index 6496aa285a..0c9854160e 100644 --- a/tests/e2e/Services/Databases/Legacy/Permissions/DatabasesPermissionsGuestTest.php +++ b/tests/e2e/Services/Databases/Legacy/Permissions/DatabasesPermissionsGuestTest.php @@ -17,6 +17,19 @@ class DatabasesPermissionsGuestTest extends Scope use SideClient; use DatabasesPermissionsScope; + private $authorization; + + public function getAuthorization(): Authorization + { + if (isset($this->authorization)) { + return $this->authorization; + } + + $this->authorization = new Authorization(); + + return $this->authorization; + } + public function createCollection(): array { $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ @@ -111,8 +124,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->getAuthorization()->getRoles(); + $this->getAuthorization()->cleanRoles(); $publicDocuments = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [ 'content-type' => 'application/json', @@ -134,7 +147,7 @@ class DatabasesPermissionsGuestTest extends Scope } foreach ($roles as $role) { - Authorization::setRole($role); + $this->getAuthorization()->addRole($role); } } @@ -145,8 +158,8 @@ class DatabasesPermissionsGuestTest extends Scope $privateCollectionId = $data['privateCollectionId']; $databaseId = $data['databaseId']; - $roles = Authorization::getRoles(); - Authorization::cleanRoles(); + $roles = $this->getAuthorization()->getRoles(); + $this->getAuthorization()->cleanRoles(); $publicResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [ 'content-type' => 'application/json', @@ -222,7 +235,7 @@ class DatabasesPermissionsGuestTest extends Scope $this->assertEquals(401, $privateDocument['headers']['status-code']); foreach ($roles as $role) { - Authorization::setRole($role); + $this->getAuthorization()->addRole($role); } } diff --git a/tests/e2e/Services/Databases/TablesDB/Permissions/DatabasesPermissionsGuestTest.php b/tests/e2e/Services/Databases/TablesDB/Permissions/DatabasesPermissionsGuestTest.php index 2f69c037d0..84cb4bce3a 100644 --- a/tests/e2e/Services/Databases/TablesDB/Permissions/DatabasesPermissionsGuestTest.php +++ b/tests/e2e/Services/Databases/TablesDB/Permissions/DatabasesPermissionsGuestTest.php @@ -17,6 +17,19 @@ class DatabasesPermissionsGuestTest extends Scope use SideClient; use DatabasesPermissionsScope; + private $authorization; + + public function getAuthorization(): Authorization + { + if (isset($this->authorization)) { + return $this->authorization; + } + + $this->authorization = new Authorization(); + return $this->authorization; + } + + public function createTable(): array { $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ @@ -111,8 +124,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->getAuthorization()->getRoles(); + $this->getAuthorization()->cleanRoles(); $publicRows = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $publicTableId . '/rows', [ 'content-type' => 'application/json', @@ -134,7 +147,7 @@ class DatabasesPermissionsGuestTest extends Scope } foreach ($roles as $role) { - Authorization::setRole($role); + $this->getAuthorization()->addRole($role); } } @@ -145,8 +158,8 @@ class DatabasesPermissionsGuestTest extends Scope $privateTableId = $data['privateTableId']; $databaseId = $data['databaseId']; - $roles = Authorization::getRoles(); - Authorization::cleanRoles(); + $roles = $this->getAuthorization()->getRoles(); + $this->getAuthorization()->cleanRoles(); $publicResponse = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $publicTableId . '/rows', [ 'content-type' => 'application/json', @@ -222,7 +235,7 @@ class DatabasesPermissionsGuestTest extends Scope $this->assertEquals(401, $privateRow['headers']['status-code']); foreach ($roles as $role) { - Authorization::setRole($role); + $this->getAuthorization()->addRole($role); } } diff --git a/tests/e2e/Services/Sites/SitesConsoleClientTest.php b/tests/e2e/Services/Sites/SitesConsoleClientTest.php index 2b75402b25..31cee13261 100644 --- a/tests/e2e/Services/Sites/SitesConsoleClientTest.php +++ b/tests/e2e/Services/Sites/SitesConsoleClientTest.php @@ -54,15 +54,22 @@ class SitesConsoleClientTest extends Scope $this->assertStringContainsString("Themed website", $response['body']); $this->assertStringContainsString("@media (prefers-color-scheme: dark)", $response['body']); - $deployment = $this->getDeployment($siteId, $deploymentId); - $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertNotEmpty($deployment['body']['screenshotLight']); - $this->assertNotEmpty($deployment['body']['screenshotDark']); + $deployment = null; + $site = null; + $this->assertEventually(function () use ($siteId, $deploymentId, &$deployment, &$site) { + $deployment = $this->getDeployment($siteId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['screenshotLight']); + $this->assertNotEmpty($deployment['body']['screenshotDark']); - $site = $this->getSite($siteId); - $this->assertEquals(200, $site['headers']['status-code']); - $this->assertEquals($deployment['body']['screenshotLight'], $site['body']['deploymentScreenshotLight']); - $this->assertEquals($deployment['body']['screenshotDark'], $site['body']['deploymentScreenshotDark']); + $site = $this->getSite($siteId); + $this->assertEquals(200, $site['headers']['status-code']); + $this->assertEquals($deployment['body']['screenshotLight'], $site['body']['deploymentScreenshotLight']); + $this->assertEquals($deployment['body']['screenshotDark'], $site['body']['deploymentScreenshotDark']); + }); + + $this->assertNotNull($site); + $this->assertNotNull($deployment); $screenshotId = $deployment['body']['screenshotLight']; $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console", array_merge($this->getHeaders(), [ diff --git a/tests/e2e/Services/Tokens/TokensBase.php b/tests/e2e/Services/Tokens/TokensBase.php index a4461c06c2..ca6feed5fa 100644 --- a/tests/e2e/Services/Tokens/TokensBase.php +++ b/tests/e2e/Services/Tokens/TokensBase.php @@ -94,7 +94,7 @@ trait TokensBase $this->assertEquals(401, $failedPreview['body']['code']); $this->assertEquals(401, $failedPreview['headers']['status-code']); $this->assertEquals('user_unauthorized', $failedPreview['body']['type']); - $this->assertEquals('The current user is not authorized to perform the requested action.', $failedPreview['body']['message']); + $this->assertEquals('No permissions provided for action \'read\'', $failedPreview['body']['message']); // Extended file preview. Should fail as an anonymous user with no form of any access to the file. $failedCustomPreview = $this->client->call( @@ -113,7 +113,7 @@ trait TokensBase $this->assertEquals(401, $failedCustomPreview['body']['code']); $this->assertEquals(401, $failedCustomPreview['headers']['status-code']); $this->assertEquals('user_unauthorized', $failedCustomPreview['body']['type']); - $this->assertEquals('The current user is not authorized to perform the requested action.', $failedCustomPreview['body']['message']); + $this->assertEquals('No permissions provided for action \'read\'', $failedCustomPreview['body']['message']); // File view. Should fail as an anonymous user with no form of any access to the file. $failedView = $this->client->call( @@ -124,7 +124,7 @@ trait TokensBase $this->assertEquals(401, $failedView['body']['code']); $this->assertEquals(401, $failedView['headers']['status-code']); $this->assertEquals('user_unauthorized', $failedView['body']['type']); - $this->assertEquals('The current user is not authorized to perform the requested action.', $failedView['body']['message']); + $this->assertEquals('No permissions provided for action \'read\'', $failedView['body']['message']); // File download. Should fail as an anonymous user with no form of any access to the file. $failedDownload = $this->client->call( @@ -135,7 +135,7 @@ trait TokensBase $this->assertEquals(401, $failedDownload['body']['code']); $this->assertEquals(401, $failedDownload['headers']['status-code']); $this->assertEquals('user_unauthorized', $failedDownload['body']['type']); - $this->assertEquals('The current user is not authorized to perform the requested action.', $failedDownload['body']['message']); + $this->assertEquals('No permissions provided for action \'read\'', $failedDownload['body']['message']); return $data; } diff --git a/tests/unit/Messaging/MessagingChannelsTest.php b/tests/unit/Messaging/MessagingChannelsTest.php index 42e433568f..7df5b8d1e6 100644 --- a/tests/unit/Messaging/MessagingChannelsTest.php +++ b/tests/unit/Messaging/MessagingChannelsTest.php @@ -7,6 +7,7 @@ use Appwrite\Utopia\Database\Documents\User; use PHPUnit\Framework\TestCase; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; +use Utopia\Database\Validator\Authorization; class MessagingChannelsTest extends TestCase { @@ -33,6 +34,19 @@ class MessagingChannelsTest extends TestCase 'functions.1', ]; + + private $authorization; + + public function getAuthorization(): Authorization + { + if (isset($this->authorization)) { + return $this->authorization; + } + + $this->authorization = new Authorization(); + return $this->authorization; + } + public function setUp(): void { /** @@ -65,7 +79,7 @@ class MessagingChannelsTest extends TestCase ] ]); - $roles = $user->getRoles(); + $roles = $user->getRoles($this->getAuthorization()); $parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId()); @@ -89,7 +103,7 @@ class MessagingChannelsTest extends TestCase '$id' => '' ]); - $roles = $user->getRoles(); + $roles = $user->getRoles($this->getAuthorization()); $parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId()); diff --git a/tests/unit/Utopia/Database/Documents/UserTest.php b/tests/unit/Utopia/Database/Documents/UserTest.php index 4675e8d73f..d5706e7bec 100644 --- a/tests/unit/Utopia/Database/Documents/UserTest.php +++ b/tests/unit/Utopia/Database/Documents/UserTest.php @@ -14,13 +14,25 @@ use Utopia\Database\Validator\Roles; class UserTest extends TestCase { + private $authorization; + + public function getAuthorization(): Authorization + { + if (isset($this->authorization)) { + return $this->authorization; + } + + $this->authorization = new Authorization(); + return $this->authorization; + } + /** * Reset Roles */ public function tearDown(): void { - Authorization::cleanRoles(); - Authorization::setRole(Role::any()->toString()); + $this->getAuthorization()->cleanRoles(); + $this->getAuthorization()->addRole(Role::any()->toString()); } public function testSessionVerify(): void @@ -197,7 +209,7 @@ class UserTest extends TestCase '$id' => '' ]); - $roles = $user->getRoles(); + $roles = $user->getRoles($this->getAuthorization()); $this->assertCount(1, $roles); $this->assertContains(Role::guests()->toString(), $roles); } @@ -233,7 +245,7 @@ class UserTest extends TestCase ] ]); - $roles = $user->getRoles(); + $roles = $user->getRoles($this->getAuthorization()); $this->assertCount(13, $roles); $this->assertContains(Role::users()->toString(), $roles); @@ -254,21 +266,21 @@ class UserTest extends TestCase $user['emailVerification'] = false; $user['phoneVerification'] = false; - $roles = $user->getRoles(); + $roles = $user->getRoles($this->getAuthorization()); $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 = $user->getRoles(); + $roles = $user->getRoles($this->getAuthorization()); $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(User::ROLE_OWNER); + $this->getAuthorization()->addRole(User::ROLE_OWNER); $user = new User([ '$id' => ID::custom('123'), 'emailVerification' => true, @@ -293,8 +305,7 @@ class UserTest extends TestCase ] ] ]); - - $roles = $user->getRoles(); + $roles = $user->getRoles($this->getAuthorization()); $this->assertCount(7, $roles); $this->assertNotContains(Role::users()->toString(), $roles); @@ -312,7 +323,7 @@ class UserTest extends TestCase public function testAppUserRoles(): void { - Authorization::setRole(User::ROLE_APPS); + $this->getAuthorization()->addRole(User::ROLE_APPS); $user = new User([ '$id' => ID::custom('123'), 'memberships' => [ @@ -336,7 +347,7 @@ class UserTest extends TestCase ] ]); - $roles = $user->getRoles(); + $roles = $user->getRoles($this->getAuthorization()); $this->assertCount(7, $roles); $this->assertNotContains(Role::users()->toString(), $roles);