Merge branch '1.8.x' into release-cli-13.0.0.rc3

This commit is contained in:
Chirag Aggarwal 2026-01-13 13:39:27 +05:30 committed by GitHub
commit dd78b6dbce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
221 changed files with 3878 additions and 2671 deletions

View file

@ -77,6 +77,7 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/queue-count-success && \ chmod +x /usr/local/bin/queue-count-success && \
chmod +x /usr/local/bin/worker-audits && \ chmod +x /usr/local/bin/worker-audits && \
chmod +x /usr/local/bin/worker-builds && \ 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-certificates && \
chmod +x /usr/local/bin/worker-databases && \ chmod +x /usr/local/bin/worker-databases && \
chmod +x /usr/local/bin/worker-deletes && \ chmod +x /usr/local/bin/worker-deletes && \

View file

@ -41,8 +41,6 @@ Config::setParam('runtimes', (new Runtimes('v5'))->getAll(supported: false));
// require controllers after overwriting runtimes // require controllers after overwriting runtimes
require_once __DIR__ . '/controllers/general.php'; require_once __DIR__ . '/controllers/general.php';
Authorization::disable();
CLI::setResource('register', fn () => $register); CLI::setResource('register', fn () => $register);
CLI::setResource('cache', function ($pools) { CLI::setResource('cache', function ($pools) {
@ -60,7 +58,13 @@ CLI::setResource('pools', function (Registry $register) {
return $register->get('pools'); return $register->get('pools');
}, ['register']); }, ['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; $sleep = 3;
$maxAttempts = 5; $maxAttempts = 5;
$attempts = 0; $attempts = 0;
@ -74,6 +78,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
$dbForPlatform = new Database($adapter, $cache); $dbForPlatform = new Database($adapter, $cache);
$dbForPlatform $dbForPlatform
->setAuthorization($authorization)
->setNamespace('_console') ->setNamespace('_console')
->setMetadata('host', \gethostname()) ->setMetadata('host', \gethostname())
->setMetadata('project', 'console'); ->setMetadata('project', 'console');
@ -99,7 +104,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
} }
return $dbForPlatform; return $dbForPlatform;
}, ['pools', 'cache']); }, ['pools', 'cache', 'authorization']);
CLI::setResource('console', function () { CLI::setResource('console', function () {
return new Document(Config::getParam('console')); return new Document(Config::getParam('console'));
@ -110,10 +115,10 @@ CLI::setResource(
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false 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 $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') { if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform; return $dbForPlatform;
} }
@ -146,6 +151,7 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
$adapter = new DatabasePool($pools->get($dsn->getHost())); $adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache); $database = new Database($adapter, $cache);
$databases[$dsn->getHost()] = $database; $databases[$dsn->getHost()] = $database;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
@ -162,17 +168,18 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
} }
$database $database
->setAuthorization($authorization)
->setMetadata('host', \gethostname()) ->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId()); ->setMetadata('project', $project->getId());
return $database; 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; $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') { if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int)$project->getSequence()); $database->setTenant((int)$project->getSequence());
return $database; return $database;
@ -182,6 +189,7 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = new Database($adapter, $cache); $database = new Database($adapter, $cache);
$database $database
->setAuthorization($authorization)
->setSharedTables(true) ->setSharedTables(true)
->setNamespace('logsV1') ->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_TASK) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_TASK)
@ -194,7 +202,7 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
return $database; return $database;
}; };
}, ['pools', 'cache']); }, ['pools', 'cache', 'authorization']);
CLI::setResource('publisher', function (Group $pools) { CLI::setResource('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher')); return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']); }, ['pools']);

View file

@ -48,7 +48,7 @@ return [
'name' => 'Avatars', 'name' => 'Avatars',
'subtitle' => 'The Avatars service aims to help you complete everyday tasks related to your app image, icons, and 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', 'description' => '/docs/services/avatars.md',
'controller' => 'api/avatars.php', 'controller' => '', // Uses modules
'sdk' => true, 'sdk' => true,
'docs' => true, 'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/client/avatars', 'docsUrl' => 'https://appwrite.io/docs/client/avatars',
@ -146,7 +146,7 @@ return [
'name' => 'Storage', 'name' => 'Storage',
'subtitle' => 'The Storage service allows you to manage your project files.', 'subtitle' => 'The Storage service allows you to manage your project files.',
'description' => '/docs/services/storage.md', 'description' => '/docs/services/storage.md',
'controller' => '', 'controller' => '', // Uses modules
'sdk' => true, 'sdk' => true,
'docs' => true, 'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/client/storage', 'docsUrl' => 'https://appwrite.io/docs/client/storage',

View file

@ -3,4 +3,6 @@
use Utopia\Image\Image; use Utopia\Image\Image;
use Utopia\System\System; 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)));
}

View file

@ -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 */ /** @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()) { if ($userFromRequest->isEmpty()) {
throw new Exception(Exception::USER_INVALID_TOKEN); throw new Exception(Exception::USER_INVALID_TOKEN);
@ -266,7 +266,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
$detector->getDevice() $detector->getDevice()
)); ));
Authorization::setRole(Role::user($user->getId())->toString()); $authorization->addRole(Role::user($user->getId())->toString());
$session = $dbForProject->createDocument('sessions', $session $session = $dbForProject->createDocument('sessions', $session
->setAttribute('$permissions', [ ->setAttribute('$permissions', [
@ -275,7 +275,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
Permission::delete(Role::user($user->getId())), 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()); $dbForProject->purgeCachedDocument('users', $user->getId());
// Magic URL + Email OTP // Magic URL + Email OTP
@ -376,8 +376,9 @@ App::post('/v1/account')
->inject('user') ->inject('user')
->inject('project') ->inject('project')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->inject('hooks') ->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); $email = \strtolower($email);
if ('console' === $project->getId()) { if ('console' === $project->getId()) {
@ -469,9 +470,9 @@ App::post('/v1/account')
]); ]);
$user->removeAttribute('$sequence'); $user->removeAttribute('$sequence');
$user = Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
try { try {
$target = Authorization::skip(fn () => $dbForProject->createDocument('targets', new Document([ $target = $authorization->skip(fn () => $dbForProject->createDocument('targets', new Document([
'$permissions' => [ '$permissions' => [
Permission::read(Role::user($user->getId())), Permission::read(Role::user($user->getId())),
Permission::update(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); throw new Exception(Exception::USER_ALREADY_EXISTS);
} }
Authorization::unsetRole(Role::guests()->toString()); $authorization->removeRole(Role::guests()->toString());
Authorization::setRole(Role::user($user->getId())->toString()); $authorization->addRole(Role::user($user->getId())->toString());
Authorization::setRole(Role::users()->toString()); $authorization->addRole(Role::users()->toString());
$response $response
->setStatusCode(Response::STATUS_CODE_CREATED) ->setStatusCode(Response::STATUS_CODE_CREATED)
@ -976,7 +977,8 @@ App::post('/v1/account/sessions/email')
->inject('store') ->inject('store')
->inject('proofForPassword') ->inject('proofForPassword')
->inject('proofForToken') ->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); $email = \strtolower($email);
$protocol = $request->getProtocol(); $protocol = $request->getProtocol();
@ -1021,7 +1023,7 @@ App::post('/v1/account/sessions/email')
$detector->getDevice() $detector->getDevice()
)); ));
Authorization::setRole(Role::user($user->getId())->toString()); $authorization->addRole(Role::user($user->getId())->toString());
// Re-hash if not using recommended algo // Re-hash if not using recommended algo
if ($user->getAttribute('hash') !== $proofForPassword->getHash()->getName()) { if ($user->getAttribute('hash') !== $proofForPassword->getHash()->getName()) {
@ -1120,7 +1122,8 @@ App::post('/v1/account/sessions/anonymous')
->inject('store') ->inject('store')
->inject('proofForPassword') ->inject('proofForPassword')
->inject('proofForToken') ->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(); $protocol = $request->getProtocol();
if ('console' === $project->getId()) { if ('console' === $project->getId()) {
@ -1165,7 +1168,7 @@ App::post('/v1/account/sessions/anonymous')
'accessedAt' => DateTime::now(), 'accessedAt' => DateTime::now(),
]); ]);
$user->removeAttribute('$sequence'); $user->removeAttribute('$sequence');
Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
// Create session token // Create session token
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG; $duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
@ -1191,7 +1194,7 @@ App::post('/v1/account/sessions/anonymous')
$detector->getDevice() $detector->getDevice()
)); ));
Authorization::setRole(Role::user($user->getId())->toString()); $authorization->addRole(Role::user($user->getId())->toString());
$session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [ $session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())), Permission::read(Role::user($user->getId())),
@ -1274,6 +1277,7 @@ App::post('/v1/account/sessions/token')
->inject('store') ->inject('store')
->inject('proofForToken') ->inject('proofForToken')
->inject('proofForCode') ->inject('proofForCode')
->inject('authorization')
->action($createSession); ->action($createSession);
App::get('/v1/account/sessions/oauth2/:provider') App::get('/v1/account/sessions/oauth2/:provider')
@ -1470,7 +1474,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->inject('store') ->inject('store')
->inject('proofForPassword') ->inject('proofForPassword')
->inject('proofForToken') ->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'; $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort(); $port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname(); $callbackBase = $protocol . '://' . $request->getHostname();
@ -1726,7 +1731,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
]); ]);
$user->removeAttribute('$sequence'); $user->removeAttribute('$sequence');
$userDoc = Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); $userDoc = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
$dbForProject->createDocument('targets', new Document([ $dbForProject->createDocument('targets', new Document([
'$permissions' => [ '$permissions' => [
Permission::read(Role::user($user->getId())), 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->addRole(Role::user($user->getId())->toString());
Authorization::setRole(Role::users()->toString()); $authorization->addRole(Role::users()->toString());
if (false === $user->getAttribute('status')) { // Account is blocked if (false === $user->getAttribute('status')) { // Account is blocked
$failureRedirect(Exception::USER_BLOCKED); // User is in status 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); $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']); $state['success'] = URLParser::parse($state['success']);
$query = URLParser::parseQuery($state['success']['query']); $query = URLParser::parseQuery($state['success']['query']);
@ -1840,7 +1845,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'ip' => $request->getIP(), 'ip' => $request->getIP(),
]); ]);
Authorization::setRole(Role::user($user->getId())->toString()); $authorization->addRole(Role::user($user->getId())->toString());
$token = $dbForProject->createDocument('tokens', $token $token = $dbForProject->createDocument('tokens', $token
->setAttribute('$permissions', [ ->setAttribute('$permissions', [
@ -2077,7 +2082,8 @@ App::post('/v1/account/tokens/magic-url')
->inject('queueForMails') ->inject('queueForMails')
->inject('proofForPassword') ->inject('proofForPassword')
->inject('platform') ->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'))) { if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
} }
@ -2150,7 +2156,7 @@ App::post('/v1/account/tokens/magic-url')
]); ]);
$user->removeAttribute('$sequence'); $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); $proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
@ -2170,7 +2176,7 @@ App::post('/v1/account/tokens/magic-url')
'ip' => $request->getIP(), 'ip' => $request->getIP(),
]); ]);
Authorization::setRole(Role::user($user->getId())->toString()); $authorization->addRole(Role::user($user->getId())->toString());
$token = $dbForProject->createDocument('tokens', $token $token = $dbForProject->createDocument('tokens', $token
->setAttribute('$permissions', [ ->setAttribute('$permissions', [
@ -2356,7 +2362,8 @@ App::post('/v1/account/tokens/email')
->inject('queueForMails') ->inject('queueForMails')
->inject('proofForPassword') ->inject('proofForPassword')
->inject('proofForCode') ->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'))) { if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
} }
@ -2425,9 +2432,9 @@ App::post('/v1/account/tokens/email')
]); ]);
$user->removeAttribute('$sequence'); $user->removeAttribute('$sequence');
$user = Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
try { try {
$target = Authorization::skip(fn () => $dbForProject->createDocument('targets', new Document([ $target = $authorization->skip(fn () => $dbForProject->createDocument('targets', new Document([
'$permissions' => [ '$permissions' => [
Permission::read(Role::user($user->getId())), Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())), Permission::update(Role::user($user->getId())),
@ -2465,7 +2472,7 @@ App::post('/v1/account/tokens/email')
'ip' => $request->getIP(), 'ip' => $request->getIP(),
]); ]);
Authorization::setRole(Role::user($user->getId())->toString()); $authorization->addRole(Role::user($user->getId())->toString());
$token = $dbForProject->createDocument('tokens', $token $token = $dbForProject->createDocument('tokens', $token
->setAttribute('$permissions', [ ->setAttribute('$permissions', [
@ -2662,10 +2669,11 @@ App::put('/v1/account/sessions/magic-url')
->inject('queueForMails') ->inject('queueForMails')
->inject('store') ->inject('store')
->inject('proofForCode') ->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 = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
$proofForToken->setHash(new Sha()); $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') App::put('/v1/account/sessions/phone')
@ -2711,6 +2719,7 @@ App::put('/v1/account/sessions/phone')
->inject('store') ->inject('store')
->inject('proofForToken') ->inject('proofForToken')
->inject('proofForCode') ->inject('proofForCode')
->inject('authorization')
->action($createSession); ->action($createSession);
App::post('/v1/account/tokens/phone') App::post('/v1/account/tokens/phone')
@ -2754,7 +2763,8 @@ App::post('/v1/account/tokens/phone')
->inject('plan') ->inject('plan')
->inject('store') ->inject('store')
->inject('proofForCode') ->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'))) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
} }
@ -2804,9 +2814,9 @@ App::post('/v1/account/tokens/phone')
]); ]);
$user->removeAttribute('$sequence'); $user->removeAttribute('$sequence');
Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
try { try {
$target = Authorization::skip(fn () => $dbForProject->createDocument('targets', new Document([ $target = $authorization->skip(fn () => $dbForProject->createDocument('targets', new Document([
'$permissions' => [ '$permissions' => [
Permission::read(Role::user($user->getId())), Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())), Permission::update(Role::user($user->getId())),
@ -2852,7 +2862,7 @@ App::post('/v1/account/tokens/phone')
'ip' => $request->getIP(), 'ip' => $request->getIP(),
]); ]);
Authorization::setRole(Role::user($user->getId())->toString()); $authorization->addRole(Role::user($user->getId())->toString());
$token = $dbForProject->createDocument('tokens', $token $token = $dbForProject->createDocument('tokens', $token
->setAttribute('$permissions', [ ->setAttribute('$permissions', [
@ -3243,7 +3253,8 @@ App::patch('/v1/account/email')
->inject('project') ->inject('project')
->inject('hooks') ->inject('hooks')
->inject('proofForPassword') ->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 will be empty if the user has never set a password
$passwordUpdate = $user->getAttribute('passwordUpdate'); $passwordUpdate = $user->getAttribute('passwordUpdate');
@ -3295,7 +3306,7 @@ App::patch('/v1/account/email')
->setAttribute('passwordUpdate', DateTime::now()); ->setAttribute('passwordUpdate', DateTime::now());
} }
$target = Authorization::skip(fn () => $dbForProject->findOne('targets', [ $target = $authorization->skip(fn () => $dbForProject->findOne('targets', [
Query::equal('identifier', [$email]), Query::equal('identifier', [$email]),
])); ]));
@ -3311,7 +3322,7 @@ App::patch('/v1/account/email')
$oldTarget = $user->find('identifier', $oldEmail, 'targets'); $oldTarget = $user->find('identifier', $oldEmail, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { 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()); $dbForProject->purgeCachedDocument('users', $user->getId());
} catch (Duplicate) { } catch (Duplicate) {
@ -3352,8 +3363,9 @@ App::patch('/v1/account/phone')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('project') ->inject('project')
->inject('hooks') ->inject('hooks')
->inject('proofForPassword') ->inject('proofForPassword')
->action(function (string $phone, string $password, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $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 will be empty if the user has never set a password
$passwordUpdate = $user->getAttribute('passwordUpdate'); $passwordUpdate = $user->getAttribute('passwordUpdate');
@ -3368,7 +3380,7 @@ App::patch('/v1/account/phone')
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]); $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]), Query::equal('identifier', [$phone]),
])); ]));
@ -3399,7 +3411,7 @@ App::patch('/v1/account/phone')
$oldTarget = $user->find('identifier', $oldPhone, 'targets'); $oldTarget = $user->find('identifier', $oldPhone, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { 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()); $dbForProject->purgeCachedDocument('users', $user->getId());
} catch (Duplicate $th) { } catch (Duplicate $th) {
@ -3535,7 +3547,9 @@ App::post('/v1/account/recovery')
->inject('queueForMails') ->inject('queueForMails')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('proofForToken') ->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'))) { if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
} }
@ -3571,7 +3585,7 @@ App::post('/v1/account/recovery')
'ip' => $request->getIP(), 'ip' => $request->getIP(),
]); ]);
Authorization::setRole(Role::user($profile->getId())->toString()); $authorization->addRole(Role::user($profile->getId())->toString());
$recovery = $dbForProject->createDocument('tokens', $recovery $recovery = $dbForProject->createDocument('tokens', $recovery
->setAttribute('$permissions', [ ->setAttribute('$permissions', [
@ -3727,7 +3741,8 @@ App::put('/v1/account/recovery')
->inject('hooks') ->inject('hooks')
->inject('proofForPassword') ->inject('proofForPassword')
->inject('proofForToken') ->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 */ /** @var Appwrite\Utopia\Database\Documents\User $profile */
$profile = $dbForProject->getDocument('users', $userId); $profile = $dbForProject->getDocument('users', $userId);
@ -3741,7 +3756,7 @@ App::put('/v1/account/recovery')
throw new Exception(Exception::USER_INVALID_TOKEN); 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); $newPassword = $proofForPassword->hash($password);
@ -3844,7 +3859,8 @@ App::post('/v1/account/verifications/email')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('queueForMails') ->inject('queueForMails')
->inject('proofForToken') ->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'))) { if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
@ -3873,7 +3889,7 @@ App::post('/v1/account/verifications/email')
'ip' => $request->getIP(), 'ip' => $request->getIP(),
]); ]);
Authorization::setRole(Role::user($user->getId())->toString()); $authorization->addRole(Role::user($user->getId())->toString());
$verification = $dbForProject->createDocument('tokens', $verification $verification = $dbForProject->createDocument('tokens', $verification
->setAttribute('$permissions', [ ->setAttribute('$permissions', [
@ -4072,9 +4088,10 @@ App::put('/v1/account/verifications/email')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('proofForToken') ->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 */ /** @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()) { if ($profile->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND); throw new Exception(Exception::USER_NOT_FOUND);
@ -4086,7 +4103,7 @@ App::put('/v1/account/verifications/email')
throw new Exception(Exception::USER_INVALID_TOKEN); 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)); $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true));
@ -4146,7 +4163,8 @@ App::post('/v1/account/verifications/phone')
->inject('queueForStatsUsage') ->inject('queueForStatsUsage')
->inject('plan') ->inject('plan')
->inject('proofForCode') ->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'))) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
} }
@ -4185,7 +4203,7 @@ App::post('/v1/account/verifications/phone')
'ip' => $request->getIP(), 'ip' => $request->getIP(),
]); ]);
Authorization::setRole(Role::user($user->getId())->toString()); $authorization->addRole(Role::user($user->getId())->toString());
$verification = $dbForProject->createDocument('tokens', $verification $verification = $dbForProject->createDocument('tokens', $verification
->setAttribute('$permissions', [ ->setAttribute('$permissions', [
@ -4291,9 +4309,10 @@ App::put('/v1/account/verifications/phone')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('proofForCode') ->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 */ /** @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()) { if ($profile->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND); throw new Exception(Exception::USER_NOT_FOUND);
@ -4305,7 +4324,7 @@ App::put('/v1/account/verifications/phone')
throw new Exception(Exception::USER_INVALID_TOKEN); 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)); $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true));
@ -4358,12 +4377,13 @@ App::post('/v1/account/targets/push')
->inject('dbForProject') ->inject('dbForProject')
->inject('store') ->inject('store')
->inject('proofForToken') ->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; $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()) { if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
@ -4438,9 +4458,10 @@ App::put('/v1/account/targets/:targetId/push')
->inject('request') ->inject('request')
->inject('response') ->inject('response')
->inject('dbForProject') ->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()) { if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND); throw new Exception(Exception::USER_TARGET_NOT_FOUND);
@ -4503,8 +4524,9 @@ App::delete('/v1/account/targets/:targetId/push')
->inject('request') ->inject('request')
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject) { ->inject('authorization')
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); ->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()) { if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND); throw new Exception(Exception::USER_TARGET_NOT_FOUND);

File diff suppressed because it is too large Load diff

View file

@ -28,11 +28,12 @@ use Utopia\Validator\Text;
App::init() App::init()
->groups(['graphql']) ->groups(['graphql'])
->inject('project') ->inject('project')
->action(function (Document $project) { ->inject('authorization')
->action(function (Document $project, Authorization $authorization) {
if ( if (
array_key_exists('graphql', $project->getAttribute('apis', [])) array_key_exists('graphql', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['graphql'] && !$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); throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
} }

View file

@ -11,6 +11,7 @@ use Appwrite\Event\Func;
use Appwrite\Event\Mail; use Appwrite\Event\Mail;
use Appwrite\Event\Messaging; use Appwrite\Event\Messaging;
use Appwrite\Event\Migration; use Appwrite\Event\Migration;
use Appwrite\Event\Screenshot;
use Appwrite\Event\StatsResources; use Appwrite\Event\StatsResources;
use Appwrite\Event\StatsUsage; use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook; use Appwrite\Event\Webhook;
@ -26,6 +27,7 @@ use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Config\Config; use Utopia\Config\Config;
use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Domains\Validator\PublicDomain; use Utopia\Domains\Validator\PublicDomain;
use Utopia\Pools\Group; use Utopia\Pools\Group;
use Utopia\Registry\Registry; use Utopia\Registry\Registry;
@ -100,7 +102,8 @@ App::get('/v1/health/db')
)) ))
->inject('response') ->inject('response')
->inject('pools') ->inject('pools')
->action(function (Response $response, Group $pools) { ->inject('authorization')
->action(action: function (Response $response, Group $pools, Authorization $authorization) {
$output = []; $output = [];
$failures = []; $failures = [];
@ -113,14 +116,14 @@ App::get('/v1/health/db')
foreach ($config as $database) { foreach ($config as $database) {
try { try {
$adapter = new DatabasePool($pools->get($database)); $adapter = new DatabasePool($pools->get($database));
$adapter->setAuthorization($authorization);
$checkStart = \microtime(true); $checkStart = \microtime(true);
if ($adapter->ping()) { if ($adapter->ping()) {
$output[] = new Document([ $output[] = new Document([
'name' => $key . " ($database)", 'name' => $key . " ($database)",
'status' => 'pass', 'status' => 'pass',
'ping' => \round((\microtime(true) - $checkStart) / 1000) 'ping' => \round((\microtime(true) - $checkStart) * 1000)
]); ]);
} else { } else {
$failures[] = $database; $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)) { if (!empty($failures)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'DB failure on: ' . implode(", ", $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([ $output[] = new Document([
'name' => $key . " ($cache)", 'name' => $key . " ($cache)",
'status' => 'pass', 'status' => 'pass',
'ping' => \round((\microtime(true) - $checkStart) / 1000) 'ping' => \round((\microtime(true) - $checkStart) * 1000)
]); ]);
} else { } else {
$failures[] = $cache; $failures[] = $cache;
@ -240,7 +245,7 @@ App::get('/v1/health/pubsub')
$output[] = new Document([ $output[] = new Document([
'name' => $key . " ($pubsub)", 'name' => $key . " ($pubsub)",
'status' => 'pass', 'status' => 'pass',
'ping' => \round((\microtime(true) - $checkStart) / 1000) 'ping' => \round((\microtime(true) - $checkStart) * 1000)
]); ]);
} else { } else {
$failures[] = $pubsub; $failures[] = $pubsub;
@ -822,7 +827,7 @@ App::get('/v1/health/storage/local')
$output = [ $output = [
'status' => 'pass', 'status' => 'pass',
'ping' => \round((\microtime(true) - $checkStart) / 1000) 'ping' => \round((\microtime(true) - $checkStart) * 1000)
]; ];
$response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); $response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS);
@ -874,7 +879,7 @@ App::get('/v1/health/storage')
$output = [ $output = [
'status' => 'pass', 'status' => 'pass',
'ping' => \round((\microtime(true) - $checkStart) / 1000) 'ping' => \round((\microtime(true) - $checkStart) * 1000)
]; ];
$response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); $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_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME),
System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_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_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_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME),
System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME)
]), 'The name of the queue') ]), 'The name of the queue')
@ -972,6 +978,7 @@ App::get('/v1/health/queue/failed/:name')
->inject('queueForBuilds') ->inject('queueForBuilds')
->inject('queueForMessaging') ->inject('queueForMessaging')
->inject('queueForMigrations') ->inject('queueForMigrations')
->inject('queueForScreenshots')
->action(function ( ->action(function (
string $name, string $name,
int|string $threshold, int|string $threshold,
@ -987,7 +994,8 @@ App::get('/v1/health/queue/failed/:name')
Certificate $queueForCertificates, Certificate $queueForCertificates,
Build $queueForBuilds, Build $queueForBuilds,
Messaging $queueForMessaging, Messaging $queueForMessaging,
Migration $queueForMigrations Migration $queueForMigrations,
Screenshot $queueForScreenshots,
) { ) {
$threshold = \intval($threshold); $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_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME) => $queueForWebhooks,
System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $queueForCertificates, 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_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_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $queueForMessaging,
System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $queueForMigrations, System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $queueForMigrations,
}; };

View file

@ -36,6 +36,7 @@ use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\ID;
use Utopia\Database\Query; use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Queries; use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Cursor; 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('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) ->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('dbForProject')
->inject('authorization')
->inject('response') ->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 { try {
$queries = Query::parseQueries($queries); $queries = Query::parseQueries($queries);
} catch (QueryException $e) { } catch (QueryException $e) {
@ -1100,7 +1102,7 @@ App::get('/v1/messaging/providers')
} }
$providerId = $cursor->getValue(); $providerId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId)); $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('providers', $providerId));
if ($cursorDocument->isEmpty()) { if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Provider '{$providerId}' for the 'cursor' value not found."); 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('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) ->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('dbForProject')
->inject('authorization')
->inject('response') ->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 { try {
$queries = Query::parseQueries($queries); $queries = Query::parseQueries($queries);
} catch (QueryException $e) { } catch (QueryException $e) {
@ -2508,7 +2511,7 @@ App::get('/v1/messaging/topics')
} }
$topicId = $cursor->getValue(); $topicId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($cursorDocument->isEmpty()) { if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Topic '{$topicId}' for the 'cursor' value not found."); 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.') ->param('targetId', '', new UID(), 'Target ID. The target ID to link to the specified Topic ID.')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->inject('response') ->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; $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()) { if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND); throw new Exception(Exception::TOPIC_NOT_FOUND);
} }
if (!$authorization->isValid(new Input('subscribe', $topic->getAttribute('subscribe')))) {
$validator = new Authorization('subscribe'); throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
if (!$validator->isValid($topic->getAttribute('subscribe'))) {
throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription());
} }
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId));
if ($target->isEmpty()) { if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND); 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([ $subscriber = new Document([
'$id' => $subscriberId, '$id' => $subscriberId,
@ -2837,7 +2838,7 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE), default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE),
}; };
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute( $authorization->skip(fn () => $dbForProject->increaseDocumentAttribute(
'topics', 'topics',
$topicId, $topicId,
$totalAttribute, $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('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) ->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('dbForProject')
->inject('authorization')
->inject('response') ->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 { try {
$queries = Query::parseQueries($queries); $queries = Query::parseQueries($queries);
} catch (QueryException $e) { } catch (QueryException $e) {
@ -2894,7 +2896,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
$queries[] = Query::search('search', $search); $queries[] = Query::search('search', $search);
} }
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($topic->isEmpty()) { if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND); throw new Exception(Exception::TOPIC_NOT_FOUND);
@ -2917,7 +2919,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
} }
$subscriberId = $cursor->getValue(); $subscriberId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId)); $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId));
if ($cursorDocument->isEmpty()) { if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Subscriber '{$subscriberId}' for the 'cursor' value not found."); 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."); 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) { $subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject, $authorization) {
return function () use ($subscriber, $dbForProject) { return function () use ($subscriber, $dbForProject, $authorization) {
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); $user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
return $subscriber return $subscriber
->setAttribute('target', $target) ->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('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.')
->param('subscriberId', '', new UID(), 'Subscriber ID.') ->param('subscriberId', '', new UID(), 'Subscriber ID.')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->inject('response') ->inject('response')
->action(function (string $topicId, string $subscriberId, Database $dbForProject, Response $response) { ->action(function (string $topicId, string $subscriberId, Database $dbForProject, Authorization $authorization, Response $response) {
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($topic->isEmpty()) { if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND); 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); throw new Exception(Exception::SUBSCRIBER_NOT_FOUND);
} }
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); $user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$subscriber $subscriber
->setAttribute('target', $target) ->setAttribute('target', $target)
@ -3118,9 +3121,10 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
->param('subscriberId', '', new UID(), 'Subscriber ID.') ->param('subscriberId', '', new UID(), 'Subscriber ID.')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->inject('response') ->inject('response')
->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Response $response) { ->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Authorization $authorization, Response $response) {
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($topic->isEmpty()) { if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND); 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), default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE),
}; };
Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute( $authorization->skip(fn () => $dbForProject->decreaseDocumentAttribute(
'topics', 'topics',
$topicId, $topicId,
$totalAttribute, $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('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) ->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('dbForProject')
->inject('authorization')
->inject('response') ->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 { try {
$queries = Query::parseQueries($queries); $queries = Query::parseQueries($queries);
} catch (QueryException $e) { } catch (QueryException $e) {
@ -3729,7 +3734,7 @@ App::get('/v1/messaging/messages')
} }
$messageId = $cursor->getValue(); $messageId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('messages', $messageId)); $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('messages', $messageId));
if ($cursorDocument->isEmpty()) { if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Message '{$messageId}' for the 'cursor' value not found."); throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Message '{$messageId}' for the 'cursor' value not found.");

View file

@ -342,6 +342,7 @@ App::post('/v1/migrations/csv/imports')
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('dbForPlatform') ->inject('dbForPlatform')
->inject('authorization')
->inject('project') ->inject('project')
->inject('platform') ->inject('platform')
->inject('deviceForFiles') ->inject('deviceForFiles')
@ -356,6 +357,7 @@ App::post('/v1/migrations/csv/imports')
Response $response, Response $response,
Database $dbForProject, Database $dbForProject,
Database $dbForPlatform, Database $dbForPlatform,
Authorization $authorization,
Document $project, Document $project,
array $platform, array $platform,
Device $deviceForFiles, Device $deviceForFiles,
@ -363,7 +365,7 @@ App::post('/v1/migrations/csv/imports')
Event $queueForEvents, Event $queueForEvents,
Migration $queueForMigrations Migration $queueForMigrations
) { ) {
$bucket = Authorization::skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) { $bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
if ($internalFile) { if ($internalFile) {
return $dbForPlatform->getDocument('buckets', 'default'); return $dbForPlatform->getDocument('buckets', 'default');
} }
@ -374,7 +376,7 @@ App::post('/v1/migrations/csv/imports')
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); 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()) { if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
} }
@ -491,6 +493,7 @@ App::post('/v1/migrations/csv/exports')
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('dbForPlatform') ->inject('dbForPlatform')
->inject('authorization')
->inject('project') ->inject('project')
->inject('platform') ->inject('platform')
->inject('queueForEvents') ->inject('queueForEvents')
@ -509,6 +512,7 @@ App::post('/v1/migrations/csv/exports')
Response $response, Response $response,
Database $dbForProject, Database $dbForProject,
Database $dbForPlatform, Database $dbForPlatform,
Authorization $authorization,
Document $project, Document $project,
array $platform, array $platform,
Event $queueForEvents, Event $queueForEvents,
@ -520,7 +524,7 @@ App::post('/v1/migrations/csv/exports')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); 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()) { if ($bucket->isEmpty()) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); 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); 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()) { if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND); 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()) { if ($collection->isEmpty()) {
throw new Exception(Exception::COLLECTION_NOT_FOUND); throw new Exception(Exception::COLLECTION_NOT_FOUND);
} }

View file

@ -45,9 +45,10 @@ App::get('/v1/project/usage')
->inject('response') ->inject('response')
->inject('project') ->inject('project')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->inject('getLogsDB') ->inject('getLogsDB')
->inject('smsRates') ->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 = []; $stats = $total = $usage = [];
$format = 'Y-m-d 00:00:00'; $format = 'Y-m-d 00:00:00';
$firstDay = (new DateTime($startDate))->format($format); $firstDay = (new DateTime($startDate))->format($format);
@ -102,7 +103,7 @@ App::get('/v1/project/usage')
'1d' => 'Y-m-d\T00:00:00.000P', '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) { foreach ($metrics['total'] as $metric) {
$db = ($metric === METRIC_FILES_IMAGES_TRANSFORMED) ? $dbForLogs : $dbForProject; $db = ($metric === METRIC_FILES_IMAGES_TRANSFORMED) ? $dbForLogs : $dbForProject;
@ -286,7 +287,7 @@ App::get('/v1/project/usage')
}, $dbForProject->find('functions')); }, $dbForProject->find('functions'));
// This total is includes free and paid SMS usage // 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('metric', [METRIC_AUTH_METHOD_PHONE]),
Query::equal('period', ['1d']), Query::equal('period', ['1d']),
Query::greaterThanEqual('time', $firstDay), Query::greaterThanEqual('time', $firstDay),
@ -294,7 +295,7 @@ App::get('/v1/project/usage')
])); ]));
// This estimate is only for paid SMS 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::startsWith('metric', METRIC_AUTH_METHOD_PHONE . '.'),
Query::equal('period', ['1d']), Query::equal('period', ['1d']),
Query::greaterThanEqual('time', $firstDay), Query::greaterThanEqual('time', $firstDay),

View file

@ -86,16 +86,17 @@ App::post('/v1/teams')
->inject('response') ->inject('response')
->inject('user') ->inject('user')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->inject('queueForEvents') ->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()); $isPrivilegedUser = User::isPrivileged($authorization->getRoles());
$isAppUser = User::isApp(Authorization::getRoles()); $isAppUser = User::isApp($authorization->getRoles());
$teamId = $teamId == 'unique()' ? ID::unique() : $teamId; $teamId = $teamId == 'unique()' ? ID::unique() : $teamId;
try { try {
$team = Authorization::skip(fn () => $dbForProject->createDocument('teams', new Document([ $team = $authorization->skip(fn () => $dbForProject->createDocument('teams', new Document([
'$id' => $teamId, '$id' => $teamId,
'$permissions' => [ '$permissions' => [
Permission::read(Role::team($teamId)), Permission::read(Role::team($teamId)),
@ -491,6 +492,7 @@ App::post('/v1/teams/:teamId/memberships')
->inject('project') ->inject('project')
->inject('user') ->inject('user')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->inject('locale') ->inject('locale')
->inject('queueForMails') ->inject('queueForMails')
->inject('queueForMessaging') ->inject('queueForMessaging')
@ -500,9 +502,9 @@ App::post('/v1/teams/:teamId/memberships')
->inject('plan') ->inject('plan')
->inject('proofForPassword') ->inject('proofForPassword')
->inject('proofForToken') ->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) { ->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()); $isAppUser = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); $isPrivilegedUser = User::isPrivileged($authorization->getRoles());
$url = htmlentities($url); $url = htmlentities($url);
if (empty($url)) { if (empty($url)) {
@ -619,13 +621,13 @@ App::post('/v1/teams/:teamId/memberships')
]); ]);
try { try {
$invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', $userDocument)); $invitee = $authorization->skip(fn () => $dbForProject->createDocument('users', $userDocument));
} catch (Duplicate $th) { } catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS); 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) 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'); 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) ? $membership = ($isPrivilegedUser || $isAppUser) ?
Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)) : $authorization->skip(fn () => $dbForProject->createDocument('memberships', $membership)) :
$dbForProject->createDocument('memberships', $membership); $dbForProject->createDocument('memberships', $membership);
if ($isPrivilegedUser || $isAppUser) { 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) { } elseif ($membership->getAttribute('confirm') === false) {
$membership->setAttribute('secret', $proofForToken->hash($secret)); $membership->setAttribute('secret', $proofForToken->hash($secret));
@ -677,7 +679,7 @@ App::post('/v1/teams/:teamId/memberships')
} }
$membership = ($isPrivilegedUser || $isAppUser) ? $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); $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
} else { } else {
throw new Exception(Exception::MEMBERSHIP_ALREADY_CONFIRMED); throw new Exception(Exception::MEMBERSHIP_ALREADY_CONFIRMED);
@ -863,7 +865,8 @@ App::get('/v1/teams/:teamId/memberships')
->inject('response') ->inject('response')
->inject('project') ->inject('project')
->inject('dbForProject') ->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); $team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) { if ($team->isEmpty()) {
@ -933,7 +936,7 @@ App::get('/v1/teams/:teamId/memberships')
'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true, 'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true,
]; ];
$roles = Authorization::getRoles(); $roles = $authorization->getRoles();
$isPrivilegedUser = User::isPrivileged($roles); $isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles); $isAppUser = User::isApp($roles);
@ -1004,7 +1007,8 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
->inject('response') ->inject('response')
->inject('project') ->inject('project')
->inject('dbForProject') ->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); $team = $dbForProject->getDocument('teams', $teamId);
@ -1024,7 +1028,7 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true, 'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true,
]; ];
$roles = Authorization::getRoles(); $roles = $authorization->getRoles();
$isPrivilegedUser = User::isPrivileged($roles); $isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles); $isAppUser = User::isApp($roles);
@ -1103,8 +1107,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->inject('user') ->inject('user')
->inject('project') ->inject('project')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->inject('queueForEvents') ->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); $team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) { if ($team->isEmpty()) {
@ -1121,9 +1126,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
throw new Exception(Exception::USER_NOT_FOUND); throw new Exception(Exception::USER_NOT_FOUND);
} }
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); $isPrivilegedUser = User::isPrivileged($authorization->getRoles());
$isAppUser = User::isApp(Authorization::getRoles()); $isAppUser = User::isApp($authorization->getRoles());
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner'); $isOwner = $authorization->hasRole('team:' . $team->getId() . '/owner');
if ($project->getId() === 'console') { if ($project->getId() === 'console') {
// Quick check: fetch up to 2 owners to determine if only one exists // 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('response')
->inject('user') ->inject('user')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->inject('project') ->inject('project')
->inject('geodb') ->inject('geodb')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('store') ->inject('store')
->inject('proofForToken') ->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(); $protocol = $request->getProtocol();
$membership = $dbForProject->getDocument('memberships', $membershipId); $membership = $dbForProject->getDocument('memberships', $membershipId);
@ -1218,7 +1224,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
throw new Exception(Exception::MEMBERSHIP_NOT_FOUND); 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()) { if ($team->isEmpty()) {
throw new Exception(Exception::TEAM_NOT_FOUND); throw new Exception(Exception::TEAM_NOT_FOUND);
@ -1254,11 +1260,11 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->setAttribute('confirm', true) ->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 // Create session for the user if not logged in
if (!$hasSession) { if (!$hasSession) {
Authorization::setRole(Role::user($user->getId())->toString()); $authorization->addRole(Role::user($user->getId())->toString());
$detector = new Detector($request->getUserAgent('UNKNOWN')); $detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP()); $record = $geodb->get($request->getIP());
@ -1286,7 +1292,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$session = $dbForProject->createDocument('sessions', $session); $session = $dbForProject->createDocument('sessions', $session);
Authorization::setRole(Role::user($userId)->toString()); $authorization->addRole(Role::user($userId)->toString());
$encoded = $store $encoded = $store
->setProperty('id', $user->getId()) ->setProperty('id', $user->getId())
@ -1324,7 +1330,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$dbForProject->purgeCachedDocument('users', $user->getId()); $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 $queueForEvents
->setParam('userId', $user->getId()) ->setParam('userId', $user->getId())
@ -1368,8 +1374,9 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
->inject('project') ->inject('project')
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->inject('queueForEvents') ->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); $membership = $dbForProject->getDocument('memberships', $membershipId);
@ -1427,7 +1434,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
$dbForProject->purgeCachedDocument('users', $profile->getId()); $dbForProject->purgeCachedDocument('users', $profile->getId());
if ($membership->getAttribute('confirm')) { // Count only confirmed members 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 $queueForEvents

View file

@ -2678,8 +2678,8 @@ App::get('/v1/users/usage')
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('register') ->inject('authorization')
->action(function (string $range, Response $response, Database $dbForProject) { ->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) {
$periods = Config::getParam('usage', []); $periods = Config::getParam('usage', []);
$stats = $usage = []; $stats = $usage = [];
@ -2689,7 +2689,7 @@ App::get('/v1/users/usage')
METRIC_SESSIONS, METRIC_SESSIONS,
]; ];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $count => $metric) { foreach ($metrics as $count => $metric) {
$result = $dbForProject->findOne('stats', [ $result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),

View file

@ -76,7 +76,7 @@ use Utopia\VCS\Exception\RepositoryNotFound;
use function Swoole\Coroutine\batch; 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 = []; $errors = [];
foreach ($repositories as $repository) { foreach ($repositories as $repository) {
try { try {
@ -87,12 +87,12 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
} }
$projectId = $repository->getAttribute('projectId'); $projectId = $repository->getAttribute('projectId');
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
$dbForProject = $getProjectDB($project); $dbForProject = $getProjectDB($project);
$resourceCollection = $resourceType === "function" ? 'functions' : 'sites'; $resourceCollection = $resourceType === "function" ? 'functions' : 'sites';
$resourceId = $repository->getAttribute('resourceId'); $resourceId = $repository->getAttribute('resourceId');
$resource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId)); $resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
$resourceInternalId = $resource->getSequence(); $resourceInternalId = $resource->getSequence();
$deploymentId = ID::unique(); $deploymentId = ID::unique();
@ -141,7 +141,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = ''; $latestCommentId = '';
if (!empty($providerPullRequestId) && $resource->getAttribute('providerSilentMode', false) === false) { 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('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerPullRequestId', [$providerPullRequestId]), Query::equal('providerPullRequestId', [$providerPullRequestId]),
Query::orderDesc('$createdAt'), Query::orderDesc('$createdAt'),
@ -180,7 +180,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment())); $latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally { } finally {
Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId)); $authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
} }
} }
} else { } else {
@ -191,7 +191,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
if (!empty($latestCommentId)) { if (!empty($latestCommentId)) {
$teamId = $project->getAttribute('teamId', ''); $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(), '$id' => ID::unique(),
'$permissions' => [ '$permissions' => [
Permission::read(Role::team(ID::custom($teamId))), Permission::read(Role::team(ID::custom($teamId))),
@ -212,7 +212,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
} }
} }
} elseif (!empty($providerBranch)) { } elseif (!empty($providerBranch)) {
$latestComments = Authorization::skip(fn () => $dbForPlatform->find('vcsComments', [ $latestComments = $authorization->skip(fn () => $dbForPlatform->find('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerBranch', [$providerBranch]), Query::equal('providerBranch', [$providerBranch]),
Query::orderDesc('$createdAt'), Query::orderDesc('$createdAt'),
@ -251,7 +251,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment())); $latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally { } 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', ''); $commands[] = $resource->getAttribute('commands', '');
} }
$deployment = Authorization::skip(fn () => $dbForProject->createDocument('deployments', new Document([ $deployment = $authorization->skip(fn () => $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId, '$id' => $deploymentId,
'$permissions' => [ '$permissions' => [
Permission::read(Role::any()), Permission::read(Role::any()),
@ -334,7 +334,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); ->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') { if ($resource->getCollection() === 'sites') {
$projectId = $project->getId(); $projectId = $project->getId();
@ -344,7 +344,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$domain = ID::unique() . "." . $sitesDomain; $domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain); $ruleId = md5($domain);
$previewRuleId = $ruleId; $previewRuleId = $ruleId;
Authorization::skip( $authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([ fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId, '$id' => $ruleId,
'projectId' => $project->getId(), 'projectId' => $project->getId(),
@ -377,7 +377,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$domain = "branch-{$branchPrefix}-{$resourceProjectHash}.{$sitesDomain}"; $domain = "branch-{$branchPrefix}-{$resourceProjectHash}.{$sitesDomain}";
$ruleId = md5($domain); $ruleId = md5($domain);
try { try {
Authorization::skip( $authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([ fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId, '$id' => $ruleId,
'projectId' => $project->getId(), 'projectId' => $project->getId(),
@ -408,7 +408,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$domain = "commit-" . substr($providerCommitHash, 0, 16) . ".{$sitesDomain}"; $domain = "commit-" . substr($providerCommitHash, 0, 16) . ".{$sitesDomain}";
$ruleId = md5($domain); $ruleId = md5($domain);
try { try {
Authorization::skip( $authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([ fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId, '$id' => $ruleId,
'projectId' => $project->getId(), 'projectId' => $project->getId(),
@ -460,7 +460,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
if ($lockAcquired) { if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted // Wrap in try/finally to ensure lock file gets deleted
try { 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'; $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : ''; $previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
@ -472,7 +472,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()); $github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
} }
} finally { } 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('request')
->inject('response') ->inject('response')
->inject('dbForPlatform') ->inject('dbForPlatform')
->inject('authorization')
->inject('getProjectDB') ->inject('getProjectDB')
->inject('queueForBuilds') ->inject('queueForBuilds')
->inject('platform') ->inject('platform')
->action( ->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(); $payload = $request->getRawPayload();
$signatureRemote = $request->getHeader('x-hub-signature-256', ''); $signatureRemote = $request->getHeader('x-hub-signature-256', '');
$signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', ''); $signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', '');
@ -1516,14 +1517,14 @@ App::post('/v1/vcs/github/events')
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
//find resourceId from relevant resources table //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::equal('providerRepositoryId', [$providerRepositoryId]),
Query::limit(100), Query::limit(100),
])); ]));
// create new deployment only on push (not committed by us) and not when branch is created or deleted // 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) { 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) { } elseif ($event == $github::EVENT_INSTALLATION) {
if ($parsedPayload["action"] == "deleted") { if ($parsedPayload["action"] == "deleted") {
@ -1536,16 +1537,16 @@ App::post('/v1/vcs/github/events')
]); ]);
foreach ($installations as $installation) { foreach ($installations as $installation) {
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [ $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('installationInternalId', [$installation->getSequence()]), Query::equal('installationInternalId', [$installation->getSequence()]),
Query::limit(1000) Query::limit(1000)
])); ]));
foreach ($repositories as $repository) { 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) { } elseif ($event == $github::EVENT_PULL_REQUEST) {
@ -1574,12 +1575,12 @@ App::post('/v1/vcs/github/events')
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; $providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
$providerCommitMessage = $commitDetails["commitMessage"] ?? ''; $providerCommitMessage = $commitDetails["commitMessage"] ?? '';
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [ $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt') 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") { } elseif ($parsedPayload["action"] == "closed") {
// Allowed external contributions cleanup // Allowed external contributions cleanup
@ -1588,7 +1589,7 @@ App::post('/v1/vcs/github/events')
$external = $parsedPayload["external"] ?? true; $external = $parsedPayload["external"] ?? true;
if ($external) { if ($external) {
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [ $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt') Query::orderDesc('$createdAt')
])); ]));
@ -1599,7 +1600,7 @@ App::post('/v1/vcs/github/events')
if (\in_array($providerPullRequestId, $providerPullRequestIds)) { if (\in_array($providerPullRequestId, $providerPullRequestIds)) {
$providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]); $providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]);
$repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds); $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('response')
->inject('project') ->inject('project')
->inject('dbForPlatform') ->inject('dbForPlatform')
->inject('authorization')
->inject('getProjectDB') ->inject('getProjectDB')
->inject('queueForBuilds') ->inject('queueForBuilds')
->inject('platform') ->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); $installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) { if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND); 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('$id', [$repositoryId]),
Query::equal('projectInternalId', [$project->getSequence()]) 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 // 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'); $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
@ -1846,7 +1848,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
$providerCommitMessage = $pullRequestResponse['title'] ?? ''; $providerCommitMessage = $pullRequestResponse['title'] ?? '';
$providerCommitUrl = $pullRequestResponse['html_url'] ?? ''; $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(); $response->noContent();
}); });

View file

@ -59,7 +59,7 @@ Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); 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() ?? ''; $host = $request->getHostname() ?? '';
if (!empty($previewHostname)) { if (!empty($previewHostname)) {
@ -67,16 +67,16 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
} }
// TODO: (@Meldiron) Remove after 1.7.x migration // TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = Authorization::skip(function () use ($dbForPlatform, $host, $isMd5) { $rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($host)));
if ($isMd5) { } else {
return $dbForPlatform->getDocument('rules', md5($host)); $rule = $authorization->skip(
} fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$host]),
return $dbForPlatform->findOne('rules', [ Query::limit(1)
Query::equal('domain', [$host]), ])
]) ?? new Document(); )[0] ?? new Document();
}); }
$errorView = __DIR__ . '/../views/general/error.phtml'; $errorView = __DIR__ . '/../views/general/error.phtml';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; $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'); $projectId = $rule->getAttribute('projectId');
$project = Authorization::skip( $project = $authorization->skip(
fn () => $dbForPlatform->getDocument('projects', $projectId) fn () => $dbForPlatform->getDocument('projects', $projectId)
); );
@ -119,7 +119,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$accessedAt = $project->getAttribute('accessedAt', 0); $accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now()); $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 */ /** @var Document $deployment */
if (!empty($rule->getAttribute('deploymentId', ''))) { 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 { } else {
// 1.6.x DB schema compatibility // 1.6.x DB schema compatibility
// TODO: Make sure deploymentId is never empty, and remove this code // 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 // Document of site or function
$resource = $resourceType === 'function' ? $resource = $resourceType === 'function' ?
Authorization::skip(fn () => $dbForProject->getDocument('functions', $resourceId)) : $authorization->skip(fn () => $dbForProject->getDocument('functions', $resourceId)) :
Authorization::skip(fn () => $dbForProject->getDocument('sites', $resourceId)); $authorization->skip(fn () => $dbForProject->getDocument('sites', $resourceId));
// ID of active deployments // ID of active deployments
// Attempts to use attribute from both schemas (1.6 and 1.7) // Attempts to use attribute from both schemas (1.6 and 1.7)
$activeDeploymentId = $resource->getAttribute('deploymentId', $resource->getAttribute('deployment', '')); $activeDeploymentId = $resource->getAttribute('deploymentId', $resource->getAttribute('deployment', ''));
// Get deployment document, as intended originally // 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') { if ($deployment->getAttribute('resourceType', '') === 'functions') {
@ -199,8 +199,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
} }
$resource = $type === 'function' ? $resource = $type === 'function' ?
Authorization::skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) : $authorization->skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) :
Authorization::skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', ''))); $authorization->skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', '')));
$isPreview = $type === 'function' ? false : ($rule->getAttribute('trigger', '') !== 'manual'); $isPreview = $type === 'function' ? false : ($rule->getAttribute('trigger', '') !== 'manual');
@ -242,7 +242,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$userExists = false; $userExists = false;
$userId = $payload['userId'] ?? ''; $userId = $payload['userId'] ?? '';
if (!empty($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)) { if (!$user->isEmpty() && $user->getAttribute('status', false)) {
$userExists = true; $userExists = true;
} }
@ -255,7 +255,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
} }
$membershipExists = false; $membershipExists = false;
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
if (!$project->isEmpty() && isset($user)) { if (!$project->isEmpty() && isset($user)) {
$teamId = $project->getAttribute('teamId', ''); $teamId = $project->getAttribute('teamId', '');
$membership = $user->find('teamId', $teamId, 'memberships'); $membership = $user->find('teamId', $teamId, 'memberships');
@ -862,15 +862,16 @@ App::init()
->inject('devKey') ->inject('devKey')
->inject('apiKey') ->inject('apiKey')
->inject('cors') ->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 * Appwrite Router
*/ */
$hostname = $request->getHostname() ?? ''; $hostname = $request->getHostname() ?? '';
$platformHostnames = $platform['hostnames'] ?? []; $platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain // Only run Router when external domain
if (!in_array($hostname, $platformHostnames) || !empty($previewHostname)) { 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 (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey)) {
$utopia->getRoute()?->label('router', true); $utopia->getRoute()?->label('router', true);
} }
} }
@ -1033,7 +1034,8 @@ App::init()
->inject('dbForPlatform') ->inject('dbForPlatform')
->inject('queueForCertificates') ->inject('queueForCertificates')
->inject('platform') ->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(); $hostname = $request->getHostname();
$cache = Config::getParam('hostnames', []); $cache = Config::getParam('hostnames', []);
$platformHostnames = $platform['hostnames'] ?? []; $platformHostnames = $platform['hostnames'] ?? [];
@ -1061,64 +1063,64 @@ App::init()
} }
// 4. Check/create rule (requires DB access) // 4. Check/create rule (requires DB access)
Authorization::disable(); $authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, &$cache) {
try { try {
// TODO: (@Meldiron) Remove after 1.7.x migration // TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$document = $isMd5 $document = $isMd5
? $dbForPlatform->getDocument('rules', md5($domain->get())) ? $dbForPlatform->getDocument('rules', md5($domain->get()))
: $dbForPlatform->findOne('rules', [ : $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()]), 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()) { $dbForPlatform->createDocument('rules', $document);
return;
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() App::options()
@ -1141,7 +1143,8 @@ App::options()
->inject('devKey') ->inject('devKey')
->inject('apiKey') ->inject('apiKey')
->inject('cors') ->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 * Appwrite Router
*/ */
@ -1182,7 +1185,8 @@ App::error()
->inject('log') ->inject('log')
->inject('queueForStatsUsage') ->inject('queueForStatsUsage')
->inject('devKey') ->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'); $version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$route = $utopia->getRoute(); $route = $utopia->getRoute();
$class = \get_class($error); $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 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 (!$publish && $project->getId() !== 'console') {
if (!DBUser::isPrivileged(Authorization::getRoles())) { if (!DBUser::isPrivileged($authorization->getRoles())) {
$fileSize = 0; $fileSize = 0;
$file = $request->getFiles('file'); $file = $request->getFiles('file');
if (!empty($file)) { if (!empty($file)) {
@ -1326,7 +1330,7 @@ App::error()
$log->addExtra('file', $error->getFile()); $log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine()); $log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString()); $log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('roles', Authorization::getRoles()); $log->addExtra('roles', $authorization->getRoles());
try { try {
/* add queries to log */ /* add queries to log */
@ -1530,13 +1534,14 @@ App::get('/robots.txt')
->inject('platform') ->inject('platform')
->inject('previewHostname') ->inject('previewHostname')
->inject('apiKey') ->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'] ?? []; $platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) { if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml'); $template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false)); $response->text($template->render(false));
} else { } 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); $utopia->getRoute()?->label('router', true);
} }
} }
@ -1562,13 +1567,14 @@ App::get('/humans.txt')
->inject('platform') ->inject('platform')
->inject('previewHostname') ->inject('previewHostname')
->inject('apiKey') ->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'] ?? []; $platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) { if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml'); $template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false)); $response->text($template->render(false));
} else { } 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); $utopia->getRoute()?->label('router', true);
} }
} }
@ -1652,7 +1658,8 @@ App::get('/v1/ping')
->inject('project') ->inject('project')
->inject('dbForPlatform') ->inject('dbForPlatform')
->inject('queueForEvents') ->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') { if ($project->isEmpty() || $project->getId() === 'console') {
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND); throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
} }
@ -1664,7 +1671,7 @@ App::get('/v1/ping')
->setAttribute('pingCount', $pingCount) ->setAttribute('pingCount', $pingCount)
->setAttribute('pingedAt', $pingedAt); ->setAttribute('pingedAt', $pingedAt);
Authorization::skip(function () use ($dbForPlatform, $project) { $authorization->skip(function () use ($dbForPlatform, $project) {
$dbForPlatform->updateDocument('projects', $project->getId(), $project); $dbForPlatform->updateDocument('projects', $project->getId(), $project);
}); });

View file

@ -30,6 +30,7 @@ use Utopia\Database\DateTime;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Helpers\Role; use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Queue\Publisher; use Utopia\Queue\Publisher;
use Utopia\System\System; use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry; use Utopia\Telemetry\Adapter as Telemetry;
@ -233,7 +234,8 @@ App::init()
->inject('mode') ->inject('mode')
->inject('team') ->inject('team')
->inject('apiKey') ->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(); $route = $utopia->getRoute();
/** /**
@ -318,7 +320,7 @@ App::init()
// Handle special app role case // Handle special app role case
if ($apiKey->getRole() === User::ROLE_APPS) { if ($apiKey->getRole() === User::ROLE_APPS) {
// Disable authorization checks for API keys // Disable authorization checks for API keys
Authorization::setDefaultStatus(false); $authorization->setDefaultStatus(false);
$user = new User([ $user = new User([
'$id' => '', '$id' => '',
@ -392,14 +394,14 @@ App::init()
$scopes = \array_merge($scopes, $roles[$role]['scopes']); $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); $scopes = \array_unique($scopes);
Authorization::setRole($role); $authorization->addRole($role);
foreach ($user->getRoles() as $authRole) { foreach ($user->getRoles($authorization) as $authRole) {
Authorization::setRole($authRole); $authorization->addRole($authRole);
} }
// Step 6: Update project and user last activity // Step 6: Update project and user last activity
@ -407,7 +409,7 @@ App::init()
$accessedAt = $project->getAttribute('accessedAt', 0); $accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now()); $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 ( if (
array_key_exists($namespace, $project->getAttribute('services', [])) array_key_exists($namespace, $project->getAttribute('services', []))
&& !$project->getAttribute('services', [])[$namespace] && !$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); throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
} }
@ -509,14 +511,15 @@ App::init()
->inject('devKey') ->inject('devKey')
->inject('telemetry') ->inject('telemetry')
->inject('platform') ->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(); $route = $utopia->getRoute();
if ( if (
array_key_exists('rest', $project->getAttribute('apis', [])) array_key_exists('rest', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['rest'] && !$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); throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
} }
@ -546,7 +549,7 @@ App::init()
$closestLimit = null; $closestLimit = null;
$roles = Authorization::getRoles(); $roles = $authorization->getRoles();
$isPrivilegedUser = User::isPrivileged($roles); $isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles); $isAppUser = User::isApp($roles);
@ -657,10 +660,10 @@ App::init()
if ($useCache) { if ($useCache) {
$route = $utopia->match($request); $route = $utopia->match($request);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview'; $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(); $key = $request->cacheIdentifier();
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); $cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
$cache = new Cache( $cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId()) new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
); );
@ -677,10 +680,10 @@ App::init()
if ($type === 'bucket' && (!$isImageTransformation || !$isDisabled)) { if ($type === 'bucket' && (!$isImageTransformation || !$isDisabled)) {
$bucketId = $parts[1] ?? null; $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(); $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)) { if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@ -691,8 +694,7 @@ App::init()
} }
$fileSecurity = $bucket->getAttribute('fileSecurity', false); $fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ); $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead()));
$valid = $validator->isValid($bucket->getRead());
if (!$fileSecurity && !$valid && !$isToken) { if (!$fileSecurity && !$valid && !$isToken) {
throw new Exception(Exception::USER_UNAUTHORIZED); throw new Exception(Exception::USER_UNAUTHORIZED);
} }
@ -703,7 +705,7 @@ App::init()
if ($fileSecurity && !$valid && !$isToken) { if ($fileSecurity && !$valid && !$isToken) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId); $file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
} else { } 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()) { if (!$resourceToken->isEmpty() && $resourceToken->getAttribute('fileInternalId') !== $file->getSequence()) {
@ -714,11 +716,11 @@ App::init()
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
} }
//Do not update transformedAt if it's a console user //Do not update transformedAt if it's a console user
if (!User::isPrivileged(Authorization::getRoles())) { if (!User::isPrivileged($authorization->getRoles())) {
$transformedAt = $file->getAttribute('transformedAt', ''); $transformedAt = $file->getAttribute('transformedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) { if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
$file->setAttribute('transformedAt', DateTime::now()); $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('queueForWebhooks')
->inject('queueForRealtime') ->inject('queueForRealtime')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->inject('timelimit') ->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(); $responsePayload = $response->getPayload();
@ -976,11 +979,11 @@ App::shutdown()
$key = $request->cacheIdentifier(); $key = $request->cacheIdentifier();
$signature = md5($data['payload']); $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); $accessedAt = $cacheLog->getAttribute('accessedAt', 0);
$now = DateTime::now(); $now = DateTime::now();
if ($cacheLog->isEmpty()) { if ($cacheLog->isEmpty()) {
Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([ $authorization->skip(fn () => $dbForProject->createDocument('cache', new Document([
'$id' => $key, '$id' => $key,
'resource' => $resource, 'resource' => $resource,
'resourceType' => $resourceType, 'resourceType' => $resourceType,
@ -990,7 +993,7 @@ App::shutdown()
]))); ])));
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) { } elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
$cacheLog->setAttribute('accessedAt', $now); $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() // 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']); $cache->save($key, $data['payload']);
} }
@ -1002,7 +1005,7 @@ App::shutdown()
} }
if ($project->getId() !== 'console') { if ($project->getId() !== 'console') {
if (!User::isPrivileged(Authorization::getRoles())) { if (!User::isPrivileged($authorization->getRoles())) {
$fileSize = 0; $fileSize = 0;
$file = $request->getFiles('file'); $file = $request->getFiles('file');
if (!empty($file)) { if (!empty($file)) {

View file

@ -36,7 +36,8 @@ App::init()
->inject('request') ->inject('request')
->inject('project') ->inject('project')
->inject('geodb') ->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', ''); $denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', '');
if (!empty($denylist && $project->getId() === 'console')) { if (!empty($denylist && $project->getId() === 'console')) {
$countries = explode(',', $denylist); $countries = explode(',', $denylist);
@ -49,8 +50,8 @@ App::init()
$route = $utopia->match($request); $route = $utopia->match($request);
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); $isPrivilegedUser = User::isPrivileged($authorization->getRoles());
$isAppUser = User::isApp(Authorization::getRoles()); $isAppUser = User::isApp($authorization->getRoles());
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
return; return;

View file

@ -27,7 +27,6 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role; use Utopia\Database\Helpers\Role;
use Utopia\Database\Query; use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log; use Utopia\Logger\Log;
use Utopia\Logger\Log\User; use Utopia\Logger\Log\User;
use Utopia\Pools\Group; 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); createDatabase($app, 'getLogsDB', 'logs', $collections['logs'], $pools);
// create appwrite database, `dbForPlatform` is a direct access call. // 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()) { if ($dbForPlatform->getCollection(AuditAdapterSQL::COLLECTION)->isEmpty()) {
$adapter = new AdapterDatabase($dbForPlatform); $adapter = new AdapterDatabase($dbForPlatform);
$audit = new Audit($adapter); $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); $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..."); 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'), '$id' => ID::custom('screenshots'),
'$collection' => ID::custom('buckets'), '$collection' => ID::custom('buckets'),
'name' => 'Screenshots', 'name' => 'Screenshots',
@ -338,7 +339,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
'search' => 'buckets Screenshots', '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..."); Console::info(" └── Creating files collection for screenshots bucket...");
$files = $collections['buckets']['files'] ?? []; $files = $collections['buckets']['files'] ?? [];
@ -366,7 +367,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
'orders' => $index['orders'], 'orders' => $index['orders'],
]), $files['indexes']); ]), $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); App::setResource('pools', fn () => $pools);
try { try {
Authorization::cleanRoles(); $authorization = $app->getResource('authorization');
Authorization::setRole(Role::any()->toString());
$request->setAuthorization($authorization);
$response->setAuthorization($authorization);
$authorization->cleanRoles();
$authorization->addRole(Role::any()->toString());
$app->run($request, $response); $app->run($request, $response);
} catch (\Throwable $th) { } catch (\Throwable $th) {
@ -501,7 +506,7 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
$log->addExtra('file', $th->getFile()); $log->addExtra('file', $th->getFile());
$log->addExtra('line', $th->getLine()); $log->addExtra('line', $th->getLine());
$log->addExtra('trace', $th->getTraceAsString()); $log->addExtra('trace', $th->getTraceAsString());
$log->addExtra('roles', Authorization::getRoles()); $log->addExtra('roles', isset($authorization) ? $authorization->getRoles() : []);
$sdk = $route->getLabel("sdk", false); $sdk = $route->getLabel("sdk", false);
@ -560,7 +565,7 @@ $http->on(Constant::EVENT_TASK, function () use ($register, $domains) {
/** @var Utopia\Database\Database $dbForPlatform */ /** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('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 { try {
$time = DateTime::now(); $time = DateTime::now();
$limit = 1000; $limit = 1000;
@ -577,7 +582,8 @@ $http->on(Constant::EVENT_TASK, function () use ($register, $domains) {
} }
$results = []; $results = [];
try { try {
$results = Authorization::skip(fn () => $dbForPlatform->find('rules', $queries)); $authorization = $app->getResource('authorization');
$results = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries));
} catch (Throwable $th) { } catch (Throwable $th) {
Console::error($th->getMessage()); Console::error($th->getMessage());
} }

View file

@ -4,7 +4,6 @@ use Appwrite\OpenSSL\OpenSSL;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Query; use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\System\System; use Utopia\System\System;
Database::addFilter( Database::addFilter(
@ -70,11 +69,11 @@ Database::addFilter(
return; return;
}, },
function (mixed $value, Document $document, Database $database) { 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('collectionInternalId', [$document->getSequence()]),
Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]),
Query::limit($database->getLimitForAttributes()), Query::limit($database->getLimitForAttributes()),
]); ]));
foreach ($attributes as $attribute) { foreach ($attributes as $attribute) {
$attributeType = $attribute->getAttribute('type'); $attributeType = $attribute->getAttribute('type');
@ -105,12 +104,12 @@ Database::addFilter(
return; return;
}, },
function (mixed $value, Document $document, Database $database) { function (mixed $value, Document $document, Database $database) {
return $database return $database->getAuthorization()->skip(fn () => $database
->find('indexes', [ ->find('indexes', [
Query::equal('collectionInternalId', [$document->getSequence()]), Query::equal('collectionInternalId', [$document->getSequence()]),
Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]),
Query::limit($database->getLimitForIndexes()), Query::limit($database->getLimitForIndexes()),
]); ]));
} }
); );
@ -120,11 +119,11 @@ Database::addFilter(
return; return;
}, },
function (mixed $value, Document $document, Database $database) { function (mixed $value, Document $document, Database $database) {
return $database return $database->getAuthorization()->skip(fn () => $database
->find('platforms', [ ->find('platforms', [
Query::equal('projectInternalId', [$document->getSequence()]), Query::equal('projectInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY), Query::limit(APP_LIMIT_SUBQUERY),
]); ]));
} }
); );
@ -134,12 +133,12 @@ Database::addFilter(
return; return;
}, },
function (mixed $value, Document $document, Database $database) { function (mixed $value, Document $document, Database $database) {
return $database return $database->getAuthorization()->skip(fn () => $database
->find('keys', [ ->find('keys', [
Query::equal('resourceType', ['projects']), Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$document->getSequence()]), Query::equal('resourceInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY), Query::limit(APP_LIMIT_SUBQUERY),
]); ]));
} }
); );
@ -149,11 +148,11 @@ Database::addFilter(
return; return;
}, },
function (mixed $value, Document $document, Database $database) { function (mixed $value, Document $document, Database $database) {
return $database return $database->getAuthorization()->skip(fn () => $database
->find('devKeys', [ ->find('devKeys', [
Query::equal('projectInternalId', [$document->getSequence()]), Query::equal('projectInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY), Query::limit(APP_LIMIT_SUBQUERY),
]); ]));
} }
); );
@ -163,11 +162,11 @@ Database::addFilter(
return; return;
}, },
function (mixed $value, Document $document, Database $database) { function (mixed $value, Document $document, Database $database) {
return $database return $database->getAuthorization()->skip(fn () => $database
->find('webhooks', [ ->find('webhooks', [
Query::equal('projectInternalId', [$document->getSequence()]), Query::equal('projectInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY), Query::limit(APP_LIMIT_SUBQUERY),
]); ]));
} }
); );
@ -177,7 +176,7 @@ Database::addFilter(
return; return;
}, },
function (mixed $value, Document $document, Database $database) { 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::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY), Query::limit(APP_LIMIT_SUBQUERY),
])); ]));
@ -190,7 +189,7 @@ Database::addFilter(
return; return;
}, },
function (mixed $value, Document $document, Database $database) { function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database return $database->getAuthorization()->skip(fn () => $database
->find('tokens', [ ->find('tokens', [
Query::equal('userInternalId', [$document->getSequence()]), Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY), Query::limit(APP_LIMIT_SUBQUERY),
@ -204,7 +203,7 @@ Database::addFilter(
return; return;
}, },
function (mixed $value, Document $document, Database $database) { function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database return $database->getAuthorization()->skip(fn () => $database
->find('challenges', [ ->find('challenges', [
Query::equal('userInternalId', [$document->getSequence()]), Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY), Query::limit(APP_LIMIT_SUBQUERY),
@ -218,7 +217,7 @@ Database::addFilter(
return; return;
}, },
function (mixed $value, Document $document, Database $database) { function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database return $database->getAuthorization()->skip(fn () => $database
->find('authenticators', [ ->find('authenticators', [
Query::equal('userInternalId', [$document->getSequence()]), Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY), Query::limit(APP_LIMIT_SUBQUERY),
@ -232,7 +231,7 @@ Database::addFilter(
return; return;
}, },
function (mixed $value, Document $document, Database $database) { function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database return $database->getAuthorization()->skip(fn () => $database
->find('memberships', [ ->find('memberships', [
Query::equal('userInternalId', [$document->getSequence()]), Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY), Query::limit(APP_LIMIT_SUBQUERY),
@ -252,14 +251,14 @@ Database::addFilter(
default => ['function', 'site'] default => ['function', 'site']
}; };
return $database return $database->getAuthorization()->skip(fn () => $database
->find('variables', [ ->find('variables', [
Query::equal('resourceInternalId', [$document->getSequence()]), Query::equal('resourceInternalId', [$document->getSequence()]),
Query::equal('resourceType', $resourceType), Query::equal('resourceType', $resourceType),
Query::orderAsc('resourceType'), Query::orderAsc('resourceType'),
Query::orderAsc(), Query::orderAsc(),
Query::limit(APP_LIMIT_SUBQUERY), Query::limit(APP_LIMIT_SUBQUERY),
]); ]));
} }
); );
@ -295,11 +294,11 @@ Database::addFilter(
return; return;
}, },
function (mixed $value, Document $document, Database $database) { function (mixed $value, Document $document, Database $database) {
return $database return $database->getAuthorization()->skip(fn () => $database
->find('variables', [ ->find('variables', [
Query::equal('resourceType', ['project']), Query::equal('resourceType', ['project']),
Query::limit(APP_LIMIT_SUBQUERY) Query::limit(APP_LIMIT_SUBQUERY)
]); ]));
} }
); );
@ -332,7 +331,7 @@ Database::addFilter(
return; return;
}, },
function (mixed $value, Document $document, Database $database) { function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database return $database->getAuthorization()->skip(fn () => $database
->find('targets', [ ->find('targets', [
Query::equal('userInternalId', [$document->getSequence()]), Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY) Query::limit(APP_LIMIT_SUBQUERY)
@ -346,7 +345,7 @@ Database::addFilter(
return; return;
}, },
function (mixed $value, Document $document, Database $database) { 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'), fn ($document) => $document->getAttribute('targetInternalId'),
$database->find('subscribers', [ $database->find('subscribers', [
Query::equal('topicInternalId', [$document->getSequence()]), Query::equal('topicInternalId', [$document->getSequence()]),

View file

@ -15,6 +15,7 @@ use Appwrite\Event\Mail;
use Appwrite\Event\Messaging; use Appwrite\Event\Messaging;
use Appwrite\Event\Migration; use Appwrite\Event\Migration;
use Appwrite\Event\Realtime; use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\StatsResources; use Appwrite\Event\StatsResources;
use Appwrite\Event\StatsUsage; use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook; use Appwrite\Event\Webhook;
@ -129,6 +130,9 @@ App::setResource('queueForMails', function (Publisher $publisher) {
App::setResource('queueForBuilds', function (Publisher $publisher) { App::setResource('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher); return new Build($publisher);
}, ['publisher']); }, ['publisher']);
App::setResource('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
App::setResource('queueForDatabase', function (Publisher $publisher) { App::setResource('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher); return new EventDatabase($publisher);
}, ['publisher']); }, ['publisher']);
@ -226,7 +230,7 @@ App::setResource('allowedSchemes', function (Document $project) {
/** /**
* Rule associated with a request origin. * 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); $domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($domain)) { if (empty($domain)) {
return new Document(); return new Document();
@ -234,7 +238,7 @@ App::setResource('rule', function (Request $request, Database $dbForPlatform, Do
// TODO: (@Meldiron) Remove after 1.7.x migration // TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; $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) { if ($isMd5) {
return $dbForPlatform->getDocument('rules', md5($domain)); return $dbForPlatform->getDocument('rules', md5($domain));
} }
@ -249,7 +253,7 @@ App::setResource('rule', function (Request $request, Database $dbForPlatform, Do
} }
return $rule; return $rule;
}, ['request', 'dbForPlatform', 'project']); }, ['request', 'dbForPlatform', 'project', 'authorization']);
/** /**
* CORS service * CORS service
@ -317,7 +321,7 @@ App::setResource('redirectValidator', function (Document $devKey, array $allowed
return new Redirect($allowedHostnames, $allowedSchemes); return new Redirect($allowedHostnames, $allowedSchemes);
}, ['devKey', '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. * Handles user authentication and session validation.
* *
@ -337,7 +341,7 @@ App::setResource('user', function (string $mode, Document $project, Document $co
* overwriting the previous value. * overwriting the previous value.
*/ */
Authorization::setDefaultStatus(true); $authorization->setDefaultStatus(true);
$store->setKey('a_session_' . $project->getId()); $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 (APP_MODE_ADMIN === $mode) {
// if ($user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships')) { // 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 { // } else {
// $user = new Document([]); // $user = new Document([]);
// } // }
@ -436,9 +440,9 @@ App::setResource('user', function (string $mode, Document $project, Document $co
$dbForPlatform->setMetadata('user', $user->getId()); $dbForPlatform->setMetadata('user', $user->getId());
return $user; 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 Appwrite\Utopia\Request $request */
/** @var Utopia\Database\Database $dbForPlatform */ /** @var Utopia\Database\Database $dbForPlatform */
/** @var Utopia\Database\Document $console */ /** @var Utopia\Database\Document $console */
@ -449,10 +453,10 @@ App::setResource('project', function ($dbForPlatform, $request, $console) {
return $console; return $console;
} }
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
return $project; return $project;
}, ['dbForPlatform', 'request', 'console']); }, ['dbForPlatform', 'request', 'console', 'authorization']);
App::setResource('session', function (User $user, Store $store, Token $proofForToken) { App::setResource('session', function (User $user, Store $store, Token $proofForToken) {
if ($user->isEmpty()) { if ($user->isEmpty()) {
@ -475,10 +479,6 @@ App::setResource('session', function (User $user, Store $store, Token $proofForT
return; return;
}, ['user', 'store', 'proofForToken']); }, ['user', 'store', 'proofForToken']);
App::setResource('console', function () {
return new Document(Config::getParam('console'));
}, []);
App::setResource('store', function (): Store { App::setResource('store', function (): Store {
return new Store(); return new Store();
}); });
@ -509,7 +509,15 @@ App::setResource('proofForCode', function (): Code {
return $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') { if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform; return $dbForPlatform;
} }
@ -525,6 +533,7 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform
$database = new Database($adapter, $cache); $database = new Database($adapter, $cache);
$database $database
->setAuthorization($authorization)
->setMetadata('host', \gethostname()) ->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId()) ->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
@ -546,13 +555,15 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform
} }
return $database; 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')); $adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, $cache); $database = new Database($adapter, $cache);
$database $database
->setAuthorization($authorization)
->setNamespace('_console') ->setNamespace('_console')
->setMetadata('host', \gethostname()) ->setMetadata('host', \gethostname())
->setMetadata('project', 'console') ->setMetadata('project', 'console')
@ -562,12 +573,12 @@ App::setResource('dbForPlatform', function (Group $pools, Cache $cache) {
$database->setDocumentType('users', User::class); $database->setDocumentType('users', User::class);
return $database; 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 = []; $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') { if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform; return $dbForPlatform;
} }
@ -579,13 +590,15 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
$dsn = new DSN('mysql://' . $project->getAttribute('database')); $dsn = new DSN('mysql://' . $project->getAttribute('database'));
} }
$configure = (function (Database $database) use ($project, $dsn) { $configure = (function (Database $database) use ($project, $dsn, $authorization) {
$database $database
->setAuthorization($authorization)
->setMetadata('host', \gethostname()) ->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId()) ->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES)
$database->setDocumentType('users', User::class); ->setDocumentType('users', User::class)
;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
@ -615,12 +628,12 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
return $database; 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; $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') { if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int) $project->getSequence()); $database->setTenant((int) $project->getSequence());
return $database; return $database;
@ -630,6 +643,7 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = new Database($adapter, $cache); $database = new Database($adapter, $cache);
$database $database
->setAuthorization($authorization)
->setSharedTables(true) ->setSharedTables(true)
->setNamespace('logsV1') ->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
@ -642,7 +656,7 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
return $database; return $database;
}; };
}, ['pools', 'cache']); }, ['pools', 'cache', 'authorization']);
App::setResource('audit', function ($dbForProject) { App::setResource('audit', function ($dbForProject) {
$adapter = new AdapterDatabase($dbForProject); $adapter = new AdapterDatabase($dbForProject);
@ -841,7 +855,7 @@ App::setResource('promiseAdapter', function ($register) {
return $register->get('promiseAdapter'); return $register->get('promiseAdapter');
}, ['register']); }, ['register']);
App::setResource('schema', function ($utopia, $dbForProject) { App::setResource('schema', function ($utopia, $dbForProject, $authorization) {
$complexity = function (int $complexity, array $args) { $complexity = function (int $complexity, array $args) {
$queries = Query::parseQueries($args['queries'] ?? []); $queries = Query::parseQueries($args['queries'] ?? []);
@ -851,8 +865,8 @@ App::setResource('schema', function ($utopia, $dbForProject) {
return $complexity * $limit; return $complexity * $limit;
}; };
$attributes = function (int $limit, int $offset) use ($dbForProject) { $attributes = function (int $limit, int $offset) use ($dbForProject, $authorization) {
$attrs = Authorization::skip(fn () => $dbForProject->find('attributes', [ $attrs = $authorization->skip(fn () => $dbForProject->find('attributes', [
Query::limit($limit), Query::limit($limit),
Query::offset($offset), Query::offset($offset),
])); ]));
@ -926,7 +940,7 @@ App::setResource('schema', function ($utopia, $dbForProject) {
$urls, $urls,
$params, $params,
); );
}, ['utopia', 'dbForProject']); }, ['utopia', 'dbForProject', 'authorization']);
App::setResource('gitHub', function (Cache $cache) { App::setResource('gitHub', function (Cache $cache) {
return new VcsGitHub($cache); return new VcsGitHub($cache);
@ -954,7 +968,7 @@ App::setResource('smsRates', function () {
return []; 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', '')); $devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
// Check if given key match project's development keys // 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); $accessedAt = $key->getAttribute('accessedAt', 0);
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) { if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
$key->setAttribute('accessedAt', DatabaseDateTime::now()); $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()); $dbForPlatform->purgeCachedDocument('projects', $project->getId());
} }
@ -990,15 +1004,15 @@ App::setResource('devKey', function (Request $request, Document $project, array
/** Update access time as well */ /** Update access time as well */
$key->setAttribute('accessedAt', DatabaseDateTime::now()); $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()); $dbForPlatform->purgeCachedDocument('projects', $project->getId());
} }
} }
return $key; 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 = ''; $teamInternalId = '';
if ($project->getId() !== 'console') { if ($project->getId() !== 'console') {
$teamInternalId = $project->getAttribute('teamInternalId', ''); $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')) { if (str_starts_with($path, '/v1/projects/:projectId')) {
$uri = $request->getURI(); $uri = $request->getURI();
$pid = explode('/', $uri)[3]; $pid = explode('/', $uri)[3];
$p = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $pid)); $p = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $pid));
$teamInternalId = $p->getAttribute('teamInternalId', ''); $teamInternalId = $p->getAttribute('teamInternalId', '');
} elseif ($path === '/v1/projects') { } elseif ($path === '/v1/projects') {
$teamId = $request->getParam('teamId', ''); $teamId = $request->getParam('teamId', '');
@ -1017,7 +1031,7 @@ App::setResource('team', function (Document $project, Database $dbForPlatform, A
return new Document([]); return new Document([]);
} }
$team = Authorization::skip(fn () => $dbForPlatform->getDocument('teams', $teamId)); $team = $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $teamId));
return $team; return $team;
} }
} }
@ -1026,14 +1040,14 @@ App::setResource('team', function (Document $project, Database $dbForPlatform, A
return new Document([]); return new Document([]);
} }
$team = Authorization::skip(function () use ($dbForPlatform, $teamInternalId) { $team = $authorization->skip(function () use ($dbForPlatform, $teamInternalId) {
return $dbForPlatform->findOne('teams', [ return $dbForPlatform->findOne('teams', [
Query::equal('$sequence', [$teamInternalId]), Query::equal('$sequence', [$teamInternalId]),
]); ]);
}); });
return $team; return $team;
}, ['project', 'dbForPlatform', 'utopia', 'request']); }, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']);
App::setResource( App::setResource(
'isResourceBlocked', 'isResourceBlocked',
@ -1071,7 +1085,7 @@ App::setResource('apiKey', function (Request $request, Document $project): ?Key
App::setResource('executor', fn () => new Executor()); 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'); $tokenJWT = $request->getParam('token');
if (!empty($tokenJWT) && !$project->isEmpty()) { // JWT authentication if (!empty($tokenJWT) && !$project->isEmpty()) { // JWT authentication
@ -1089,7 +1103,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
return new Document([]); return new Document([]);
} }
$token = Authorization::skip(fn () => $dbForProject->getDocument('resourceTokens', $tokenId)); $token = $authorization->skip(fn () => $dbForProject->getDocument('resourceTokens', $tokenId));
if ($token->isEmpty()) { if ($token->isEmpty()) {
return new Document([]); return new Document([]);
@ -1107,7 +1121,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
} }
return match ($token->getAttribute('resourceType')) { 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')); $sequences = explode(':', $token->getAttribute('resourceInternalId'));
$ids = explode(':', $token->getAttribute('resourceId')); $ids = explode(':', $token->getAttribute('resourceId'));
@ -1118,7 +1132,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
$accessedAt = $token->getAttribute('accessedAt', 0); $accessedAt = $token->getAttribute('accessedAt', 0);
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) { if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) {
$token->setAttribute('accessedAt', DatabaseDateTime::now()); $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([ return new Document([
@ -1133,8 +1147,8 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
}; };
} }
return new Document([]); return new Document([]);
}, ['project', 'dbForProject', 'request']); }, ['project', 'dbForProject', 'request', 'authorization']);
App::setResource('transactionState', function (Database $dbForProject) { App::setResource('transactionState', function (Database $dbForProject, Authorization $authorization) {
return new TransactionState($dbForProject); return new TransactionState($dbForProject, $authorization);
}, ['dbForProject']); }, ['dbForProject', 'authorization']);

View file

@ -32,7 +32,6 @@ use Utopia\Database\Document;
use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role; use Utopia\Database\Helpers\Role;
use Utopia\Database\Query; use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN; use Utopia\DSN\DSN;
use Utopia\Logger\Log; use Utopia\Logger\Log;
use Utopia\Pools\Group; use Utopia\Pools\Group;
@ -309,7 +308,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
'value' => '{}' 'value' => '{}'
]); ]);
$statsDocument = Authorization::skip(fn () => $database->createDocument('realtime', $document)); $statsDocument = $database->getAuthorization()->skip(fn () => $database->createDocument('realtime', $document));
break; break;
} catch (Throwable) { } catch (Throwable) {
Console::warning("Collection not ready. Retrying connection ({$attempts})..."); 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('timestamp', DateTime::now())
->setAttribute('value', json_encode($payload)); ->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) { } catch (Throwable $th) {
$logError($th, "updateWorkerDocument"); $logError($th, "updateWorkerDocument");
} }
@ -370,7 +369,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$payload = []; $payload = [];
$list = Authorization::skip(fn () => $database->find('realtime', [ $list = $database->getAuthorization()->skip(fn () => $database->find('realtime', [
Query::greaterThan('timestamp', DateTime::addSeconds(new \DateTime(), -15)), 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)) { if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) {
$connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId])); $connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId]));
$consoleDatabase = getConsoleDB(); $consoleDatabase = getConsoleDB();
$project = Authorization::skip(fn () => $consoleDatabase->getDocument('projects', $projectId)); $project = $consoleDatabase->getAuthorization()->skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
$database = getProjectDB($project); $database = getProjectDB($project);
/** @var Appwrite\Utopia\Database\Documents\User $user */ /** @var Appwrite\Utopia\Database\Documents\User $user */
$user = $database->getDocument('users', $userId); $user = $database->getDocument('users', $userId);
$roles = $user->getRoles(); $roles = $user->getRoles($database->getAuthorization());
$channels = $realtime->connections[$connection]['channels']; $channels = $realtime->connections[$connection]['channels'];
$realtime->unsubscribe($connection); $realtime->unsubscribe($connection);
@ -526,6 +525,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
try { try {
/** @var Document $project */ /** @var Document $project */
$project = $app->getResource('project'); $project = $app->getResource('project');
$authorization = $app->getResource('authorization');
/* /*
* Project Check * Project Check
@ -537,7 +537,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
if ( if (
array_key_exists('realtime', $project->getAttribute('apis', [])) array_key_exists('realtime', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['realtime'] && !$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); 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()); throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
} }
$roles = $user->getRoles(); $roles = $user->getRoles($authorization);
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId()); $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->subscribe($project->getId(), $connection, $roles, $channels);
$realtime->connections[$connection]['authorization'] = $authorization;
$user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT); $user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
$server->send([$connection], json_encode([ $server->send([$connection], json_encode([
@ -614,6 +616,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$code = 500; $code = 500;
} }
$message = $th->getMessage(); $message = $th->getMessage();
// sanitize 0 && 5xx errors // 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) { $server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
try { try {
$response = new Response(new SwooleResponse()); $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 = getConsoleDB();
$database->setAuthorization($authorization);
if ($projectId !== 'console') { if ($projectId !== 'console') {
$project = Authorization::skip(fn () => $database->getDocument('projects', $projectId)); $project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
$database = getProjectDB($project); $database = getProjectDB($project);
$database->setAuthorization($authorization);
} else { } else {
$project = null; $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.'); 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()); $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); $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); $user = $response->output($user, Response::MODEL_ACCOUNT);
$server->send([$connection], json_encode([ $server->send([$connection], json_encode([
'type' => 'response', 'type' => 'response',

View file

@ -14,6 +14,7 @@ use Appwrite\Event\Mail;
use Appwrite\Event\Messaging; use Appwrite\Event\Messaging;
use Appwrite\Event\Migration; use Appwrite\Event\Migration;
use Appwrite\Event\Realtime; use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\StatsUsage; use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook; use Appwrite\Event\Webhook;
use Appwrite\Platform\Appwrite; use Appwrite\Platform\Appwrite;
@ -48,19 +49,30 @@ use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry; use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Telemetry\Adapter\None as NoTelemetry; use Utopia\Telemetry\Adapter\None as NoTelemetry;
Authorization::disable();
Runtime::enableCoroutine(); Runtime::enableCoroutine();
Server::setResource('register', fn () => $register); 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'); $pools = $register->get('pools');
$adapter = new DatabasePool($pools->get('console')); $adapter = new DatabasePool($pools->get('console'));
$dbForPlatform = new Database($adapter, $cache); $dbForPlatform = new Database($adapter, $cache);
$dbForPlatform->setNamespace('_console');
$dbForPlatform->setDocumentType('users', User::class); $dbForPlatform
->setAuthorization($authorization)
->setNamespace('_console')
->setDocumentType('users', User::class)
;
return $dbForPlatform; return $dbForPlatform;
}, ['cache', 'register']); }, ['cache', 'register', 'authorization']);
Server::setResource('project', function (Message $message, Database $dbForPlatform) { Server::setResource('project', function (Message $message, Database $dbForPlatform) {
$payload = $message->getPayload() ?? []; $payload = $message->getPayload() ?? [];
@ -73,7 +85,7 @@ Server::setResource('project', function (Message $message, Database $dbForPlatfo
return $dbForPlatform->getDocument('projects', $project->getId()); return $dbForPlatform->getDocument('projects', $project->getId());
}, ['message', 'dbForPlatform']); }, ['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') { if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform; return $dbForPlatform;
} }
@ -105,15 +117,17 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
->setNamespace('_' . $project->getSequence()); ->setNamespace('_' . $project->getSequence());
} }
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER); $database
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database; 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 $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') { if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform; return $dbForPlatform;
} }
@ -127,7 +141,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
if (isset($databases[$dsn->getHost()])) { if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()]; $database = $databases[$dsn->getHost()];
$database->setAuthorization($authorization);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) { if (\in_array($dsn->getHost(), $sharedTables)) {
@ -164,15 +178,17 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
->setNamespace('_' . $project->getSequence()); ->setNamespace('_' . $project->getSequence());
} }
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER); $database
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database; 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; $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') { if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int)$project->getSequence()); $database->setTenant((int)$project->getSequence());
return $database; return $database;
@ -182,6 +198,7 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = new Database($adapter, $cache); $database = new Database($adapter, $cache);
$database $database
->setAuthorization($authorization)
->setSharedTables(true) ->setSharedTables(true)
->setNamespace('logsV1') ->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
@ -194,7 +211,7 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache) {
return $database; return $database;
}; };
}, ['pools', 'cache']); }, ['pools', 'cache', 'authorization']);
Server::setResource('abuseRetention', function () { Server::setResource('abuseRetention', function () {
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day 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); return new Build($publisher);
}, ['publisher']); }, ['publisher']);
Server::setResource('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
Server::setResource('queueForDeletes', function (Publisher $publisher) { Server::setResource('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher); return new Delete($publisher);
}, ['publisher']); }, ['publisher']);
@ -509,7 +530,8 @@ $worker
->inject('log') ->inject('log')
->inject('pools') ->inject('pools')
->inject('project') ->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'); $version = System::getEnv('_APP_VERSION', 'UNKNOWN');
if ($logger) { if ($logger) {
@ -525,7 +547,7 @@ $worker
$log->addExtra('file', $error->getFile()); $log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine()); $log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString()); $log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('roles', Authorization::getRoles()); $log->addExtra('roles', $authorization->getRoles());
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; $isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);

3
bin/worker-screenshots Normal file
View file

@ -0,0 +1,3 @@
#!/bin/sh
exec php /usr/src/code/app/worker.php screenshots "$@"

View file

@ -45,14 +45,14 @@
"ext-sockets": "*", "ext-sockets": "*",
"appwrite/php-runtimes": "0.19.*", "appwrite/php-runtimes": "0.19.*",
"appwrite/php-clamav": "2.0.*", "appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "1.*.*", "utopia-php/abuse": "1.*",
"utopia-php/analytics": "0.10.*", "utopia-php/analytics": "0.10.*",
"utopia-php/audit": "2.0.2-rc3", "utopia-php/audit": "2.*",
"utopia-php/auth": "0.5.*", "utopia-php/auth": "0.5.*",
"utopia-php/cache": "0.13.*", "utopia-php/cache": "0.13.*",
"utopia-php/cli": "0.15.*", "utopia-php/cli": "0.15.*",
"utopia-php/config": "1.*.*", "utopia-php/config": "1.*",
"utopia-php/database": "3.*.*", "utopia-php/database": "4.*",
"utopia-php/detector": "0.2.*", "utopia-php/detector": "0.2.*",
"utopia-php/domains": "0.9.*", "utopia-php/domains": "0.9.*",
"utopia-php/emails": "0.6.*", "utopia-php/emails": "0.6.*",
@ -64,7 +64,7 @@
"utopia-php/locale": "0.8.*", "utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*", "utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.20.*", "utopia-php/messaging": "0.20.*",
"utopia-php/migration": "1.3.*", "utopia-php/migration": "1.*",
"utopia-php/orchestration": "0.9.*", "utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.*", "utopia-php/platform": "0.7.*",
"utopia-php/pools": "0.8.*", "utopia-php/pools": "0.8.*",

113
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "ff3172688b600aa3c560131c9d9c5588", "content-hash": "ad3d3cc3b265daf8657cb43836fc9879",
"packages": [ "packages": [
{ {
"name": "adhocore/jwt", "name": "adhocore/jwt",
@ -756,16 +756,16 @@
}, },
{ {
"name": "google/protobuf", "name": "google/protobuf",
"version": "v4.33.2", "version": "v4.33.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/protocolbuffers/protobuf-php.git", "url": "https://github.com/protocolbuffers/protobuf-php.git",
"reference": "fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318" "reference": "22d28025cda0d223a2e48c2e16c5284ecc9f5402"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318", "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/22d28025cda0d223a2e48c2e16c5284ecc9f5402",
"reference": "fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318", "reference": "22d28025cda0d223a2e48c2e16c5284ecc9f5402",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -794,9 +794,9 @@
"proto" "proto"
], ],
"support": { "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", "name": "league/csv",
@ -3455,25 +3455,24 @@
}, },
{ {
"name": "utopia-php/abuse", "name": "utopia-php/abuse",
"version": "1.2.0", "version": "1.0.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/utopia-php/abuse.git", "url": "https://github.com/utopia-php/abuse.git",
"reference": "3339d057c6bb1fa3e5ac5b2598923f6938425ec2" "reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/3339d057c6bb1fa3e5ac5b2598923f6938425ec2", "url": "https://api.github.com/repos/utopia-php/abuse/zipball/611fa66a97e87c0dbbc133a717d970da7a5ca828",
"reference": "3339d057c6bb1fa3e5ac5b2598923f6938425ec2", "reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"appwrite/appwrite": "19.*.*",
"ext-curl": "*", "ext-curl": "*",
"ext-pdo": "*", "ext-pdo": "*",
"ext-redis": "*", "ext-redis": "*",
"php": ">=8.0", "php": ">=8.0",
"utopia-php/database": "3.*.*" "utopia-php/database": "*"
}, },
"require-dev": { "require-dev": {
"laravel/pint": "1.*", "laravel/pint": "1.*",
@ -3501,9 +3500,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/utopia-php/abuse/issues", "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", "name": "utopia-php/analytics",
@ -3553,21 +3552,21 @@
}, },
{ {
"name": "utopia-php/audit", "name": "utopia-php/audit",
"version": "2.0.2-rc3", "version": "2.0.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/utopia-php/audit.git", "url": "https://github.com/utopia-php/audit.git",
"reference": "f60a298b516300f56a328403b334b7d62a96e7e7" "reference": "27d66630f528473cb563bbcf362d7d9a711b384e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/f60a298b516300f56a328403b334b7d62a96e7e7", "url": "https://api.github.com/repos/utopia-php/audit/zipball/27d66630f528473cb563bbcf362d7d9a711b384e",
"reference": "f60a298b516300f56a328403b334b7d62a96e7e7", "reference": "27d66630f528473cb563bbcf362d7d9a711b384e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.0", "php": ">=8.0",
"utopia-php/database": "3.*", "utopia-php/database": "4.*",
"utopia-php/fetch": "0.5.*", "utopia-php/fetch": "0.5.*",
"utopia-php/validators": "0.1.*" "utopia-php/validators": "0.1.*"
}, },
@ -3596,9 +3595,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/utopia-php/audit/issues", "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", "name": "utopia-php/auth",
@ -3899,16 +3898,16 @@
}, },
{ {
"name": "utopia-php/database", "name": "utopia-php/database",
"version": "3.6.1", "version": "4.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/utopia-php/database.git", "url": "https://github.com/utopia-php/database.git",
"reference": "c8c1b2f5770245dd4006e2680681e3efbe8b1fa7" "reference": "783193d5cdc723b3784e8fb399068b17d4228d53"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/c8c1b2f5770245dd4006e2680681e3efbe8b1fa7", "url": "https://api.github.com/repos/utopia-php/database/zipball/783193d5cdc723b3784e8fb399068b17d4228d53",
"reference": "c8c1b2f5770245dd4006e2680681e3efbe8b1fa7", "reference": "783193d5cdc723b3784e8fb399068b17d4228d53",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3951,9 +3950,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/utopia-php/database/issues", "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", "name": "utopia-php/detector",
@ -4267,16 +4266,16 @@
}, },
{ {
"name": "utopia-php/framework", "name": "utopia-php/framework",
"version": "0.33.35", "version": "0.33.36",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/utopia-php/http.git", "url": "https://github.com/utopia-php/http.git",
"reference": "82b139fb04f30045db51b0d322224f222da32313" "reference": "fd835ed77e1cdf327067ce4e650cce86304e7098"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/82b139fb04f30045db51b0d322224f222da32313", "url": "https://api.github.com/repos/utopia-php/http/zipball/fd835ed77e1cdf327067ce4e650cce86304e7098",
"reference": "82b139fb04f30045db51b0d322224f222da32313", "reference": "fd835ed77e1cdf327067ce4e650cce86304e7098",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4309,9 +4308,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/utopia-php/http/issues", "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", "name": "utopia-php/image",
@ -4516,16 +4515,16 @@
}, },
{ {
"name": "utopia-php/migration", "name": "utopia-php/migration",
"version": "1.3.13", "version": "1.4.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/utopia-php/migration.git", "url": "https://github.com/utopia-php/migration.git",
"reference": "c5e3f5e970e62e8f7db97b5b90baae2af800a715" "reference": "4cb7a0e65a36058d153ef5643090414c6525e4a2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/c5e3f5e970e62e8f7db97b5b90baae2af800a715", "url": "https://api.github.com/repos/utopia-php/migration/zipball/4cb7a0e65a36058d153ef5643090414c6525e4a2",
"reference": "c5e3f5e970e62e8f7db97b5b90baae2af800a715", "reference": "4cb7a0e65a36058d153ef5643090414c6525e4a2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4534,7 +4533,7 @@
"ext-openssl": "*", "ext-openssl": "*",
"php": ">=8.1", "php": ">=8.1",
"utopia-php/console": "0.0.*", "utopia-php/console": "0.0.*",
"utopia-php/database": "3.*", "utopia-php/database": "4.*",
"utopia-php/dsn": "0.2.*", "utopia-php/dsn": "0.2.*",
"utopia-php/storage": "0.18.*" "utopia-php/storage": "0.18.*"
}, },
@ -4565,9 +4564,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/utopia-php/migration/issues", "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", "name": "utopia-php/mongo",
@ -5014,22 +5013,22 @@
}, },
{ {
"name": "utopia-php/swoole", "name": "utopia-php/swoole",
"version": "0.8.5", "version": "0.8.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/utopia-php/swoole.git", "url": "https://github.com/utopia-php/swoole.git",
"reference": "e42b6b8e44c457a7b35d8a857d7af1d67d667c58" "reference": "14b00277c35a258cb263706fd4e05c50368feb4f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/e42b6b8e44c457a7b35d8a857d7af1d67d667c58", "url": "https://api.github.com/repos/utopia-php/swoole/zipball/14b00277c35a258cb263706fd4e05c50368feb4f",
"reference": "e42b6b8e44c457a7b35d8a857d7af1d67d667c58", "reference": "14b00277c35a258cb263706fd4e05c50368feb4f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-swoole": "*", "ext-swoole": "*",
"php": ">=8.0", "php": ">=8.0",
"utopia-php/framework": "0.33.35" "utopia-php/framework": "0.33.36"
}, },
"require-dev": { "require-dev": {
"laravel/pint": "1.2.*", "laravel/pint": "1.2.*",
@ -5059,9 +5058,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/utopia-php/swoole/issues", "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", "name": "utopia-php/system",
@ -5439,16 +5438,16 @@
"packages-dev": [ "packages-dev": [
{ {
"name": "appwrite/sdk-generator", "name": "appwrite/sdk-generator",
"version": "1.8.9", "version": "1.8.11",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/appwrite/sdk-generator.git", "url": "https://github.com/appwrite/sdk-generator.git",
"reference": "5fc210f7403f9ecfa068cd2a74210ec6e2a3cec1" "reference": "936404bbcbf4cd692bac102f2912b6c97ac87215"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/5fc210f7403f9ecfa068cd2a74210ec6e2a3cec1", "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/936404bbcbf4cd692bac102f2912b6c97ac87215",
"reference": "5fc210f7403f9ecfa068cd2a74210ec6e2a3cec1", "reference": "936404bbcbf4cd692bac102f2912b6c97ac87215",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -5484,9 +5483,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": { "support": {
"issues": "https://github.com/appwrite/sdk-generator/issues", "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", "name": "doctrine/annotations",
@ -8945,9 +8944,7 @@
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": { "stability-flags": {},
"utopia-php/audit": 5
},
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
@ -8971,5 +8968,5 @@
"platform-overrides": { "platform-overrides": {
"php": "8.3" "php": "8.3"
}, },
"plugin-api-version": "2.6.0" "plugin-api-version": "2.9.0"
} }

View file

@ -466,14 +466,12 @@ services:
- appwrite-functions:/storage/functions:rw - appwrite-functions:/storage/functions:rw
- appwrite-sites:/storage/sites:rw - appwrite-sites:/storage/sites:rw
- appwrite-builds:/storage/builds:rw - appwrite-builds:/storage/builds:rw
- appwrite-uploads:/storage/uploads:rw
- ./app:/usr/src/code/app - ./app:/usr/src/code/app
- ./src:/usr/src/code/src - ./src:/usr/src/code/src
depends_on: depends_on:
- redis - redis
- mariadb - mariadb
environment: environment:
- _APP_BROWSER_HOST
- _APP_ENV - _APP_ENV
- _APP_WORKER_PER_CORE - _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1 - _APP_OPENSSL_KEY_V1
@ -529,6 +527,65 @@ services:
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "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: appwrite-worker-certificates:
entrypoint: worker-certificates entrypoint: worker-certificates
<<: *x-logging <<: *x-logging

View file

@ -20,10 +20,12 @@ use Utopia\Database\Validator\Authorization;
class TransactionState class TransactionState
{ {
private Database $dbForProject; private Database $dbForProject;
private Authorization $authorization;
public function __construct(Database $dbForProject) /** @var Authorization $authorization */
public function __construct(Database $dbForProject, Authorization $authorization)
{ {
$this->dbForProject = $dbForProject; $this->dbForProject = $dbForProject;
$this->authorization = $authorization;
} }
@ -342,12 +344,12 @@ class TransactionState
*/ */
private function getTransactionState(string $transactionId): array 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') { if ($transaction->isEmpty() || $transaction->getAttribute('status') !== 'pending') {
return []; return [];
} }
$operations = Authorization::skip(fn () => $this->dbForProject->find('transactionLogs', [ $operations = $this->authorization->skip(fn () => $this->dbForProject->find('transactionLogs', [
Query::equal('transactionInternalId', [$transaction->getSequence()]), Query::equal('transactionInternalId', [$transaction->getSequence()]),
Query::orderAsc(), Query::orderAsc(),
Query::limit(PHP_INT_MAX) Query::limit(PHP_INT_MAX)

View file

@ -3,8 +3,10 @@
namespace Appwrite\Deletes; namespace Appwrite\Deletes;
use Appwrite\Extend\Exception; use Appwrite\Extend\Exception;
use Utopia\Console;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Query; use Utopia\Database\Query;
class Targets class Targets
@ -42,12 +44,17 @@ class Targets
MESSAGE_TYPE_PUSH => 'pushTotal', MESSAGE_TYPE_PUSH => 'pushTotal',
default => throw new Exception('Invalid target provider type'), default => throw new Exception('Invalid target provider type'),
}; };
$database->decreaseDocumentAttribute(
'topics', try {
$topicId, $database->decreaseDocumentAttribute(
$totalAttribute, 'topics',
min: 0 $topicId,
); $totalAttribute,
min: 0
);
} catch (LimitException $e) {
Console::error("Delete subscribers decreaseDocumentAttribute (topicId={$topicId}): {$e->getMessage()}");
}
} }
} }
); );

View file

@ -39,6 +39,9 @@ class Event
public const BUILDS_QUEUE_NAME = 'v1-builds'; public const BUILDS_QUEUE_NAME = 'v1-builds';
public const BUILDS_CLASS_NAME = 'BuildsV1'; 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_QUEUE_NAME = 'v1-messaging';
public const MESSAGING_CLASS_NAME = 'MessagingV1'; public const MESSAGING_CLASS_NAME = 'MessagingV1';

View file

@ -0,0 +1,50 @@
<?php
namespace Appwrite\Event;
use Utopia\Config\Config;
use Utopia\Queue\Publisher;
use Utopia\System\System;
class Screenshot extends Event
{
protected string $deploymentId = '';
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
$this
->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;
}
}

View file

@ -100,8 +100,6 @@ abstract class Migration
public function __construct() public function __construct()
{ {
Authorization::disable();
Authorization::setDefaultStatus(false);
$this->collections = Config::getParam('collections', []); $this->collections = Config::getParam('collections', []);
@ -129,6 +127,7 @@ abstract class Migration
Document $project, Document $project,
Database $dbForProject, Database $dbForProject,
Database $dbForPlatform, Database $dbForPlatform,
Authorization $authorization,
?callable $getProjectDB = null ?callable $getProjectDB = null
): self { ): self {
$this->project = $project; $this->project = $project;
@ -136,6 +135,9 @@ abstract class Migration
$this->dbForPlatform = $dbForPlatform; $this->dbForPlatform = $dbForPlatform;
$this->getProjectDB = $getProjectDB; $this->getProjectDB = $getProjectDB;
$authorization->disable();
$authorization->setDefaultStatus(false);
return $this; return $this;
} }

View file

@ -3,6 +3,7 @@
namespace Appwrite\Platform; namespace Appwrite\Platform;
use Appwrite\Platform\Modules\Account; use Appwrite\Platform\Modules\Account;
use Appwrite\Platform\Modules\Avatars;
use Appwrite\Platform\Modules\Console; use Appwrite\Platform\Modules\Console;
use Appwrite\Platform\Modules\Core; use Appwrite\Platform\Modules\Core;
use Appwrite\Platform\Modules\Databases; use Appwrite\Platform\Modules\Databases;
@ -20,6 +21,7 @@ class Appwrite extends Platform
{ {
parent::__construct(new Core()); parent::__construct(new Core());
$this->addModule(new Account\Module()); $this->addModule(new Account\Module());
$this->addModule(new Avatars\Module());
$this->addModule(new Databases\Module()); $this->addModule(new Databases\Module());
$this->addModule(new Projects\Module()); $this->addModule(new Projects\Module());
$this->addModule(new Functions\Module()); $this->addModule(new Functions\Module());

View file

@ -0,0 +1,158 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action as PlatformAction;
use Appwrite\Utopia\Response;
use Throwable;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Image\Image;
use Utopia\Logger\Logger;
class Action extends PlatformAction
{
protected function getAppRoot(): string
{
return \dirname(__DIR__, 6);
}
protected function avatar(string $type, string $code, int $width, int $height, int $quality, Response $response): void
{
$code = \strtolower($code);
$type = \strtolower($type);
$set = Config::getParam('avatar-' . $type, []);
if (empty($set)) {
throw new Exception(Exception::AVATAR_SET_NOT_FOUND);
}
if (!\array_key_exists($code, $set)) {
throw new Exception(Exception::AVATAR_NOT_FOUND);
}
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
}
$output = 'png';
$path = $set[$code]['path'];
$type = 'png';
if (!\is_readable($path)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'File not readable in ' . $path);
}
$image = new Image(\file_get_contents($path));
$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);
}
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 [];
}
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Browsers;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getBrowser';
}
public function __construct()
{
$this
->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);
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\Back;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\Utopia\Response;
use Imagick;
use ImagickDraw;
use ImagickPixel;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Logger;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getCloudCardBack';
}
public function __construct()
{
$this
->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());
}
}

View file

@ -0,0 +1,245 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\Front;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\Utopia\Response;
use Imagick;
use ImagickDraw;
use ImagickPixel;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Logger;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getCloudCard';
}
public function __construct()
{
$this
->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());
}
}

View file

@ -0,0 +1,428 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\OG;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\Utopia\Response;
use Imagick;
use ImagickDraw;
use ImagickPixel;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Logger;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getCloudCardOG';
}
public function __construct()
{
$this
->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());
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\CreditCards;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getCreditCard';
}
public function __construct()
{
$this
->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);
}
}

View file

@ -0,0 +1,216 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Favicon;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\URL\URL as URLParse;
use Appwrite\Utopia\Response;
use DOMDocument;
use DOMElement;
use enshrined\svgSanitize\Sanitizer as SvgSanitizer;
use Utopia\Domains\Domain;
use Utopia\Fetch\Client;
use Utopia\Image\Image;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\URL;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getFavicon';
}
public function __construct()
{
$this
->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, '<html') === 0 ||
stripos($data, '<!doc') === 0
) {
throw new Exception(Exception::AVATAR_ICON_NOT_FOUND, 'Favicon not found');
}
$response
->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);
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Flags;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getFlag';
}
public function __construct()
{
$this
->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);
}
}

View file

@ -0,0 +1,107 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Image;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Domains\Domain;
use Utopia\Fetch\Client;
use Utopia\Image\Image;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\URL;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getImage';
}
public function __construct()
{
$this
->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);
}
}

View file

@ -0,0 +1,127 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Initials;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Imagick;
use ImagickDraw;
use ImagickPixel;
use Utopia\Database\Document;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\HexColor;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getInitials';
}
public function __construct()
{
$this
->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());
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\QR;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use Utopia\Image\Image;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getQR';
}
public function __construct()
{
$this
->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));
}
}

View file

@ -0,0 +1,225 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Screenshots;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Domains\Domain;
use Utopia\Fetch\Client;
use Utopia\Image\Image;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getScreenshot';
}
public function __construct()
{
$this
->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());
}
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace Appwrite\Platform\Modules\Avatars;
use Appwrite\Platform\Modules\Avatars\Services\Http;
use Utopia\Platform;
class Module extends Platform\Module
{
public function __construct()
{
$this->addService('http', new Http());
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Services;
use Appwrite\Platform\Modules\Avatars\Http\Browsers\Get as GetBrowser;
use Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\Back\Get as GetCloudCardBack;
use Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\Front\Get as GetCloudCard;
use Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\OG\Get as GetCloudCardOG;
use Appwrite\Platform\Modules\Avatars\Http\CreditCards\Get as GetCreditCard;
use Appwrite\Platform\Modules\Avatars\Http\Favicon\Get as GetFavicon;
use Appwrite\Platform\Modules\Avatars\Http\Flags\Get as GetFlag;
use Appwrite\Platform\Modules\Avatars\Http\Image\Get as GetImage;
use Appwrite\Platform\Modules\Avatars\Http\Initials\Get as GetInitials;
use Appwrite\Platform\Modules\Avatars\Http\QR\Get as GetQR;
use Appwrite\Platform\Modules\Avatars\Http\Screenshots\Get as GetScreenshot;
use Utopia\Platform\Service;
class Http extends Service
{
public function __construct()
{
$this->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());
}
}

View file

@ -13,6 +13,7 @@ use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role; use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Authorization;
use Utopia\Swoole\Request; use Utopia\Swoole\Request;
use Utopia\System\System; use Utopia\System\System;
@ -142,7 +143,7 @@ class Base extends Action
return $deployment; 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(); $deploymentId = ID::unique();
$providerInstallationId = $installation->getAttribute('providerInstallationId', ''); $providerInstallationId = $installation->getAttribute('providerInstallationId', '');
@ -239,7 +240,7 @@ class Base extends Action
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$ruleId = $isMd5 ? md5($domain) : ID::unique(); $ruleId = $isMd5 ? md5($domain) : ID::unique();
Authorization::skip( $authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([ fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId, '$id' => $ruleId,
'projectId' => $project->getId(), 'projectId' => $project->getId(),
@ -265,7 +266,7 @@ class Base extends Action
$domain = "commit-" . substr($commitDetails['commitHash'], 0, 16) . ".{$sitesDomain}"; $domain = "commit-" . substr($commitDetails['commitHash'], 0, 16) . ".{$sitesDomain}";
$ruleId = md5($domain); $ruleId = md5($domain);
try { try {
Authorization::skip( $authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([ fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId, '$id' => $ruleId,
'projectId' => $project->getId(), 'projectId' => $project->getId(),
@ -302,7 +303,7 @@ class Base extends Action
$domain = "branch-{$branchPrefix}-{$resourceProjectHash}.{$sitesDomain}"; $domain = "branch-{$branchPrefix}-{$resourceProjectHash}.{$sitesDomain}";
$ruleId = md5($domain); $ruleId = md5($domain);
try { try {
Authorization::skip( $authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([ fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId, '$id' => $ruleId,
'projectId' => $project->getId(), 'projectId' => $project->getId(),
@ -328,6 +329,8 @@ class Base extends Action
} }
} }
$this->updateEmptyManualRule($project, $site, $deployment, $dbForPlatform, $authorization);
$queueForBuilds $queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT) ->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($site) ->setResource($site)
@ -336,4 +339,34 @@ class Base extends Action
return $deployment; 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);
}
} }

View file

@ -60,6 +60,7 @@ class Get extends Action
->inject('response') ->inject('response')
->inject('dbForPlatform') ->inject('dbForPlatform')
->inject('platform') ->inject('platform')
->inject('authorization')
->callback($this->action(...)); ->callback($this->action(...));
} }
@ -68,7 +69,8 @@ class Get extends Action
string $type, string $type,
Response $response, Response $response,
Database $dbForPlatform, Database $dbForPlatform,
array $platform array $platform,
Authorization $authorization,
) { ) {
$domains = $platform['hostnames'] ?? []; $domains = $platform['hostnames'] ?? [];
if ($type === 'rules') { 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://.'); 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]), Query::equal('domain', [$value]),
])); ]));

View file

@ -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'); $key = $attribute->getAttribute('key');
$type = $attribute->getAttribute('type', ''); $type = $attribute->getAttribute('type', '');
@ -310,7 +310,7 @@ abstract class Action extends UtopiaAction
throw new Exception($this->getSpatialTypeNotSupportedException(), params: [$type]); 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()) { if ($db->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); 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) && \in_array($attribute->getAttribute('type'), Database::SPATIAL_TYPES) &&
$attribute->getAttribute('required') $attribute->getAttribute('required')
) { ) {
$hasData = !Authorization::skip(fn () => $dbForProject $hasData = !$authorization->skip(fn () => $dbForProject
->findOne('database_' . $db->getSequence() . '_collection_' . $collection->getSequence())) ->findOne('database_' . $db->getSequence() . '_collection_' . $collection->getSequence()))
->isEmpty(); ->isEmpty();
@ -472,9 +472,9 @@ abstract class Action extends UtopiaAction
return $attribute; 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()) { if ($db->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);

View file

@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -69,10 +70,11 @@ class Create extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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([ $attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key, 'key' => $key,
@ -81,7 +83,7 @@ class Create extends Action
'required' => $required, 'required' => $required,
'default' => $default, 'default' => $default,
'array' => $array, 'array' => $array,
]), $response, $dbForProject, $queueForDatabase, $queueForEvents); ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
$response $response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)

View file

@ -11,6 +11,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -68,10 +69,11 @@ class Update extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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( $attribute = $this->updateAttribute(
databaseId: $databaseId, databaseId: $databaseId,
@ -79,6 +81,7 @@ class Update extends Action
key: $key, key: $key,
dbForProject: $dbForProject, dbForProject: $dbForProject,
queueForEvents: $queueForEvents, queueForEvents: $queueForEvents,
authorization: $authorization,
type: Database::VAR_BOOLEAN, type: Database::VAR_BOOLEAN,
default: $default, default: $default,
required: $required, required: $required,

View file

@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
@ -70,10 +71,11 @@ class Create extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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( $attribute = $this->createAttribute(
$databaseId, $databaseId,
@ -90,7 +92,8 @@ class Create extends Action
$response, $response,
$dbForProject, $dbForProject,
$queueForDatabase, $queueForDatabase,
$queueForEvents $queueForEvents,
$authorization
); );
$response $response

View file

@ -11,6 +11,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
@ -69,10 +70,11 @@ class Update extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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( $attribute = $this->updateAttribute(
databaseId: $databaseId, databaseId: $databaseId,
@ -80,6 +82,7 @@ class Update extends Action
key: $key, key: $key,
dbForProject: $dbForProject, dbForProject: $dbForProject,
queueForEvents: $queueForEvents, queueForEvents: $queueForEvents,
authorization: $authorization,
type: Database::VAR_DATETIME, type: Database::VAR_DATETIME,
default: $default, default: $default,
required: $required, required: $required,

View file

@ -67,12 +67,13 @@ class Delete extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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()) { if ($db->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
} }

View file

@ -13,6 +13,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -70,10 +71,11 @@ class Create extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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( $attribute = $this->createAttribute(
$databaseId, $databaseId,
@ -90,7 +92,8 @@ class Create extends Action
$response, $response,
$dbForProject, $dbForProject,
$queueForDatabase, $queueForDatabase,
$queueForEvents $queueForEvents,
$authorization
); );
$response $response

View file

@ -12,6 +12,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -69,10 +70,11 @@ class Update extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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( $attribute = $this->updateAttribute(
databaseId: $databaseId, databaseId: $databaseId,
@ -80,6 +82,7 @@ class Update extends Action
key: $key, key: $key,
dbForProject: $dbForProject, dbForProject: $dbForProject,
queueForEvents: $queueForEvents, queueForEvents: $queueForEvents,
authorization: $authorization,
type: Database::VAR_STRING, type: Database::VAR_STRING,
filter: APP_DATABASE_ATTRIBUTE_EMAIL, filter: APP_DATABASE_ATTRIBUTE_EMAIL,
default: $default, default: $default,

View file

@ -13,6 +13,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -73,10 +74,11 @@ class Create extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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)) { if (!is_null($default) && !\in_array($default, $elements, true)) {
throw new Exception($this->getInvalidValueException(), 'Default value not found in elements'); throw new Exception($this->getInvalidValueException(), 'Default value not found in elements');
@ -98,7 +100,8 @@ class Create extends Action
$response, $response,
$dbForProject, $dbForProject,
$queueForDatabase, $queueForDatabase,
$queueForEvents $queueForEvents,
$authorization
); );
$response $response

View file

@ -11,6 +11,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -71,10 +72,11 @@ class Update extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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( $attribute = $this->updateAttribute(
databaseId: $databaseId, databaseId: $databaseId,
@ -82,6 +84,7 @@ class Update extends Action
key: $key, key: $key,
dbForProject: $dbForProject, dbForProject: $dbForProject,
queueForEvents: $queueForEvents, queueForEvents: $queueForEvents,
authorization: $authorization,
type: Database::VAR_STRING, type: Database::VAR_STRING,
filter: APP_DATABASE_ATTRIBUTE_ENUM, filter: APP_DATABASE_ATTRIBUTE_ENUM,
default: $default, default: $default,

View file

@ -13,6 +13,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -74,10 +75,11 @@ class Create extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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; $min ??= -PHP_FLOAT_MAX;
$max ??= PHP_FLOAT_MAX; $max ??= PHP_FLOAT_MAX;
@ -100,7 +102,7 @@ class Create extends Action
'array' => $array, 'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, 'format' => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE,
'formatOptions' => ['min' => $min, 'max' => $max], 'formatOptions' => ['min' => $min, 'max' => $max],
]), $response, $dbForProject, $queueForDatabase, $queueForEvents); ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
$formatOptions = $attribute->getAttribute('formatOptions', []); $formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) { if (!empty($formatOptions)) {

View file

@ -11,6 +11,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -71,10 +72,11 @@ class Update extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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( $attribute = $this->updateAttribute(
databaseId: $databaseId, databaseId: $databaseId,
@ -82,6 +84,7 @@ class Update extends Action
key: $key, key: $key,
dbForProject: $dbForProject, dbForProject: $dbForProject,
queueForEvents: $queueForEvents, queueForEvents: $queueForEvents,
authorization: $authorization,
type: Database::VAR_FLOAT, type: Database::VAR_FLOAT,
default: $default, default: $default,
required: $required, required: $required,

View file

@ -68,12 +68,13 @@ class Get extends Action
->param('key', '', new Key(), 'Attribute Key.') ->param('key', '', new Key(), 'Attribute Key.')
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->callback($this->action(...)); ->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()) { if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
} }

View file

@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -70,10 +71,11 @@ class Create extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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( $attribute = $this->createAttribute(
$databaseId, $databaseId,
@ -90,7 +92,8 @@ class Create extends Action
$response, $response,
$dbForProject, $dbForProject,
$queueForDatabase, $queueForDatabase,
$queueForEvents $queueForEvents,
$authorization
); );
$response $response

View file

@ -11,6 +11,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -69,10 +70,11 @@ class Update extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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( $attribute = $this->updateAttribute(
databaseId: $databaseId, databaseId: $databaseId,
@ -80,6 +82,7 @@ class Update extends Action
key: $key, key: $key,
dbForProject: $dbForProject, dbForProject: $dbForProject,
queueForEvents: $queueForEvents, queueForEvents: $queueForEvents,
authorization: $authorization,
type: Database::VAR_STRING, type: Database::VAR_STRING,
filter: APP_DATABASE_ATTRIBUTE_IP, filter: APP_DATABASE_ATTRIBUTE_IP,
default: $default, default: $default,

View file

@ -13,6 +13,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -74,10 +75,11 @@ class Create extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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; $min ??= \PHP_INT_MIN;
$max ??= \PHP_INT_MAX; $max ??= \PHP_INT_MAX;
@ -102,7 +104,7 @@ class Create extends Action
'array' => $array, 'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_INT_RANGE, 'format' => APP_DATABASE_ATTRIBUTE_INT_RANGE,
'formatOptions' => ['min' => $min, 'max' => $max], 'formatOptions' => ['min' => $min, 'max' => $max],
]), $response, $dbForProject, $queueForDatabase, $queueForEvents); ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
$formatOptions = $attribute->getAttribute('formatOptions', []); $formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) { if (!empty($formatOptions)) {

View file

@ -11,6 +11,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -71,10 +72,11 @@ class Update extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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( $attribute = $this->updateAttribute(
databaseId: $databaseId, databaseId: $databaseId,
@ -82,6 +84,7 @@ class Update extends Action
key: $key, key: $key,
dbForProject: $dbForProject, dbForProject: $dbForProject,
queueForEvents: $queueForEvents, queueForEvents: $queueForEvents,
authorization: $authorization,
type: Database::VAR_INTEGER, type: Database::VAR_INTEGER,
default: $default, default: $default,
required: $required, required: $required,

View file

@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\Spatial; use Utopia\Database\Validator\Spatial;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
@ -69,17 +70,18 @@ class Create extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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([ $attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key, 'key' => $key,
'type' => Database::VAR_LINESTRING, 'type' => Database::VAR_LINESTRING,
'required' => $required, 'required' => $required,
'default' => $default 'default' => $default
]), $response, $dbForProject, $queueForDatabase, $queueForEvents); ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
$response $response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)

View file

@ -11,6 +11,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\Spatial; use Utopia\Database\Validator\Spatial;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
@ -69,10 +70,11 @@ class Update extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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( $attribute = $this->updateAttribute(
databaseId: $databaseId, databaseId: $databaseId,
@ -80,6 +82,7 @@ class Update extends Action
key: $key, key: $key,
dbForProject: $dbForProject, dbForProject: $dbForProject,
queueForEvents: $queueForEvents, queueForEvents: $queueForEvents,
authorization: $authorization,
type: Database::VAR_LINESTRING, type: Database::VAR_LINESTRING,
default: $default, default: $default,
required: $required, required: $required,

View file

@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\Spatial; use Utopia\Database\Validator\Spatial;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
@ -69,17 +70,18 @@ class Create extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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([ $attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key, 'key' => $key,
'type' => Database::VAR_POINT, 'type' => Database::VAR_POINT,
'required' => $required, 'required' => $required,
'default' => $default, 'default' => $default,
]), $response, $dbForProject, $queueForDatabase, $queueForEvents); ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
$response $response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)

View file

@ -11,6 +11,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\Spatial; use Utopia\Database\Validator\Spatial;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
@ -69,10 +70,11 @@ class Update extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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( $attribute = $this->updateAttribute(
databaseId: $databaseId, databaseId: $databaseId,
@ -80,6 +82,7 @@ class Update extends Action
key: $key, key: $key,
dbForProject: $dbForProject, dbForProject: $dbForProject,
queueForEvents: $queueForEvents, queueForEvents: $queueForEvents,
authorization: $authorization,
type: Database::VAR_POINT, type: Database::VAR_POINT,
default: $default, default: $default,
required: $required, required: $required,

View file

@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\Spatial; use Utopia\Database\Validator\Spatial;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
@ -69,17 +70,18 @@ class Create extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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([ $attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key, 'key' => $key,
'type' => Database::VAR_POLYGON, 'type' => Database::VAR_POLYGON,
'required' => $required, 'required' => $required,
'default' => $default, 'default' => $default,
]), $response, $dbForProject, $queueForDatabase, $queueForEvents); ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
$response $response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)

View file

@ -11,6 +11,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\Spatial; use Utopia\Database\Validator\Spatial;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
@ -69,10 +70,11 @@ class Update extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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( $attribute = $this->updateAttribute(
databaseId: $databaseId, databaseId: $databaseId,
@ -80,6 +82,7 @@ class Update extends Action
key: $key, key: $key,
dbForProject: $dbForProject, dbForProject: $dbForProject,
queueForEvents: $queueForEvents, queueForEvents: $queueForEvents,
authorization: $authorization,
type: Database::VAR_POLYGON, type: Database::VAR_POLYGON,
default: $default, default: $default,
required: $required, required: $required,

View file

@ -83,16 +83,17 @@ class Create extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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; $key ??= $relatedCollectionId;
$twoWayKeyWasProvided = $twoWayKey !== null; $twoWayKeyWasProvided = $twoWayKey !== null;
$twoWayKey ??= $collectionId; $twoWayKey ??= $collectionId;
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) { if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
} }
@ -154,7 +155,7 @@ class Create extends Action
'twoWayKey' => $twoWayKey, 'twoWayKey' => $twoWayKey,
'onDelete' => $onDelete, 'onDelete' => $onDelete,
] ]
]), $response, $dbForProject, $queueForDatabase, $queueForEvents); ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
foreach ($attribute->getAttribute('options', []) as $k => $option) { foreach ($attribute->getAttribute('options', []) as $k => $option) {
$attribute->setAttribute($k, $option); $attribute->setAttribute($k, $option);

View file

@ -11,6 +11,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -71,6 +72,7 @@ class Update extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->callback($this->action(...));
} }
@ -82,7 +84,8 @@ class Update extends Action
?string $newKey, ?string $newKey,
UtopiaResponse $response, UtopiaResponse $response,
Database $dbForProject, Database $dbForProject,
Event $queueForEvents Event $queueForEvents,
Authorization $authorization
): void { ): void {
$attribute = $this->updateAttribute( $attribute = $this->updateAttribute(
databaseId: $databaseId, databaseId: $databaseId,
@ -90,6 +93,7 @@ class Update extends Action
key: $key, key: $key,
dbForProject: $dbForProject, dbForProject: $dbForProject,
queueForEvents: $queueForEvents, queueForEvents: $queueForEvents,
authorization: $authorization,
type: Database::VAR_RELATIONSHIP, type: Database::VAR_RELATIONSHIP,
required: false, required: false,
options: [ options: [

View file

@ -14,6 +14,7 @@ use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\App; use Utopia\App;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -77,6 +78,7 @@ class Create extends Action
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('plan') ->inject('plan')
->inject('authorization')
->callback($this->action(...)); ->callback($this->action(...));
} }
@ -93,7 +95,8 @@ class Create extends Action
Database $dbForProject, Database $dbForProject,
EventDatabase $queueForDatabase, EventDatabase $queueForDatabase,
Event $queueForEvents, Event $queueForEvents,
array $plan array $plan,
Authorization $authorization
): void { ): void {
if (!App::isDevelopment() && $encrypt && !empty($plan) && !($plan['databasesAllowEncrypt'] ?? false)) { 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() . '.'); 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, $response,
$dbForProject, $dbForProject,
$queueForDatabase, $queueForDatabase,
$queueForEvents $queueForEvents,
$authorization
); );
$attribute->setAttribute('encrypt', $encrypt); $attribute->setAttribute('encrypt', $encrypt);

View file

@ -11,6 +11,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -72,6 +73,7 @@ class Update extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->callback($this->action(...));
} }
@ -85,7 +87,8 @@ class Update extends Action
?string $newKey, ?string $newKey,
UtopiaResponse $response, UtopiaResponse $response,
Database $dbForProject, Database $dbForProject,
Event $queueForEvents Event $queueForEvents,
Authorization $authorization
): void { ): void {
$attribute = $this->updateAttribute( $attribute = $this->updateAttribute(
databaseId: $databaseId, databaseId: $databaseId,
@ -93,6 +96,7 @@ class Update extends Action
key: $key, key: $key,
dbForProject: $dbForProject, dbForProject: $dbForProject,
queueForEvents: $queueForEvents, queueForEvents: $queueForEvents,
authorization: $authorization,
type: Database::VAR_STRING, type: Database::VAR_STRING,
size: $size, size: $size,
default: $default, default: $default,

View file

@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -70,6 +71,7 @@ class Create extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->callback($this->action(...));
} }
@ -83,7 +85,8 @@ class Create extends Action
UtopiaResponse $response, UtopiaResponse $response,
Database $dbForProject, Database $dbForProject,
EventDatabase $queueForDatabase, EventDatabase $queueForDatabase,
Event $queueForEvents Event $queueForEvents,
Authorization $authorization
): void { ): void {
$attribute = $this->createAttribute($databaseId, $collectionId, new Document([ $attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key, 'key' => $key,
@ -93,7 +96,7 @@ class Create extends Action
'default' => $default, 'default' => $default,
'array' => $array, 'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_URL, 'format' => APP_DATABASE_ATTRIBUTE_URL,
]), $response, $dbForProject, $queueForDatabase, $queueForEvents); ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
$response $response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)

View file

@ -11,6 +11,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse; use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -69,6 +70,7 @@ class Update extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->callback($this->action(...));
} }
@ -81,7 +83,8 @@ class Update extends Action
?string $newKey, ?string $newKey,
UtopiaResponse $response, UtopiaResponse $response,
Database $dbForProject, Database $dbForProject,
Event $queueForEvents Event $queueForEvents,
Authorization $authorization
): void { ): void {
$attribute = $this->updateAttribute( $attribute = $this->updateAttribute(
$databaseId, $databaseId,
@ -89,6 +92,7 @@ class Update extends Action
$key, $key,
$dbForProject, $dbForProject,
$queueForEvents, $queueForEvents,
$authorization,
type: Database::VAR_STRING, type: Database::VAR_STRING,
filter: APP_DATABASE_ATTRIBUTE_URL, filter: APP_DATABASE_ATTRIBUTE_URL,
default: $default, default: $default,

View file

@ -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) ->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('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->callback($this->action(...)); ->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()) { if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
} }

View file

@ -85,12 +85,13 @@ class Create extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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()) { if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);

View file

@ -64,12 +64,13 @@ class Delete extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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()) { if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
} }

View file

@ -258,9 +258,9 @@ abstract class Action extends DatabasesAction
Document $collection, Document $collection,
Document $document, Document $document,
Database $dbForProject, Database $dbForProject,
/* options */ /* options */
array &$collectionsCache, array &$collectionsCache,
Authorization $authorization,
?int &$operations = null, ?int &$operations = null,
): bool { ): bool {
@ -297,7 +297,7 @@ abstract class Action extends DatabasesAction
$relatedCollectionId = $relationship->getAttribute('relatedCollection'); $relatedCollectionId = $relationship->getAttribute('relatedCollection');
if (!isset($collectionsCache[$relatedCollectionId])) { if (!isset($collectionsCache[$relatedCollectionId])) {
$relatedCollectionDoc = Authorization::skip( $relatedCollectionDoc = $authorization->skip(
fn () => $dbForProject->getDocument( fn () => $dbForProject->getDocument(
'database_' . $database->getSequence(), 'database_' . $database->getSequence(),
$relatedCollectionId $relatedCollectionId
@ -323,7 +323,8 @@ abstract class Action extends DatabasesAction
document: $relation, document: $relation,
dbForProject: $dbForProject, dbForProject: $dbForProject,
collectionsCache: $collectionsCache, collectionsCache: $collectionsCache,
operations: $operations operations: $operations,
authorization: $authorization
); );
} }
} }

View file

@ -85,20 +85,21 @@ class Decrement extends Action
->inject('queueForEvents') ->inject('queueForEvents')
->inject('queueForStatsUsage') ->inject('queueForStatsUsage')
->inject('plan') ->inject('plan')
->inject('authorization')
->callback($this->action(...)); ->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()); $isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged(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()) { if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); 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()) { if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
} }
@ -106,7 +107,7 @@ class Decrement extends Action
// Handle transaction staging // Handle transaction staging
if ($transactionId !== null) { if ($transactionId !== null) {
$transaction = ($isAPIKey || $isPrivilegedUser) $transaction = ($isAPIKey || $isPrivilegedUser)
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) ? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
: $dbForProject->getDocument('transactions', $transactionId); : $dbForProject->getDocument('transactions', $transactionId);
if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or nonpending transaction'); throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or nonpending transaction');

View file

@ -85,20 +85,21 @@ class Increment extends Action
->inject('queueForEvents') ->inject('queueForEvents')
->inject('queueForStatsUsage') ->inject('queueForStatsUsage')
->inject('plan') ->inject('plan')
->inject('authorization')
->callback($this->action(...)); ->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()); $isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged(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()) { if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); 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()) { if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
} }
@ -106,7 +107,7 @@ class Increment extends Action
// Handle transaction staging // Handle transaction staging
if ($transactionId !== null) { if ($transactionId !== null) {
$transaction = ($isAPIKey || $isPrivilegedUser) $transaction = ($isAPIKey || $isPrivilegedUser)
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) ? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
: $dbForProject->getDocument('transactions', $transactionId); : $dbForProject->getDocument('transactions', $transactionId);
if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or nonpending transaction'); throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or nonpending transaction');

View file

@ -24,6 +24,7 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role; use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse; use Utopia\Swoole\Response as SwooleResponse;
@ -132,9 +133,10 @@ class Create extends Action
->inject('queueForFunctions') ->inject('queueForFunctions')
->inject('queueForWebhooks') ->inject('queueForWebhooks')
->inject('plan') ->inject('plan')
->inject('authorization')
->callback($this->action(...)); ->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) $data = \is_string($data)
? \json_decode($data, true) ? \json_decode($data, true)
@ -178,19 +180,19 @@ class Create extends Action
$documents = [$data]; $documents = [$data];
} }
$isAPIKey = User::isApp(Authorization::getRoles()); $isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); $isPrivilegedUser = User::isPrivileged($authorization->getRoles());
if ($isBulk && !$isAPIKey && !$isPrivilegedUser) { if ($isBulk && !$isAPIKey && !$isPrivilegedUser) {
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE); 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)) { if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); 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)) { if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); 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()); 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 = [ $allowedPermissions = [
Database::PERMISSION_READ, Database::PERMISSION_READ,
Database::PERMISSION_UPDATE, Database::PERMISSION_UPDATE,
@ -247,8 +249,8 @@ class Create extends Action
$permission->getIdentifier(), $permission->getIdentifier(),
$permission->getDimension() $permission->getDimension()
))->toString(); ))->toString();
if (!Authorization::isRole($role)) { if (!$authorization->hasRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', Authorization::getRoles()) . ')'); throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $authorization->getRoles()) . ')');
} }
} }
} }
@ -259,21 +261,25 @@ class Create extends Action
$operations = 0; $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++; $operations++;
$documentSecurity = $collection->getAttribute('documentSecurity', false); $documentSecurity = $collection->getAttribute('documentSecurity', false);
$validator = new Authorization($permission);
$valid = $validator->isValid($collection->getPermissionsByType($permission)); $validCollection = $authorization->isValid(
if (($permission === Database::PERMISSION_UPDATE && !$documentSecurity) || !$valid) { new Input($permission, $collection->getPermissionsByType($permission))
throw new Exception(Exception::USER_UNAUTHORIZED); );
if (($permission === Database::PERMISSION_UPDATE && !$documentSecurity) || !$validCollection) {
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
} }
if ($permission === Database::PERMISSION_UPDATE) { 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) { 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'); $relatedCollectionId = $relationship->getAttribute('relatedCollection');
$relatedCollection = Authorization::skip( $relatedCollection = $authorization->skip(
fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId) fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId)
); );
@ -314,7 +320,7 @@ class Create extends Action
if ($relation instanceof Document) { if ($relation instanceof Document) {
$relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser);
$current = Authorization::skip( $current = $authorization->skip(
fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId()) fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId())
); );
@ -369,7 +375,7 @@ class Create extends Action
// Handle transaction staging // Handle transaction staging
if ($transactionId !== null) { if ($transactionId !== null) {
$transaction = ($isAPIKey || $isPrivilegedUser) $transaction = ($isAPIKey || $isPrivilegedUser)
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) ? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
: $dbForProject->getDocument('transactions', $transactionId); : $dbForProject->getDocument('transactions', $transactionId);
if ($transaction->isEmpty()) { if ($transaction->isEmpty()) {
throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]); throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]);
@ -468,6 +474,7 @@ class Create extends Action
document: $document, document: $document,
dbForProject: $dbForProject, dbForProject: $dbForProject,
collectionsCache: $collectionsCache, collectionsCache: $collectionsCache,
authorization: $authorization
); );
} }

View file

@ -83,6 +83,7 @@ class Delete extends Action
->inject('queueForStatsUsage') ->inject('queueForStatsUsage')
->inject('transactionState') ->inject('transactionState')
->inject('plan') ->inject('plan')
->inject('authorization')
->callback($this->action(...)); ->callback($this->action(...));
} }
@ -97,18 +98,19 @@ class Delete extends Action
Event $queueForEvents, Event $queueForEvents,
StatsUsage $queueForStatsUsage, StatsUsage $queueForStatsUsage,
TransactionState $transactionState, TransactionState $transactionState,
array $plan array $plan,
Authorization $authorization
): void { ): void {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$isAPIKey = User::isApp(Authorization::getRoles()); $isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); $isPrivilegedUser = User::isPrivileged($authorization->getRoles());
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); 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)) { if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); 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 // Use transaction-aware document retrieval to see changes from same transaction
$document = $transactionState->getDocument($collectionTableId, $documentId, $transactionId); $document = $transactionState->getDocument($collectionTableId, $documentId, $transactionId);
} else { } else {
$document = Authorization::skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId)); $document = $authorization->skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId));
} }
if ($document->isEmpty()) { if ($document->isEmpty()) {
@ -131,7 +133,7 @@ class Delete extends Action
// Handle transaction staging // Handle transaction staging
if ($transactionId !== null) { if ($transactionId !== null) {
$transaction = ($isAPIKey || $isPrivilegedUser) $transaction = ($isAPIKey || $isPrivilegedUser)
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) ? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
: $dbForProject->getDocument('transactions', $transactionId); : $dbForProject->getDocument('transactions', $transactionId);
if ($transaction->isEmpty()) { if ($transaction->isEmpty()) {
throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]); throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]);
@ -205,6 +207,7 @@ class Delete extends Action
document: $document, document: $document,
dbForProject: $dbForProject, dbForProject: $dbForProject,
collectionsCache: $collectionsCache, collectionsCache: $collectionsCache,
authorization: $authorization
); );
$queueForStatsUsage $queueForStatsUsage

View file

@ -70,20 +70,21 @@ class Get extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForStatsUsage') ->inject('queueForStatsUsage')
->inject('transactionState') ->inject('transactionState')
->inject('authorization')
->callback($this->action(...)); ->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()); $isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged(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)) { if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); 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)) { if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
@ -125,6 +126,7 @@ class Get extends Action
document: $document, document: $document,
dbForProject: $dbForProject, dbForProject: $dbForProject,
collectionsCache: $collectionsCache, collectionsCache: $collectionsCache,
authorization: $authorization,
operations: $operations operations: $operations
); );

View file

@ -72,13 +72,14 @@ class XList extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('locale') ->inject('locale')
->inject('geodb') ->inject('geodb')
->inject('authorization')
->inject('audit') ->inject('audit')
->callback($this->action(...)); ->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()) { if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
} }

View file

@ -87,10 +87,11 @@ class Update extends Action
->inject('queueForStatsUsage') ->inject('queueForStatsUsage')
->inject('transactionState') ->inject('transactionState')
->inject('plan') ->inject('plan')
->inject('authorization')
->callback($this->action(...)); ->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 $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()); 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()); $isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); $isPrivilegedUser = User::isPrivileged($authorization->getRoles());
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); 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)) { if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); 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 // Use transaction-aware document retrieval to see changes from same transaction
$document = $transactionState->getDocument($collectionTableId, $documentId, $transactionId); $document = $transactionState->getDocument($collectionTableId, $documentId, $transactionId);
} else { } else {
$document = Authorization::skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId)); $document = $authorization->skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId));
} }
if ($document->isEmpty()) { 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 // 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)) { if (!$isAPIKey && !$isPrivilegedUser && !\is_null($permissions)) {
foreach (Database::PERMISSIONS as $type) { foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) { foreach ($permissions as $permission) {
@ -153,7 +154,7 @@ class Update extends Action
$permission->getIdentifier(), $permission->getIdentifier(),
$permission->getDimension() $permission->getDimension()
))->toString(); ))->toString();
if (!Authorization::isRole($role)) { if (!$authorization->hasRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')');
} }
} }
@ -171,7 +172,7 @@ class Update extends Action
$operations = 0; $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++; $operations++;
$relationships = \array_filter( $relationships = \array_filter(
@ -195,7 +196,7 @@ class Update extends Action
} }
$relatedCollectionId = $relationship->getAttribute('relatedCollection'); $relatedCollectionId = $relationship->getAttribute('relatedCollection');
$relatedCollection = Authorization::skip( $relatedCollection = $authorization->skip(
fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId) fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId)
); );
@ -212,7 +213,7 @@ class Update extends Action
if ($relation instanceof Document) { if ($relation instanceof Document) {
$relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser);
$oldDocument = Authorization::skip(fn () => $dbForProject->getDocument( $oldDocument = $authorization->skip(fn () => $dbForProject->getDocument(
'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), 'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(),
$relation->getId() $relation->getId()
)); ));
@ -249,7 +250,7 @@ class Update extends Action
// Handle transaction staging // Handle transaction staging
if ($transactionId !== null) { if ($transactionId !== null) {
$transaction = ($isAPIKey || $isPrivilegedUser) $transaction = ($isAPIKey || $isPrivilegedUser)
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) ? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
: $dbForProject->getDocument('transactions', $transactionId); : $dbForProject->getDocument('transactions', $transactionId);
if ($transaction->isEmpty()) { if ($transaction->isEmpty()) {
throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]); throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]);
@ -340,6 +341,7 @@ class Update extends Action
document: $document, document: $document,
dbForProject: $dbForProject, dbForProject: $dbForProject,
collectionsCache: $collectionsCache, collectionsCache: $collectionsCache,
authorization: $authorization,
); );
$response->dynamic($document, $this->getResponseModel()); $response->dynamic($document, $this->getResponseModel());

View file

@ -91,10 +91,11 @@ class Upsert extends Action
->inject('queueForStatsUsage') ->inject('queueForStatsUsage')
->inject('transactionState') ->inject('transactionState')
->inject('plan') ->inject('plan')
->inject('authorization')
->callback($this->action(...)); ->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 $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()); throw new Exception($this->getMissingPayloadException());
} }
$isAPIKey = User::isApp(Authorization::getRoles()); $isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged(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)) { if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); 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)) { if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); 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 // Use transaction-aware document retrieval to see changes from same transaction
$oldDocument = $transactionState->getDocument($collectionTableId, $documentId, $transactionId); $oldDocument = $transactionState->getDocument($collectionTableId, $documentId, $transactionId);
} else { } else {
$oldDocument = Authorization::skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId)); $oldDocument = $authorization->skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId));
} }
if ($oldDocument->isEmpty()) { if ($oldDocument->isEmpty()) {
if (!empty($user->getId())) { 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 // 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)) { if (!$isAPIKey && !$isPrivilegedUser && !\is_null($permissions)) {
foreach (Database::PERMISSIONS as $type) { foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) { foreach ($permissions as $permission) {
@ -168,7 +169,7 @@ class Upsert extends Action
$permission->getIdentifier(), $permission->getIdentifier(),
$permission->getDimension() $permission->getDimension()
))->toString(); ))->toString();
if (!Authorization::isRole($role)) { if (!$authorization->hasRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); 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); $newDocument = new Document($data);
$operations = 0; $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++; $operations++;
$relationships = \array_filter( $relationships = \array_filter(
@ -205,7 +206,7 @@ class Upsert extends Action
} }
$relatedCollectionId = $relationship->getAttribute('relatedCollection'); $relatedCollectionId = $relationship->getAttribute('relatedCollection');
$relatedCollection = Authorization::skip( $relatedCollection = $authorization->skip(
fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId) fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId)
); );
@ -222,7 +223,7 @@ class Upsert extends Action
if ($relation instanceof Document) { if ($relation instanceof Document) {
$relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser);
$oldDocument = Authorization::skip(fn () => $dbForProject->getDocument( $oldDocument = $authorization->skip(fn () => $dbForProject->getDocument(
'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), 'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(),
$relation->getId() $relation->getId()
)); ));
@ -259,7 +260,7 @@ class Upsert extends Action
// Handle transaction staging // Handle transaction staging
if ($transactionId !== null) { if ($transactionId !== null) {
$transaction = ($isAPIKey || $isPrivilegedUser) $transaction = ($isAPIKey || $isPrivilegedUser)
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) ? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
: $dbForProject->getDocument('transactions', $transactionId); : $dbForProject->getDocument('transactions', $transactionId);
if ($transaction->isEmpty()) { if ($transaction->isEmpty()) {
throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]); throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]);
@ -361,6 +362,7 @@ class Upsert extends Action
document: $document, document: $document,
dbForProject: $dbForProject, dbForProject: $dbForProject,
collectionsCache: $collectionsCache, collectionsCache: $collectionsCache,
authorization: $authorization
); );
$relationships = \array_map( $relationships = \array_map(

View file

@ -74,20 +74,21 @@ class XList extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForStatsUsage') ->inject('queueForStatsUsage')
->inject('transactionState') ->inject('transactionState')
->inject('authorization')
->callback($this->action(...)); ->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()); $isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged(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)) { if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); 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)) { if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]); throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
} }
@ -115,7 +116,7 @@ class XList extends Action
$documentId = $cursor->getValue(); $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()) { if ($cursorDocument->isEmpty()) {
$type = ucfirst($this->getContext()); $type = ucfirst($this->getContext());
@ -161,7 +162,8 @@ class XList extends Action
document: $document, document: $document,
dbForProject: $dbForProject, dbForProject: $dbForProject,
collectionsCache: $collectionsCache, collectionsCache: $collectionsCache,
operations: $operations, authorization: $authorization,
operations: $operations
); );
} }

View file

@ -57,12 +57,13 @@ class Get extends Action
->param('collectionId', '', new UID(), 'Collection ID.') ->param('collectionId', '', new UID(), 'Collection ID.')
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->callback($this->action(...)); ->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()) { if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);

View file

@ -79,12 +79,13 @@ class Create extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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()) { if ($db->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);

View file

@ -70,12 +70,13 @@ class Delete extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForDatabase') ->inject('queueForDatabase')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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()) { if ($db->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);

View file

@ -59,12 +59,13 @@ class Get extends Action
->param('key', null, new Key(), 'Index Key.') ->param('key', null, new Key(), 'Index Key.')
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->callback($this->action(...)); ->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()) { if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);

View file

@ -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) ->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('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->callback($this->action(...)); ->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 */ /** @var Document $database */
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) { if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
@ -112,7 +113,7 @@ class XList extends Action
} }
$indexId = $cursor->getValue(); $indexId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->find('indexes', [ $cursorDocument = $authorization->skip(fn () => $dbForProject->find('indexes', [
Query::equal('collectionInternalId', [$collection->getSequence()]), Query::equal('collectionInternalId', [$collection->getSequence()]),
Query::equal('databaseInternalId', [$database->getSequence()]), Query::equal('databaseInternalId', [$database->getSequence()]),
Query::equal('key', [$indexId]), Query::equal('key', [$indexId]),

View file

@ -71,13 +71,14 @@ class XList extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('locale') ->inject('locale')
->inject('geodb') ->inject('geodb')
->inject('authorization')
->inject('audit') ->inject('audit')
->callback($this->action(...)); ->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()) { if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
@ -112,9 +113,9 @@ class XList extends Action
$detector = new Detector($log['userAgent']); $detector = new Detector($log['userAgent']);
$detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$os = $detector->getOS(); $os = $detector->getOS() ?: [];
$client = $detector->getClient(); $client = $detector->getClient() ?: [];
$device = $detector->getDevice(); $device = $detector->getDevice() ?: [];
$output[$i] = new Document([ $output[$i] = new Document([
'event' => $log['event'], 'event' => $log['event'],
@ -122,20 +123,20 @@ class XList extends Action
'userEmail' => $log['data']['userEmail'] ?? null, 'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null, 'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null, 'mode' => $log['data']['mode'] ?? null,
'ip' => $log['ip'], 'ip' => $log['ip'] ?? null,
'time' => $log['time'], 'time' => $log['time'] ?? null,
'osCode' => $os['osCode'], 'osCode' => $os['osCode'] ?? null,
'osName' => $os['osName'], 'osName' => $os['osName'] ?? null,
'osVersion' => $os['osVersion'], 'osVersion' => $os['osVersion'] ?? null,
'clientType' => $client['clientType'], 'clientType' => $client['clientType'] ?? null,
'clientCode' => $client['clientCode'], 'clientCode' => $client['clientCode'] ?? null,
'clientName' => $client['clientName'], 'clientName' => $client['clientName'] ?? null,
'clientVersion' => $client['clientVersion'], 'clientVersion' => $client['clientVersion'] ?? null,
'clientEngine' => $client['clientEngine'], 'clientEngine' => $client['clientEngine'] ?? null,
'clientEngineVersion' => $client['clientEngineVersion'], 'clientEngineVersion' => $client['clientEngineVersion'] ?? null,
'deviceName' => $device['deviceName'], 'deviceName' => $device['deviceName'] ?? null,
'deviceBrand' => $device['deviceBrand'], 'deviceBrand' => $device['deviceBrand'] ?? null,
'deviceModel' => $device['deviceModel'] 'deviceModel' => $device['deviceModel'] ?? null
]); ]);
$record = $geodb->get($log['ip']); $record = $geodb->get($log['ip']);

View file

@ -71,12 +71,13 @@ class Update extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...)); ->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()) { if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
} }

View file

@ -63,10 +63,11 @@ class Get extends Action
->param('collectionId', '', new UID(), 'Collection ID.') ->param('collectionId', '', new UID(), 'Collection ID.')
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->callback($this->action(...)); ->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); $database = $dbForProject->getDocument('databases', $databaseId);
$collectionDocument = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId); $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), 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) { foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [ $result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),

View file

@ -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) ->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('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('authorization')
->callback($this->action(...)); ->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()) { if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]); throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);

View file

@ -55,10 +55,11 @@ class Create extends Action
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('user') ->inject('user')
->inject('authorization')
->callback($this->action(...)); ->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 = []; $permissions = [];
if (!empty($user->getId())) { 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(), '$id' => ID::unique(),
'$permissions' => $permissions, '$permissions' => $permissions,
'status' => 'pending', 'status' => 'pending',

Some files were not shown because too many files have changed in this diff Show more