From cf3ffcdf32bdf4909d53e0de290bd3b9324fb1a4 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 18 Jul 2021 16:19:50 +0545 Subject: [PATCH 01/27] update create membership doc --- docs/references/teams/create-team-membership.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/references/teams/create-team-membership.md b/docs/references/teams/create-team-membership.md index a920c260be..c6d81de484 100644 --- a/docs/references/teams/create-team-membership.md +++ b/docs/references/teams/create-team-membership.md @@ -1,5 +1,5 @@ -Use this endpoint to invite a new member to join your team. An email with a link to join the team will be sent to the new member email address if the member doesn't exist in the project it will be created automatically. +Use this endpoint to invite a new member to join your team. If initiated from Client SDK, an email with a link to join the team will be sent to the new member's email address if the member doesn't exist in the project it will be created automatically. If initiated from server side SDKs, new member will automatically be added to the team. -Use the 'URL' parameter to redirect the user from the invitation email back to your app. When the user is redirected, use the [Update Team Membership Status](/docs/client/teams#teamsUpdateMembershipStatus) endpoint to allow the user to accept the invitation to the team. +Use the 'URL' parameter to redirect the user from the invitation email back to your app. When the user is redirected, use the [Update Team Membership Status](/docs/client/teams#teamsUpdateMembershipStatus) endpoint to allow the user to accept the invitation to the team. While calling from side SDKs the redirect url can be empty string. Please note that in order to avoid a [Redirect Attacks](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URL's are the once from domains you have set when added your platforms in the console interface. \ No newline at end of file From c2f1389a568653fffdc385bbfce956ecea613421 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 19 Jul 2021 17:09:39 +0200 Subject: [PATCH 02/27] fix(jwt): correct session validation --- app/init.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/init.php b/app/init.php index a139a4f841..6461f50acb 100644 --- a/app/init.php +++ b/app/init.php @@ -467,7 +467,7 @@ App::setResource('user', function($mode, $project, $console, $request, $response $user = $projectDB->getDocument($jwtUserId); } - if (empty($user->search('$id', $jwtSessionId, $user->getAttribute('tokens')))) { // Match JWT to active token + if (empty($user->search('$id', $jwtSessionId, $user->getAttribute('sessions')))) { // Match JWT to active token $user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]); } } From d8dee84be1ff7cf429a7b099f6eb8c2382f96c93 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Mon, 19 Jul 2021 12:46:34 -0400 Subject: [PATCH 03/27] Obtain db connection without coroutine --- app/workers/certificates.php | 273 +++++++++++++++++------------------ 1 file changed, 132 insertions(+), 141 deletions(-) diff --git a/app/workers/certificates.php b/app/workers/certificates.php index 1db2a46360..8bebcca82e 100644 --- a/app/workers/certificates.php +++ b/app/workers/certificates.php @@ -30,178 +30,169 @@ class CertificatesV1 extends Worker { global $register; - go(function() use ($register) { - $db = $register->get('dbPool')->get(); - $redis = $register->get('redisPool')->get(); + $cache = new Cache(new Redis($register->get('cache'))); + $dbForConsole = new Database(new MariaDB($register->get('db')), $cache); + $dbForConsole->setNamespace('project_console_internal'); // Main DB - $cache = new Cache(new Redis($redis)); - $dbForConsole = new Database(new MariaDB($db), $cache); - $dbForConsole->setNamespace('project_console_internal'); + /** + * 1. Get new domain document - DONE + * 1.1. Validate domain is valid, public suffix is known and CNAME records are verified - DONE + * 2. Check if a certificate already exists - DONE + * 3. Check if certificate is about to expire, if not - skip it + * 3.1. Create / renew certificate + * 3.2. Update loadblancer + * 3.3. Update database (domains, change date, expiry) + * 3.4. Set retry on failure + * 3.5. Schedule to renew certificate in 60 days + */ - /** - * 1. Get new domain document - DONE - * 1.1. Validate domain is valid, public suffix is known and CNAME records are verified - DONE - * 2. Check if a certificate already exists - DONE - * 3. Check if certificate is about to expire, if not - skip it - * 3.1. Create / renew certificate - * 3.2. Update loadblancer - * 3.3. Update database (domains, change date, expiry) - * 3.4. Set retry on failure - * 3.5. Schedule to renew certificate in 60 days - */ + Authorization::disable(); - Authorization::disable(); + // Args + $document = $this->args['document']; + $domain = $this->args['domain']; - // Args - $document = $this->args['document']; - $domain = $this->args['domain']; - - // Validation Args - $validateTarget = $this->args['validateTarget'] ?? true; - $validateCNAME = $this->args['validateCNAME'] ?? true; - - // Options - $domain = new Domain((!empty($domain)) ? $domain : ''); - $expiry = 60 * 60 * 24 * 30 * 2; // 60 days - $safety = 60 * 60; // 1 hour - $renew = (\time() + $expiry); - - if(empty($domain->get())) { - throw new Exception('Missing domain'); - } - - if(!$domain->isKnown() || $domain->isTest()) { - throw new Exception('Unknown public suffix for domain'); - } - - if($validateTarget) { - $target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', '')); + // Validation Args + $validateTarget = $this->args['validateTarget'] ?? true; + $validateCNAME = $this->args['validateCNAME'] ?? true; - if(!$target->isKnown() || $target->isTest()) { - throw new Exception('Unreachable CNAME target ('.$target->get().'), please use a domain with a public suffix.'); - } + // Options + $domain = new Domain((!empty($domain)) ? $domain : ''); + $expiry = 60 * 60 * 24 * 30 * 2; // 60 days + $safety = 60 * 60; // 1 hour + $renew = (\time() + $expiry); + + if(empty($domain->get())) { + throw new Exception('Missing domain'); + } + + if(!$domain->isKnown() || $domain->isTest()) { + throw new Exception('Unknown public suffix for domain'); + } + + if($validateTarget) { + $target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', '')); + + if(!$target->isKnown() || $target->isTest()) { + throw new Exception('Unreachable CNAME target ('.$target->get().'), please use a domain with a public suffix.'); } + } - if($validateCNAME) { - $validator = new CNAME($target->get()); // Verify Domain with DNS records - - if(!$validator->isValid($domain->get())) { - throw new Exception('Failed to verify domain DNS records'); - } + if($validateCNAME) { + $validator = new CNAME($target->get()); // Verify Domain with DNS records + + if(!$validator->isValid($domain->get())) { + throw new Exception('Failed to verify domain DNS records'); } + } - $certificate = $dbForConsole->findFirst('certificates', [ - new Query('domain', QUERY::TYPE_EQUAL, [$domain->get()]) - ], /*limit*/ 1); + $certificate = $dbForConsole->findFirst('certificates', [ + new Query('domain', QUERY::TYPE_EQUAL, [$domain->get()]) + ], /*limit*/ 1); - // $condition = ($certificate - // && $certificate instanceof Document - // && isset($certificate['issueDate']) - // && (($certificate['issueDate'] + ($expiry)) > time())) ? 'true' : 'false'; + // $condition = ($certificate + // && $certificate instanceof Document + // && isset($certificate['issueDate']) + // && (($certificate['issueDate'] + ($expiry)) > time())) ? 'true' : 'false'; - // throw new Exception('cert issued at'.date('d.m.Y H:i', $certificate['issueDate']).' | renew date is: '.date('d.m.Y H:i', ($certificate['issueDate'] + ($expiry))).' | condition is '.$condition); + // throw new Exception('cert issued at'.date('d.m.Y H:i', $certificate['issueDate']).' | renew date is: '.date('d.m.Y H:i', ($certificate['issueDate'] + ($expiry))).' | condition is '.$condition); - $certificate = (!empty($certificate) && $certificate instanceof $certificate) ? $certificate->getArrayCopy() : []; + $certificate = (!empty($certificate) && $certificate instanceof $certificate) ? $certificate->getArrayCopy() : []; - if(!empty($certificate) - && isset($certificate['issueDate']) - && (($certificate['issueDate'] + ($expiry)) > \time())) { // Check last issue time - throw new Exception('Renew isn\'t required'); + if(!empty($certificate) + && isset($certificate['issueDate']) + && (($certificate['issueDate'] + ($expiry)) > \time())) { // Check last issue time + throw new Exception('Renew isn\'t required'); + } + + $staging = (App::isProduction()) ? '' : ' --dry-run'; + $email = App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'); + + if(empty($email)) { + throw new Exception('You must set a valid security email address (_APP_SYSTEM_SECURITY_EMAIL_ADDRESS) to issue an SSL certificate'); + } + + $stdout = ''; + $stderr = ''; + + $exit = Console::execute("certbot certonly --webroot --noninteractive --agree-tos{$staging}" + ." --email ".$email + ." -w ".APP_STORAGE_CERTIFICATES + ." -d {$domain->get()}", '', $stdout, $stderr); + + if($exit !== 0) { + throw new Exception('Failed to issue a certificate with message: '.$stderr); + } + + $path = APP_STORAGE_CERTIFICATES.'/'.$domain->get(); + + if(!\is_readable($path)) { + if (!\mkdir($path, 0755, true)) { + throw new Exception('Failed to create path...'); } + } - $staging = (App::isProduction()) ? '' : ' --dry-run'; - $email = App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'); + if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/cert.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/cert.pem')) { + throw new Exception('Failed to rename certificate cert.pem: '.\json_encode($stdout)); + } - if(empty($email)) { - throw new Exception('You must set a valid security email address (_APP_SYSTEM_SECURITY_EMAIL_ADDRESS) to issue an SSL certificate'); - } + if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/chain.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/chain.pem')) { + throw new Exception('Failed to rename certificate chain.pem: '.\json_encode($stdout)); + } - $stdout = ''; - $stderr = ''; + if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/fullchain.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/fullchain.pem')) { + throw new Exception('Failed to rename certificate fullchain.pem: '.\json_encode($stdout)); + } - $exit = Console::execute("certbot certonly --webroot --noninteractive --agree-tos{$staging}" - ." --email ".$email - ." -w ".APP_STORAGE_CERTIFICATES - ." -d {$domain->get()}", '', $stdout, $stderr); + if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/privkey.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/privkey.pem')) { + throw new Exception('Failed to rename certificate privkey.pem: '.\json_encode($stdout)); + } - if($exit !== 0) { - throw new Exception('Failed to issue a certificate with message: '.$stderr); - } + $certificate = new Document(\array_merge($certificate, [ + 'domain' => $domain->get(), + 'issueDate' => \time(), + 'renewDate' => $renew, + 'attempts' => 0, + 'log' => \json_encode($stdout), + ])); - $path = APP_STORAGE_CERTIFICATES.'/'.$domain->get(); + $certificate = $dbForConsole->createDocument('certificates', $certificate); - if(!\is_readable($path)) { - if (!\mkdir($path, 0755, true)) { - throw new Exception('Failed to create path...'); - } - } - - if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/cert.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/cert.pem')) { - throw new Exception('Failed to rename certificate cert.pem: '.\json_encode($stdout)); - } + if(!$certificate) { + throw new Exception('Failed saving certificate to DB'); + } - if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/chain.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/chain.pem')) { - throw new Exception('Failed to rename certificate chain.pem: '.\json_encode($stdout)); - } - - if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/fullchain.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/fullchain.pem')) { - throw new Exception('Failed to rename certificate fullchain.pem: '.\json_encode($stdout)); - } - - if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/privkey.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/privkey.pem')) { - throw new Exception('Failed to rename certificate privkey.pem: '.\json_encode($stdout)); - } - - $certificate = new Document(\array_merge($certificate, [ - 'domain' => $domain->get(), - 'issueDate' => \time(), - 'renewDate' => $renew, - 'attempts' => 0, - 'log' => \json_encode($stdout), + if(!empty($document)) { + $certificate = new Document(\array_merge($document, [ + 'updated' => \time(), + 'certificateId' => $certificate->getId(), ])); - $certificate = $dbForConsole->createDocument('certificates', $certificate); + $certificate = $dbForConsole->updateDocument('certificates', $certificate->getId(), $certificate); if(!$certificate) { - throw new Exception('Failed saving certificate to DB'); + throw new Exception('Failed saving domain to DB'); } + } - if(!empty($document)) { - $certificate = new Document(\array_merge($document, [ - 'updated' => \time(), - 'certificateId' => $certificate->getId(), - ])); - - $certificate = $dbForConsole->updateDocument('certificates', $certificate->getId(), $certificate); - - if(!$certificate) { - throw new Exception('Failed saving domain to DB'); - } - } - - $config = - "tls: - certificates: - - certFile: /storage/certificates/{$domain->get()}/fullchain.pem - keyFile: /storage/certificates/{$domain->get()}/privkey.pem"; + $config = +"tls: + certificates: + - certFile: /storage/certificates/{$domain->get()}/fullchain.pem + keyFile: /storage/certificates/{$domain->get()}/privkey.pem"; - if(!\file_put_contents(APP_STORAGE_CONFIG.'/'.$domain->get().'.yml', $config)) { - throw new Exception('Failed to save SSL configuration'); - } + if(!\file_put_contents(APP_STORAGE_CONFIG.'/'.$domain->get().'.yml', $config)) { + throw new Exception('Failed to save SSL configuration'); + } - ResqueScheduler::enqueueAt($renew + $safety, 'v1-certificates', 'CertificatesV1', [ - 'document' => [], - 'domain' => $domain->get(), - 'validateTarget' => $validateTarget, - 'validateCNAME' => $validateCNAME, - ]); // Async task rescheduale + ResqueScheduler::enqueueAt($renew + $safety, 'v1-certificates', 'CertificatesV1', [ + 'document' => [], + 'domain' => $domain->get(), + 'validateTarget' => $validateTarget, + 'validateCNAME' => $validateCNAME, + ]); // Async task rescheduale - Authorization::reset(); - - // Return db connections to pool - $register->get('dbPool')->put($db); - $register->get('redisPool')->put($redis); - }); + Authorization::reset(); } public function shutdown(): void From f56e2fede98648dde6813b19c1dba11958f807f6 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Mon, 19 Jul 2021 12:47:02 -0400 Subject: [PATCH 04/27] Harmonize syntax for db connections --- app/workers/database.php | 31 +++++++------------------------ app/workers/deletes.php | 9 ++++----- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/app/workers/database.php b/app/workers/database.php index b024871045..55c485f304 100644 --- a/app/workers/database.php +++ b/app/workers/database.php @@ -2,7 +2,7 @@ use Appwrite\Resque\Worker; use Utopia\Cache\Cache; -use Utopia\Cache\Adapter\Redis as RedisCache; +use Utopia\Cache\Adapter\Redis; use Utopia\CLI\Console; use Utopia\Database\Database; use Utopia\Database\Document; @@ -141,17 +141,9 @@ class DatabaseV1 extends Worker { global $register; - $dbForInternal = null; - - go(function() use ($register, $projectId, &$dbForInternal) { - $db = $register->get('dbPool')->get(); - $redis = $register->get('redisPool')->get(); - - $cache = new Cache(new RedisCache($redis)); - $dbForInternal = new Database(new MariaDB($db), $cache); - $dbForInternal->setNamespace('project_'.$projectId.'_internal'); // Main DB - - }); + $cache = new Cache(new Redis($register->get('cache'))); + $dbForInternal = new Database(new MariaDB($register->get('db')), $cache); + $dbForInternal->setNamespace('project_'.$projectId.'_internal'); // Main DB return $dbForInternal; } @@ -165,18 +157,9 @@ class DatabaseV1 extends Worker { global $register; - /** @var Database $dbForExternal */ - $dbForExternal = null; - - go(function() use ($register, $projectId, &$dbForExternal) { - $db = $register->get('dbPool')->get(); - $redis = $register->get('redisPool')->get(); - - $cache = new Cache(new RedisCache($redis)); - $dbForExternal = new Database(new MariaDB($db), $cache); - $dbForExternal->setNamespace('project_'.$projectId.'_external'); // Main DB - - }); + $cache = new Cache(new Redis($register->get('cache'))); + $dbForExternal = new Database(new MariaDB($register->get('db')), $cache); + $dbForExternal->setNamespace('project_'.$projectId.'_external'); // Main DB return $dbForExternal; } diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 1242d7a8ab..c855043254 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -3,14 +3,13 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; -use Utopia\Cache\Adapter\Redis as RedisCache; +use Utopia\Cache\Adapter\Redis; use Utopia\Database\Validator\Authorization; use Appwrite\Resque\Worker; use Utopia\Storage\Device\Local; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit; use Utopia\CLI\Console; -use Utopia\Config\Config; use Utopia\Audit\Audit; use Utopia\Cache\Cache; use Utopia\Database\Adapter\MariaDB; @@ -360,7 +359,7 @@ class DeletesV1 extends Worker { global $register; - $cache = new Cache(new RedisCache($register->get('cache'))); + $cache = new Cache(new Redis($register->get('cache'))); $dbForInternal = new Database(new MariaDB($register->get('db')), $cache); $dbForInternal->setNamespace('project_'.$projectId.'_internal'); // Main DB @@ -375,7 +374,7 @@ class DeletesV1 extends Worker { global $register; - $cache = new Cache(new RedisCache($register->get('cache'))); + $cache = new Cache(new Redis($register->get('cache'))); $dbForExternal = new Database(new MariaDB($register->get('db')), $cache); $dbForExternal->setNamespace('project_'.$projectId.'_external'); // Main DB @@ -389,7 +388,7 @@ class DeletesV1 extends Worker { global $register; - $cache = new Cache(new RedisCache($register->get('cache'))); + $cache = new Cache(new Redis($register->get('cache'))); $dbForConsole = new Database(new MariaDB($register->get('db')), $cache); $dbForConsole->setNamespace('project_console_internal'); // Main DB From 8dab66d455a7d986847bec8edb39029ae73b4d1a Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Mon, 19 Jul 2021 14:48:03 -0400 Subject: [PATCH 05/27] Require correct dependencies in database worker --- app/workers/database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/workers/database.php b/app/workers/database.php index 55c485f304..c9c02deeb0 100644 --- a/app/workers/database.php +++ b/app/workers/database.php @@ -8,7 +8,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Adapter\MariaDB; -require_once __DIR__.'/../init.php'; +require_once __DIR__.'/../workers.php'; Console::title('Database V1 Worker'); Console::success(APP_NAME.' database worker v1 has started'."\n"); From 8a205d1b6381ae0c2fdabe68ae35ee5d8a93eebc Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Mon, 19 Jul 2021 15:39:31 -0400 Subject: [PATCH 06/27] Retry initial db connection once before throwing exception --- app/http.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/http.php b/app/http.php index b148e52d1e..cac1819b2c 100644 --- a/app/http.php +++ b/app/http.php @@ -72,7 +72,13 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) { // wait for database to be ready sleep(5); - $dbForConsole = $app->getResource('dbForConsole'); /** @var Utopia\Database\Database $dbForConsole */ + try { + $dbForConsole = $app->getResource('dbForConsole'); /** @var Utopia\Database\Database $dbForConsole */ + } catch(\Exception $e) { + Console::warning('Database not ready. Retrying connection...'); + sleep(5); + $dbForConsole = $app->getResource('dbForConsole'); /** @var Utopia\Database\Database $dbForConsole */ + } if(!$dbForConsole->exists()) { Console::success('[Setup] - Server database init started...'); From 9870c6fdff76983b9eb957b007545b5a8714ce77 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Tue, 20 Jul 2021 13:01:47 -0400 Subject: [PATCH 07/27] Attempt to reconnect five times before throwing exception --- app/http.php | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/app/http.php b/app/http.php index cac1819b2c..9b42ccff9f 100644 --- a/app/http.php +++ b/app/http.php @@ -70,15 +70,20 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) { }); // wait for database to be ready - sleep(5); - - try { - $dbForConsole = $app->getResource('dbForConsole'); /** @var Utopia\Database\Database $dbForConsole */ - } catch(\Exception $e) { - Console::warning('Database not ready. Retrying connection...'); - sleep(5); - $dbForConsole = $app->getResource('dbForConsole'); /** @var Utopia\Database\Database $dbForConsole */ - } + $attempts = 0; + do { + try { + $dbForConsole = $app->getResource('dbForConsole'); /** @var Utopia\Database\Database $dbForConsole */ + break; // leave the do-while if successful + } catch(\Exception $e) { + $attempts++; + Console::warning("Database not ready. Retrying connection ({$attempts})..."); + if ($attempts >= 5) { + throw new \Exception('Failed to connect to database: '. $e->getMessage()); + } + sleep(5); + } + } while (!$dbForConsole || $attempts < 5); if(!$dbForConsole->exists()) { Console::success('[Setup] - Server database init started...'); From e16bfe7fd3fd25c17fb96ef7b541dfb9d0cb0e52 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Tue, 20 Jul 2021 14:59:58 -0400 Subject: [PATCH 08/27] Move reconnection loop to first call to db --- app/http.php | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/app/http.php b/app/http.php index 9b42ccff9f..b85d6a051b 100644 --- a/app/http.php +++ b/app/http.php @@ -54,8 +54,23 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) { $app = new App('UTC'); go(function() use ($register, $app) { - $db = $register->get('dbPool')->get(); - $redis = $register->get('redisPool')->get(); + // wait for database to be ready + $attempts = 0; + do { + try { + $attempts++; + $db = $register->get('dbPool')->get(); + $redis = $register->get('redisPool')->get(); + break; // leave the do-while if successful + } catch(\Exception $e) { + Console::warning("Database not ready. Retrying connection ({$attempts})..."); + if ($attempts >= 10) { + throw new \Exception('Failed to connect to database: '. $e->getMessage()); + } + sleep(1); + continue; + } + } while ($attempts < 10); App::setResource('db', function () use (&$db) { return $db; @@ -69,21 +84,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) { return $app; }); - // wait for database to be ready - $attempts = 0; - do { - try { - $dbForConsole = $app->getResource('dbForConsole'); /** @var Utopia\Database\Database $dbForConsole */ - break; // leave the do-while if successful - } catch(\Exception $e) { - $attempts++; - Console::warning("Database not ready. Retrying connection ({$attempts})..."); - if ($attempts >= 5) { - throw new \Exception('Failed to connect to database: '. $e->getMessage()); - } - sleep(5); - } - } while (!$dbForConsole || $attempts < 5); + $dbForConsole = $app->getResource('dbForConsole'); /** @var Utopia\Database\Database $dbForConsole */ if(!$dbForConsole->exists()) { Console::success('[Setup] - Server database init started...'); From 12e2ebc2b5a899b404c11ae638cfea3351d9888e Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Tue, 20 Jul 2021 15:11:54 -0400 Subject: [PATCH 09/27] Define loop control vars outside loop --- app/http.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/http.php b/app/http.php index b85d6a051b..74bbd1b0c2 100644 --- a/app/http.php +++ b/app/http.php @@ -56,6 +56,9 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) { go(function() use ($register, $app) { // wait for database to be ready $attempts = 0; + $max = 10; + $sleep = 1; + do { try { $attempts++; @@ -64,13 +67,13 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) { break; // leave the do-while if successful } catch(\Exception $e) { Console::warning("Database not ready. Retrying connection ({$attempts})..."); - if ($attempts >= 10) { + if ($attempts >= $max) { throw new \Exception('Failed to connect to database: '. $e->getMessage()); } - sleep(1); + sleep($sleep); continue; } - } while ($attempts < 10); + } while ($attempts < $max); App::setResource('db', function () use (&$db) { return $db; From 1b051d32feefdee034fa5e6a3bbb8bc062f665de Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Tue, 20 Jul 2021 15:13:00 -0400 Subject: [PATCH 10/27] Attempt to reconnect deletes worker to db --- app/workers/deletes.php | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 1242d7a8ab..751c7db054 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -389,9 +389,27 @@ class DeletesV1 extends Worker { global $register; - $cache = new Cache(new RedisCache($register->get('cache'))); - $dbForConsole = new Database(new MariaDB($register->get('db')), $cache); - $dbForConsole->setNamespace('project_console_internal'); // Main DB + // wait for database to be ready + $attempts = 0; + $max = 5; + $sleep = 5; + + do { + try { + $attempts++; + $cache = new Cache(new RedisCache($register->get('cache'))); + $dbForConsole = new Database(new MariaDB($register->get('db')), $cache); + $dbForConsole->setNamespace('project_console_internal'); // Main DB + break; // leave the do-while if successful + } catch(\Exception $e) { + Console::warning("Database not ready. Retrying connection ({$attempts})..."); + if ($attempts >= $max) { + throw new \Exception('Failed to connect to database: '. $e->getMessage()); + } + sleep($sleep); + continue; + } + } while ($attempts < $max); return $dbForConsole; } From 4242ba75f5a212bc80d4b85e6b1410801e155b61 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 21 Jul 2021 15:14:18 -0400 Subject: [PATCH 11/27] Remove unneeded continue statements --- app/http.php | 1 - app/workers/deletes.php | 1 - 2 files changed, 2 deletions(-) diff --git a/app/http.php b/app/http.php index 74bbd1b0c2..55adbb2359 100644 --- a/app/http.php +++ b/app/http.php @@ -71,7 +71,6 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) { throw new \Exception('Failed to connect to database: '. $e->getMessage()); } sleep($sleep); - continue; } } while ($attempts < $max); diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 751c7db054..9c69b57927 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -407,7 +407,6 @@ class DeletesV1 extends Worker throw new \Exception('Failed to connect to database: '. $e->getMessage()); } sleep($sleep); - continue; } } while ($attempts < $max); From 4e1bc7aa1d0c24cf4af43c08a88d082ece58cc0d Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 22 Jul 2021 11:02:07 +0200 Subject: [PATCH 12/27] feat(0.9.2): release --- app/init.php | 4 ++-- src/Appwrite/Migration/Migration.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/init.php b/app/init.php index a139a4f841..3ad673249c 100644 --- a/app/init.php +++ b/app/init.php @@ -47,8 +47,8 @@ const APP_USERAGENT = APP_NAME.'-Server v%s. Please report abuse at %s'; const APP_MODE_DEFAULT = 'default'; const APP_MODE_ADMIN = 'admin'; const APP_PAGING_LIMIT = 12; -const APP_CACHE_BUSTER = 149; -const APP_VERSION_STABLE = '0.9.1'; +const APP_CACHE_BUSTER = 150; +const APP_VERSION_STABLE = '0.9.2'; const APP_STORAGE_UPLOADS = '/storage/uploads'; const APP_STORAGE_FUNCTIONS = '/storage/functions'; const APP_STORAGE_CACHE = '/storage/cache'; diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index bd03e7d339..8eae515b9b 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -40,6 +40,7 @@ abstract class Migration '0.8.0' => 'V07', '0.9.0' => 'V08', '0.9.1' => 'V08', + '0.9.2' => 'V08', ]; /** From d235ed71908c9bdcf5c1ba1bfe66a8aa92e06dec Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 22 Jul 2021 11:04:17 +0200 Subject: [PATCH 13/27] chore(changelog): add 0.9.2 --- CHANGES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 94b56bc7a0..54b353bd91 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +# Version 0.9.2 + +## Bugs + +- Fixed JWT session validation (#1408) + # Version 0.9.1 ## Bugs From ca3d677596d952eeb992f3bb73787771fc672c8a Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Thu, 22 Jul 2021 11:08:08 +0100 Subject: [PATCH 14/27] Make untarring the code file a blocking task --- app/workers/functions.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/workers/functions.php b/app/workers/functions.php index a85843b38d..bbb20b823e 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -425,15 +425,24 @@ class FunctionsV1 extends Worker " --workdir /usr/local/src". " ".\implode(" ", $vars). " {$runtime['image']}". - " sh -c 'mv /tmp/code.tar.gz /usr/local/src/code.tar.gz && tar -zxf /usr/local/src/code.tar.gz --strip 1 && rm /usr/local/src/code.tar.gz && tail -f /dev/null'" + " tail -f /dev/null" , '', $stdout, $stderr, 30); - $executionEnd = \microtime(true); - if($exitCode !== 0) { throw new Exception('Failed to create function environment: '.$stderr); } + $exitCodeUntar = Console::execute("docker exec ". + $container. + " sh -c 'mv /tmp/code.tar.gz /usr/local/src/code.tar.gz && tar -zxf /usr/local/src/code.tar.gz --strip 1 && rm /usr/local/src/code.tar.gz'" + , '', $stdout, $stderr, 60); + + if($exitCodeUntar !== 0) { + throw new Exception('Failed to extract tar: '.$stderr); + } + + $executionEnd = \microtime(true); + $list[$container] = [ 'name' => $container, 'online' => true, From 70d4c4d7beb7d51ba33501b7e3cb785347f211d4 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Thu, 22 Jul 2021 11:46:05 +0100 Subject: [PATCH 15/27] Update functions.php --- app/workers/functions.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/workers/functions.php b/app/workers/functions.php index bbb20b823e..2ab1f1f309 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -433,9 +433,9 @@ class FunctionsV1 extends Worker } $exitCodeUntar = Console::execute("docker exec ". - $container. - " sh -c 'mv /tmp/code.tar.gz /usr/local/src/code.tar.gz && tar -zxf /usr/local/src/code.tar.gz --strip 1 && rm /usr/local/src/code.tar.gz'" - , '', $stdout, $stderr, 60); + $container. + " sh -c 'mv /tmp/code.tar.gz /usr/local/src/code.tar.gz && tar -zxf /usr/local/src/code.tar.gz --strip 1 && rm /usr/local/src/code.tar.gz'" + , '', $stdout, $stderr, 60); if($exitCodeUntar !== 0) { throw new Exception('Failed to extract tar: '.$stderr); From d0e4e5cb29e73f45adbb9fb28b15d6e8bdc6113a Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Thu, 22 Jul 2021 17:49:52 +0300 Subject: [PATCH 16/27] Fixed bug --- app/controllers/api/functions.php | 14 ++++----- .../Functions/FunctionsCustomClientTest.php | 25 ++++++++++++++++ .../Functions/FunctionsCustomServerTest.php | 23 +++++++++++++++ tests/resources/functions/php-fn.tar.gz | Bin 24549 -> 24954 bytes tests/resources/functions/php-fn/index.php | 27 ++++++++++-------- 5 files changed, 70 insertions(+), 19 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index dd58108893..1560355aa0 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -748,20 +748,20 @@ App::post('/v1/functions/:functionId/executions') $jwt = ''; // initialize if (!empty($user->getId())) { // If userId exists, generate a JWT for function - $tokens = $user->getAttribute('tokens', []); - $session = new Document(); + $sessions = $user->getAttribute('sessions', []); + $current = new Document(); - foreach ($tokens as $token) { /** @var Appwrite\Database\Document $token */ - if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too - $session = $token; + foreach ($sessions as $session) { /** @var Appwrite\Database\Document $session */ + if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + $current = $session; } } - if(!$session->isEmpty()) { + if(!$current->isEmpty()) { $jwtObj = new JWT(App::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway. $jwt = $jwtObj->encode([ 'userId' => $user->getId(), - 'sessionId' => $session->getId(), + 'sessionId' => $current->getId(), ]); } } diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index 8ba16f8145..454a149c3c 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -185,7 +185,32 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals(201, $execution['headers']['status-code']); + $executionId = $execution['body']['$id'] ?? ''; + sleep(10); + + $executions = $this->client->call(Client::METHOD_GET, '/functions/'.$functionId.'/executions/'.$executionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $apikey, + ]); + + $output = json_decode($executions['body']['stdout'], true); + + $this->assertEquals(200, $executions['headers']['status-code']); + $this->assertEquals('completed', $executions['body']['status']); + $this->assertEquals($functionId, $output['APPWRITE_FUNCTION_ID']); + $this->assertEquals('Test', $output['APPWRITE_FUNCTION_NAME']); + $this->assertEquals($tagId, $output['APPWRITE_FUNCTION_TAG']); + $this->assertEquals('http', $output['APPWRITE_FUNCTION_TRIGGER']); + $this->assertEquals('PHP', $output['APPWRITE_FUNCTION_RUNTIME_NAME']); + $this->assertEquals('8.0', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']); + $this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT']); + $this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT_DATA']); + $this->assertEquals('foobar', $output['APPWRITE_FUNCTION_DATA']); + $this->assertEquals($this->getUser()['$id'], $output['APPWRITE_FUNCTION_USER_ID']); + $this->assertNotEmpty($output['APPWRITE_FUNCTION_JWT']); + $executions = $this->client->call(Client::METHOD_GET, '/functions/'.$functionId.'/executions', [ 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index cf0a1cafc4..775eca06e2 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -582,8 +582,31 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(201, $execution['headers']['status-code']); + $executionId = $execution['body']['$id'] ?? ''; + sleep(10); + $executions = $this->client->call(Client::METHOD_GET, '/functions/'.$functionId.'/executions/'.$executionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $output = json_decode($executions['body']['stdout'], true); + + $this->assertEquals(200, $executions['headers']['status-code']); + $this->assertEquals('completed', $executions['body']['status']); + $this->assertEquals($functionId, $output['APPWRITE_FUNCTION_ID']); + $this->assertEquals('Test '.$name, $output['APPWRITE_FUNCTION_NAME']); + $this->assertEquals($tagId, $output['APPWRITE_FUNCTION_TAG']); + $this->assertEquals('http', $output['APPWRITE_FUNCTION_TRIGGER']); + $this->assertEquals('PHP', $output['APPWRITE_FUNCTION_RUNTIME_NAME']); + $this->assertEquals('8.0', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']); + $this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT']); + $this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT_DATA']); + $this->assertEquals('foobar', $output['APPWRITE_FUNCTION_DATA']); + $this->assertEquals('', $output['APPWRITE_FUNCTION_USER_ID']); + $this->assertEmpty($output['APPWRITE_FUNCTION_JWT']); + $executions = $this->client->call(Client::METHOD_GET, '/functions/'.$functionId.'/executions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], diff --git a/tests/resources/functions/php-fn.tar.gz b/tests/resources/functions/php-fn.tar.gz index a97a2e23528ce5fd28269fd6d8e8e0798b994fb5..5e359122fa62f29d9de63305b82d9bb0b5680417 100644 GIT binary patch literal 24954 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 bDpQ%tRHibOsZ3>h?CJjlk4HwS0B{2Ub;Z~Q literal 24549 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['APPWRITE_FILEID']); -echo $_ENV['APPWRITE_FUNCTION_ID']."\n"; -echo $_ENV['APPWRITE_FUNCTION_NAME']."\n"; -echo $_ENV['APPWRITE_FUNCTION_TAG']."\n"; -echo $_ENV['APPWRITE_FUNCTION_TRIGGER']."\n"; -echo $_ENV['APPWRITE_FUNCTION_RUNTIME_NAME']."\n"; -echo $_ENV['APPWRITE_FUNCTION_RUNTIME_VERSION']."\n"; -// echo $result['$id']; -echo $_ENV['APPWRITE_FUNCTION_EVENT']."\n"; -echo $_ENV['APPWRITE_FUNCTION_EVENT_DATA']."\n"; -echo 'data:'.$_ENV['APPWRITE_FUNCTION_DATA']."\n"; -echo 'userId:'.$_ENV['APPWRITE_FUNCTION_USER_ID']."\n"; -echo 'jwt:'.$_ENV['APPWRITE_FUNCTION_JWT']."\n"; +$output = [ + 'APPWRITE_FUNCTION_ID' => $_ENV['APPWRITE_FUNCTION_ID'], + 'APPWRITE_FUNCTION_NAME' => $_ENV['APPWRITE_FUNCTION_NAME'], + 'APPWRITE_FUNCTION_TAG' => $_ENV['APPWRITE_FUNCTION_TAG'], + 'APPWRITE_FUNCTION_TRIGGER' => $_ENV['APPWRITE_FUNCTION_TRIGGER'], + 'APPWRITE_FUNCTION_RUNTIME_NAME' => $_ENV['APPWRITE_FUNCTION_RUNTIME_NAME'], + 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $_ENV['APPWRITE_FUNCTION_RUNTIME_VERSION'], + 'APPWRITE_FUNCTION_EVENT' => $_ENV['APPWRITE_FUNCTION_EVENT'], + 'APPWRITE_FUNCTION_EVENT_DATA' => $_ENV['APPWRITE_FUNCTION_EVENT_DATA'], + 'APPWRITE_FUNCTION_DATA' => $_ENV['APPWRITE_FUNCTION_DATA'], + 'APPWRITE_FUNCTION_USER_ID' => $_ENV['APPWRITE_FUNCTION_USER_ID'], + 'APPWRITE_FUNCTION_JWT' => $_ENV['APPWRITE_FUNCTION_JWT'], +]; + +echo json_encode($output); From 5815caf01e72f3f7073a4a276a606a323192404a Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 22 Jul 2021 17:45:15 +0200 Subject: [PATCH 17/27] chore(changelog): update changelog --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 54b353bd91..ca647e7489 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,8 @@ ## Bugs - Fixed JWT session validation (#1408) +- Fixed passing valid JWT session to Cloud Functions (#1421) +- Fixed race condition when uploading and extracting bigger Cloud Functions (#1419) # Version 0.9.1 From b431c0ee41638200b553ca5bb23a5bf1dbb9a331 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 24 Jul 2021 18:48:11 +0300 Subject: [PATCH 18/27] Added missing sessions index --- app/config/collections2.php | 14 ++++++------- composer.lock | 39 +++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/app/config/collections2.php b/app/config/collections2.php index 018066cc23..7cfa518668 100644 --- a/app/config/collections2.php +++ b/app/config/collections2.php @@ -609,13 +609,13 @@ $collections = [ ], ], 'indexes' => [ - // [ - // '$id' => '_key_provider_providerUid', - // 'type' => Database::INDEX_KEY, - // 'attributes' => ['provider', 'providerUid'], - // 'lengths' => [100, 100], - // 'orders' => [Database::ORDER_ASC, Database::ORDER_ASC], - // ] + [ + '$id' => '_key_provider_providerUid', + 'type' => Database::INDEX_KEY, + 'attributes' => ['provider', 'providerUid'], + 'lengths' => [100, 100], + 'orders' => [Database::ORDER_ASC, Database::ORDER_ASC], + ] ], ], diff --git a/composer.lock b/composer.lock index 4a34e6accb..120590859f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ea49a8cc41d837be81393c1457051bcb", + "content-hash": "17c1c46d283e4745df9915d082c22078", "packages": [ { "name": "adhocore/jwt", @@ -2079,16 +2079,16 @@ }, { "name": "utopia-php/framework", - "version": "0.16.1", + "version": "0.16.2", "source": { "type": "git", "url": "https://github.com/utopia-php/framework.git", - "reference": "e7440b91077ccf2f4f3908fde4f31d177f8ea692" + "reference": "df02354a670df366b92e2e927fbf128be9a8e64e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/framework/zipball/e7440b91077ccf2f4f3908fde4f31d177f8ea692", - "reference": "e7440b91077ccf2f4f3908fde4f31d177f8ea692", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/df02354a670df366b92e2e927fbf128be9a8e64e", + "reference": "df02354a670df366b92e2e927fbf128be9a8e64e", "shasum": "" }, "require": { @@ -2122,9 +2122,9 @@ ], "support": { "issues": "https://github.com/utopia-php/framework/issues", - "source": "https://github.com/utopia-php/framework/tree/0.16.1" + "source": "https://github.com/utopia-php/framework/tree/0.16.2" }, - "time": "2021-07-18T10:59:00+00:00" + "time": "2021-07-20T10:24:56+00:00" }, { "name": "utopia-php/image", @@ -3367,16 +3367,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.11.0", + "version": "v4.12.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "fe14cf3672a149364fb66dfe11bf6549af899f94" + "reference": "6608f01670c3cc5079e18c1dab1104e002579143" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/fe14cf3672a149364fb66dfe11bf6549af899f94", - "reference": "fe14cf3672a149364fb66dfe11bf6549af899f94", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6608f01670c3cc5079e18c1dab1104e002579143", + "reference": "6608f01670c3cc5079e18c1dab1104e002579143", "shasum": "" }, "require": { @@ -3417,9 +3417,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.11.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.12.0" }, - "time": "2021-07-03T13:36:55+00:00" + "time": "2021-07-21T10:44:31+00:00" }, { "name": "openlss/lib-array2xml", @@ -3476,16 +3476,16 @@ }, { "name": "phar-io/manifest", - "version": "2.0.1", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", "shasum": "" }, "require": { @@ -3530,9 +3530,9 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/master" + "source": "https://github.com/phar-io/manifest/tree/2.0.3" }, - "time": "2020-06-27T14:33:11+00:00" + "time": "2021-07-20T11:28:43+00:00" }, { "name": "phar-io/version", @@ -5132,6 +5132,7 @@ "type": "github" } ], + "abandoned": true, "time": "2020-09-28T06:45:17+00:00" }, { From 69cd8c468c4d43e5f2b352906393a0076d3532f7 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 25 Jul 2021 14:37:36 +0300 Subject: [PATCH 19/27] Fixed conflict --- app/init.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/init.php b/app/init.php index c2be4abf9b..a42513259e 100644 --- a/app/init.php +++ b/app/init.php @@ -508,13 +508,8 @@ App::setResource('user', function($mode, $project, $console, $request, $response $user = $dbForInternal->getDocument('users', $jwtUserId); } -<<<<<<< HEAD if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token $user = new Document2(['$id' => '', '$collection' => 'users']); -======= - if (empty($user->search('$id', $jwtSessionId, $user->getAttribute('sessions')))) { // Match JWT to active token - $user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]); ->>>>>>> 7fb7f1073fb00964bb64c45408a052524d6a9584 } } From 13c1bb045da152865075a56fbd6fb6b310d4cff7 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 25 Jul 2021 17:47:18 +0300 Subject: [PATCH 20/27] Cleanup old db library --- app/controllers/api/account.php | 64 ++++++++-------- app/controllers/api/avatars.php | 2 +- app/controllers/api/database.php | 36 ++++----- app/controllers/api/functions.php | 36 ++++----- app/controllers/api/locale.php | 2 +- app/controllers/api/projects.php | 56 +++++++------- app/controllers/api/storage.php | 14 ++-- app/controllers/api/teams.php | 30 ++++---- app/controllers/api/users.php | 26 +++---- app/controllers/general.php | 10 +-- app/controllers/mock.php | 2 +- app/controllers/shared/api.php | 8 +- app/controllers/web/home.php | 2 - app/http.php | 4 - src/Appwrite/Auth/Auth.php | 2 +- .../Specification/Format/OpenAPI3.php | 2 +- .../Specification/Format/Swagger2.php | 2 +- src/Appwrite/Utopia/Response.php | 76 +------------------ 18 files changed, 144 insertions(+), 230 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 044664d655..27d9f82006 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -53,7 +53,7 @@ App::post('/v1/account') ->action(function ($email, $password, $name, $request, $response, $project, $dbForInternal, $audits) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Document $project */ + /** @var Utopia\Database\Document $project */ /** @var Utopia\Database\Database $dbForInternal */ /** @var Appwrite\Event\Event $audits */ @@ -119,7 +119,7 @@ App::post('/v1/account') ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_USER); }); App::post('/v1/account/sessions') @@ -229,7 +229,7 @@ App::post('/v1/account/sessions') ->setAttribute('countryName', $countryName) ; - $response->dynamic2($session, Response::MODEL_SESSION); + $response->dynamic($session, Response::MODEL_SESSION); }); App::get('/v1/account/sessions/oauth2/:provider') @@ -256,7 +256,7 @@ App::get('/v1/account/sessions/oauth2/:provider') ->action(function ($provider, $success, $failure, $scopes, $request, $response, $project) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Document $project */ + /** @var Utopia\Database\Document $project */ $protocol = $request->getProtocol(); $callback = $protocol.'://'.$request->getHostname().'/v1/account/sessions/oauth2/callback/'.$provider.'/'.$project->getId(); @@ -362,7 +362,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->action(function ($provider, $code, $state, $request, $response, $project, $user, $dbForInternal, $geodb, $audits, $events) use ($oauthDefaultSuccess) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Document $project */ + /** @var Utopia\Database\Document $project */ /** @var Utopia\Database\Document $user */ /** @var Utopia\Database\Database $dbForInternal */ /** @var MaxMind\Db\Reader $geodb */ @@ -546,7 +546,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->setParam('data', ['provider' => $provider]) ; - $events->setParam('eventData', $response->output2($session, Response::MODEL_SESSION)); + $events->setParam('eventData', $response->output($session, Response::MODEL_SESSION)); if (!Config::getParam('domainVerification')) { $response @@ -603,7 +603,7 @@ App::post('/v1/account/sessions/anonymous') /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Locale\Locale $locale */ /** @var Utopia\Database\Document $user */ - /** @var Appwrite\Database\Document $project */ + /** @var Utopia\Database\Document $project */ /** @var Utopia\Database\Database $dbForInternal */ /** @var MaxMind\Db\Reader $geodb */ /** @var Appwrite\Event\Event $audits */ @@ -710,7 +710,7 @@ App::post('/v1/account/sessions/anonymous') ->setAttribute('countryName', $countryName) ; - $response->dynamic2($session, Response::MODEL_SESSION); + $response->dynamic($session, Response::MODEL_SESSION); }); App::post('/v1/account/jwt') @@ -737,7 +737,7 @@ App::post('/v1/account/jwt') $current = new Document(); foreach ($sessions as $session) { - /** @var Appwrite\Database\Document $session */ + /** @var Utopia\Database\Document $session */ if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too $current = $session; @@ -751,7 +751,7 @@ App::post('/v1/account/jwt') $jwt = new JWT(App::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway. $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2(new Document(['jwt' => $jwt->encode([ + $response->dynamic(new Document(['jwt' => $jwt->encode([ // 'uid' => 1, // 'aud' => 'http://site.com', // 'scopes' => ['user'], @@ -778,7 +778,7 @@ App::get('/v1/account') /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Document $user */ - $response->dynamic2($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_USER); }); App::get('/v1/account/prefs') @@ -800,7 +800,7 @@ App::get('/v1/account/prefs') $prefs = $user->getAttribute('prefs', new \stdClass()); - $response->dynamic2(new Document($prefs), Response::MODEL_PREFERENCES); + $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); App::get('/v1/account/sessions') @@ -837,7 +837,7 @@ App::get('/v1/account/sessions') $sessions[$key] = $session; } - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'sessions' => $sessions, 'sum' => count($sessions), ]), Response::MODEL_SESSION_LIST); @@ -861,7 +861,7 @@ App::get('/v1/account/logs') ->inject('dbForInternal') ->action(function ($response, $user, $locale, $geodb, $dbForInternal) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Document $project */ + /** @var Utopia\Database\Document $project */ /** @var Utopia\Database\Document $user */ /** @var Utopia\Locale\Locale $locale */ /** @var MaxMind\Db\Reader $geodb */ @@ -914,7 +914,7 @@ App::get('/v1/account/logs') } - $response->dynamic2(new Document(['logs' => $output]), Response::MODEL_LOG_LIST); + $response->dynamic(new Document(['logs' => $output]), Response::MODEL_LOG_LIST); }); App::get('/v1/account/sessions/:sessionId') @@ -935,7 +935,7 @@ App::get('/v1/account/sessions/:sessionId') ->inject('dbForInternal') ->action(function ($sessionId, $response, $user, $locale, $dbForInternal) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Document $user */ + /** @var Utopia\Database\Document $user */ /** @var Utopia\Locale\Locale $locale */ /** @var Utopia\Database\Database $dbForInternal */ @@ -956,7 +956,7 @@ App::get('/v1/account/sessions/:sessionId') ->setAttribute('countryName', $countryName) ; - return $response->dynamic2($session, Response::MODEL_SESSION); + return $response->dynamic($session, Response::MODEL_SESSION); } } @@ -994,7 +994,7 @@ App::patch('/v1/account/name') ->setParam('resource', 'users/'.$user->getId()) ; - $response->dynamic2($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_USER); }); App::patch('/v1/account/password') @@ -1037,7 +1037,7 @@ App::patch('/v1/account/password') ->setParam('resource', 'users/'.$user->getId()) ; - $response->dynamic2($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_USER); }); App::patch('/v1/account/email') @@ -1092,7 +1092,7 @@ App::patch('/v1/account/email') ->setParam('resource', 'users/'.$user->getId()) ; - $response->dynamic2($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_USER); }); App::patch('/v1/account/prefs') @@ -1125,7 +1125,7 @@ App::patch('/v1/account/prefs') ->setParam('resource', 'users/'.$user->getId()) ; - $response->dynamic2($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_USER); }); App::delete('/v1/account') @@ -1172,7 +1172,7 @@ App::delete('/v1/account') ; $events - ->setParam('eventData', $response->output2($user, Response::MODEL_USER)) + ->setParam('eventData', $response->output($user, Response::MODEL_USER)) ; if (!Config::getParam('domainVerification')) { @@ -1259,7 +1259,7 @@ App::delete('/v1/account/sessions/:sessionId') $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('sessions', $sessions)); $events - ->setParam('eventData', $response->output2($session, Response::MODEL_SESSION)) + ->setParam('eventData', $response->output($session, Response::MODEL_SESSION)) ; return $response->noContent(); @@ -1332,7 +1332,7 @@ App::delete('/v1/account/sessions') $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('sessions', [])); $events - ->setParam('eventData', $response->output2(new Document([ + ->setParam('eventData', $response->output(new Document([ 'sessions' => $sessions, 'sum' => count($sessions), ]), Response::MODEL_SESSION_LIST)) @@ -1369,7 +1369,7 @@ App::post('/v1/account/recovery') /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForInternal */ - /** @var Appwrite\Database\Document $project */ + /** @var Utopia\Database\Document $project */ /** @var Utopia\Locale\Locale $locale */ /** @var Appwrite\Event\Event $mails */ /** @var Appwrite\Event\Event $audits */ @@ -1443,7 +1443,7 @@ App::post('/v1/account/recovery') $events ->setParam('eventData', - $response->output2($recovery->setAttribute('secret', $secret), + $response->output($recovery->setAttribute('secret', $secret), Response::MODEL_TOKEN )) ; @@ -1459,7 +1459,7 @@ App::post('/v1/account/recovery') ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($recovery, Response::MODEL_TOKEN); + $response->dynamic($recovery, Response::MODEL_TOKEN); }); App::put('/v1/account/recovery') @@ -1533,7 +1533,7 @@ App::put('/v1/account/recovery') ->setParam('resource', 'users/'.$profile->getId()) ; - $response->dynamic2($recovery, Response::MODEL_TOKEN); + $response->dynamic($recovery, Response::MODEL_TOKEN); }); App::post('/v1/account/verification') @@ -1563,7 +1563,7 @@ App::post('/v1/account/verification') ->action(function ($url, $request, $response, $project, $user, $dbForInternal, $locale, $audits, $events, $mails) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Document $project */ + /** @var Utopia\Database\Document $project */ /** @var Utopia\Database\Document $user */ /** @var Utopia\Database\Database $dbForInternal */ /** @var Utopia\Locale\Locale $locale */ @@ -1629,7 +1629,7 @@ App::post('/v1/account/verification') $events ->setParam('eventData', - $response->output2($verification->setAttribute('secret', $verificationSecret), + $response->output($verification->setAttribute('secret', $verificationSecret), Response::MODEL_TOKEN )) ; @@ -1645,7 +1645,7 @@ App::post('/v1/account/verification') ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($verification, Response::MODEL_TOKEN); + $response->dynamic($verification, Response::MODEL_TOKEN); }); App::put('/v1/account/verification') @@ -1710,5 +1710,5 @@ App::put('/v1/account/verification') ->setParam('resource', 'users/'.$user->getId()) ; - $response->dynamic2($verification, Response::MODEL_TOKEN); + $response->dynamic($verification, Response::MODEL_TOKEN); }); diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 2b6951295e..13f335f4f4 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -424,7 +424,7 @@ App::get('/v1/avatars/initials') ->inject('user') ->action(function ($name, $width, $height, $color, $background, $response, $user) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Document $user */ + /** @var Utopia\Database\Document $user */ $themes = [ ['color' => '#27005e', 'background' => '#e1d2f6'], // VIOLET diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 92a4f802f7..4fcf64ee1f 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -67,7 +67,7 @@ App::post('/v1/database/collections') ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($collection, Response::MODEL_COLLECTION); + $response->dynamic($collection, Response::MODEL_COLLECTION); }); App::get('/v1/database/collections') @@ -93,7 +93,7 @@ App::get('/v1/database/collections') $queries = ($search) ? [new Query('name', Query::TYPE_SEARCH, [$search])] : []; - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'collections' => $dbForExternal->find(Database::COLLECTIONS, $queries, $limit, $offset, ['_id'], [$orderType]), 'sum' => $dbForExternal->count(Database::COLLECTIONS, $queries, APP_LIMIT_COUNT), ]), Response::MODEL_COLLECTION_LIST); @@ -123,7 +123,7 @@ App::get('/v1/database/collections/:collectionId') throw new Exception('Collection not found', 404); } - $response->dynamic2($collection, Response::MODEL_COLLECTION); + $response->dynamic($collection, Response::MODEL_COLLECTION); }); App::put('/v1/database/collections/:collectionId') @@ -177,7 +177,7 @@ App::put('/v1/database/collections/:collectionId') ->setParam('data', $collection->getArrayCopy()) ; - $response->dynamic2($collection, Response::MODEL_COLLECTION); + $response->dynamic($collection, Response::MODEL_COLLECTION); }); App::delete('/v1/database/collections/:collectionId') @@ -212,7 +212,7 @@ App::delete('/v1/database/collections/:collectionId') $dbForExternal->deleteCollection($collectionId); $events - ->setParam('eventData', $response->output2($collection, Response::MODEL_COLLECTION)) + ->setParam('eventData', $response->output($collection, Response::MODEL_COLLECTION)) ; $audits @@ -296,7 +296,7 @@ App::post('/v1/database/collections/:collectionId/attributes') ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($attribute, Response::MODEL_ATTRIBUTE); + $response->dynamic($attribute, Response::MODEL_ATTRIBUTE); }); App::get('/v1/database/collections/:collectionId/attributes') @@ -331,7 +331,7 @@ App::get('/v1/database/collections/:collectionId/attributes') ])]); }, $attributes); - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'sum' => \count($attributes), 'attributes' => $attributes ]), Response::MODEL_ATTRIBUTE_LIST); @@ -375,7 +375,7 @@ App::get('/v1/database/collections/:collectionId/attributes/:attributeId') 'collectionId' => $collectionId, ])]); - $response->dynamic2($attribute, Response::MODEL_ATTRIBUTE); + $response->dynamic($attribute, Response::MODEL_ATTRIBUTE); }); App::delete('/v1/database/collections/:collectionId/attributes/:attributeId') @@ -428,7 +428,7 @@ App::delete('/v1/database/collections/:collectionId/attributes/:attributeId') ; $events - ->setParam('payload', $response->output2($attribute, Response::MODEL_ATTRIBUTE)) + ->setParam('payload', $response->output($attribute, Response::MODEL_ATTRIBUTE)) ; $audits @@ -526,7 +526,7 @@ App::post('/v1/database/collections/:collectionId/indexes') ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($index, Response::MODEL_INDEX); + $response->dynamic($index, Response::MODEL_INDEX); }); @@ -562,7 +562,7 @@ App::get('/v1/database/collections/:collectionId/indexes') ])]); }, $indexes); - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'sum' => \count($indexes), 'attributes' => $indexes, ]), Response::MODEL_INDEX_LIST); @@ -606,7 +606,7 @@ App::get('/v1/database/collections/:collectionId/indexes/:indexId') 'collectionId' => $collectionId, ])]); - $response->dynamic2($index, Response::MODEL_INDEX); + $response->dynamic($index, Response::MODEL_INDEX); }); App::delete('/v1/database/collections/:collectionId/indexes/:indexId') @@ -659,7 +659,7 @@ App::delete('/v1/database/collections/:collectionId/indexes/:indexId') ; $events - ->setParam('payload', $response->output2($index, Response::MODEL_INDEX)) + ->setParam('payload', $response->output($index, Response::MODEL_INDEX)) ; $audits @@ -731,7 +731,7 @@ App::post('/v1/database/collections/:collectionId/documents') ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($document, Response::MODEL_DOCUMENT); + $response->dynamic($document, Response::MODEL_DOCUMENT); }); App::get('/v1/database/collections/:collectionId/documents') @@ -782,7 +782,7 @@ App::get('/v1/database/collections/:collectionId/documents') $documents = $dbForExternal->find($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes); - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'sum' => \count($documents), 'documents' => $documents, ]), Response::MODEL_DOCUMENT_LIST); @@ -819,7 +819,7 @@ App::get('/v1/database/collections/:collectionId/documents/:documentId') throw new Exception('No document found', 404); } - $response->dynamic2($document, Response::MODEL_DOCUMENT); + $response->dynamic($document, Response::MODEL_DOCUMENT); }); App::patch('/v1/database/collections/:collectionId/documents/:documentId') @@ -890,7 +890,7 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId') ->setParam('data', $document->getArrayCopy()) ; - $response->dynamic2($document, Response::MODEL_DOCUMENT); + $response->dynamic($document, Response::MODEL_DOCUMENT); }); App::delete('/v1/database/collections/:collectionId/documents/:documentId') @@ -931,7 +931,7 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId') $success = $dbForExternal->deleteDocument($collectionId, $documentId); $events - ->setParam('eventData', $response->output2($document, Response::MODEL_DOCUMENT)) + ->setParam('eventData', $response->output($document, Response::MODEL_DOCUMENT)) ; $audits diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index c0ea527765..8d2f28a87a 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -2,7 +2,7 @@ use Ahc\Jwt\JWT; use Appwrite\Auth\Auth; -use Appwrite\Database\Validator\UID; +use Utopia\Database\Validator\UID; use Utopia\Storage\Storage; use Utopia\Storage\Validator\File; use Utopia\Storage\Validator\FileExt; @@ -68,7 +68,7 @@ App::post('/v1/functions') ])); $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($function, Response::MODEL_FUNCTION); + $response->dynamic($function, Response::MODEL_FUNCTION); }); App::get('/v1/functions') @@ -94,7 +94,7 @@ App::get('/v1/functions') $queries = ($search) ? [new Query('name', Query::TYPE_SEARCH, [$search])] : []; - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'functions' => $dbForInternal->find('functions', $queries, $limit, $offset, ['_id'], [$orderType]), 'sum' => $dbForInternal->count('functions', $queries, APP_LIMIT_COUNT), ]), Response::MODEL_FUNCTION_LIST); @@ -124,7 +124,7 @@ App::get('/v1/functions/:functionId') throw new Exception('Function not found', 404); } - $response->dynamic2($function, Response::MODEL_FUNCTION); + $response->dynamic($function, Response::MODEL_FUNCTION); }); App::get('/v1/functions/:functionId/usage') @@ -142,7 +142,7 @@ App::get('/v1/functions/:functionId/usage') ->inject('register') ->action(function ($functionId, $range, $response, $project, $dbForInternal, $register) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Document $project */ + /** @var Utopia\Database\Document $project */ /** @var Utopia\Database\Database $dbForInternal */ /** @var Utopia\Registry\Registry $register */ @@ -272,7 +272,7 @@ App::put('/v1/functions/:functionId') ->action(function ($functionId, $name, $execute, $vars, $events, $schedule, $timeout, $response, $dbForInternal, $project) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForInternal */ - /** @var Appwrite\Database\Document $project */ + /** @var Utopia\Database\Document $project */ $function = $dbForInternal->getDocument('functions', $functionId); @@ -305,7 +305,7 @@ App::put('/v1/functions/:functionId') ]); // Async task rescheduale } - $response->dynamic2($function, Response::MODEL_FUNCTION); + $response->dynamic($function, Response::MODEL_FUNCTION); }); App::patch('/v1/functions/:functionId/tag') @@ -328,7 +328,7 @@ App::patch('/v1/functions/:functionId/tag') ->action(function ($functionId, $tag, $response, $dbForInternal, $project) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForInternal */ - /** @var Appwrite\Database\Document $project */ + /** @var Utopia\Database\Document $project */ $function = $dbForInternal->getDocument('functions', $functionId); $tag = $dbForInternal->getDocument('tags', $tag); @@ -360,7 +360,7 @@ App::patch('/v1/functions/:functionId/tag') ]); // Async task rescheduale } - $response->dynamic2($function, Response::MODEL_FUNCTION); + $response->dynamic($function, Response::MODEL_FUNCTION); }); App::delete('/v1/functions/:functionId') @@ -484,7 +484,7 @@ App::post('/v1/functions/:functionId/tags') ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($tag, Response::MODEL_TAG); + $response->dynamic($tag, Response::MODEL_TAG); }); App::get('/v1/functions/:functionId/tags') @@ -520,7 +520,7 @@ App::get('/v1/functions/:functionId/tags') $results = $dbForInternal->find('tags', $queries, $limit, $offset, ['_id'], [$orderType]); $sum = $dbForInternal->count('tags', $queries, APP_LIMIT_COUNT); - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'tags' => $results, 'sum' => $sum, ]), Response::MODEL_TAG_LIST); @@ -561,7 +561,7 @@ App::get('/v1/functions/:functionId/tags/:tagId') throw new Exception('Tag not found', 404); } - $response->dynamic2($tag, Response::MODEL_TAG); + $response->dynamic($tag, Response::MODEL_TAG); }); App::delete('/v1/functions/:functionId/tags/:tagId') @@ -645,9 +645,9 @@ App::post('/v1/functions/:functionId/executions') ->inject('user') ->action(function ($functionId, $data, /*$async,*/ $response, $project, $dbForInternal, $user) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Document $project */ + /** @var Utopia\Database\Document $project */ /** @var Utopia\Database\Database $dbForInternal */ - /** @var Appwrite\Database\Document $user */ + /** @var Utopia\Database\Document $user */ Authorization::disable(); @@ -699,7 +699,7 @@ App::post('/v1/functions/:functionId/executions') $sessions = $user->getAttribute('sessions', []); $current = new Document(); - foreach ($sessions as $session) { /** @var Appwrite\Database\Document $session */ + foreach ($sessions as $session) { /** @var Utopia\Database\Document $session */ if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too $current = $session; } @@ -726,7 +726,7 @@ App::post('/v1/functions/:functionId/executions') ]); $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($execution, Response::MODEL_EXECUTION); + $response->dynamic($execution, Response::MODEL_EXECUTION); }); App::get('/v1/functions/:functionId/executions') @@ -765,7 +765,7 @@ App::get('/v1/functions/:functionId/executions') new Query('functionId', Query::TYPE_EQUAL, [$function->getId()]), ], APP_LIMIT_COUNT); - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'executions' => $results, 'sum' => $sum, ]), Response::MODEL_EXECUTION_LIST); @@ -808,5 +808,5 @@ App::get('/v1/functions/:functionId/executions/:executionId') throw new Exception('Execution not found', 404); } - $response->dynamic2($execution, Response::MODEL_EXECUTION); + $response->dynamic($execution, Response::MODEL_EXECUTION); }); diff --git a/app/controllers/api/locale.php b/app/controllers/api/locale.php index 0494789180..a1f9c1035b 100644 --- a/app/controllers/api/locale.php +++ b/app/controllers/api/locale.php @@ -1,6 +1,6 @@ createNamespace($project->getId()); $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($project, Response::MODEL_PROJECT); + $response->dynamic($project, Response::MODEL_PROJECT); }); App::get('/v1/projects') @@ -162,7 +162,7 @@ App::get('/v1/projects') $results = $dbForConsole->find('projects', $queries, $limit, $offset, ['_id'], [$orderType]); $sum = $dbForConsole->count('projects', $queries, APP_LIMIT_COUNT); - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'projects' => $results, 'sum' => $sum, ]), Response::MODEL_PROJECT_LIST); @@ -191,7 +191,7 @@ App::get('/v1/projects/:projectId') throw new Exception('Project not found', 404); } - $response->dynamic2($project, Response::MODEL_PROJECT); + $response->dynamic($project, Response::MODEL_PROJECT); }); App::get('/v1/projects/:projectId/usage') @@ -440,7 +440,7 @@ App::patch('/v1/projects/:projectId') ->setAttribute('legalTaxId', $legalTaxId) ); - $response->dynamic2($project, Response::MODEL_PROJECT); + $response->dynamic($project, Response::MODEL_PROJECT); }); App::patch('/v1/projects/:projectId/oauth2') @@ -474,7 +474,7 @@ App::patch('/v1/projects/:projectId/oauth2') ->setAttribute('usersOauth2' . \ucfirst($provider) . 'Secret', $secret) ); - $response->dynamic2($project, Response::MODEL_PROJECT); + $response->dynamic($project, Response::MODEL_PROJECT); }); App::patch('/v1/projects/:projectId/auth/limit') @@ -505,7 +505,7 @@ App::patch('/v1/projects/:projectId/auth/limit') ->setAttribute('usersAuthLimit', $limit) ); - $response->dynamic2($project, Response::MODEL_PROJECT); + $response->dynamic($project, Response::MODEL_PROJECT); }); App::patch('/v1/projects/:projectId/auth/:method') @@ -540,7 +540,7 @@ App::patch('/v1/projects/:projectId/auth/:method') ->setAttribute($authKey, $status) ); - $response->dynamic2($project, Response::MODEL_PROJECT); + $response->dynamic($project, Response::MODEL_PROJECT); }); App::delete('/v1/projects/:projectId') @@ -638,7 +638,7 @@ App::post('/v1/projects/:projectId/webhooks') ); $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($webhook, Response::MODEL_WEBHOOK); + $response->dynamic($webhook, Response::MODEL_WEBHOOK); }); App::get('/v1/projects/:projectId/webhooks') @@ -666,7 +666,7 @@ App::get('/v1/projects/:projectId/webhooks') $webhooks = $project->getAttribute('webhooks', []); - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'webhooks' => $webhooks, 'sum' => count($webhooks), ]), Response::MODEL_WEBHOOK_LIST); @@ -702,7 +702,7 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId') throw new Exception('Webhook not found', 404); } - $response->dynamic2($webhook, Response::MODEL_WEBHOOK); + $response->dynamic($webhook, Response::MODEL_WEBHOOK); }); App::put('/v1/projects/:projectId/webhooks/:webhookId') @@ -754,7 +754,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId') $dbForConsole->updateDocument('projects', $project->getId(), $project); - $response->dynamic2($webhook, Response::MODEL_WEBHOOK); + $response->dynamic($webhook, Response::MODEL_WEBHOOK); }); App::delete('/v1/projects/:projectId/webhooks/:webhookId') @@ -828,7 +828,7 @@ App::post('/v1/projects/:projectId/keys') ); $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($key, Response::MODEL_KEY); + $response->dynamic($key, Response::MODEL_KEY); }); App::get('/v1/projects/:projectId/keys') @@ -856,7 +856,7 @@ App::get('/v1/projects/:projectId/keys') $keys = $project->getAttribute('keys', []); - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'keys' => $keys, 'sum' => count($keys), ]), Response::MODEL_KEY_LIST); @@ -889,7 +889,7 @@ App::get('/v1/projects/:projectId/keys/:keyId') throw new Exception('Key not found', 404); } - $response->dynamic2($key, Response::MODEL_KEY); + $response->dynamic($key, Response::MODEL_KEY); }); App::put('/v1/projects/:projectId/keys/:keyId') @@ -931,7 +931,7 @@ App::put('/v1/projects/:projectId/keys/:keyId') $dbForConsole->updateDocument('projects', $project->getId(), $project); - $response->dynamic2($key, Response::MODEL_KEY); + $response->dynamic($key, Response::MODEL_KEY); }); App::delete('/v1/projects/:projectId/keys/:keyId') @@ -1032,7 +1032,7 @@ App::post('/v1/projects/:projectId/tasks') } $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($task, Response::MODEL_TASK); + $response->dynamic($task, Response::MODEL_TASK); }); App::get('/v1/projects/:projectId/tasks') @@ -1060,7 +1060,7 @@ App::get('/v1/projects/:projectId/tasks') $tasks = $project->getAttribute('tasks', []); - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'tasks' => $tasks, 'sum' => count($tasks), ]), Response::MODEL_TASK_LIST); @@ -1097,7 +1097,7 @@ App::get('/v1/projects/:projectId/tasks/:taskId') throw new Exception('Task not found', 404); } - $response->dynamic2($task, Response::MODEL_TASK); + $response->dynamic($task, Response::MODEL_TASK); }); App::put('/v1/projects/:projectId/tasks/:taskId') @@ -1163,7 +1163,7 @@ App::put('/v1/projects/:projectId/tasks/:taskId') ResqueScheduler::enqueueAt($next, 'v1-tasks', 'TasksV1', $task->getArrayCopy()); } - $response->dynamic2($task, Response::MODEL_TASK); + $response->dynamic($task, Response::MODEL_TASK); }); App::delete('/v1/projects/:projectId/tasks/:taskId') @@ -1244,7 +1244,7 @@ App::post('/v1/projects/:projectId/platforms') ); $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($platform, Response::MODEL_PLATFORM); + $response->dynamic($platform, Response::MODEL_PLATFORM); }); App::get('/v1/projects/:projectId/platforms') @@ -1272,7 +1272,7 @@ App::get('/v1/projects/:projectId/platforms') $platforms = $project->getAttribute('platforms', []); - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'platforms' => $platforms, 'sum' => count($platforms), ]), Response::MODEL_PLATFORM_LIST); @@ -1308,7 +1308,7 @@ App::get('/v1/projects/:projectId/platforms/:platformId') throw new Exception('Platform not found', 404); } - $response->dynamic2($platform, Response::MODEL_PLATFORM); + $response->dynamic($platform, Response::MODEL_PLATFORM); }); App::put('/v1/projects/:projectId/platforms/:platformId') @@ -1363,7 +1363,7 @@ App::put('/v1/projects/:projectId/platforms/:platformId') $dbForConsole->updateDocument('projects', $project->getId(), $project); - $response->dynamic2($platform, Response::MODEL_PLATFORM); + $response->dynamic($platform, Response::MODEL_PLATFORM); }); App::delete('/v1/projects/:projectId/platforms/:platformId') @@ -1453,7 +1453,7 @@ App::post('/v1/projects/:projectId/domains') ); $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($domain, Response::MODEL_DOMAIN); + $response->dynamic($domain, Response::MODEL_DOMAIN); }); App::get('/v1/projects/:projectId/domains') @@ -1481,7 +1481,7 @@ App::get('/v1/projects/:projectId/domains') $domains = $project->getAttribute('domains', []); - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'domains' => $domains, 'sum' => count($domains), ]), Response::MODEL_DOMAIN_LIST); @@ -1517,7 +1517,7 @@ App::get('/v1/projects/:projectId/domains/:domainId') throw new Exception('Domain not found', 404); } - $response->dynamic2($domain, Response::MODEL_DOMAIN); + $response->dynamic($domain, Response::MODEL_DOMAIN); }); App::patch('/v1/projects/:projectId/domains/:domainId/verification') @@ -1557,7 +1557,7 @@ App::patch('/v1/projects/:projectId/domains/:domainId/verification') } if ($domain->getAttribute('verification') === true) { - return $response->dynamic2($domain, Response::MODEL_DOMAIN); + return $response->dynamic($domain, Response::MODEL_DOMAIN); } $validator = new CNAME($target->get()); // Verify Domain with DNS records @@ -1578,7 +1578,7 @@ App::patch('/v1/projects/:projectId/domains/:domainId/verification') 'domain' => $domain->getAttribute('domain'), ]); - $response->dynamic2($domain, Response::MODEL_DOMAIN); + $response->dynamic($domain, Response::MODEL_DOMAIN); }); App::delete('/v1/projects/:projectId/domains/:domainId') diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index b006ed38cd..552b0a9df1 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -11,7 +11,7 @@ use Utopia\Cache\Cache; use Utopia\Cache\Adapter\Filesystem; use Appwrite\ClamAV\Network; use Utopia\Database\Document; -use Appwrite\Database\Validator\UID; +use Utopia\Database\Validator\UID; use Utopia\Storage\Storage; use Utopia\Storage\Validator\File; use Utopia\Storage\Validator\FileSize; @@ -50,7 +50,7 @@ App::post('/v1/storage/files') /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForInternal */ - /** @var Appwrite\Database\Document $user */ + /** @var Utopia\Database\Document $user */ /** @var Appwrite\Event\Event $audits */ /** @var Appwrite\Event\Event $usage */ @@ -150,7 +150,7 @@ App::post('/v1/storage/files') ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($file, Response::MODEL_FILE); + $response->dynamic($file, Response::MODEL_FILE); ; }); @@ -177,7 +177,7 @@ App::get('/v1/storage/files') $queries = ($search) ? [new Query('name', Query::TYPE_SEARCH, $search)] : []; - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'files' => $dbForInternal->find('files', $queries, $limit, $offset, ['_id'], [$orderType]), 'sum' => $dbForInternal->count('files', $queries, APP_LIMIT_COUNT), ]), Response::MODEL_FILE_LIST); @@ -207,7 +207,7 @@ App::get('/v1/storage/files/:fileId') throw new Exception('File not found', 404); } - $response->dynamic2($file, Response::MODEL_FILE); + $response->dynamic($file, Response::MODEL_FILE); }); App::get('/v1/storage/files/:fileId/preview') @@ -530,7 +530,7 @@ App::put('/v1/storage/files/:fileId') ->setParam('resource', 'storage/files/'.$file->getId()) ; - $response->dynamic2($file, Response::MODEL_FILE); + $response->dynamic($file, Response::MODEL_FILE); }); App::delete('/v1/storage/files/:fileId') @@ -581,7 +581,7 @@ App::delete('/v1/storage/files/:fileId') ; $events - ->setParam('eventData', $response->output2($file, Response::MODEL_FILE)) + ->setParam('eventData', $response->output($file, Response::MODEL_FILE)) ; $response->noContent(); diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 5ad8ecd6c3..4e82260343 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1,7 +1,6 @@ desc('Create Team') @@ -80,7 +80,7 @@ App::post('/v1/teams') } $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($team, Response::MODEL_TEAM); + $response->dynamic($team, Response::MODEL_TEAM); }); App::get('/v1/teams') @@ -109,7 +109,7 @@ App::get('/v1/teams') $results = $dbForInternal->find('teams', $queries, $limit, $offset, ['_id'], [$orderType]); $sum = $dbForInternal->count('teams', $queries, APP_LIMIT_COUNT); - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'teams' => $results, 'sum' => $sum, ]), Response::MODEL_TEAM_LIST); @@ -139,7 +139,7 @@ App::get('/v1/teams/:teamId') throw new Exception('Team not found', 404); } - $response->dynamic2($team, Response::MODEL_TEAM); + $response->dynamic($team, Response::MODEL_TEAM); }); App::put('/v1/teams/:teamId') @@ -170,7 +170,7 @@ App::put('/v1/teams/:teamId') $team = $dbForInternal->updateDocument('teams', $team->getId(), $team->setAttribute('name', $name)); - $response->dynamic2($team, Response::MODEL_TEAM); + $response->dynamic($team, Response::MODEL_TEAM); }); App::delete('/v1/teams/:teamId') @@ -222,7 +222,7 @@ App::delete('/v1/teams/:teamId') ; $events - ->setParam('eventData', $response->output2($team, Response::MODEL_TEAM)) + ->setParam('eventData', $response->output($team, Response::MODEL_TEAM)) ; $response->noContent(); @@ -256,8 +256,8 @@ App::post('/v1/teams/:teamId/memberships') ->inject('mails') ->action(function ($teamId, $email, $name, $roles, $url, $response, $project, $user, $dbForInternal, $locale, $audits, $mails) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Document $project */ - /** @var Appwrite\Database\Document $user */ + /** @var Utopia\Database\Document $project */ + /** @var Utopia\Database\Document $user */ /** @var Utopia\Database\Database $dbForInternal */ /** @var Appwrite\Event\Event $audits */ /** @var Appwrite\Event\Event $mails */ @@ -409,7 +409,7 @@ App::post('/v1/teams/:teamId/memberships') ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($membership + $response->dynamic($membership ->setAttribute('email', $email) ->setAttribute('name', $name) , Response::MODEL_MEMBERSHIP); @@ -457,7 +457,7 @@ App::get('/v1/teams/:teamId/memberships') $users[] = new Document(\array_merge($temp, $membership->getArrayCopy())); } - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'memberships' => $users, 'sum' => $sum, ]), Response::MODEL_MEMBERSHIP_LIST); @@ -486,7 +486,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') ->action(function ($teamId, $membershipId, $roles, $request, $response, $user, $dbForInternal, $audits) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Document $user */ + /** @var Utopia\Database\Document $user */ /** @var Utopia\Database\Database $dbForInternal */ /** @var Appwrite\Event\Event $audits */ @@ -525,7 +525,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') ->setParam('resource', 'teams/'.$teamId) ; - $response->dynamic2($membership, Response::MODEL_MEMBERSHIP); + $response->dynamic($membership, Response::MODEL_MEMBERSHIP); }); App::patch('/v1/teams/:teamId/memberships/:membershipId/status') @@ -553,7 +553,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->action(function ($teamId, $membershipId, $userId, $secret, $request, $response, $user, $dbForInternal, $geodb, $audits) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Document $user */ + /** @var Utopia\Database\Document $user */ /** @var Utopia\Database\Database $dbForInternal */ /** @var MaxMind\Db\Reader $geodb */ /** @var Appwrite\Event\Event $audits */ @@ -661,7 +661,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ; - $response->dynamic2($membership + $response->dynamic($membership ->setAttribute('email', $user->getAttribute('email')) ->setAttribute('name', $user->getAttribute('name')) , Response::MODEL_MEMBERSHIP); @@ -744,7 +744,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') ; $events - ->setParam('eventData', $response->output2($membership, Response::MODEL_MEMBERSHIP)) + ->setParam('eventData', $response->output($membership, Response::MODEL_MEMBERSHIP)) ; $response->noContent(); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 8a1be03744..2abbc5b50a 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -65,7 +65,7 @@ App::post('/v1/users') } $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_USER); }); App::get('/v1/users') @@ -92,7 +92,7 @@ App::get('/v1/users') $results = $dbForInternal->find('users', [], $limit, $offset, ['_id'], [$orderType]); $sum = $dbForInternal->count('users', [], APP_LIMIT_COUNT); - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'users' => $results, 'sum' => $sum, ]), Response::MODEL_USER_LIST); @@ -122,7 +122,7 @@ App::get('/v1/users/:userId') throw new Exception('User not found', 404); } - $response->dynamic2($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_USER); }); App::get('/v1/users/:userId/prefs') @@ -151,7 +151,7 @@ App::get('/v1/users/:userId/prefs') $prefs = $user->getAttribute('prefs', new \stdClass()); - $response->dynamic2(new Document($prefs), Response::MODEL_PREFERENCES); + $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); App::get('/v1/users/:userId/sessions') @@ -194,7 +194,7 @@ App::get('/v1/users/:userId/sessions') $sessions[$key] = $session; } - $response->dynamic2(new Document([ + $response->dynamic(new Document([ 'sessions' => $sessions, 'sum' => count($sessions), ]), Response::MODEL_SESSION_LIST); @@ -218,7 +218,7 @@ App::get('/v1/users/:userId/logs') ->inject('geodb') ->action(function ($userId, $response, $dbForInternal, $locale, $geodb) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Document $project */ + /** @var Utopia\Database\Document $project */ /** @var Utopia\Database\Database $dbForInternal */ /** @var Utopia\Locale\Locale $locale */ /** @var MaxMind\Db\Reader $geodb */ @@ -305,7 +305,7 @@ App::get('/v1/users/:userId/logs') } } - $response->dynamic2(new Document(['logs' => $output]), Response::MODEL_LOG_LIST); + $response->dynamic(new Document(['logs' => $output]), Response::MODEL_LOG_LIST); }); App::patch('/v1/users/:userId/status') @@ -336,7 +336,7 @@ App::patch('/v1/users/:userId/status') $user = $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('status', (bool) $status)); - $response->dynamic2($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_USER); }); App::patch('/v1/users/:userId/verification') @@ -367,7 +367,7 @@ App::patch('/v1/users/:userId/verification') $user = $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification)); - $response->dynamic2($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_USER); }); App::patch('/v1/users/:userId/prefs') @@ -398,7 +398,7 @@ App::patch('/v1/users/:userId/prefs') $user = $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs)); - $response->dynamic2(new Document($prefs), Response::MODEL_PREFERENCES); + $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); App::delete('/v1/users/:userId/sessions/:sessionId') @@ -440,7 +440,7 @@ App::delete('/v1/users/:userId/sessions/:sessionId') $user->setAttribute('sessions', $sessions); $events - ->setParam('eventData', $response->output2($user, Response::MODEL_USER)) + ->setParam('eventData', $response->output($user, Response::MODEL_USER)) ; $dbForInternal->updateDocument('users', $user->getId(), $user); @@ -486,7 +486,7 @@ App::delete('/v1/users/:userId/sessions') $dbForInternal->updateDocument('users', $user->getId(), $user->getAttribute('sessions', [])); $events - ->setParam('eventData', $response->output2($user, Response::MODEL_USER)) + ->setParam('eventData', $response->output($user, Response::MODEL_USER)) ; $response->noContent(); @@ -535,7 +535,7 @@ App::delete('/v1/users/:userId') ; $events - ->setParam('eventData', $response->output2($user, Response::MODEL_USER)) + ->setParam('eventData', $response->output($user, Response::MODEL_USER)) ; // TODO : Response filter implementation diff --git a/app/controllers/general.php b/app/controllers/general.php index d8f24019f8..0df0d1f06a 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -10,7 +10,6 @@ use Utopia\Exception; use Utopia\Config\Config; use Utopia\Domains\Domain; use Appwrite\Auth\Auth; -use Appwrite\Database\Validator\Authorization; use Appwrite\Network\Validator\Origin; use Appwrite\Utopia\Response\Filters\V06; use Appwrite\Utopia\Response\Filters\V07; @@ -238,26 +237,21 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons $role = Auth::USER_ROLE_APP; $scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', [])); - Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys. Authorization2::setDefaultStatus(false); // Cancel security segmentation for API keys. } } if ($user->getId()) { - Authorization::setRole('user:'.$user->getId()); Authorization2::setRole('user:'.$user->getId()); } - Authorization::setRole('role:'.$role); Authorization2::setRole('role:'.$role); \array_map(function ($node) { if (isset($node['teamId']) && isset($node['roles'])) { - Authorization::setRole('team:'.$node['teamId']); Authorization2::setRole('team:'.$node['teamId']); foreach ($node['roles'] as $nodeRole) { // Set all team roles - Authorization::setRole('team:'.$node['teamId'].'/'.$nodeRole); Authorization2::setRole('team:'.$node['teamId'].'/'.$nodeRole); } } @@ -305,7 +299,7 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\View $layout */ - /** @var Appwrite\Database\Document $project */ + /** @var Utopia\Database\Document $project */ if ($error instanceof PDOException) { throw $error; @@ -393,7 +387,7 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project) { $response->html($layout->render()); } - $response->dynamic2(new Document($output), + $response->dynamic(new Document($output), $utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR); }, ['error', 'utopia', 'request', 'response', 'layout', 'project']); diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 17bade5793..9d4479c01f 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -2,7 +2,7 @@ global $utopia, $request, $response; -use Appwrite\Database\Document; +use Utopia\Database\Document; use Appwrite\Network\Validator\Host; use Appwrite\Utopia\Response; use Utopia\App; diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 24fed96611..836ef8f714 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -1,7 +1,7 @@ on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo }); try { - Authorization::cleanRoles(); - Authorization::setRole('role:all'); - Authorization2::cleanRoles(); Authorization2::setRole('role:all'); diff --git a/src/Appwrite/Auth/Auth.php b/src/Appwrite/Auth/Auth.php index 3ae28997b1..56f708493b 100644 --- a/src/Appwrite/Auth/Auth.php +++ b/src/Appwrite/Auth/Auth.php @@ -2,7 +2,7 @@ namespace Appwrite\Auth; -use Appwrite\Database\Document; +use Utopia\Database\Document; class Auth { diff --git a/src/Appwrite/Specification/Format/OpenAPI3.php b/src/Appwrite/Specification/Format/OpenAPI3.php index b14134cdf2..02b3c315d0 100644 --- a/src/Appwrite/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/Specification/Format/OpenAPI3.php @@ -238,7 +238,7 @@ class OpenAPI3 extends Format $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = false; break; - case 'Appwrite\Database\Validator\UID': + case 'Utopia\Database\Validator\UID': $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = '['.\strtoupper(Template::fromCamelCaseToSnake($node['name'])).']'; break; diff --git a/src/Appwrite/Specification/Format/Swagger2.php b/src/Appwrite/Specification/Format/Swagger2.php index 9809220f87..d8a84e3ccf 100644 --- a/src/Appwrite/Specification/Format/Swagger2.php +++ b/src/Appwrite/Specification/Format/Swagger2.php @@ -234,7 +234,7 @@ class Swagger2 extends Format $node['type'] = $validator->getType(); $node['x-example'] = false; break; - case 'Appwrite\Database\Validator\UID': + case 'Utopia\Database\Validator\UID': $node['type'] = $validator->getType(); $node['x-example'] = '['.\strtoupper(Template::fromCamelCaseToSnake($node['name'])).']'; break; diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index bbae1c3e90..981d18f4b2 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -5,7 +5,7 @@ namespace Appwrite\Utopia; use Exception; use Utopia\Swoole\Response as SwooleResponse; use Swoole\Http\Response as SwooleHTTPResponse; -use Appwrite\Database\Document; +use Utopia\Database\Document; use Appwrite\Utopia\Response\Filter; use Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response\Model\None; @@ -45,7 +45,6 @@ use Appwrite\Utopia\Response\Model\Webhook; use Appwrite\Utopia\Response\Model\Preferences; use Appwrite\Utopia\Response\Model\Mock; // Keep last use stdClass; -use Utopia\Database\Document as DatabaseDocument; /** * @method Response public function setStatusCode(int $code = 200) @@ -286,27 +285,6 @@ class Response extends SwooleResponse $this->json(!empty($output) ? $output : new stdClass()); } - /** - * Validate response objects and outputs - * the response according to given format type - * - * @param DatabaseDocument $document - * @param string $model - * - * return void - */ - public function dynamic2(DatabaseDocument $document, string $model): void - { - $output = $this->output2($document, $model); - - // If filter is set, parse the output - if(self::isFilter()){ - $output = self::getFilter()->parse($output, $model); - } - - $this->json(!empty($output) ? $output : new stdClass()); - } - /** * Generate valid response object from document data * @@ -359,58 +337,6 @@ class Response extends SwooleResponse return $this->payload; } - /** - * Generate valid response object from document data - * - * @param DatabaseDocument $document - * @param string $model - * - * return array - */ - public function output2(DatabaseDocument $document, string $model): array - { - $data = $document; - $model = $this->getModel($model); - $output = []; - - if ($model->isAny()) { - $this->payload = $document->getArrayCopy(); - return $this->payload; - } - - foreach ($model->getRules() as $key => $rule) { - if (!$document->isSet($key)) { - if (!is_null($rule['default'])) { - $document->setAttribute($key, $rule['default']); - } else { - throw new Exception('Model '.$model->getName().' is missing response key: '.$key); - } - } - - if ($rule['array']) { - if (!is_array($data[$key])) { - throw new Exception($key.' must be an array of type '.$rule['type']); - } - - foreach ($data[$key] as &$item) { - if ($item instanceof Document) { - if (!array_key_exists($rule['type'], $this->models)) { - throw new Exception('Missing model for rule: '. $rule['type']); - } - - $item = $this->output($item, $rule['type']); - } - } - } - - $output[$key] = $data[$key]; - } - - $this->payload = $output; - - return $this->payload; - } - /** * YAML * From f7c415cd2c83add53efb2b321252af0fa7190d4d Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 25 Jul 2021 17:51:04 +0300 Subject: [PATCH 21/27] Removed Authorization2 references --- app/controllers/general.php | 19 +++++++++---------- app/http.php | 6 +++--- app/init.php | 7 +------ 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 0df0d1f06a..46bd81689d 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -15,10 +15,9 @@ use Appwrite\Utopia\Response\Filters\V06; use Appwrite\Utopia\Response\Filters\V07; use Appwrite\Utopia\Response\Filters\V08; use Utopia\CLI\Console; -use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization as Authorization2; +use Utopia\Database\Validator\Authorization; Config::setParam('domainVerification', false); Config::setParam('cookieDomain', 'localhost'); @@ -43,7 +42,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons $domains[$domain->get()] = false; Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.'); } else { - Authorization2::disable(); + Authorization::disable(); $certificate = $dbForConsole->findFirst('certificates', [ new Query('domain', QUERY::TYPE_EQUAL, [$domain->get()]) @@ -54,7 +53,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons 'domain' => $domain->get(), ]); $certificate = $dbForConsole->createDocument('certificates', $certificate); - Authorization2::enable(); + Authorization::enable(); Console::info('Issuing a TLS certificate for the master domain (' . $domain->get() . ') in a few seconds...'); @@ -65,7 +64,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons 'validateCNAME' => false, ]); } else { - Authorization2::enable(); // ensure authorization is reenabled + Authorization::enable(); // ensure authorization is reenabled } $domains[$domain->get()] = true; } @@ -237,22 +236,22 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons $role = Auth::USER_ROLE_APP; $scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', [])); - Authorization2::setDefaultStatus(false); // Cancel security segmentation for API keys. + Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys. } } if ($user->getId()) { - Authorization2::setRole('user:'.$user->getId()); + Authorization::setRole('user:'.$user->getId()); } - Authorization2::setRole('role:'.$role); + Authorization::setRole('role:'.$role); \array_map(function ($node) { if (isset($node['teamId']) && isset($node['roles'])) { - Authorization2::setRole('team:'.$node['teamId']); + Authorization::setRole('team:'.$node['teamId']); foreach ($node['roles'] as $nodeRole) { // Set all team roles - Authorization2::setRole('team:'.$node['teamId'].'/'.$nodeRole); + Authorization::setRole('team:'.$node['teamId'].'/'.$nodeRole); } } }, $user->getAttribute('memberships', [])); diff --git a/app/http.php b/app/http.php index a47a882206..0585f9a24c 100644 --- a/app/http.php +++ b/app/http.php @@ -10,7 +10,7 @@ use Swoole\Http\Response as SwooleResponse; use Utopia\App; use Utopia\CLI\Console; use Utopia\Config\Config; -use Utopia\Database\Validator\Authorization as Authorization2; +use Utopia\Database\Validator\Authorization; use Utopia\Audit\Audit; use Utopia\Abuse\Adapters\TimeLimit; use Utopia\Database\Document; @@ -166,8 +166,8 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo }); try { - Authorization2::cleanRoles(); - Authorization2::setRole('role:all'); + Authorization::cleanRoles(); + Authorization::setRole('role:all'); $app->run($request, $response); } catch (\Throwable $th) { diff --git a/app/init.php b/app/init.php index a42513259e..af409497aa 100644 --- a/app/init.php +++ b/app/init.php @@ -24,7 +24,6 @@ use Appwrite\Database\Database; use Appwrite\Database\Adapter\MySQL as MySQLAdapter; use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Database\Document; -use Appwrite\Database\Validator\Authorization; use Appwrite\Event\Event; use Appwrite\OpenSSL\OpenSSL; use Utopia\App; @@ -39,7 +38,7 @@ use Utopia\Cache\Cache; use Utopia\Database\Adapter\MariaDB; use Utopia\Database\Document as Document2; use Utopia\Database\Database as Database2; -use Utopia\Database\Validator\Authorization as Authorization2; +use Utopia\Database\Validator\Authorization; use Swoole\Database\PDOConfig; use Swoole\Database\PDOPool; use Swoole\Database\RedisConfig; @@ -439,7 +438,6 @@ App::setResource('user', function($mode, $project, $console, $request, $response /** @var string $mode */ Authorization::setDefaultStatus(true); - Authorization2::setDefaultStatus(true); Auth::setCookieName('a_session_'.$project->getId()); @@ -484,7 +482,6 @@ App::setResource('user', function($mode, $project, $console, $request, $response if (APP_MODE_ADMIN === $mode) { if ($user->find('teamId', $project->getAttribute('teamId'), 'memberships')) { Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users. - Authorization2::setDefaultStatus(false); // Cancel security segmentation for admin users. } else { $user = new Document2(['$id' => '', '$collection' => 'users']); } @@ -529,12 +526,10 @@ App::setResource('project', function($dbForConsole, $request, $console) { } Authorization::disable(); - Authorization2::disable(); $project = $dbForConsole->getDocument('projects', $projectId); Authorization::reset(); - Authorization2::reset(); return $project; }, ['dbForConsole', 'request', 'console']); From 03288ffbfac4a09d8365af724955002049bde46d Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Sun, 25 Jul 2021 18:20:31 -0400 Subject: [PATCH 22/27] Refactor database methods to parent worker script --- app/workers.php | 104 ++++++++++++++++++++++++++++++++++++++- app/workers/audits.php | 8 +-- app/workers/database.php | 61 ++--------------------- app/workers/deletes.php | 87 +++++--------------------------- app/workers/tasks.php | 9 +--- 5 files changed, 120 insertions(+), 149 deletions(-) diff --git a/app/workers.php b/app/workers.php index ff5ba1dbd3..b33ec9db61 100644 --- a/app/workers.php +++ b/app/workers.php @@ -1,7 +1,13 @@ set('db', function () { return $pdo; }); + $register->set('cache', function () { // Register cache connection $redis = new Redis(); $redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); return $redis; -}); \ No newline at end of file +}); + +/** + * Get internal project database + * @param string $projectId + * @return Database + */ +function getInternalDB(string $projectId): Database +{ + global $register; + + $attempts = 0; + $max = 10; + $sleep = 2; + + do { + try { + $attempts++; + $cache = new Cache(new RedisCache($register->get('cache'))); + $dbForInternal = new Database(new MariaDB($register->get('db')), $cache); + $dbForInternal->setNamespace("project_{$projectId}_internal"); // Main DB + break; // leave loop if successful + } catch(\Exception $e) { + Console::warning("Database not ready. Retrying connection ({$attempts})..."); + if ($attempts >= $max) { + throw new \Exception('Failed to connect to database: '. $e->getMessage()); + } + sleep($sleep); + } + } while ($attempts < $max); + + return $dbForInternal; +} + +/** + * Get external project database + * @param string $projectId + * @return Database + */ +function getExternalDB(string $projectId): Database +{ + global $register; + + $attempts = 0; + $max = 10; + $sleep = 2; + + do { + try { + $attempts++; + $cache = new Cache(new RedisCache($register->get('cache'))); + $dbForExternal = new Database(new MariaDB($register->get('db')), $cache); + $dbForExternal->setNamespace("project_{$projectId}_external"); // Main DB + break; // leave loop if successful + } catch(\Exception $e) { + Console::warning("Database not ready. Retrying connection ({$attempts})..."); + if ($attempts >= $max) { + throw new \Exception('Failed to connect to database: '. $e->getMessage()); + } + sleep($sleep); + } + } while ($attempts < $max); + + return $dbForExternal; +} + +/** + * Get console database + * @return Database + */ +function getConsoleDB(): Database +{ + global $register; + + $attempts = 0; + $max = 5; + $sleep = 5; + + do { + try { + $attempts++; + $cache = new Cache(new RedisCache($register->get('cache'))); + $dbForConsole = new Database(new MariaDB($register->get('db')), $cache); + $dbForConsole->setNamespace('project_console_internal'); // Main DB + break; // leave loop if successful + } catch(\Exception $e) { + Console::warning("Database not ready. Retrying connection ({$attempts})..."); + if ($attempts >= $max) { + throw new \Exception('Failed to connect to database: '. $e->getMessage()); + } + sleep($sleep); + } + } while ($attempts < $max); + + return $dbForConsole; +} diff --git a/app/workers/audits.php b/app/workers/audits.php index 495c93bc48..2b603092fe 100644 --- a/app/workers/audits.php +++ b/app/workers/audits.php @@ -23,8 +23,6 @@ class AuditsV1 extends Worker public function run(): void { - global $register; - $projectId = $this->args['projectId']; $userId = $this->args['userId']; $event = $this->args['event']; @@ -32,12 +30,8 @@ class AuditsV1 extends Worker $userAgent = $this->args['userAgent']; $ip = $this->args['ip']; $data = $this->args['data']; - $db = $register->get('db', true); - $cache = new Cache(new Redis($register->get('cache'))); - $dbForInternal = new Database(new MariaDB($db), $cache); - $dbForInternal->setNamespace('project_'.$projectId.'_internal'); - + $dbForInternal = getInternalDB($projectId); $audit = new Audit($dbForInternal); $audit->log($userId, $event, $resource, $userAgent, $ip, '', $data); diff --git a/app/workers/database.php b/app/workers/database.php index b024871045..bdf33a4b37 100644 --- a/app/workers/database.php +++ b/app/workers/database.php @@ -1,12 +1,8 @@ getExternalDB($projectId); + $dbForExternal = getExternalDB($projectId); $collectionId = $attribute->getCollection(); $id = $attribute->getAttribute('$id', ''); @@ -89,7 +85,7 @@ class DatabaseV1 extends Worker */ protected function deleteAttribute($attribute, $projectId): void { - $dbForExternal = $this->getExternalDB($projectId); + $dbForExternal = getExternalDB($projectId); $collectionId = $attribute->getCollection(); $id = $attribute->getAttribute('$id'); @@ -103,7 +99,7 @@ class DatabaseV1 extends Worker */ protected function createIndex($index, $projectId): void { - $dbForExternal = $this->getExternalDB($projectId); + $dbForExternal = getExternalDB($projectId); $collectionId = $index->getCollection(); $id = $index->getAttribute('$id', ''); @@ -124,60 +120,11 @@ class DatabaseV1 extends Worker */ protected function deleteIndex($index, $projectId): void { - $dbForExternal = $this->getExternalDB($projectId); + $dbForExternal = getExternalDB($projectId); $collectionId = $index->getCollection(); $id = $index->getAttribute('$id'); $success = $dbForExternal->deleteIndex($collectionId, $id); } - - /** - * @param string $projectId - * - * @return Database - */ - protected function getInternalDB($projectId): Database - { - global $register; - - $dbForInternal = null; - - go(function() use ($register, $projectId, &$dbForInternal) { - $db = $register->get('dbPool')->get(); - $redis = $register->get('redisPool')->get(); - - $cache = new Cache(new RedisCache($redis)); - $dbForInternal = new Database(new MariaDB($db), $cache); - $dbForInternal->setNamespace('project_'.$projectId.'_internal'); // Main DB - - }); - - return $dbForInternal; - } - - /** - * @param string $projectId - * - * @return Database - */ - protected function getExternalDB($projectId): Database - { - global $register; - - /** @var Database $dbForExternal */ - $dbForExternal = null; - - go(function() use ($register, $projectId, &$dbForExternal) { - $db = $register->get('dbPool')->get(); - $redis = $register->get('redisPool')->get(); - - $cache = new Cache(new RedisCache($redis)); - $dbForExternal = new Database(new MariaDB($db), $cache); - $dbForExternal->setNamespace('project_'.$projectId.'_external'); // Main DB - - }); - - return $dbForExternal; - } } diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 9c69b57927..8ecce1d483 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -3,17 +3,13 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; -use Utopia\Cache\Adapter\Redis as RedisCache; use Utopia\Database\Validator\Authorization; use Appwrite\Resque\Worker; use Utopia\Storage\Device\Local; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit; use Utopia\CLI\Console; -use Utopia\Config\Config; use Utopia\Audit\Audit; -use Utopia\Cache\Cache; -use Utopia\Database\Adapter\MariaDB; require_once __DIR__.'/../workers.php'; @@ -97,7 +93,7 @@ class DeletesV1 extends Worker // Delete Memberships $this->deleteByGroup('memberships', [ new Query('teamId', Query::TYPE_EQUAL, [$teamId]) - ], $this->getInternalDB($projectId)); + ], getInternalDB($projectId)); } /** @@ -107,8 +103,8 @@ class DeletesV1 extends Worker { $projectId = $document->getId(); // Delete all DBs - $this->getExternalDB($projectId)->delete(); - $this->getInternalDB($projectId)->delete(); + getExternalDB($projectId)->delete(); + getInternalDB($projectId)->delete(); // Delete all storage directories $uploads = new Local(APP_STORAGE_UPLOADS.'/app-'.$document->getId()); @@ -130,13 +126,13 @@ class DeletesV1 extends Worker // Delete Memberships and decrement team membership counts $this->deleteByGroup('memberships', [ new Query('userId', Query::TYPE_EQUAL, [$userId]) - ], $this->getInternalDB($projectId), function(Document $document) use ($projectId, $userId) { + ], getInternalDB($projectId), function(Document $document) use ($projectId, $userId) { if ($document->getAttribute('confirm')) { // Count only confirmed members $teamId = $document->getAttribute('teamId'); - $team = $this->getInternalDB($projectId)->getDocument('teams', $teamId); + $team = getInternalDB($projectId)->getDocument('teams', $teamId); if(!$team->isEmpty()) { - $team = $this->getInternalDB($projectId)->updateDocument('teams', $teamId, new Document(\array_merge($team->getArrayCopy(), [ + $team = getInternalDB($projectId)->updateDocument('teams', $teamId, new Document(\array_merge($team->getArrayCopy(), [ 'sum' => \max($team->getAttribute('sum', 0) - 1, 0), // Ensure that sum >= 0 ]))); } @@ -150,7 +146,7 @@ class DeletesV1 extends Worker protected function deleteExecutionLogs($timestamp) { $this->deleteForProjectIds(function($projectId) use ($timestamp) { - if (!($dbForInternal = $this->getInternalDB($projectId))) { + if (!($dbForInternal = getInternalDB($projectId))) { throw new Exception('Failed to get projectDB for project '.$projectId); } @@ -171,7 +167,7 @@ class DeletesV1 extends Worker } $this->deleteForProjectIds(function($projectId) use ($timestamp){ - $timeLimit = new TimeLimit("", 0, 1, $this->getInternalDB($projectId)); + $timeLimit = new TimeLimit("", 0, 1, getInternalDB($projectId)); $abuse = new Abuse($timeLimit); $status = $abuse->cleanup($timestamp); @@ -190,7 +186,7 @@ class DeletesV1 extends Worker throw new Exception('Failed to delete audit logs. No timestamp provided'); } $this->deleteForProjectIds(function($projectId) use ($timestamp){ - $audit = new Audit($this->getInternalDB($projectId)); + $audit = new Audit(getInternalDB($projectId)); $status = $audit->cleanup($timestamp); if (!$status) { throw new Exception('Failed to delete Audit logs for project'.$projectId); @@ -204,7 +200,7 @@ class DeletesV1 extends Worker */ protected function deleteFunction(Document $document, $projectId) { - $dbForInternal = $this->getInternalDB($projectId); + $dbForInternal = getInternalDB($projectId); $device = new Local(APP_STORAGE_FUNCTIONS.'/app-'.$projectId); // Delete Tags @@ -273,7 +269,7 @@ class DeletesV1 extends Worker $chunk++; Authorization::disable(); - $projects = $this->getConsoleDB()->find('projects', [], $limit); + $projects = getConsoleDB()->find('projects', [], $limit); Authorization::reset(); $projectIds = array_map (function ($project) { @@ -351,65 +347,4 @@ class DeletesV1 extends Worker Console::info("No certificate files found for {$domain}"); } } - - /** - * @param string $projectId - * @return Database - */ - protected function getInternalDB($projectId): Database - { - global $register; - - $cache = new Cache(new RedisCache($register->get('cache'))); - $dbForInternal = new Database(new MariaDB($register->get('db')), $cache); - $dbForInternal->setNamespace('project_'.$projectId.'_internal'); // Main DB - - return $dbForInternal; - } - - /** - * @param string $projectId - * @return Database - */ - protected function getExternalDB($projectId): Database - { - global $register; - - $cache = new Cache(new RedisCache($register->get('cache'))); - $dbForExternal = new Database(new MariaDB($register->get('db')), $cache); - $dbForExternal->setNamespace('project_'.$projectId.'_external'); // Main DB - - return $dbForExternal; - } - - /** - * @return Database - */ - protected function getConsoleDB(): Database - { - global $register; - - // wait for database to be ready - $attempts = 0; - $max = 5; - $sleep = 5; - - do { - try { - $attempts++; - $cache = new Cache(new RedisCache($register->get('cache'))); - $dbForConsole = new Database(new MariaDB($register->get('db')), $cache); - $dbForConsole->setNamespace('project_console_internal'); // Main DB - break; // leave the do-while if successful - } catch(\Exception $e) { - Console::warning("Database not ready. Retrying connection ({$attempts})..."); - if ($attempts >= $max) { - throw new \Exception('Failed to connect to database: '. $e->getMessage()); - } - sleep($sleep); - } - } while ($attempts < $max); - - return $dbForConsole; - } } diff --git a/app/workers/tasks.php b/app/workers/tasks.php index cb24c65a9f..cbc8d9573e 100644 --- a/app/workers/tasks.php +++ b/app/workers/tasks.php @@ -28,11 +28,6 @@ class TasksV1 extends Worker public function run(): void { - global $register; - - $db = $register->get('db'); - $cache = $register->get('cache'); - $projectId = $this->args['projectId'] ?? null; $taskId = $this->args['$id'] ?? null; $updated = $this->args['updated'] ?? null; @@ -44,9 +39,7 @@ class TasksV1 extends Worker $logLimit = 5; $alert = ''; - $cache = new Cache(new Redis($cache)); - $dbForConsole = new Database(new MariaDB($db), $cache); - $dbForConsole->setNamespace('project_console_internal'); + $dbForConsole = getConsoleDB(); /* * 1. Get Original Task From 51100fda0aa4559396f90ed0a87df97890422b90 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Sun, 25 Jul 2021 18:24:29 -0400 Subject: [PATCH 23/27] Ensure namespace exists before proceeding --- app/workers.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/workers.php b/app/workers.php index b33ec9db61..7ae36c1922 100644 --- a/app/workers.php +++ b/app/workers.php @@ -57,6 +57,9 @@ function getInternalDB(string $projectId): Database $cache = new Cache(new RedisCache($register->get('cache'))); $dbForInternal = new Database(new MariaDB($register->get('db')), $cache); $dbForInternal->setNamespace("project_{$projectId}_internal"); // Main DB + if (!$dbForInternal->exists()) { + throw new Exception("Table does not exist: {$dbForInternal->getNamespace()}"); + } break; // leave loop if successful } catch(\Exception $e) { Console::warning("Database not ready. Retrying connection ({$attempts})..."); @@ -89,6 +92,9 @@ function getExternalDB(string $projectId): Database $cache = new Cache(new RedisCache($register->get('cache'))); $dbForExternal = new Database(new MariaDB($register->get('db')), $cache); $dbForExternal->setNamespace("project_{$projectId}_external"); // Main DB + if (!$dbForExternal->exists()) { + throw new Exception("Table does not exist: {$dbForExternal->getNamespace()}"); + } break; // leave loop if successful } catch(\Exception $e) { Console::warning("Database not ready. Retrying connection ({$attempts})..."); @@ -120,6 +126,9 @@ function getConsoleDB(): Database $cache = new Cache(new RedisCache($register->get('cache'))); $dbForConsole = new Database(new MariaDB($register->get('db')), $cache); $dbForConsole->setNamespace('project_console_internal'); // Main DB + if (!$dbForConsole->exists()) { + throw new Exception("Table does not exist: {$dbForConsole->getNamespace()}"); + } break; // leave loop if successful } catch(\Exception $e) { Console::warning("Database not ready. Retrying connection ({$attempts})..."); From 51449841bf8a0170208291ee2222a719d4e9644a Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Sun, 25 Jul 2021 19:14:19 -0400 Subject: [PATCH 24/27] Refactor db methods to worker class instead of worker script --- app/workers.php | 109 -------------------------------- app/workers/audits.php | 6 +- app/workers/database.php | 10 +-- app/workers/deletes.php | 22 +++---- app/workers/functions.php | 10 +-- app/workers/tasks.php | 2 +- src/Appwrite/Resque/Worker.php | 110 +++++++++++++++++++++++++++++++++ 7 files changed, 129 insertions(+), 140 deletions(-) diff --git a/app/workers.php b/app/workers.php index 7ae36c1922..7dd48eaff8 100644 --- a/app/workers.php +++ b/app/workers.php @@ -1,13 +1,7 @@ set('cache', function () { // Register cache connection return $redis; }); -/** - * Get internal project database - * @param string $projectId - * @return Database - */ -function getInternalDB(string $projectId): Database -{ - global $register; - - $attempts = 0; - $max = 10; - $sleep = 2; - - do { - try { - $attempts++; - $cache = new Cache(new RedisCache($register->get('cache'))); - $dbForInternal = new Database(new MariaDB($register->get('db')), $cache); - $dbForInternal->setNamespace("project_{$projectId}_internal"); // Main DB - if (!$dbForInternal->exists()) { - throw new Exception("Table does not exist: {$dbForInternal->getNamespace()}"); - } - break; // leave loop if successful - } catch(\Exception $e) { - Console::warning("Database not ready. Retrying connection ({$attempts})..."); - if ($attempts >= $max) { - throw new \Exception('Failed to connect to database: '. $e->getMessage()); - } - sleep($sleep); - } - } while ($attempts < $max); - - return $dbForInternal; -} - -/** - * Get external project database - * @param string $projectId - * @return Database - */ -function getExternalDB(string $projectId): Database -{ - global $register; - - $attempts = 0; - $max = 10; - $sleep = 2; - - do { - try { - $attempts++; - $cache = new Cache(new RedisCache($register->get('cache'))); - $dbForExternal = new Database(new MariaDB($register->get('db')), $cache); - $dbForExternal->setNamespace("project_{$projectId}_external"); // Main DB - if (!$dbForExternal->exists()) { - throw new Exception("Table does not exist: {$dbForExternal->getNamespace()}"); - } - break; // leave loop if successful - } catch(\Exception $e) { - Console::warning("Database not ready. Retrying connection ({$attempts})..."); - if ($attempts >= $max) { - throw new \Exception('Failed to connect to database: '. $e->getMessage()); - } - sleep($sleep); - } - } while ($attempts < $max); - - return $dbForExternal; -} - -/** - * Get console database - * @return Database - */ -function getConsoleDB(): Database -{ - global $register; - - $attempts = 0; - $max = 5; - $sleep = 5; - - do { - try { - $attempts++; - $cache = new Cache(new RedisCache($register->get('cache'))); - $dbForConsole = new Database(new MariaDB($register->get('db')), $cache); - $dbForConsole->setNamespace('project_console_internal'); // Main DB - if (!$dbForConsole->exists()) { - throw new Exception("Table does not exist: {$dbForConsole->getNamespace()}"); - } - break; // leave loop if successful - } catch(\Exception $e) { - Console::warning("Database not ready. Retrying connection ({$attempts})..."); - if ($attempts >= $max) { - throw new \Exception('Failed to connect to database: '. $e->getMessage()); - } - sleep($sleep); - } - } while ($attempts < $max); - - return $dbForConsole; -} diff --git a/app/workers/audits.php b/app/workers/audits.php index 2b603092fe..663de3ce4f 100644 --- a/app/workers/audits.php +++ b/app/workers/audits.php @@ -2,11 +2,7 @@ use Appwrite\Resque\Worker; use Utopia\Audit\Audit; -use Utopia\Cache\Adapter\Redis; -use Utopia\Cache\Cache; use Utopia\CLI\Console; -use Utopia\Database\Adapter\MariaDB; -use Utopia\Database\Database; require_once __DIR__.'/../workers.php'; @@ -31,7 +27,7 @@ class AuditsV1 extends Worker $ip = $this->args['ip']; $data = $this->args['data']; - $dbForInternal = getInternalDB($projectId); + $dbForInternal = $this->getInternalDB($projectId); $audit = new Audit($dbForInternal); $audit->log($userId, $event, $resource, $userAgent, $ip, '', $data); diff --git a/app/workers/database.php b/app/workers/database.php index bdf33a4b37..544ffe1060 100644 --- a/app/workers/database.php +++ b/app/workers/database.php @@ -4,7 +4,7 @@ use Appwrite\Resque\Worker; use Utopia\CLI\Console; use Utopia\Database\Document; -require_once __DIR__.'/../init.php'; +require_once __DIR__.'/../workers.php'; Console::title('Database V1 Worker'); Console::success(APP_NAME.' database worker v1 has started'."\n"); @@ -61,7 +61,7 @@ class DatabaseV1 extends Worker */ protected function createAttribute($attribute, $projectId): void { - $dbForExternal = getExternalDB($projectId); + $dbForExternal = $this->getExternalDB($projectId); $collectionId = $attribute->getCollection(); $id = $attribute->getAttribute('$id', ''); @@ -85,7 +85,7 @@ class DatabaseV1 extends Worker */ protected function deleteAttribute($attribute, $projectId): void { - $dbForExternal = getExternalDB($projectId); + $dbForExternal = $this->getExternalDB($projectId); $collectionId = $attribute->getCollection(); $id = $attribute->getAttribute('$id'); @@ -99,7 +99,7 @@ class DatabaseV1 extends Worker */ protected function createIndex($index, $projectId): void { - $dbForExternal = getExternalDB($projectId); + $dbForExternal = $this->getExternalDB($projectId); $collectionId = $index->getCollection(); $id = $index->getAttribute('$id', ''); @@ -120,7 +120,7 @@ class DatabaseV1 extends Worker */ protected function deleteIndex($index, $projectId): void { - $dbForExternal = getExternalDB($projectId); + $dbForExternal = $this->getExternalDB($projectId); $collectionId = $index->getCollection(); $id = $index->getAttribute('$id'); diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 8ecce1d483..e98723a5ea 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -93,7 +93,7 @@ class DeletesV1 extends Worker // Delete Memberships $this->deleteByGroup('memberships', [ new Query('teamId', Query::TYPE_EQUAL, [$teamId]) - ], getInternalDB($projectId)); + ], $this->getInternalDB($projectId)); } /** @@ -103,8 +103,8 @@ class DeletesV1 extends Worker { $projectId = $document->getId(); // Delete all DBs - getExternalDB($projectId)->delete(); - getInternalDB($projectId)->delete(); + $this->getExternalDB($projectId)->delete(); + $this->getInternalDB($projectId)->delete(); // Delete all storage directories $uploads = new Local(APP_STORAGE_UPLOADS.'/app-'.$document->getId()); @@ -126,13 +126,13 @@ class DeletesV1 extends Worker // Delete Memberships and decrement team membership counts $this->deleteByGroup('memberships', [ new Query('userId', Query::TYPE_EQUAL, [$userId]) - ], getInternalDB($projectId), function(Document $document) use ($projectId, $userId) { + ], $this->getInternalDB($projectId), function(Document $document) use ($projectId, $userId) { if ($document->getAttribute('confirm')) { // Count only confirmed members $teamId = $document->getAttribute('teamId'); - $team = getInternalDB($projectId)->getDocument('teams', $teamId); + $team = $this->getInternalDB($projectId)->getDocument('teams', $teamId); if(!$team->isEmpty()) { - $team = getInternalDB($projectId)->updateDocument('teams', $teamId, new Document(\array_merge($team->getArrayCopy(), [ + $team = $this->getInternalDB($projectId)->updateDocument('teams', $teamId, new Document(\array_merge($team->getArrayCopy(), [ 'sum' => \max($team->getAttribute('sum', 0) - 1, 0), // Ensure that sum >= 0 ]))); } @@ -146,7 +146,7 @@ class DeletesV1 extends Worker protected function deleteExecutionLogs($timestamp) { $this->deleteForProjectIds(function($projectId) use ($timestamp) { - if (!($dbForInternal = getInternalDB($projectId))) { + if (!($dbForInternal = $this->getInternalDB($projectId))) { throw new Exception('Failed to get projectDB for project '.$projectId); } @@ -167,7 +167,7 @@ class DeletesV1 extends Worker } $this->deleteForProjectIds(function($projectId) use ($timestamp){ - $timeLimit = new TimeLimit("", 0, 1, getInternalDB($projectId)); + $timeLimit = new TimeLimit("", 0, 1, $this->getInternalDB($projectId)); $abuse = new Abuse($timeLimit); $status = $abuse->cleanup($timestamp); @@ -186,7 +186,7 @@ class DeletesV1 extends Worker throw new Exception('Failed to delete audit logs. No timestamp provided'); } $this->deleteForProjectIds(function($projectId) use ($timestamp){ - $audit = new Audit(getInternalDB($projectId)); + $audit = new Audit($this->getInternalDB($projectId)); $status = $audit->cleanup($timestamp); if (!$status) { throw new Exception('Failed to delete Audit logs for project'.$projectId); @@ -200,7 +200,7 @@ class DeletesV1 extends Worker */ protected function deleteFunction(Document $document, $projectId) { - $dbForInternal = getInternalDB($projectId); + $dbForInternal = $this->getInternalDB($projectId); $device = new Local(APP_STORAGE_FUNCTIONS.'/app-'.$projectId); // Delete Tags @@ -269,7 +269,7 @@ class DeletesV1 extends Worker $chunk++; Authorization::disable(); - $projects = getConsoleDB()->find('projects', [], $limit); + $projects = $this->getConsoleDB()->find('projects', [], $limit); Authorization::reset(); $projectIds = array_map (function ($project) { diff --git a/app/workers/functions.php b/app/workers/functions.php index 8db7d64753..b6263cc5f8 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -6,11 +6,8 @@ use Appwrite\Utopia\Response\Model\Execution; use Cron\CronExpression; use Swoole\Runtime; use Utopia\App; -use Utopia\Cache\Adapter\Redis; -use Utopia\Cache\Cache; use Utopia\CLI\Console; use Utopia\Config\Config; -use Utopia\Database\Adapter\MariaDB; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; @@ -139,9 +136,6 @@ class FunctionsV1 extends Worker { global $register; - $db = $register->get('db'); - $cache = $register->get('cache'); - $projectId = $this->args['projectId'] ?? ''; $functionId = $this->args['functionId'] ?? ''; $webhooks = $this->args['webhooks'] ?? []; @@ -154,9 +148,7 @@ class FunctionsV1 extends Worker $userId = $this->args['userId'] ?? ''; $jwt = $this->args['jwt'] ?? ''; - $cache = new Cache(new Redis($cache)); - $database = new Database(new MariaDB($db), $cache); - $database->setNamespace('project_'.$projectId.'_internal'); + $database = $this->getInternalDB($projectId); switch ($trigger) { case 'event': diff --git a/app/workers/tasks.php b/app/workers/tasks.php index cbc8d9573e..b3469393ef 100644 --- a/app/workers/tasks.php +++ b/app/workers/tasks.php @@ -39,7 +39,7 @@ class TasksV1 extends Worker $logLimit = 5; $alert = ''; - $dbForConsole = getConsoleDB(); + $dbForConsole = $this->getConsoleDB(); /* * 1. Get Original Task diff --git a/src/Appwrite/Resque/Worker.php b/src/Appwrite/Resque/Worker.php index db8dc91ce7..93c666723e 100644 --- a/src/Appwrite/Resque/Worker.php +++ b/src/Appwrite/Resque/Worker.php @@ -2,6 +2,13 @@ namespace Appwrite\Resque; +use Exception; +use Utopia\Cache\Cache; +use Utopia\Cache\Adapter\Redis as RedisCache; +use Utopia\CLI\Console; +use Utopia\Database\Database; +use Utopia\Database\Adapter\MariaDB; + abstract class Worker { public $args = []; @@ -26,4 +33,107 @@ abstract class Worker { $this->shutdown(); } + /** + * Get internal project database + * @param string $projectId + * @return Database + */ + protected function getInternalDB(string $projectId): Database + { + global $register; + + $attempts = 0; + $max = 10; + $sleep = 2; + + do { + try { + $attempts++; + $cache = new Cache(new RedisCache($register->get('cache'))); + $dbForInternal = new Database(new MariaDB($register->get('db')), $cache); + $dbForInternal->setNamespace("project_{$projectId}_internal"); // Main DB + if (!$dbForInternal->exists()) { + throw new Exception("Table does not exist: {$dbForInternal->getNamespace()}"); + } + break; // leave loop if successful + } catch(\Exception $e) { + Console::warning("Database not ready. Retrying connection ({$attempts})..."); + if ($attempts >= $max) { + throw new \Exception('Failed to connect to database: '. $e->getMessage()); + } + sleep($sleep); + } + } while ($attempts < $max); + + return $dbForInternal; + } + + /** + * Get external project database + * @param string $projectId + * @return Database + */ + protected function getExternalDB(string $projectId): Database + { + global $register; + + $attempts = 0; + $max = 10; + $sleep = 2; + + do { + try { + $attempts++; + $cache = new Cache(new RedisCache($register->get('cache'))); + $dbForExternal = new Database(new MariaDB($register->get('db')), $cache); + $dbForExternal->setNamespace("project_{$projectId}_external"); // Main DB + if (!$dbForExternal->exists()) { + throw new Exception("Table does not exist: {$dbForExternal->getNamespace()}"); + } + break; // leave loop if successful + } catch(\Exception $e) { + Console::warning("Database not ready. Retrying connection ({$attempts})..."); + if ($attempts >= $max) { + throw new \Exception('Failed to connect to database: '. $e->getMessage()); + } + sleep($sleep); + } + } while ($attempts < $max); + + return $dbForExternal; + } + + /** + * Get console database + * @return Database + */ + protected function getConsoleDB(): Database + { + global $register; + + $attempts = 0; + $max = 5; + $sleep = 5; + + do { + try { + $attempts++; + $cache = new Cache(new RedisCache($register->get('cache'))); + $dbForConsole = new Database(new MariaDB($register->get('db')), $cache); + $dbForConsole->setNamespace('project_console_internal'); // Main DB + if (!$dbForConsole->exists()) { + throw new Exception("Table does not exist: {$dbForConsole->getNamespace()}"); + } + break; // leave loop if successful + } catch(\Exception $e) { + Console::warning("Database not ready. Retrying connection ({$attempts})..."); + if ($attempts >= $max) { + throw new \Exception('Failed to connect to database: '. $e->getMessage()); + } + sleep($sleep); + } + } while ($attempts < $max); + + return $dbForConsole; + } } \ No newline at end of file From b340d236dbf3afe701dacd4b56653bb3f751a151 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Mon, 26 Jul 2021 10:59:38 -0400 Subject: [PATCH 25/27] Refactor retry logic to private method --- src/Appwrite/Resque/Worker.php | 114 +++++++++++++++------------------ 1 file changed, 52 insertions(+), 62 deletions(-) diff --git a/src/Appwrite/Resque/Worker.php b/src/Appwrite/Resque/Worker.php index 93c666723e..84241173f5 100644 --- a/src/Appwrite/Resque/Worker.php +++ b/src/Appwrite/Resque/Worker.php @@ -2,7 +2,6 @@ namespace Appwrite\Resque; -use Exception; use Utopia\Cache\Cache; use Utopia\Cache\Adapter\Redis as RedisCache; use Utopia\CLI\Console; @@ -19,6 +18,13 @@ abstract class Worker abstract public function shutdown(): void; + const MAX_ATTEMPTS = 10; + const SLEEP_TIME = 2; + + const DATABASE_INTERNAL = 'internal'; + const DATABASE_EXTERNAL = 'external'; + const DATABASE_CONSOLE = 'console'; + public function setUp(): void { $this->init(); @@ -40,32 +46,7 @@ abstract class Worker */ protected function getInternalDB(string $projectId): Database { - global $register; - - $attempts = 0; - $max = 10; - $sleep = 2; - - do { - try { - $attempts++; - $cache = new Cache(new RedisCache($register->get('cache'))); - $dbForInternal = new Database(new MariaDB($register->get('db')), $cache); - $dbForInternal->setNamespace("project_{$projectId}_internal"); // Main DB - if (!$dbForInternal->exists()) { - throw new Exception("Table does not exist: {$dbForInternal->getNamespace()}"); - } - break; // leave loop if successful - } catch(\Exception $e) { - Console::warning("Database not ready. Retrying connection ({$attempts})..."); - if ($attempts >= $max) { - throw new \Exception('Failed to connect to database: '. $e->getMessage()); - } - sleep($sleep); - } - } while ($attempts < $max); - - return $dbForInternal; + return $this->getDB(self::DATABASE_INTERNAL, $projectId); } /** @@ -75,32 +56,7 @@ abstract class Worker */ protected function getExternalDB(string $projectId): Database { - global $register; - - $attempts = 0; - $max = 10; - $sleep = 2; - - do { - try { - $attempts++; - $cache = new Cache(new RedisCache($register->get('cache'))); - $dbForExternal = new Database(new MariaDB($register->get('db')), $cache); - $dbForExternal->setNamespace("project_{$projectId}_external"); // Main DB - if (!$dbForExternal->exists()) { - throw new Exception("Table does not exist: {$dbForExternal->getNamespace()}"); - } - break; // leave loop if successful - } catch(\Exception $e) { - Console::warning("Database not ready. Retrying connection ({$attempts})..."); - if ($attempts >= $max) { - throw new \Exception('Failed to connect to database: '. $e->getMessage()); - } - sleep($sleep); - } - } while ($attempts < $max); - - return $dbForExternal; + return $this->getDB(self::DATABASE_EXTERNAL, $projectId); } /** @@ -108,32 +64,66 @@ abstract class Worker * @return Database */ protected function getConsoleDB(): Database + { + return $this->getDB(self::DATABASE_CONSOLE); + } + + /** + * Get console database + * @param string $type One of (internal, external, console) + * @param string $projectId of internal or external DB + * @return Database + */ + private function getDB($type, $projectId = ''): Database { global $register; + $namespace = ''; + $sleep = self::SLEEP_TIME; // overwritten when necessary + + switch ($type) { + case self::DATABASE_INTERNAL: + if (!$projectId) { + throw new \Exception('ProjectID not provided - cannot get database'); + } + $namespace = "project_{$projectId}_internal"; + break; + case self::DATABASE_EXTERNAL: + if (!$projectId) { + throw new \Exception('ProjectID not provided - cannot get database'); + } + $namespace = "project_{$projectId}_external"; + break; + case self::DATABASE_CONSOLE: + $namespace = "project_console_internal"; + $sleep = 5; // ConsoleDB needs extra sleep time to ensure tables are created + break; + default: + throw new \Exception('Unknown database type: ' . $type); + break; + } + $attempts = 0; - $max = 5; - $sleep = 5; do { try { $attempts++; $cache = new Cache(new RedisCache($register->get('cache'))); - $dbForConsole = new Database(new MariaDB($register->get('db')), $cache); - $dbForConsole->setNamespace('project_console_internal'); // Main DB - if (!$dbForConsole->exists()) { - throw new Exception("Table does not exist: {$dbForConsole->getNamespace()}"); + $database = new Database(new MariaDB($register->get('db')), $cache); + $database->setNamespace($namespace); // Main DB + if (!$database->exists()) { + throw new \Exception("Table does not exist: {$database->getNamespace()}"); } break; // leave loop if successful } catch(\Exception $e) { Console::warning("Database not ready. Retrying connection ({$attempts})..."); - if ($attempts >= $max) { + if ($attempts >= self::MAX_ATTEMPTS) { throw new \Exception('Failed to connect to database: '. $e->getMessage()); } sleep($sleep); } - } while ($attempts < $max); + } while ($attempts < self::MAX_ATTEMPTS); - return $dbForConsole; + return $database; } } \ No newline at end of file From 56da7e48f61de8e49858c1417085420b2d42aada Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Mon, 26 Jul 2021 12:10:08 -0400 Subject: [PATCH 26/27] Use external swagger validator --- docker-compose.yml | 11 ----------- tests/e2e/General/HTTPTest.php | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 32ec332314..ed50016ed5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -508,17 +508,6 @@ services: networks: - appwrite - swagger-validator: - image: 'swaggerapi/swagger-validator-v2:v2.0.5' - container_name: appwrite-swagger-validator - ports: - - '9506:8080' - environment: - - REJECT_LOCAL=false - - REJECT_REDIRECT=false - networks: - - appwrite - # redis-commander: # image: rediscommander/redis-commander:latest # restart: unless-stopped diff --git a/tests/e2e/General/HTTPTest.php b/tests/e2e/General/HTTPTest.php index 78f77f4d12..4f9a8dc95d 100644 --- a/tests/e2e/General/HTTPTest.php +++ b/tests/e2e/General/HTTPTest.php @@ -133,7 +133,7 @@ class HTTPTest extends Scope } $client = new Client(); - $client->setEndpoint('http://appwrite-swagger-validator:8080'); + $client->setEndpoint('https://validator.swagger.io'); /** * Test for SUCCESS From 717799776a2d381c1fe597e16096cb2271fd5825 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Mon, 26 Jul 2021 12:37:29 -0400 Subject: [PATCH 27/27] Use parent class method to get consoleDB --- app/workers/certificates.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/app/workers/certificates.php b/app/workers/certificates.php index 8bebcca82e..1720d6adb9 100644 --- a/app/workers/certificates.php +++ b/app/workers/certificates.php @@ -3,13 +3,9 @@ use Appwrite\Network\Validator\CNAME; use Appwrite\Resque\Worker; use Utopia\App; -use Utopia\Cache\Adapter\Redis; -use Utopia\Cache\Cache; use Utopia\CLI\Console; -use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; -use Utopia\Database\Adapter\MariaDB; use Utopia\Database\Validator\Authorization; use Utopia\Domains\Domain; @@ -28,11 +24,7 @@ class CertificatesV1 extends Worker public function run(): void { - global $register; - - $cache = new Cache(new Redis($register->get('cache'))); - $dbForConsole = new Database(new MariaDB($register->get('db')), $cache); - $dbForConsole->setNamespace('project_console_internal'); // Main DB + $dbForConsole = $this->getConsoleDB(); /** * 1. Get new domain document - DONE