From 3e808d3bf49e4a54c3adcf6adb69bb94222a2658 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 30 Jun 2020 00:43:34 +0300 Subject: [PATCH] Removed global vars --- app/app.php | 378 +++--- app/controllers/api/account.php | 1980 +++++++++++++++--------------- app/controllers/api/avatars.php | 579 ++++----- app/controllers/api/database.php | 899 +++++++------- app/controllers/api/health.php | 280 ++--- app/controllers/api/locale.php | 209 ++-- app/controllers/api/projects.php | 2 - app/controllers/api/storage.php | 393 +++--- app/controllers/api/teams.php | 2 - app/controllers/api/users.php | 2 - app/controllers/mock.php | 7 +- app/controllers/shared/api.php | 4 +- app/controllers/shared/web.php | 9 +- app/controllers/web/console.php | 109 +- app/controllers/web/home.php | 8 +- app/init.php | 8 +- app/views/layouts/default.phtml | 9 +- public/index.php | 1 + 18 files changed, 2444 insertions(+), 2435 deletions(-) diff --git a/app/app.php b/app/app.php index 05606f1cf..e9e59ecc9 100644 --- a/app/app.php +++ b/app/app.php @@ -2,7 +2,7 @@ require_once __DIR__.'/init.php'; -global $request, $response, $register, $project; +global $register, $project; use Utopia\App; use Utopia\Request; @@ -18,147 +18,133 @@ use Appwrite\Database\Document; use Appwrite\Database\Validator\Authorization; use Appwrite\Database\Adapter\MySQL as MySQLAdapter; use Appwrite\Database\Adapter\Redis as RedisAdapter; -use Appwrite\Event\Event; use Appwrite\Network\Validator\Origin; -$request = new Request(); -$response = new Response(); +// Config::setParam('domain', $request->getServer('HTTP_HOST', '')); +// Config::setParam('domainVerification', false); +// Config::setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN')); +// Config::setParam('protocol', $request->getServer('HTTP_X_FORWARDED_PROTO', $request->getServer('REQUEST_SCHEME', 'https'))); +// Config::setParam('port', (string) \parse_url(Config::getParam('protocol').'://'.$request->getServer('HTTP_HOST', ''), PHP_URL_PORT)); +// Config::setParam('hostname', \parse_url(Config::getParam('protocol').'://'.$request->getServer('HTTP_HOST', null), PHP_URL_HOST)); -$locale = $request->getParam('locale', $request->getHeader('X-Appwrite-Locale', '')); +// \define('COOKIE_DOMAIN', +// ( +// $request->getServer('HTTP_HOST', null) === 'localhost' || +// $request->getServer('HTTP_HOST', null) === 'localhost:'.Config::getParam('port') || +// (\filter_var(Config::getParam('hostname'), FILTER_VALIDATE_IP) !== false) +// ) +// ? null +// : '.'.Config::getParam('hostname') +// ); +// \define('COOKIE_SAMESITE', Response::COOKIE_SAMESITE_NONE); -if (\in_array($locale, Config::getParam('locales'))) { - Locale::setDefault($locale); -} +// Authorization::disable(); -Config::setParam('env', App::getMode()); -Config::setParam('domain', $request->getServer('HTTP_HOST', '')); -Config::setParam('domainVerification', false); -Config::setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN')); -Config::setParam('protocol', $request->getServer('HTTP_X_FORWARDED_PROTO', $request->getServer('REQUEST_SCHEME', 'https'))); -Config::setParam('port', (string) \parse_url(Config::getParam('protocol').'://'.$request->getServer('HTTP_HOST', ''), PHP_URL_PORT)); -Config::setParam('hostname', \parse_url(Config::getParam('protocol').'://'.$request->getServer('HTTP_HOST', null), PHP_URL_HOST)); +// $project = $consoleDB->getDocument($request->getParam('project', $request->getHeader('X-Appwrite-Project', ''))); -\define('COOKIE_DOMAIN', - ( - $request->getServer('HTTP_HOST', null) === 'localhost' || - $request->getServer('HTTP_HOST', null) === 'localhost:'.Config::getParam('port') || - (\filter_var(Config::getParam('hostname'), FILTER_VALIDATE_IP) !== false) - ) - ? null - : '.'.Config::getParam('hostname') - ); -\define('COOKIE_SAMESITE', Response::COOKIE_SAMESITE_NONE); +// Authorization::enable(); -Authorization::disable(); +// $console = $consoleDB->getDocument('console'); -$project = $consoleDB->getDocument($request->getParam('project', $request->getHeader('X-Appwrite-Project', ''))); +// $mode = $request->getParam('mode', $request->getHeader('X-Appwrite-Mode', 'default')); -Authorization::enable(); +// Auth::setCookieName('a_session_'.$project->getId()); -$console = $consoleDB->getDocument('console'); +// if (APP_MODE_ADMIN === $mode) { +// Auth::setCookieName('a_session_'.$console->getId()); +// } -$mode = $request->getParam('mode', $request->getHeader('X-Appwrite-Mode', 'default')); +// $session = Auth::decodeSession( +// $request->getCookie(Auth::$cookieName, // Get sessions +// $request->getCookie(Auth::$cookieName.'_legacy', // Get fallback session from old clients (no SameSite support) +// $request->getHeader('X-Appwrite-Key', '')))); // Get API Key -Auth::setCookieName('a_session_'.$project->getId()); +// // Get fallback session from clients who block 3rd-party cookies +// $response->addHeader('X-Debug-Fallback', 'false'); -if (APP_MODE_ADMIN === $mode) { - Auth::setCookieName('a_session_'.$console->getId()); -} +// if(empty($session['id']) && empty($session['secret'])) { +// $response->addHeader('X-Debug-Fallback', 'true'); +// $fallback = $request->getHeader('X-Fallback-Cookies', ''); +// $fallback = \json_decode($fallback, true); +// $session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : '')); +// } -$session = Auth::decodeSession( - $request->getCookie(Auth::$cookieName, // Get sessions - $request->getCookie(Auth::$cookieName.'_legacy', // Get fallback session from old clients (no SameSite support) - $request->getHeader('X-Appwrite-Key', '')))); // Get API Key +// Auth::$unique = $session['id']; +// Auth::$secret = $session['secret']; -// Get fallback session from clients who block 3rd-party cookies -$response->addHeader('X-Debug-Fallback', 'false'); +// $projectDB = new Database(); +// $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); +// $projectDB->setNamespace('app_'.$project->getId()); +// $projectDB->setMocks(Config::getParam('collections', [])); -if(empty($session['id']) && empty($session['secret'])) { - $response->addHeader('X-Debug-Fallback', 'true'); - $fallback = $request->getHeader('X-Fallback-Cookies', ''); - $fallback = \json_decode($fallback, true); - $session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : '')); -} +// if (APP_MODE_ADMIN !== $mode) { +// $user = $projectDB->getDocument(Auth::$unique); +// } +// else { +// $user = $consoleDB->getDocument(Auth::$unique); -Auth::$unique = $session['id']; -Auth::$secret = $session['secret']; +// $user +// ->setAttribute('$id', 'admin-'.$user->getAttribute('$id')) +// ; +// } -$projectDB = new Database(); -$projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); -$projectDB->setNamespace('app_'.$project->getId()); -$projectDB->setMocks(Config::getParam('collections', [])); +// if (empty($user->getId()) // Check a document has been found in the DB +// || Database::SYSTEM_COLLECTION_USERS !== $user->getCollection() // Validate returned document is really a user document +// || !Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret)) { // Validate user has valid login token +// $user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]); +// } -if (APP_MODE_ADMIN !== $mode) { - $user = $projectDB->getDocument(Auth::$unique); -} -else { - $user = $consoleDB->getDocument(Auth::$unique); +// if (APP_MODE_ADMIN === $mode) { +// if (!empty($user->search('teamId', $project->getAttribute('teamId'), $user->getAttribute('memberships')))) { +// Authorization::disable(); +// } else { +// $user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]); +// } +// } - $user - ->setAttribute('$id', 'admin-'.$user->getAttribute('$id')) - ; -} - -if (empty($user->getId()) // Check a document has been found in the DB - || Database::SYSTEM_COLLECTION_USERS !== $user->getCollection() // Validate returned document is really a user document - || !Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret)) { // Validate user has valid login token - $user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]); -} - -if (APP_MODE_ADMIN === $mode) { - if (!empty($user->search('teamId', $project->getAttribute('teamId'), $user->getAttribute('memberships')))) { - Authorization::disable(); - } else { - $user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]); - } -} - -// Set project mail -$register->get('smtp') - ->setFrom( - App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), - ($project->getId() === 'console') - ? \urldecode(App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server')) - : \sprintf(Locale::getText('account.emails.team'), $project->getAttribute('name') - ) - ); - -/* - * Configuration files - */ -$utopia = new App('Asia/Tel_Aviv'); -$webhook = new Event('v1-webhooks', 'WebhooksV1'); -$audit = new Event('v1-audits', 'AuditsV1'); -$usage = new Event('v1-usage', 'UsageV1'); -$mail = new Event('v1-mails', 'MailsV1'); -$deletes = new Event('v1-deletes', 'DeletesV1'); +// // Set project mail +// $register->get('smtp') +// ->setFrom( +// App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), +// ($project->getId() === 'console') +// ? \urldecode(App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server')) +// : \sprintf(Locale::getText('account.emails.team'), $project->getAttribute('name') +// ) +// ); /** * Get All verified client URLs for both console and current projects * + Filter for duplicated entries */ -$clientsConsole = \array_map(function ($node) { - return $node['hostname']; - }, \array_filter($console->getAttribute('platforms', []), function ($node) { - if (isset($node['type']) && $node['type'] === 'web' && isset($node['hostname']) && !empty($node['hostname'])) { - return true; - } +// $clientsConsole = \array_map(function ($node) { +// return $node['hostname']; +// }, \array_filter($console->getAttribute('platforms', []), function ($node) { +// if (isset($node['type']) && $node['type'] === 'web' && isset($node['hostname']) && !empty($node['hostname'])) { +// return true; +// } - return false; - })); +// return false; +// })); -$clients = \array_unique(\array_merge($clientsConsole, \array_map(function ($node) { - return $node['hostname']; - }, \array_filter($project->getAttribute('platforms', []), function ($node) { - if (isset($node['type']) && $node['type'] === 'web' && isset($node['hostname']) && !empty($node['hostname'])) { - return true; - } +// $clients = \array_unique(\array_merge($clientsConsole, \array_map(function ($node) { +// return $node['hostname']; +// }, \array_filter($project->getAttribute('platforms', []), function ($node) { +// if (isset($node['type']) && $node['type'] === 'web' && isset($node['hostname']) && !empty($node['hostname'])) { +// return true; +// } - return false; - })))); +// return false; +// })))); -App::init(function () use ($utopia, $request, $response, &$user, $project, $console, $webhook, $audit, $usage, $clients) { +App::init(function ($utopia, $request, $response, $user, $project, $console, $webhooks, $audits, $usage, $clients, $locale) { + /** @var $locale Utopia\Locale\Locale */ + $localeParam = $request->getParam('locale', $request->getHeader('X-Appwrite-Locale', '')); + + if (\in_array($localeParam, Config::getParam('locales'))) { + $locale->setDefault($localeParam); + }; + $route = $utopia->match($request); if(!empty($route->getLabel('sdk.platform', [])) && empty($project->getId()) && ($route->getLabel('scope', '') !== 'public')) { @@ -312,13 +298,13 @@ App::init(function () use ($utopia, $request, $response, &$user, $project, $cons /* * Background Jobs */ - $webhook + $webhooks ->setParam('projectId', $project->getId()) ->setParam('event', $route->getLabel('webhook', '')) ->setParam('payload', []) ; - $audit + $audits ->setParam('projectId', $project->getId()) ->setParam('userId', $user->getId()) ->setParam('event', '') @@ -336,10 +322,9 @@ App::init(function () use ($utopia, $request, $response, &$user, $project, $cons ->setParam('response', 0) ->setParam('storage', 0) ; -}); - -App::shutdown(function () use ($response, $request, $webhook, $audit, $usage, $deletes, $mode, $project, $utopia) { +}, ['utopia', 'request', 'response', 'user', 'project', 'console', 'webhook', 'audit', 'usage', 'clients', 'locale']); +App::shutdown(function ($utopia, $response, $request, $webhook, $audit, $usage, $deletes, $mode, $project) { /* * Trigger events for background workers */ @@ -366,9 +351,9 @@ App::shutdown(function () use ($response, $request, $webhook, $audit, $usage, $d ->trigger() ; } -}); +}, ['utopia', 'response', 'request', 'webhook', 'audit', 'usage', 'deletes', 'mode', 'project']); -App::options(function () use ($request, $response) { +App::options(function ($request, $response) { $origin = $request->getServer('HTTP_ORIGIN'); $response @@ -378,9 +363,11 @@ App::options(function () use ($request, $response) { ->addHeader('Access-Control-Allow-Origin', $origin) ->addHeader('Access-Control-Allow-Credentials', 'true') ->send(); -}); +}, ['request', 'response']); + +App::error(function ($error, $utopia, $request, $response, $project) { + /** @var Exception $error */ -App::error(function ($error /* @var $error Exception */) use ($request, $response, $utopia, $project) { $version = Config::getParam('version'); switch ($error->getCode()) { @@ -450,91 +437,85 @@ App::error(function ($error /* @var $error Exception */) use ($request, $respons $response ->json($output) ; -}); +}, ['error', 'utopia', 'request', 'response', 'project']); App::get('/manifest.json') ->desc('Progressive app manifest file') ->label('scope', 'public') ->label('docs', false) - ->action( - function () use ($response) { - $response->json([ - 'name' => APP_NAME, - 'short_name' => APP_NAME, - 'start_url' => '.', - 'url' => 'https://appwrite.io/', - 'display' => 'standalone', - 'background_color' => '#fff', - 'theme_color' => '#f02e65', - 'description' => 'End to end backend server for frontend and mobile apps. 👩‍💻👨‍💻', - 'icons' => [ - [ - 'src' => 'images/favicon.png', - 'sizes' => '256x256', - 'type' => 'image/png', - ], + ->action(function ($response) { + /** @var Utopia\Response $response */ + + $response->json([ + 'name' => APP_NAME, + 'short_name' => APP_NAME, + 'start_url' => '.', + 'url' => 'https://appwrite.io/', + 'display' => 'standalone', + 'background_color' => '#fff', + 'theme_color' => '#f02e65', + 'description' => 'End to end backend server for frontend and mobile apps. 👩‍💻👨‍💻', + 'icons' => [ + [ + 'src' => 'images/favicon.png', + 'sizes' => '256x256', + 'type' => 'image/png', ], - ]); - } - ); + ], + ]); + }, ['response']); App::get('/robots.txt') ->desc('Robots.txt File') ->label('scope', 'public') ->label('docs', false) - ->action( - function () use ($response) { - $template = new View(__DIR__.'/views/general/robots.phtml'); - $response->text($template->render(false)); - } - ); + ->action(function ($response) { + $template = new View(__DIR__.'/views/general/robots.phtml'); + $response->text($template->render(false)); + }, ['response']); App::get('/humans.txt') ->desc('Humans.txt File') ->label('scope', 'public') ->label('docs', false) - ->action( - function () use ($response) { - $template = new View(__DIR__.'/views/general/humans.phtml'); - $response->text($template->render(false)); - } - ); + ->action(function ($response) { + $template = new View(__DIR__.'/views/general/humans.phtml'); + $response->text($template->render(false)); + }, ['response']); App::get('/.well-known/acme-challenge') ->desc('SSL Verification') ->label('scope', 'public') ->label('docs', false) - ->action( - function () use ($request, $response) { - $base = \realpath(APP_STORAGE_CERTIFICATES); - $path = \str_replace('/.well-known/acme-challenge/', '', $request->getParam('q')); - $absolute = \realpath($base.'/.well-known/acme-challenge/'.$path); + ->action(function ($request, $response) { + $base = \realpath(APP_STORAGE_CERTIFICATES); + $path = \str_replace('/.well-known/acme-challenge/', '', $request->getParam('q')); + $absolute = \realpath($base.'/.well-known/acme-challenge/'.$path); - if(!$base) { - throw new Exception('Storage error', 500); - } - - if(!$absolute) { - throw new Exception('Unknown path', 404); - } - - if(!\substr($absolute, 0, \strlen($base)) === $base) { - throw new Exception('Invalid path', 401); - } - - if(!\file_exists($absolute)) { - throw new Exception('Unknown path', 404); - } - - $content = @\file_get_contents($absolute); - - if(!$content) { - throw new Exception('Failed to get contents', 500); - } - - $response->text($content); + if(!$base) { + throw new Exception('Storage error', 500); } - ); + + if(!$absolute) { + throw new Exception('Unknown path', 404); + } + + if(!\substr($absolute, 0, \strlen($base)) === $base) { + throw new Exception('Invalid path', 401); + } + + if(!\file_exists($absolute)) { + throw new Exception('Unknown path', 404); + } + + $content = @\file_get_contents($absolute); + + if(!$content) { + throw new Exception('Failed to get contents', 500); + } + + $response->text($content); + }, ['request', 'response']); include_once __DIR__ . '/controllers/shared/api.php'; include_once __DIR__ . '/controllers/shared/web.php'; @@ -543,9 +524,30 @@ foreach(Config::getParam('services', []) as $service) { include_once $service['controller']; } -App::setResource('utopia', function() use ($utopia) {return $utopia;}); -App::setResource('request', function() use ($request) {return $request;}); -App::setResource('response', function() use ($response) {return $response;}); -App::setResource('register', function() use ($register) {return $register;}); +// Runtime Execution -$utopia->run($request, $response); \ No newline at end of file +App::setResource('register', function() use ($register) { return $register; }); +App::setResource('layout', function($locale) { + $layout = new View(__DIR__.'/views/layouts/default.phtml'); + $layout->setParam('locale', $locale); + return $layout; }, ['locale']); +App::setResource('locale', function($request) { return new Locale('en'); }, ['request']); + +// Queues +App::setResource('webhook', function($register) { return $register->get('queue-webhook'); }, ['register']); +App::setResource('audit', function($register) { return $register->get('queue-audit'); }, ['register']); +App::setResource('usage', function($register) { return $register->get('queue-usage'); }, ['register']); +App::setResource('mail', function($register) { return $register->get('queue-mails'); }, ['register']); +App::setResource('deletes', function($register) { return $register->get('queue-deletes'); }, ['register']); + +// Test Mock +App::setResource('clients', function() { return []; }); +App::setResource('user', function() { return new Document([]); }); +App::setResource('project', function() { return new Document([]); }); +App::setResource('console', function() { return new Document([]); }); +App::setResource('consoleDB', function() { return new Database(); }); +App::setResource('projectDB', function() { return new Database([]); }); +App::setResource('mode', function() { return false; }); + +$app = new App('Asia/Tel_Aviv'); +$app->run(new Request(), new Response()); \ No newline at end of file diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 5887cb46f..3931599f9 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1,8 +1,5 @@ desc('Create Account') @@ -59,92 +55,97 @@ App::post('/v1/account') ->param('email', '', function () { return new Email(); }, 'User email.') ->param('password', '', function () { return new Password(); }, 'User password. Must be between 6 to 32 chars.') ->param('name', '', function () { return new Text(100); }, 'User name.', true) - ->action( - function ($email, $password, $name) use ($request, $response, $audit, $projectDB, $project, $webhook, $oauth2Keys) { - if ('console' === $project->getId()) { - $whitlistEmails = $project->getAttribute('authWhitelistEmails'); - $whitlistIPs = $project->getAttribute('authWhitelistIPs'); - $whitlistDomains = $project->getAttribute('authWhitelistDomains'); + ->action(function ($email, $password, $name, $request, $response, $project, $projectDB, $webhook, $audit) use ($oauth2Keys) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhook */ + /** @var Appwrite\Event\Event $audit */ - if (!empty($whitlistEmails) && !\in_array($email, $whitlistEmails)) { - throw new Exception('Console registration is restricted to specific emails. Contact your administrator for more information.', 401); - } + if ('console' === $project->getId()) { + $whitlistEmails = $project->getAttribute('authWhitelistEmails'); + $whitlistIPs = $project->getAttribute('authWhitelistIPs'); + $whitlistDomains = $project->getAttribute('authWhitelistDomains'); - if (!empty($whitlistIPs) && !\in_array($request->getIP(), $whitlistIPs)) { - throw new Exception('Console registration is restricted to specific IPs. Contact your administrator for more information.', 401); - } - - if (!empty($whitlistDomains) && !\in_array(\substr(\strrchr($email, '@'), 1), $whitlistDomains)) { - throw new Exception('Console registration is restricted to specific domains. Contact your administrator for more information.', 401); - } + if (!empty($whitlistEmails) && !\in_array($email, $whitlistEmails)) { + throw new Exception('Console registration is restricted to specific emails. Contact your administrator for more information.', 401); } - $profile = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'email='.$email, - ], - ]); - - if (!empty($profile)) { - throw new Exception('Account already exists', 409); + if (!empty($whitlistIPs) && !\in_array($request->getIP(), $whitlistIPs)) { + throw new Exception('Console registration is restricted to specific IPs. Contact your administrator for more information.', 401); } - Authorization::disable(); - - try { - $user = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_USERS, - '$permissions' => [ - 'read' => ['*'], - 'write' => ['user:{self}'], - ], - 'email' => $email, - 'emailVerification' => false, - 'status' => Auth::USER_STATUS_UNACTIVATED, - 'password' => Auth::passwordHash($password), - 'password-update' => \time(), - 'registration' => \time(), - 'reset' => false, - 'name' => $name, - ], ['email' => $email]); - } catch (Duplicate $th) { - throw new Exception('Account already exists', 409); + if (!empty($whitlistDomains) && !\in_array(\substr(\strrchr($email, '@'), 1), $whitlistDomains)) { + throw new Exception('Console registration is restricted to specific domains. Contact your administrator for more information.', 401); } - - Authorization::enable(); - - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } - - $webhook - ->setParam('payload', [ - 'name' => $name, - 'email' => $email, - ]) - ; - - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.create') - ->setParam('resource', 'users/'.$user->getId()) - ; - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); } - ); + + $profile = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + 'email='.$email, + ], + ]); + + if (!empty($profile)) { + throw new Exception('Account already exists', 409); + } + + Authorization::disable(); + + try { + $user = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_USERS, + '$permissions' => [ + 'read' => ['*'], + 'write' => ['user:{self}'], + ], + 'email' => $email, + 'emailVerification' => false, + 'status' => Auth::USER_STATUS_UNACTIVATED, + 'password' => Auth::passwordHash($password), + 'password-update' => \time(), + 'registration' => \time(), + 'reset' => false, + 'name' => $name, + ], ['email' => $email]); + } catch (Duplicate $th) { + throw new Exception('Account already exists', 409); + } + + Authorization::enable(); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + $webhook + ->setParam('payload', [ + 'name' => $name, + 'email' => $email, + ]) + ; + + $audit + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.create') + ->setParam('resource', 'users/'.$user->getId()) + ; + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json(\array_merge($user->getArrayCopy(\array_merge( + [ + '$id', + 'email', + 'registration', + 'name', + ], + $oauth2Keys + )), ['roles' => Authorization::getRoles()])); + }, ['request', 'response', 'project', 'projectDB', 'webhook', 'audit']); App::post('/v1/account/sessions') ->desc('Create Account Session') @@ -159,82 +160,86 @@ App::post('/v1/account/sessions') ->label('abuse-key', 'url:{url},email:{param-email}') ->param('email', '', function () { return new Email(); }, 'User email.') ->param('password', '', function () { return new Password(); }, 'User password. Must be between 6 to 32 chars.') - ->action( - function ($email, $password) use ($response, $request, $projectDB, $audit, $webhook) { - $protocol = Config::getParam('protocol'); - $profile = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'email='.$email, - ], - ]); + ->action(function ($email, $password, $request, $response, $projectDB, $webhook, $audit) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhook */ + /** @var Appwrite\Event\Event $audit */ - if (false == $profile || !Auth::passwordVerify($password, $profile->getAttribute('password'))) { - $audit - //->setParam('userId', $profile->getId()) - ->setParam('event', 'account.sesssions.failed') - ->setParam('resource', 'users/'.($profile ? $profile->getId() : '')) - ; - - throw new Exception('Invalid credentials', 401); // Wrong password or username - } - - $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; - $secret = Auth::tokenGenerator(); - $session = new Document([ - '$collection' => Database::SYSTEM_COLLECTION_TOKENS, - '$permissions' => ['read' => ['user:'.$profile->getId()], 'write' => ['user:'.$profile->getId()]], - 'type' => Auth::TOKEN_TYPE_LOGIN, - 'secret' => Auth::hash($secret), // On way hash encryption to protect DB leak - 'expire' => $expiry, - 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), - 'ip' => $request->getIP(), - ]); - - Authorization::setRole('user:'.$profile->getId()); - - $session = $projectDB->createDocument($session->getArrayCopy()); - - if (false === $session) { - throw new Exception('Failed saving session to DB', 500); - } - - $profile->setAttribute('tokens', $session, Document::SET_TYPE_APPEND); - - $profile = $projectDB->updateDocument($profile->getArrayCopy()); - - if (false === $profile) { - throw new Exception('Failed saving user to DB', 500); - } - - $webhook - ->setParam('payload', [ - 'name' => $profile->getAttribute('name', ''), - 'email' => $profile->getAttribute('email', ''), - ]) - ; + $protocol = Config::getParam('protocol'); + $profile = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + 'email='.$email, + ], + ]); + if (false == $profile || !Auth::passwordVerify($password, $profile->getAttribute('password'))) { $audit - ->setParam('userId', $profile->getId()) - ->setParam('event', 'account.sessions.create') - ->setParam('resource', 'users/'.$profile->getId()) + //->setParam('userId', $profile->getId()) + ->setParam('event', 'account.sesssions.failed') + ->setParam('resource', 'users/'.($profile ? $profile->getId() : '')) ; - if (!Config::getParam('domainVerification')) { - $response - ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($profile->getId(), $secret)])) - ; - } - + throw new Exception('Invalid credentials', 401); // Wrong password or username + } + + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $secret = Auth::tokenGenerator(); + $session = new Document([ + '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$permissions' => ['read' => ['user:'.$profile->getId()], 'write' => ['user:'.$profile->getId()]], + 'type' => Auth::TOKEN_TYPE_LOGIN, + 'secret' => Auth::hash($secret), // On way hash encryption to protect DB leak + 'expire' => $expiry, + 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), + 'ip' => $request->getIP(), + ]); + + Authorization::setRole('user:'.$profile->getId()); + + $session = $projectDB->createDocument($session->getArrayCopy()); + + if (false === $session) { + throw new Exception('Failed saving session to DB', 500); + } + + $profile->setAttribute('tokens', $session, Document::SET_TYPE_APPEND); + + $profile = $projectDB->updateDocument($profile->getArrayCopy()); + + if (false === $profile) { + throw new Exception('Failed saving user to DB', 500); + } + + $webhook + ->setParam('payload', [ + 'name' => $profile->getAttribute('name', ''), + 'email' => $profile->getAttribute('email', ''), + ]) + ; + + $audit + ->setParam('userId', $profile->getId()) + ->setParam('event', 'account.sessions.create') + ->setParam('resource', 'users/'.$profile->getId()) + ; + + if (!Config::getParam('domainVerification')) { $response - ->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($profile->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($profile->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($session->getArrayCopy(['$id', 'type', 'expire'])) + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($profile->getId(), $secret)])) ; } - ); + + $response + ->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($profile->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, Auth::encodeSession($profile->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($session->getArrayCopy(['$id', 'type', 'expire'])) + ; + }, ['request', 'response', 'projectDB', 'webhook', 'audit']); App::get('/v1/account/sessions/oauth2/:provider') ->desc('Create Account Session with OAuth2') @@ -251,41 +256,43 @@ App::get('/v1/account/sessions/oauth2/:provider') ->label('abuse-limit', 50) ->label('abuse-key', 'ip:{ip}') ->param('provider', '', function () { return new WhiteList(\array_keys(Config::getParam('providers'))); }, 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('providers'), function($node) {return (!$node['mock']);}))).'.') - ->param('success', $oauthDefaultSuccess, function () use ($clients) { return new Host($clients); }, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true) - ->param('failure', $oauthDefaultFailure, function () use ($clients) { return new Host($clients); }, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true) + ->param('success', $oauthDefaultSuccess, function ($clients) { return new Host($clients); }, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients']) + ->param('failure', $oauthDefaultFailure, function ($clients) { return new Host($clients); }, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients']) ->param('scopes', [], function () { return new ArrayList(new Text(128)); }, 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes.', true) - ->action( - function ($provider, $success, $failure, $scopes) use ($response, $request, $project) { - $protocol = Config::getParam('protocol'); - $callback = $protocol.'://'.$request->getServer('HTTP_HOST').'/v1/account/sessions/oauth2/callback/'.$provider.'/'.$project->getId(); - $appId = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Appid', ''); - $appSecret = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'); + ->action(function ($provider, $success, $failure, $scopes, $request, $response, $project) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ - $appSecret = \json_decode($appSecret, true); + $protocol = Config::getParam('protocol'); + $callback = $protocol.'://'.$request->getServer('HTTP_HOST').'/v1/account/sessions/oauth2/callback/'.$provider.'/'.$project->getId(); + $appId = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Appid', ''); + $appSecret = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'); - if (!empty($appSecret) && isset($appSecret['version'])) { - $key = App::getEnv('_APP_OPENSSL_KEY_V'.$appSecret['version']); - $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); - } + $appSecret = \json_decode($appSecret, true); - if (empty($appId) || empty($appSecret)) { - throw new Exception('This provider is disabled. Please configure the provider app ID and app secret key from your '.APP_NAME.' console to continue.', 412); - } - - $classname = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider); - - if (!\class_exists($classname)) { - throw new Exception('Provider is not supported', 501); - } - - $oauth2 = new $classname($appId, $appSecret, $callback, ['success' => $success, 'failure' => $failure], $scopes); - - $response - ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') - ->addHeader('Pragma', 'no-cache') - ->redirect($oauth2->getLoginURL()); + if (!empty($appSecret) && isset($appSecret['version'])) { + $key = App::getEnv('_APP_OPENSSL_KEY_V'.$appSecret['version']); + $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); } - ); + + if (empty($appId) || empty($appSecret)) { + throw new Exception('This provider is disabled. Please configure the provider app ID and app secret key from your '.APP_NAME.' console to continue.', 412); + } + + $classname = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider); + + if (!\class_exists($classname)) { + throw new Exception('Provider is not supported', 501); + } + + $oauth2 = new $classname($appId, $appSecret, $callback, ['success' => $success, 'failure' => $failure], $scopes); + + $response + ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + ->addHeader('Pragma', 'no-cache') + ->redirect($oauth2->getLoginURL()); + }, ['request', 'response', 'project']); App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->desc('OAuth2 Callback') @@ -297,18 +304,16 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->param('provider', '', function () { return new WhiteList(\array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.') ->param('code', '', function () { return new Text(1024); }, 'OAuth2 code.') ->param('state', '', function () { return new Text(2048); }, 'Login state params.', true) - ->action( - function ($projectId, $provider, $code, $state) use ($response) { - $domain = Config::getParam('domain'); - $protocol = Config::getParam('protocol'); - - $response - ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') - ->addHeader('Pragma', 'no-cache') - ->redirect($protocol.'://'.$domain.'/v1/account/sessions/oauth2/'.$provider.'/redirect?' - .\http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); - } - ); + ->action(function ($projectId, $provider, $code, $state, $response) { + $domain = Config::getParam('domain'); + $protocol = Config::getParam('protocol'); + + $response + ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + ->addHeader('Pragma', 'no-cache') + ->redirect($protocol.'://'.$domain.'/v1/account/sessions/oauth2/'.$provider.'/redirect?' + .\http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); + }, ['response']); App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->desc('OAuth2 Callback') @@ -321,18 +326,16 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->param('provider', '', function () { return new WhiteList(\array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.') ->param('code', '', function () { return new Text(1024); }, 'OAuth2 code.') ->param('state', '', function () { return new Text(2048); }, 'Login state params.', true) - ->action( - function ($projectId, $provider, $code, $state) use ($response) { - $domain = Config::getParam('domain'); - $protocol = Config::getParam('protocol'); - - $response - ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') - ->addHeader('Pragma', 'no-cache') - ->redirect($protocol.'://'.$domain.'/v1/account/sessions/oauth2/'.$provider.'/redirect?' - .\http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); - } - ); + ->action(function ($projectId, $provider, $code, $state, $response) { + $domain = Config::getParam('domain'); + $protocol = Config::getParam('protocol'); + + $response + ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + ->addHeader('Pragma', 'no-cache') + ->redirect($protocol.'://'.$domain.'/v1/account/sessions/oauth2/'.$provider.'/redirect?' + .\http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); + }, ['response']); App::get('/v1/account/sessions/oauth2/:provider/redirect') ->desc('OAuth2 Redirect') @@ -346,187 +349,192 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->param('provider', '', function () { return new WhiteList(\array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.') ->param('code', '', function () { return new Text(1024); }, 'OAuth2 code.') ->param('state', '', function () { return new Text(2048); }, 'OAuth2 state params.', true) - ->action( - function ($provider, $code, $state) use ($response, $request, $user, $projectDB, $project, $audit, $oauthDefaultSuccess) { - $protocol = Config::getParam('protocol'); - $callback = $protocol.'://'.$request->getServer('HTTP_HOST').'/v1/account/sessions/oauth2/callback/'.$provider.'/'.$project->getId(); - $defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => '']; - $validateURL = new URL(); + ->action(function ($provider, $code, $state, $request, $response, $project, $user, $projectDB, $audit) use ($oauthDefaultSuccess) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audit */ + + $protocol = Config::getParam('protocol'); + $callback = $protocol.'://'.$request->getServer('HTTP_HOST').'/v1/account/sessions/oauth2/callback/'.$provider.'/'.$project->getId(); + $defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => '']; + $validateURL = new URL(); - $appId = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Appid', ''); - $appSecret = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'); + $appId = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Appid', ''); + $appSecret = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'); - $appSecret = \json_decode($appSecret, true); + $appSecret = \json_decode($appSecret, true); - if (!empty($appSecret) && isset($appSecret['version'])) { - $key = App::getEnv('_APP_OPENSSL_KEY_V'.$appSecret['version']); - $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); + if (!empty($appSecret) && isset($appSecret['version'])) { + $key = App::getEnv('_APP_OPENSSL_KEY_V'.$appSecret['version']); + $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); + } + + $classname = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider); + + if (!\class_exists($classname)) { + throw new Exception('Provider is not supported', 501); + } + + $oauth2 = new $classname($appId, $appSecret, $callback); + + if (!empty($state)) { + try { + $state = \array_merge($defaultState, $oauth2->parseState($state)); + } catch (\Exception $exception) { + throw new Exception('Failed to parse login state params as passed from OAuth2 provider'); + } + } else { + $state = $defaultState; + } + + if (!$validateURL->isValid($state['success'])) { + throw new Exception('Invalid redirect URL for success login', 400); + } + + if (!empty($state['failure']) && !$validateURL->isValid($state['failure'])) { + throw new Exception('Invalid redirect URL for failure login', 400); + } + + $state['failure'] = null; + $accessToken = $oauth2->getAccessToken($code); + + if (empty($accessToken)) { + if (!empty($state['failure'])) { + $response->redirect($state['failure'], 301, 0); } - $classname = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider); + throw new Exception('Failed to obtain access token'); + } - if (!\class_exists($classname)) { - throw new Exception('Provider is not supported', 501); + $oauth2ID = $oauth2->getUserID($accessToken); + + if (empty($oauth2ID)) { + if (!empty($state['failure'])) { + $response->redirect($state['failure'], 301, 0); } - $oauth2 = new $classname($appId, $appSecret, $callback); + throw new Exception('Missing ID from OAuth2 provider', 400); + } - if (!empty($state)) { - try { - $state = \array_merge($defaultState, $oauth2->parseState($state)); - } catch (\Exception $exception) { - throw new Exception('Failed to parse login state params as passed from OAuth2 provider'); - } - } else { - $state = $defaultState; - } + $current = Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret); - if (!$validateURL->isValid($state['success'])) { - throw new Exception('Invalid redirect URL for success login', 400); - } + if ($current) { + $projectDB->deleteDocument($current); //throw new Exception('User already logged in', 401); + } - if (!empty($state['failure']) && !$validateURL->isValid($state['failure'])) { - throw new Exception('Invalid redirect URL for failure login', 400); - } - - $state['failure'] = null; - $accessToken = $oauth2->getAccessToken($code); + $user = (empty($user->getId())) ? $projectDB->getCollectionFirst([ // Get user by provider id + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + 'oauth2'.\ucfirst($provider).'='.$oauth2ID, + ], + ]) : $user; - if (empty($accessToken)) { - if (!empty($state['failure'])) { - $response->redirect($state['failure'], 301, 0); - } + if (empty($user)) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email + $name = $oauth2->getUserName($accessToken); + $email = $oauth2->getUserEmail($accessToken); - throw new Exception('Failed to obtain access token'); - } - - $oauth2ID = $oauth2->getUserID($accessToken); - - if (empty($oauth2ID)) { - if (!empty($state['failure'])) { - $response->redirect($state['failure'], 301, 0); - } - - throw new Exception('Missing ID from OAuth2 provider', 400); - } - - $current = Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret); - - if ($current) { - $projectDB->deleteDocument($current); //throw new Exception('User already logged in', 401); - } - - $user = (empty($user->getId())) ? $projectDB->getCollectionFirst([ // Get user by provider id + $user = $projectDB->getCollectionFirst([ // Get user by provider email address 'limit' => 1, 'filters' => [ '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'oauth2'.\ucfirst($provider).'='.$oauth2ID, + 'email='.$email, ], - ]) : $user; - - if (empty($user)) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email - $name = $oauth2->getUserName($accessToken); - $email = $oauth2->getUserEmail($accessToken); - - $user = $projectDB->getCollectionFirst([ // Get user by provider email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'email='.$email, - ], - ]); - - if (!$user || empty($user->getId())) { // Last option -> create user alone, generate random password - Authorization::disable(); - - try { - $user = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_USERS, - '$permissions' => ['read' => ['*'], 'write' => ['user:{self}']], - 'email' => $email, - 'emailVerification' => true, - 'status' => Auth::USER_STATUS_ACTIVATED, // Email should already be authenticated by OAuth2 provider - 'password' => Auth::passwordHash(Auth::passwordGenerator()), - 'password-update' => \time(), - 'registration' => \time(), - 'reset' => false, - 'name' => $name, - ], ['email' => $email]); - } catch (Duplicate $th) { - throw new Exception('Account already exists', 409); - } - - Authorization::enable(); - - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } - } - } - - // Create session token, verify user account and update OAuth2 ID and Access Token - - $secret = Auth::tokenGenerator(); - $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; - $session = new Document([ - '$collection' => Database::SYSTEM_COLLECTION_TOKENS, - '$permissions' => ['read' => ['user:'.$user['$id']], 'write' => ['user:'.$user['$id']]], - 'type' => Auth::TOKEN_TYPE_LOGIN, - 'secret' => Auth::hash($secret), // On way hash encryption to protect DB leak - 'expire' => $expiry, - 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), - 'ip' => $request->getIP(), ]); - $user - ->setAttribute('oauth2'.\ucfirst($provider), $oauth2ID) - ->setAttribute('oauth2'.\ucfirst($provider).'AccessToken', $accessToken) - ->setAttribute('status', Auth::USER_STATUS_ACTIVATED) - ->setAttribute('tokens', $session, Document::SET_TYPE_APPEND) - ; + if (!$user || empty($user->getId())) { // Last option -> create user alone, generate random password + Authorization::disable(); - Authorization::setRole('user:'.$user->getId()); + try { + $user = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_USERS, + '$permissions' => ['read' => ['*'], 'write' => ['user:{self}']], + 'email' => $email, + 'emailVerification' => true, + 'status' => Auth::USER_STATUS_ACTIVATED, // Email should already be authenticated by OAuth2 provider + 'password' => Auth::passwordHash(Auth::passwordGenerator()), + 'password-update' => \time(), + 'registration' => \time(), + 'reset' => false, + 'name' => $name, + ], ['email' => $email]); + } catch (Duplicate $th) { + throw new Exception('Account already exists', 409); + } - $user = $projectDB->updateDocument($user->getArrayCopy()); + Authorization::enable(); - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } } + } - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.sessions.create') - ->setParam('resource', 'users/'.$user->getId()) - ->setParam('data', ['provider' => $provider]) - ; + // Create session token, verify user account and update OAuth2 ID and Access Token - if (!Config::getParam('domainVerification')) { - $response - ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) - ; - } - - // Add keys for non-web platforms - TODO - add verification phase to aviod session sniffing - if (parse_url($state['success'], PHP_URL_PATH) === $oauthDefaultSuccess) { - $state['success'] = URLParser::parse($state['success']); - $query = URLParser::parseQuery($state['success']['query']); - $query['project'] = $project->getId(); - $query['domain'] = COOKIE_DOMAIN; - $query['key'] = Auth::$cookieName; - $query['secret'] = Auth::encodeSession($user->getId(), $secret); - $state['success']['query'] = URLParser::unparseQuery($query); - $state['success'] = URLParser::unparse($state['success']); - } + $secret = Auth::tokenGenerator(); + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $session = new Document([ + '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$permissions' => ['read' => ['user:'.$user['$id']], 'write' => ['user:'.$user['$id']]], + 'type' => Auth::TOKEN_TYPE_LOGIN, + 'secret' => Auth::hash($secret), // On way hash encryption to protect DB leak + 'expire' => $expiry, + 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), + 'ip' => $request->getIP(), + ]); + $user + ->setAttribute('oauth2'.\ucfirst($provider), $oauth2ID) + ->setAttribute('oauth2'.\ucfirst($provider).'AccessToken', $accessToken) + ->setAttribute('status', Auth::USER_STATUS_ACTIVATED) + ->setAttribute('tokens', $session, Document::SET_TYPE_APPEND) + ; + + Authorization::setRole('user:'.$user->getId()); + + $user = $projectDB->updateDocument($user->getArrayCopy()); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + $audit + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.sessions.create') + ->setParam('resource', 'users/'.$user->getId()) + ->setParam('data', ['provider' => $provider]) + ; + + if (!Config::getParam('domainVerification')) { $response - ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') - ->addHeader('Pragma', 'no-cache') - ->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) - ->redirect($state['success']) + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) ; } - ); + + // Add keys for non-web platforms - TODO - add verification phase to aviod session sniffing + if (parse_url($state['success'], PHP_URL_PATH) === $oauthDefaultSuccess) { + $state['success'] = URLParser::parse($state['success']); + $query = URLParser::parseQuery($state['success']['query']); + $query['project'] = $project->getId(); + $query['domain'] = COOKIE_DOMAIN; + $query['key'] = Auth::$cookieName; + $query['secret'] = Auth::encodeSession($user->getId(), $secret); + $state['success']['query'] = URLParser::unparseQuery($query); + $state['success'] = URLParser::unparse($state['success']); + } + + $response + ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + ->addHeader('Pragma', 'no-cache') + ->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) + ->redirect($state['success']) + ; + }, ['request', 'response', 'project', 'user', 'projectDB', 'audit']); App::get('/v1/account') ->desc('Get Account') @@ -537,21 +545,21 @@ App::get('/v1/account') ->label('sdk.method', 'get') ->label('sdk.description', '/docs/references/account/get.md') ->label('sdk.response', ['200' => 'user']) - ->inject('response') - ->action( - function ($response) use (&$user, $oauth2Keys) { - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'emailVerification', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); - } - ); + ->action(function ($response, $user) use ($oauth2Keys) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + + $response->json(\array_merge($user->getArrayCopy(\array_merge( + [ + '$id', + 'email', + 'emailVerification', + 'registration', + 'name', + ], + $oauth2Keys + )), ['roles' => Authorization::getRoles()])); + }, ['response', ['user']]); App::get('/v1/account/prefs') ->desc('Get Account Preferences') @@ -561,20 +569,21 @@ App::get('/v1/account/prefs') ->label('sdk.namespace', 'account') ->label('sdk.method', 'getPrefs') ->label('sdk.description', '/docs/references/account/get-prefs.md') - ->action( - function () use ($response, $user) { - $prefs = $user->getAttribute('prefs', '{}'); + ->action(function ($response, $user) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ - try { - $prefs = \json_decode($prefs, true); - $prefs = ($prefs) ? $prefs : []; - } catch (\Exception $error) { - throw new Exception('Failed to parse prefs', 500); - } + $prefs = $user->getAttribute('prefs', '{}'); - $response->json($prefs); + try { + $prefs = \json_decode($prefs, true); + $prefs = ($prefs) ? $prefs : []; + } catch (\Exception $error) { + throw new Exception('Failed to parse prefs', 500); } - ); + + $response->json($prefs); + }, ['response', 'user']); App::get('/v1/account/sessions') ->desc('Get Account Sessions') @@ -584,56 +593,58 @@ App::get('/v1/account/sessions') ->label('sdk.namespace', 'account') ->label('sdk.method', 'getSessions') ->label('sdk.description', '/docs/references/account/get-sessions.md') - ->action( - function () use ($response, $user) { - $tokens = $user->getAttribute('tokens', []); - $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); - $sessions = []; - $current = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_LOGIN, Auth::$secret); - $index = 0; - $countries = Locale::getText('countries'); + ->action(function ($response, $user, $locale) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Utopia\Locale\Locale $locale */ - foreach ($tokens as $token) { /* @var $token Document */ - if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) { - continue; - } + $tokens = $user->getAttribute('tokens', []); + $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); + $sessions = []; + $current = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_LOGIN, Auth::$secret); + $index = 0; + $countries = $locale->getText('countries'); - $userAgent = (!empty($token->getAttribute('userAgent'))) ? $token->getAttribute('userAgent') : 'UNKNOWN'; - - $dd = new DeviceDetector($userAgent); - - // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - // $dd->skipBotDetection(); - - $dd->parse(); - - $sessions[$index] = [ - '$id' => $token->getId(), - 'OS' => $dd->getOs(), - 'client' => $dd->getClient(), - 'device' => $dd->getDevice(), - 'brand' => $dd->getBrand(), - 'model' => $dd->getModel(), - 'ip' => $token->getAttribute('ip', ''), - 'geo' => [], - 'current' => ($current == $token->getId()) ? true : false, - ]; - - try { - $record = $reader->country($token->getAttribute('ip', '')); - $sessions[$index]['geo']['isoCode'] = \strtolower($record->country->isoCode); - $sessions[$index]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown'); - } catch (\Exception $e) { - $sessions[$index]['geo']['isoCode'] = '--'; - $sessions[$index]['geo']['country'] = Locale::getText('locale.country.unknown'); - } - - ++$index; + foreach ($tokens as $token) { /* @var $token Document */ + if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) { + continue; } - $response->json($sessions); + $userAgent = (!empty($token->getAttribute('userAgent'))) ? $token->getAttribute('userAgent') : 'UNKNOWN'; + + $dd = new DeviceDetector($userAgent); + + // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + // $dd->skipBotDetection(); + + $dd->parse(); + + $sessions[$index] = [ + '$id' => $token->getId(), + 'OS' => $dd->getOs(), + 'client' => $dd->getClient(), + 'device' => $dd->getDevice(), + 'brand' => $dd->getBrand(), + 'model' => $dd->getModel(), + 'ip' => $token->getAttribute('ip', ''), + 'geo' => [], + 'current' => ($current == $token->getId()) ? true : false, + ]; + + try { + $record = $reader->country($token->getAttribute('ip', '')); + $sessions[$index]['geo']['isoCode'] = \strtolower($record->country->isoCode); + $sessions[$index]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : $locale->getText('locale.country.unknown'); + } catch (\Exception $e) { + $sessions[$index]['geo']['isoCode'] = '--'; + $sessions[$index]['geo']['country'] = $locale->getText('locale.country.unknown'); + } + + ++$index; } - ); + + $response->json($sessions); + }, ['response', 'user', 'locale']); App::get('/v1/account/logs') ->desc('Get Account Logs') @@ -643,70 +654,73 @@ App::get('/v1/account/logs') ->label('sdk.namespace', 'account') ->label('sdk.method', 'getLogs') ->label('sdk.description', '/docs/references/account/get-logs.md') - ->action( - function () use ($response, $register, $project, $user) { - $adapter = new AuditAdapter($register->get('db')); - $adapter->setNamespace('app_'.$project->getId()); + ->action(function ($response, $register, $project, $user) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Document $user */ + /** @var Utopia\Locale\Locale $locale */ - $audit = new Audit($adapter); - $countries = Locale::getText('countries'); + $adapter = new AuditAdapter($register->get('db')); + $adapter->setNamespace('app_'.$project->getId()); - $logs = $audit->getLogsByUserAndActions($user->getId(), [ - 'account.create', - 'account.delete', - 'account.update.name', - 'account.update.email', - 'account.update.password', - 'account.update.prefs', - 'account.sessions.create', - 'account.sessions.delete', - 'account.recovery.create', - 'account.recovery.update', - 'account.verification.create', - 'account.verification.update', - 'teams.membership.create', - 'teams.membership.update', - 'teams.membership.delete', - ]); + $audit = new Audit($adapter); + $countries = $locale->getText('countries'); - $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); - $output = []; + $logs = $audit->getLogsByUserAndActions($user->getId(), [ + 'account.create', + 'account.delete', + 'account.update.name', + 'account.update.email', + 'account.update.password', + 'account.update.prefs', + 'account.sessions.create', + 'account.sessions.delete', + 'account.recovery.create', + 'account.recovery.update', + 'account.verification.create', + 'account.verification.update', + 'teams.membership.create', + 'teams.membership.update', + 'teams.membership.delete', + ]); - foreach ($logs as $i => &$log) { - $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; + $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); + $output = []; - $dd = new DeviceDetector($log['userAgent']); + foreach ($logs as $i => &$log) { + $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; - $dd->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + $dd = new DeviceDetector($log['userAgent']); - $dd->parse(); + $dd->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - $output[$i] = [ - 'event' => $log['event'], - 'ip' => $log['ip'], - 'time' => \strtotime($log['time']), - 'OS' => $dd->getOs(), - 'client' => $dd->getClient(), - 'device' => $dd->getDevice(), - 'brand' => $dd->getBrand(), - 'model' => $dd->getModel(), - 'geo' => [], - ]; + $dd->parse(); - try { - $record = $reader->country($log['ip']); - $output[$i]['geo']['isoCode'] = \strtolower($record->country->isoCode); - $output[$i]['geo']['country'] = $record->country->name; - $output[$i]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown'); - } catch (\Exception $e) { - $output[$i]['geo']['isoCode'] = '--'; - $output[$i]['geo']['country'] = Locale::getText('locale.country.unknown'); - } + $output[$i] = [ + 'event' => $log['event'], + 'ip' => $log['ip'], + 'time' => \strtotime($log['time']), + 'OS' => $dd->getOs(), + 'client' => $dd->getClient(), + 'device' => $dd->getDevice(), + 'brand' => $dd->getBrand(), + 'model' => $dd->getModel(), + 'geo' => [], + ]; + + try { + $record = $reader->country($log['ip']); + $output[$i]['geo']['isoCode'] = \strtolower($record->country->isoCode); + $output[$i]['geo']['country'] = $record->country->name; + $output[$i]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : $locale->getText('locale.country.unknown'); + } catch (\Exception $e) { + $output[$i]['geo']['isoCode'] = '--'; + $output[$i]['geo']['country'] = $locale->getText('locale.country.unknown'); } - - $response->json($output); } - ); + + $response->json($output); + }, ['response', 'register', 'project', 'user']); App::patch('/v1/account/name') ->desc('Update Account Name') @@ -718,33 +732,36 @@ App::patch('/v1/account/name') ->label('sdk.method', 'updateName') ->label('sdk.description', '/docs/references/account/update-name.md') ->param('name', '', function () { return new Text(100); }, 'User name.') - ->action( - function ($name) use ($response, $user, $projectDB, $audit, $oauth2Keys) { - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'name' => $name, - ])); + ->action(function ($name, $response, $user, $projectDB, $audit) use ($oauth2Keys) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audit */ - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'name' => $name, + ])); - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.update.name') - ->setParam('resource', 'users/'.$user->getId()) - ; - - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); } - ); + + $audit + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.update.name') + ->setParam('resource', 'users/'.$user->getId()) + ; + + $response->json(\array_merge($user->getArrayCopy(\array_merge( + [ + '$id', + 'email', + 'registration', + 'name', + ], + $oauth2Keys + )), ['roles' => Authorization::getRoles()])); + }, ['response', 'user', 'projectDB', 'audit']); App::patch('/v1/account/password') ->desc('Update Account Password') @@ -757,37 +774,40 @@ App::patch('/v1/account/password') ->label('sdk.description', '/docs/references/account/update-password.md') ->param('password', '', function () { return new Password(); }, 'New user password. Must be between 6 to 32 chars.') ->param('oldPassword', '', function () { return new Password(); }, 'Old user password. Must be between 6 to 32 chars.') - ->action( - function ($password, $oldPassword) use ($response, $user, $projectDB, $audit, $oauth2Keys) { - if (!Auth::passwordVerify($oldPassword, $user->getAttribute('password'))) { // Double check user password - throw new Exception('Invalid credentials', 401); - } + ->action(function ($password, $oldPassword, $response, $user, $projectDB, $audit) use ($oauth2Keys) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audit */ - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'password' => Auth::passwordHash($password), - ])); - - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } - - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.update.password') - ->setParam('resource', 'users/'.$user->getId()) - ; - - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); + if (!Auth::passwordVerify($oldPassword, $user->getAttribute('password'))) { // Double check user password + throw new Exception('Invalid credentials', 401); } - ); + + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'password' => Auth::passwordHash($password), + ])); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + $audit + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.update.password') + ->setParam('resource', 'users/'.$user->getId()) + ; + + $response->json(\array_merge($user->getArrayCopy(\array_merge( + [ + '$id', + 'email', + 'registration', + 'name', + ], + $oauth2Keys + )), ['roles' => Authorization::getRoles()])); + }, ['response', 'user', 'projectDB', 'audit']); App::patch('/v1/account/email') ->desc('Update Account Email') @@ -800,52 +820,55 @@ App::patch('/v1/account/email') ->label('sdk.description', '/docs/references/account/update-email.md') ->param('email', '', function () { return new Email(); }, 'User email.') ->param('password', '', function () { return new Password(); }, 'User password. Must be between 6 to 32 chars.') - ->action( - function ($email, $password) use ($response, $user, $projectDB, $audit, $oauth2Keys) { - if (!Auth::passwordVerify($password, $user->getAttribute('password'))) { // Double check user password - throw new Exception('Invalid credentials', 401); - } + ->action(function ($email, $password, $response, $user, $projectDB, $audit) use ($oauth2Keys) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audit */ - $profile = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'email='.$email, - ], - ]); - - if (!empty($profile)) { - throw new Exception('User already registered', 400); - } - - // TODO after this user needs to confirm mail again - - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'email' => $email, - 'emailVerification' => false, - ])); - - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } - - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.update.email') - ->setParam('resource', 'users/'.$user->getId()) - ; - - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); + if (!Auth::passwordVerify($password, $user->getAttribute('password'))) { // Double check user password + throw new Exception('Invalid credentials', 401); } - ); + + $profile = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + 'email='.$email, + ], + ]); + + if (!empty($profile)) { + throw new Exception('User already registered', 400); + } + + // TODO after this user needs to confirm mail again + + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'email' => $email, + 'emailVerification' => false, + ])); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + $audit + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.update.email') + ->setParam('resource', 'users/'.$user->getId()) + ; + + $response->json(\array_merge($user->getArrayCopy(\array_merge( + [ + '$id', + 'email', + 'registration', + 'name', + ], + $oauth2Keys + )), ['roles' => Authorization::getRoles()])); + }, ['response', 'user', 'projectDB', 'audit']); App::patch('/v1/account/prefs') ->desc('Update Account Preferences') @@ -857,36 +880,39 @@ App::patch('/v1/account/prefs') ->label('sdk.method', 'updatePrefs') ->param('prefs', '', function () { return new Assoc();}, 'Prefs key-value JSON object.') ->label('sdk.description', '/docs/references/account/update-prefs.md') - ->action( - function ($prefs) use ($response, $user, $projectDB, $audit) { - $old = \json_decode($user->getAttribute('prefs', '{}'), true); - $old = ($old) ? $old : []; + ->action(function ($prefs, $response, $user, $projectDB, $audit) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audit */ - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'prefs' => \json_encode(\array_merge($old, $prefs)), - ])); + $old = \json_decode($user->getAttribute('prefs', '{}'), true); + $old = ($old) ? $old : []; - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'prefs' => \json_encode(\array_merge($old, $prefs)), + ])); - $audit - ->setParam('event', 'account.update.prefs') - ->setParam('resource', 'users/'.$user->getId()) - ; - - $prefs = $user->getAttribute('prefs', '{}'); - - try { - $prefs = \json_decode($prefs, true); - $prefs = ($prefs) ? $prefs : []; - } catch (\Exception $error) { - throw new Exception('Failed to parse prefs', 500); - } - - $response->json($prefs); + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); } - ); + + $audit + ->setParam('event', 'account.update.prefs') + ->setParam('resource', 'users/'.$user->getId()) + ; + + $prefs = $user->getAttribute('prefs', '{}'); + + try { + $prefs = \json_decode($prefs, true); + $prefs = ($prefs) ? $prefs : []; + } catch (\Exception $error) { + throw new Exception('Failed to parse prefs', 500); + } + + $response->json($prefs); + }, ['response', 'user', 'projectDB', 'audit']); App::delete('/v1/account') ->desc('Delete Account') @@ -897,52 +923,56 @@ App::delete('/v1/account') ->label('sdk.namespace', 'account') ->label('sdk.method', 'delete') ->label('sdk.description', '/docs/references/account/delete.md') - ->action( - function () use ($response, $user, $projectDB, $audit, $webhook) { - $protocol = Config::getParam('protocol'); - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'status' => Auth::USER_STATUS_BLOCKED, - ])); + ->action(function ($response, $user, $projectDB, $audit, $webhook) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audit */ + /** @var Appwrite\Event\Event $webhook */ - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } + $protocol = Config::getParam('protocol'); + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'status' => Auth::USER_STATUS_BLOCKED, + ])); - //TODO delete all tokens or only current session? - //TODO delete all user data according to GDPR. Make sure everything is backed up and backups are deleted later - /* - * Data to delete - * * Tokens - * * Memberships - */ + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.delete') - ->setParam('resource', 'users/'.$user->getId()) - ->setParam('data', $user->getArrayCopy()) - ; + //TODO delete all tokens or only current session? + //TODO delete all user data according to GDPR. Make sure everything is backed up and backups are deleted later + /* + * Data to delete + * * Tokens + * * Memberships + */ - $webhook - ->setParam('payload', [ - 'name' => $user->getAttribute('name', ''), - 'email' => $user->getAttribute('email', ''), - ]) - ; + $audit + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.delete') + ->setParam('resource', 'users/'.$user->getId()) + ->setParam('data', $user->getArrayCopy()) + ; - if (!Config::getParam('domainVerification')) { - $response - ->addHeader('X-Fallback-Cookies', \json_encode([])) - ; - } + $webhook + ->setParam('payload', [ + 'name' => $user->getAttribute('name', ''), + 'email' => $user->getAttribute('email', ''), + ]) + ; + if (!Config::getParam('domainVerification')) { $response - ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) - ->noContent() + ->addHeader('X-Fallback-Cookies', \json_encode([])) ; } - ); + + $response + ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) + ->noContent() + ; + }, ['response', 'user', 'projectDB', 'audit', 'webhook']); App::delete('/v1/account/sessions/:sessionId') ->desc('Delete Account Session') @@ -955,71 +985,22 @@ App::delete('/v1/account/sessions/:sessionId') ->label('sdk.description', '/docs/references/account/delete-session.md') ->label('abuse-limit', 100) ->param('sessionId', null, function () { return new UID(); }, 'Session unique ID. Use the string \'current\' to delete the current device session.') - ->action( - function ($sessionId) use ($response, $user, $projectDB, $webhook, $audit) { - $protocol = Config::getParam('protocol'); - $sessionId = ($sessionId === 'current') - ? Auth::tokenVerify($user->getAttribute('tokens'), Auth::TOKEN_TYPE_LOGIN, Auth::$secret) - : $sessionId; - - $tokens = $user->getAttribute('tokens', []); + ->action(function ($sessionId, $response, $user, $projectDB, $audit, $webhook) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audit */ + /** @var Appwrite\Event\Event $webhook */ - foreach ($tokens as $token) { /* @var $token Document */ - if (($sessionId == $token->getId()) && Auth::TOKEN_TYPE_LOGIN == $token->getAttribute('type')) { - if (!$projectDB->deleteDocument($token->getId())) { - throw new Exception('Failed to remove token from DB', 500); - } + $protocol = Config::getParam('protocol'); + $sessionId = ($sessionId === 'current') + ? Auth::tokenVerify($user->getAttribute('tokens'), Auth::TOKEN_TYPE_LOGIN, Auth::$secret) + : $sessionId; + + $tokens = $user->getAttribute('tokens', []); - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.sessions.delete') - ->setParam('resource', '/user/'.$user->getId()) - ; - - $webhook - ->setParam('payload', [ - 'name' => $user->getAttribute('name', ''), - 'email' => $user->getAttribute('email', ''), - ]) - ; - - if (!Config::getParam('domainVerification')) { - $response - ->addHeader('X-Fallback-Cookies', \json_encode([])) - ; - } - - if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too - $response - ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) - ; - } - - return $response->noContent(); - } - } - - throw new Exception('Session not found', 404); - } - ); - -App::delete('/v1/account/sessions') - ->desc('Delete All Account Sessions') - ->groups(['api', 'account']) - ->label('scope', 'account') - ->label('webhook', 'account.sessions.delete') - ->label('sdk.platform', [APP_PLATFORM_CLIENT]) - ->label('sdk.namespace', 'account') - ->label('sdk.method', 'deleteSessions') - ->label('sdk.description', '/docs/references/account/delete-sessions.md') - ->label('abuse-limit', 100) - ->action( - function () use ($response, $user, $projectDB, $audit, $webhook) { - $protocol = Config::getParam('protocol'); - $tokens = $user->getAttribute('tokens', []); - - foreach ($tokens as $token) { /* @var $token Document */ + foreach ($tokens as $token) { /* @var $token Document */ + if (($sessionId == $token->getId()) && Auth::TOKEN_TYPE_LOGIN == $token->getAttribute('type')) { if (!$projectDB->deleteDocument($token->getId())) { throw new Exception('Failed to remove token from DB', 500); } @@ -1049,11 +1030,68 @@ App::delete('/v1/account/sessions') ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) ; } + + return $response->noContent(); + } + } + + throw new Exception('Session not found', 404); + }, ['response', 'user', 'projectDB', 'audit', 'webhook']); + +App::delete('/v1/account/sessions') + ->desc('Delete All Account Sessions') + ->groups(['api', 'account']) + ->label('scope', 'account') + ->label('webhook', 'account.sessions.delete') + ->label('sdk.platform', [APP_PLATFORM_CLIENT]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'deleteSessions') + ->label('sdk.description', '/docs/references/account/delete-sessions.md') + ->label('abuse-limit', 100) + ->action(function ($response, $user, $projectDB, $audit, $webhook) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audit */ + /** @var Appwrite\Event\Event $webhook */ + + $protocol = Config::getParam('protocol'); + $tokens = $user->getAttribute('tokens', []); + + foreach ($tokens as $token) { /* @var $token Document */ + if (!$projectDB->deleteDocument($token->getId())) { + throw new Exception('Failed to remove token from DB', 500); } - $response->noContent(); + $audit + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.sessions.delete') + ->setParam('resource', '/user/'.$user->getId()) + ; + + $webhook + ->setParam('payload', [ + 'name' => $user->getAttribute('name', ''), + 'email' => $user->getAttribute('email', ''), + ]) + ; + + if (!Config::getParam('domainVerification')) { + $response + ->addHeader('X-Fallback-Cookies', \json_encode([])) + ; + } + + if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + $response + ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) + ; + } } - ); + + $response->noContent(); + }, ['response', 'user', 'projectDB', 'audit', 'webhook']); App::post('/v1/account/recovery') ->desc('Create Password Recovery') @@ -1066,93 +1104,99 @@ App::post('/v1/account/recovery') ->label('abuse-limit', 10) ->label('abuse-key', 'url:{url},email:{param-email}') ->param('email', '', function () { return new Email(); }, 'User email.') - ->param('url', '', function () use ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.') - ->action( - function ($email, $url) use ($request, $response, $projectDB, $mail, $audit, $project) { - $profile = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'email='.$email, - ], - ]); + ->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients']) + ->action(function ($email, $url, $request, $response, $projectDB, $project, $locale, $mail, $audit) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Database\Document $project */ + /** @var Utopia\Locale\Locale $locale */ + /** @var Appwrite\Event\Event $mail */ + /** @var Appwrite\Event\Event $audit */ - if (empty($profile)) { - throw new Exception('User not found', 404); // TODO maybe hide this - } + $profile = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + 'email='.$email, + ], + ]); - $secret = Auth::tokenGenerator(); - $recovery = new Document([ - '$collection' => Database::SYSTEM_COLLECTION_TOKENS, - '$permissions' => ['read' => ['user:'.$profile->getId()], 'write' => ['user:'.$profile->getId()]], - 'type' => Auth::TOKEN_TYPE_RECOVERY, - 'secret' => Auth::hash($secret), // On way hash encryption to protect DB leak - 'expire' => \time() + Auth::TOKEN_EXPIRATION_RECOVERY, - 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), - 'ip' => $request->getIP(), - ]); - - Authorization::setRole('user:'.$profile->getId()); - - $recovery = $projectDB->createDocument($recovery->getArrayCopy()); - - if (false === $recovery) { - throw new Exception('Failed saving recovery to DB', 500); - } - - $profile->setAttribute('tokens', $recovery, Document::SET_TYPE_APPEND); - - $profile = $projectDB->updateDocument($profile->getArrayCopy()); - - if (false === $profile) { - throw new Exception('Failed to save user to DB', 500); - } - - $url = Template::parseURL($url); - $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $profile->getId(), 'secret' => $secret]); - $url = Template::unParseURL($url); - - $body = new Template(__DIR__.'/../../config/locales/templates/_base.tpl'); - $content = new Template(__DIR__.'/../../config/locales/templates/'.Locale::getText('account.emails.recovery.body')); - $cta = new Template(__DIR__.'/../../config/locales/templates/_cta.tpl'); - - $body - ->setParam('{{content}}', $content->render()) - ->setParam('{{cta}}', $cta->render()) - ->setParam('{{title}}', Locale::getText('account.emails.recovery.title')) - ->setParam('{{direction}}', Locale::getText('settings.direction')) - ->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]'])) - ->setParam('{{name}}', $profile->getAttribute('name')) - ->setParam('{{redirect}}', $url) - ->setParam('{{bg-body}}', '#f6f6f6') - ->setParam('{{bg-content}}', '#ffffff') - ->setParam('{{bg-cta}}', '#3498db') - ->setParam('{{bg-cta-hover}}', '#34495e') - ->setParam('{{text-content}}', '#000000') - ->setParam('{{text-cta}}', '#ffffff') - ; - - $mail - ->setParam('event', 'account.recovery.create') - ->setParam('recipient', $profile->getAttribute('email', '')) - ->setParam('name', $profile->getAttribute('name', '')) - ->setParam('subject', Locale::getText('account.emails.recovery.title')) - ->setParam('body', $body->render()) - ->trigger(); - ; - - $audit - ->setParam('userId', $profile->getId()) - ->setParam('event', 'account.recovery.create') - ->setParam('resource', 'users/'.$profile->getId()) - ; - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($recovery->getArrayCopy(['$id', 'type', 'expire'])) - ; + if (empty($profile)) { + throw new Exception('User not found', 404); // TODO maybe hide this } - ); + + $secret = Auth::tokenGenerator(); + $recovery = new Document([ + '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$permissions' => ['read' => ['user:'.$profile->getId()], 'write' => ['user:'.$profile->getId()]], + 'type' => Auth::TOKEN_TYPE_RECOVERY, + 'secret' => Auth::hash($secret), // On way hash encryption to protect DB leak + 'expire' => \time() + Auth::TOKEN_EXPIRATION_RECOVERY, + 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), + 'ip' => $request->getIP(), + ]); + + Authorization::setRole('user:'.$profile->getId()); + + $recovery = $projectDB->createDocument($recovery->getArrayCopy()); + + if (false === $recovery) { + throw new Exception('Failed saving recovery to DB', 500); + } + + $profile->setAttribute('tokens', $recovery, Document::SET_TYPE_APPEND); + + $profile = $projectDB->updateDocument($profile->getArrayCopy()); + + if (false === $profile) { + throw new Exception('Failed to save user to DB', 500); + } + + $url = Template::parseURL($url); + $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $profile->getId(), 'secret' => $secret]); + $url = Template::unParseURL($url); + + $body = new Template(__DIR__.'/../../config/locales/templates/_base.tpl'); + $content = new Template(__DIR__.'/../../config/locales/templates/'.$locale->getText('account.emails.recovery.body')); + $cta = new Template(__DIR__.'/../../config/locales/templates/_cta.tpl'); + + $body + ->setParam('{{content}}', $content->render()) + ->setParam('{{cta}}', $cta->render()) + ->setParam('{{title}}', $locale->getText('account.emails.recovery.title')) + ->setParam('{{direction}}', $locale->getText('settings.direction')) + ->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]'])) + ->setParam('{{name}}', $profile->getAttribute('name')) + ->setParam('{{redirect}}', $url) + ->setParam('{{bg-body}}', '#f6f6f6') + ->setParam('{{bg-content}}', '#ffffff') + ->setParam('{{bg-cta}}', '#3498db') + ->setParam('{{bg-cta-hover}}', '#34495e') + ->setParam('{{text-content}}', '#000000') + ->setParam('{{text-cta}}', '#ffffff') + ; + + $mail + ->setParam('event', 'account.recovery.create') + ->setParam('recipient', $profile->getAttribute('email', '')) + ->setParam('name', $profile->getAttribute('name', '')) + ->setParam('subject', $locale->getText('account.emails.recovery.title')) + ->setParam('body', $body->render()) + ->trigger(); + ; + + $audit + ->setParam('userId', $profile->getId()) + ->setParam('event', 'account.recovery.create') + ->setParam('resource', 'users/'.$profile->getId()) + ; + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($recovery->getArrayCopy(['$id', 'type', 'expire'])) + ; + }, ['request', 'response', 'projectDB', 'project', 'locale', 'mail', 'audit']); App::put('/v1/account/recovery') ->desc('Complete Password Recovery') @@ -1168,61 +1212,63 @@ App::put('/v1/account/recovery') ->param('secret', '', function () { return new Text(256); }, 'Valid reset token.') ->param('password', '', function () { return new Password(); }, 'New password. Must be between 6 to 32 chars.') ->param('passwordAgain', '', function () {return new Password(); }, 'New password again. Must be between 6 to 32 chars.') - ->action( - function ($userId, $secret, $password, $passwordAgain) use ($response, $projectDB, $audit) { - if ($password !== $passwordAgain) { - throw new Exception('Passwords must match', 400); - } - - $profile = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - '$id='.$userId, - ], - ]); - - if (empty($profile)) { - throw new Exception('User not found', 404); // TODO maybe hide this - } - - $recovery = Auth::tokenVerify($profile->getAttribute('tokens', []), Auth::TOKEN_TYPE_RECOVERY, $secret); - - if (!$recovery) { - throw new Exception('Invalid recovery token', 401); - } - - Authorization::setRole('user:'.$profile->getId()); - - $profile = $projectDB->updateDocument(\array_merge($profile->getArrayCopy(), [ - 'password' => Auth::passwordHash($password), - 'password-update' => \time(), - 'emailVerification' => true, - ])); - - if (false === $profile) { - throw new Exception('Failed saving user to DB', 500); - } - - /** - * We act like we're updating and validating - * the recovery token but actually we don't need it anymore. - */ - if (!$projectDB->deleteDocument($recovery)) { - throw new Exception('Failed to remove recovery from DB', 500); - } - - $audit - ->setParam('userId', $profile->getId()) - ->setParam('event', 'account.recovery.update') - ->setParam('resource', 'users/'.$profile->getId()) - ; - - $recovery = $profile->search('$id', $recovery, $profile->getAttribute('tokens', [])); - - $response->json($recovery->getArrayCopy(['$id', 'type', 'expire'])); + ->action(function ($userId, $secret, $password, $passwordAgain, $response, $projectDB, $audit) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audit */ + + if ($password !== $passwordAgain) { + throw new Exception('Passwords must match', 400); } - ); + + $profile = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + '$id='.$userId, + ], + ]); + + if (empty($profile)) { + throw new Exception('User not found', 404); // TODO maybe hide this + } + + $recovery = Auth::tokenVerify($profile->getAttribute('tokens', []), Auth::TOKEN_TYPE_RECOVERY, $secret); + + if (!$recovery) { + throw new Exception('Invalid recovery token', 401); + } + + Authorization::setRole('user:'.$profile->getId()); + + $profile = $projectDB->updateDocument(\array_merge($profile->getArrayCopy(), [ + 'password' => Auth::passwordHash($password), + 'password-update' => \time(), + 'emailVerification' => true, + ])); + + if (false === $profile) { + throw new Exception('Failed saving user to DB', 500); + } + + /** + * We act like we're updating and validating + * the recovery token but actually we don't need it anymore. + */ + if (!$projectDB->deleteDocument($recovery)) { + throw new Exception('Failed to remove recovery from DB', 500); + } + + $audit + ->setParam('userId', $profile->getId()) + ->setParam('event', 'account.recovery.update') + ->setParam('resource', 'users/'.$profile->getId()) + ; + + $recovery = $profile->search('$id', $recovery, $profile->getAttribute('tokens', [])); + + $response->json($recovery->getArrayCopy(['$id', 'type', 'expire'])); + }, ['response', 'projectDB', 'audit']); App::post('/v1/account/verification') ->desc('Create Email Verification') @@ -1234,82 +1280,89 @@ App::post('/v1/account/verification') ->label('sdk.description', '/docs/references/account/create-verification.md') ->label('abuse-limit', 10) ->label('abuse-key', 'url:{url},email:{param-email}') - ->param('url', '', function () use ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.') // TODO add built-in confirm page - ->action( - function ($url) use ($request, $response, $mail, $user, $project, $projectDB, $audit) { - $verificationSecret = Auth::tokenGenerator(); + ->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients']) // TODO add built-in confirm page + ->action(function ($url, $request, $response, $project, $user, $projectDB, $locale, $audit, $mail) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Locale\Locale $locale */ + /** @var Appwrite\Event\Event $audit */ + /** @var Appwrite\Event\Event $mail */ + + $verificationSecret = Auth::tokenGenerator(); + + $verification = new Document([ + '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->getId()]], + 'type' => Auth::TOKEN_TYPE_VERIFICATION, + 'secret' => Auth::hash($verificationSecret), // On way hash encryption to protect DB leak + 'expire' => \time() + Auth::TOKEN_EXPIRATION_CONFIRM, + 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), + 'ip' => $request->getIP(), + ]); - $verification = new Document([ - '$collection' => Database::SYSTEM_COLLECTION_TOKENS, - '$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->getId()]], - 'type' => Auth::TOKEN_TYPE_VERIFICATION, - 'secret' => Auth::hash($verificationSecret), // On way hash encryption to protect DB leak - 'expire' => \time() + Auth::TOKEN_EXPIRATION_CONFIRM, - 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), - 'ip' => $request->getIP(), - ]); - - Authorization::setRole('user:'.$user->getId()); + Authorization::setRole('user:'.$user->getId()); - $verification = $projectDB->createDocument($verification->getArrayCopy()); + $verification = $projectDB->createDocument($verification->getArrayCopy()); - if (false === $verification) { - throw new Exception('Failed saving verification to DB', 500); - } - - $user->setAttribute('tokens', $verification, Document::SET_TYPE_APPEND); - - $user = $projectDB->updateDocument($user->getArrayCopy()); - - if (false === $user) { - throw new Exception('Failed to save user to DB', 500); - } - - $url = Template::parseURL($url); - $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $user->getId(), 'secret' => $verificationSecret]); - $url = Template::unParseURL($url); - - $body = new Template(__DIR__.'/../../config/locales/templates/_base.tpl'); - $content = new Template(__DIR__.'/../../config/locales/templates/'.Locale::getText('account.emails.verification.body')); - $cta = new Template(__DIR__.'/../../config/locales/templates/_cta.tpl'); - - $body - ->setParam('{{content}}', $content->render()) - ->setParam('{{cta}}', $cta->render()) - ->setParam('{{title}}', Locale::getText('account.emails.verification.title')) - ->setParam('{{direction}}', Locale::getText('settings.direction')) - ->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]'])) - ->setParam('{{name}}', $user->getAttribute('name')) - ->setParam('{{redirect}}', $url) - ->setParam('{{bg-body}}', '#f6f6f6') - ->setParam('{{bg-content}}', '#ffffff') - ->setParam('{{bg-cta}}', '#3498db') - ->setParam('{{bg-cta-hover}}', '#34495e') - ->setParam('{{text-content}}', '#000000') - ->setParam('{{text-cta}}', '#ffffff') - ; - - $mail - ->setParam('event', 'account.verification.create') - ->setParam('recipient', $user->getAttribute('email')) - ->setParam('name', $user->getAttribute('name')) - ->setParam('subject', Locale::getText('account.emails.verification.title')) - ->setParam('body', $body->render()) - ->trigger() - ; - - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.verification.create') - ->setParam('resource', 'users/'.$user->getId()) - ; - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($verification->getArrayCopy(['$id', 'type', 'expire'])) - ; + if (false === $verification) { + throw new Exception('Failed saving verification to DB', 500); } - ); + + $user->setAttribute('tokens', $verification, Document::SET_TYPE_APPEND); + + $user = $projectDB->updateDocument($user->getArrayCopy()); + + if (false === $user) { + throw new Exception('Failed to save user to DB', 500); + } + + $url = Template::parseURL($url); + $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $user->getId(), 'secret' => $verificationSecret]); + $url = Template::unParseURL($url); + + $body = new Template(__DIR__.'/../../config/locales/templates/_base.tpl'); + $content = new Template(__DIR__.'/../../config/locales/templates/'.$locale->getText('account.emails.verification.body')); + $cta = new Template(__DIR__.'/../../config/locales/templates/_cta.tpl'); + + $body + ->setParam('{{content}}', $content->render()) + ->setParam('{{cta}}', $cta->render()) + ->setParam('{{title}}', $locale->getText('account.emails.verification.title')) + ->setParam('{{direction}}', $locale->getText('settings.direction')) + ->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]'])) + ->setParam('{{name}}', $user->getAttribute('name')) + ->setParam('{{redirect}}', $url) + ->setParam('{{bg-body}}', '#f6f6f6') + ->setParam('{{bg-content}}', '#ffffff') + ->setParam('{{bg-cta}}', '#3498db') + ->setParam('{{bg-cta-hover}}', '#34495e') + ->setParam('{{text-content}}', '#000000') + ->setParam('{{text-cta}}', '#ffffff') + ; + + $mail + ->setParam('event', 'account.verification.create') + ->setParam('recipient', $user->getAttribute('email')) + ->setParam('name', $user->getAttribute('name')) + ->setParam('subject', $locale->getText('account.emails.verification.title')) + ->setParam('body', $body->render()) + ->trigger() + ; + + $audit + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.verification.create') + ->setParam('resource', 'users/'.$user->getId()) + ; + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($verification->getArrayCopy(['$id', 'type', 'expire'])) + ; + }, ['request', 'response', 'project', 'user', 'projectDB', 'locale', 'audit', 'mail']); App::put('/v1/account/verification') ->desc('Complete Email Verification') @@ -1323,52 +1376,55 @@ App::put('/v1/account/verification') ->label('abuse-key', 'url:{url},userId:{param-userId}') ->param('userId', '', function () { return new UID(); }, 'User unique ID.') ->param('secret', '', function () { return new Text(256); }, 'Valid verification token.') - ->action( - function ($userId, $secret) use ($response, $user, $projectDB, $audit) { - $profile = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - '$id='.$userId, - ], - ]); + ->action(function ($userId, $secret, $response, $user, $projectDB, $audit) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audit */ - if (empty($profile)) { - throw new Exception('User not found', 404); // TODO maybe hide this - } + $profile = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + '$id='.$userId, + ], + ]); - $verification = Auth::tokenVerify($profile->getAttribute('tokens', []), Auth::TOKEN_TYPE_VERIFICATION, $secret); - - if (!$verification) { - throw new Exception('Invalid verification token', 401); - } - - Authorization::setRole('user:'.$profile->getId()); - - $profile = $projectDB->updateDocument(\array_merge($profile->getArrayCopy(), [ - 'emailVerification' => true, - ])); - - if (false === $profile) { - throw new Exception('Failed saving user to DB', 500); - } - - /** - * We act like we're updating and validating - * the verification token but actually we don't need it anymore. - */ - if (!$projectDB->deleteDocument($verification)) { - throw new Exception('Failed to remove verification from DB', 500); - } - - $audit - ->setParam('userId', $profile->getId()) - ->setParam('event', 'account.verification.update') - ->setParam('resource', 'users/'.$user->getId()) - ; - - $verification = $profile->search('$id', $verification, $profile->getAttribute('tokens', [])); - - $response->json($verification->getArrayCopy(['$id', 'type', 'expire'])); + if (empty($profile)) { + throw new Exception('User not found', 404); // TODO maybe hide this } - ); \ No newline at end of file + + $verification = Auth::tokenVerify($profile->getAttribute('tokens', []), Auth::TOKEN_TYPE_VERIFICATION, $secret); + + if (!$verification) { + throw new Exception('Invalid verification token', 401); + } + + Authorization::setRole('user:'.$profile->getId()); + + $profile = $projectDB->updateDocument(\array_merge($profile->getArrayCopy(), [ + 'emailVerification' => true, + ])); + + if (false === $profile) { + throw new Exception('Failed saving user to DB', 500); + } + + /** + * We act like we're updating and validating + * the verification token but actually we don't need it anymore. + */ + if (!$projectDB->deleteDocument($verification)) { + throw new Exception('Failed to remove verification from DB', 500); + } + + $audit + ->setParam('userId', $profile->getId()) + ->setParam('event', 'account.verification.update') + ->setParam('resource', 'users/'.$user->getId()) + ; + + $verification = $profile->search('$id', $verification, $profile->getAttribute('tokens', [])); + + $response->json($verification->getArrayCopy(['$id', 'type', 'expire'])); + }, ['response', 'user', 'projectDB', 'audit']); \ No newline at end of file diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 9aa07827f..850a3a3ed 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -1,7 +1,5 @@ desc('Get Credit Card Icon') ->groups(['api', 'avatars']) - ->param('code', '', function () { return new WhiteList(\array_keys(Config::getParam('avatar-credit-cards'))); }, 'Credit Card Code. Possible values: '.\implode(', ', \array_keys(Config::getParam('avatar-credit-cards'))).'.') - ->param('width', 100, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('height', 100, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('quality', 100, function () { return new Range(0, 100); }, 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) ->label('scope', 'avatars.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getCreditCard') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-credit-card.md') - ->action(function ($code, $width, $height, $quality) use ($avatarCallback) { - return $avatarCallback('credit-cards', $code, $width, $height, $quality); - }); + ->param('code', '', function () { return new WhiteList(\array_keys(Config::getParam('avatar-credit-cards'))); }, 'Credit Card Code. Possible values: '.\implode(', ', \array_keys(Config::getParam('avatar-credit-cards'))).'.') + ->param('width', 100, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('height', 100, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('quality', 100, function () { return new Range(0, 100); }, 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) + ->action(function ($code, $width, $height, $quality, $response) use ($avatarCallback) { + return $avatarCallback('credit-cards', $code, $width, $height, $quality, $response); + }, ['response']); App::get('/v1/avatars/browsers/:code') ->desc('Get Browser Icon') ->groups(['api', 'avatars']) - ->param('code', '', function () { return new WhiteList(\array_keys(Config::getParam('avatar-browsers'))); }, 'Browser Code.') - ->param('width', 100, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('height', 100, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('quality', 100, function () { return new Range(0, 100); }, 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) ->label('scope', 'avatars.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getBrowser') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-browser.md') - ->action(function ($code, $width, $height, $quality) use ($avatarCallback) { - return $avatarCallback('browsers', $code, $width, $height, $quality); - }); + ->param('code', '', function () { return new WhiteList(\array_keys(Config::getParam('avatar-browsers'))); }, 'Browser Code.') + ->param('width', 100, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('height', 100, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('quality', 100, function () { return new Range(0, 100); }, 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) + ->action(function ($code, $width, $height, $quality, $response) use ($avatarCallback) { + return $avatarCallback('browsers', $code, $width, $height, $quality, $response); + }, ['response']); App::get('/v1/avatars/flags/:code') ->desc('Get Country Flag') ->groups(['api', 'avatars']) - ->param('code', '', function () { return new WhiteList(\array_keys(Config::getParam('avatar-flags'))); }, 'Country Code. ISO Alpha-2 country code format.') - ->param('width', 100, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('height', 100, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('quality', 100, function () { return new Range(0, 100); }, 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) ->label('scope', 'avatars.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getFlag') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-flag.md') - ->action(function ($code, $width, $height, $quality) use ($avatarCallback) { - return $avatarCallback('flags', $code, $width, $height, $quality); - }); + ->param('code', '', function () { return new WhiteList(\array_keys(Config::getParam('avatar-flags'))); }, 'Country Code. ISO Alpha-2 country code format.') + ->param('width', 100, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('height', 100, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('quality', 100, function () { return new Range(0, 100); }, 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) + ->action(function ($code, $width, $height, $quality, $response) use ($avatarCallback) { + return $avatarCallback('flags', $code, $width, $height, $quality, $response); + }, ['response']); App::get('/v1/avatars/image') ->desc('Get Image from URL') ->groups(['api', 'avatars']) - ->param('url', '', function () { return new URL(); }, 'Image URL which you want to crop.') - ->param('width', 400, function () { return new Range(0, 2000); }, 'Resize preview image width, Pass an integer between 0 to 2000.', true) - ->param('height', 400, function () { return new Range(0, 2000); }, 'Resize preview image height, Pass an integer between 0 to 2000.', true) ->label('scope', 'avatars.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getImage') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-image.md') - ->action( - function ($url, $width, $height) use ($response) { - $quality = 80; - $output = 'png'; - $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache - $key = \md5('/v2/avatars/images-'.$url.'-'.$width.'/'.$height.'/'.$quality); - $type = 'png'; - $cache = new Cache(new Filesystem(APP_STORAGE_CACHE.'/app-0')); // Limit file number or size - $data = $cache->load($key, 60 * 60 * 24 * 7 /* 1 week */); + ->param('url', '', function () { return new URL(); }, 'Image URL which you want to crop.') + ->param('width', 400, function () { return new Range(0, 2000); }, 'Resize preview image width, Pass an integer between 0 to 2000.', true) + ->param('height', 400, function () { return new Range(0, 2000); }, 'Resize preview image height, Pass an integer between 0 to 2000.', true) + ->action(function ($url, $width, $height, $response) { + /** @var Utopia\Response $response */ - if ($data) { - $response - ->setContentType('image/png') - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'hit') - ->send($data, 0) - ; - } - - if (!\extension_loaded('imagick')) { - throw new Exception('Imagick extension is missing', 500); - } - - $fetch = @\file_get_contents($url, false); - - if (!$fetch) { - throw new Exception('Image not found', 404); - } - - try { - $resize = new Resize($fetch); - } catch (\Exception $exception) { - throw new Exception('Unable to parse image', 500); - } - - $resize->crop((int) $width, (int) $height); - - $output = (empty($output)) ? $type : $output; + $quality = 80; + $output = 'png'; + $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache + $key = \md5('/v2/avatars/images-'.$url.'-'.$width.'/'.$height.'/'.$quality); + $type = 'png'; + $cache = new Cache(new Filesystem(APP_STORAGE_CACHE.'/app-0')); // Limit file number or size + $data = $cache->load($key, 60 * 60 * 24 * 7 /* 1 week */); + if ($data) { $response ->setContentType('image/png') ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'miss') - ->send('', null) + ->addHeader('X-Appwrite-Cache', 'hit') + ->send($data, 0) ; - - $data = $resize->output($output, $quality); - - $cache->save($key, $data); - - echo $data; - - unset($resize); } - ); + + if (!\extension_loaded('imagick')) { + throw new Exception('Imagick extension is missing', 500); + } + + $fetch = @\file_get_contents($url, false); + + if (!$fetch) { + throw new Exception('Image not found', 404); + } + + try { + $resize = new Resize($fetch); + } catch (\Exception $exception) { + throw new Exception('Unable to parse image', 500); + } + + $resize->crop((int) $width, (int) $height); + + $output = (empty($output)) ? $type : $output; + + $response + ->setContentType('image/png') + ->addHeader('Expires', $date) + ->addHeader('X-Appwrite-Cache', 'miss') + ->send('', null) + ; + + $data = $resize->output($output, $quality); + + $cache->save($key, $data); + + echo $data; + + unset($resize); + }, ['response']); App::get('/v1/avatars/favicon') ->desc('Get Favicon') ->groups(['api', 'avatars']) - ->param('url', '', function () { return new URL(); }, 'Website URL which you want to fetch the favicon from.') ->label('scope', 'avatars.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getFavicon') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-favicon.md') - ->action( - function ($url) use ($response) { - $width = 56; - $height = 56; - $quality = 80; - $output = 'png'; - $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache - $key = \md5('/v2/avatars/favicon-'.$url); - $type = 'png'; - $cache = new Cache(new Filesystem(APP_STORAGE_CACHE.'/app-0')); // Limit file number or size - $data = $cache->load($key, 60 * 60 * 24 * 30 * 3 /* 3 months */); + ->param('url', '', function () { return new URL(); }, 'Website URL which you want to fetch the favicon from.') + ->action(function ($url, $response) { + /** @var Utopia\Response $response */ - if ($data) { - $response - ->setContentType('image/png') - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'hit') - ->send($data, 0) - ; - } - - if (!\extension_loaded('imagick')) { - throw new Exception('Imagick extension is missing', 500); - } - - $curl = \curl_init(); - - \curl_setopt_array($curl, [ - CURLOPT_RETURNTRANSFER => 1, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_MAXREDIRS => 3, - CURLOPT_URL => $url, - CURLOPT_USERAGENT => \sprintf(APP_USERAGENT, - Config::getParam('version'), - App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY) - ), - ]); - - $html = \curl_exec($curl); - - \curl_close($curl); - - if (!$html) { - throw new Exception('Failed to fetch remote URL', 404); - } - - $doc = new DOMDocument(); - $doc->strictErrorChecking = false; - @$doc->loadHTML($html); - - $links = $doc->getElementsByTagName('link'); - $outputHref = ''; - $outputExt = ''; - $space = 0; - - foreach ($links as $link) { /* @var $link DOMElement */ - $href = $link->getAttribute('href'); - $rel = $link->getAttribute('rel'); - $sizes = $link->getAttribute('sizes'); - $absolute = URLParse::unparse(\array_merge(\parse_url($url), \parse_url($href))); - - switch (\strtolower($rel)) { - case 'icon': - case 'shortcut icon': - //case 'apple-touch-icon': - $ext = \pathinfo(\parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION); - - switch ($ext) { - case 'ico': - case 'png': - case 'jpg': - case 'jpeg': - $size = \explode('x', \strtolower($sizes)); - - $sizeWidth = (isset($size[0])) ? (int) $size[0] : 0; - $sizeHeight = (isset($size[1])) ? (int) $size[1] : 0; - - if (($sizeWidth * $sizeHeight) >= $space) { - $space = $sizeWidth * $sizeHeight; - $outputHref = $absolute; - $outputExt = $ext; - } - - break; - } - - break; - } - } - - if (empty($outputHref) || empty($outputExt)) { - $default = \parse_url($url); - - $outputHref = $default['scheme'].'://'.$default['host'].'/favicon.ico'; - $outputExt = 'ico'; - } - - if ('ico' == $outputExt) { // Skip crop, Imagick isn\'t supporting icon files - $data = @\file_get_contents($outputHref, false); - - if (empty($data) || (\mb_substr($data, 0, 5) === 'save($key, $data); - - $response - ->setContentType('image/x-icon') - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'miss') - ->send($data, 0) - ; - } - - $fetch = @\file_get_contents($outputHref, false); - - if (!$fetch) { - throw new Exception('Icon not found', 404); - } - - $resize = new Resize($fetch); - - $resize->crop((int) $width, (int) $height); - - $output = (empty($output)) ? $type : $output; + $width = 56; + $height = 56; + $quality = 80; + $output = 'png'; + $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache + $key = \md5('/v2/avatars/favicon-'.$url); + $type = 'png'; + $cache = new Cache(new Filesystem(APP_STORAGE_CACHE.'/app-0')); // Limit file number or size + $data = $cache->load($key, 60 * 60 * 24 * 30 * 3 /* 3 months */); + if ($data) { $response ->setContentType('image/png') ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'miss') - ->send('', null) + ->addHeader('X-Appwrite-Cache', 'hit') + ->send($data, 0) ; + } - $data = $resize->output($output, $quality); + if (!\extension_loaded('imagick')) { + throw new Exception('Imagick extension is missing', 500); + } + + $curl = \curl_init(); + + \curl_setopt_array($curl, [ + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 3, + CURLOPT_URL => $url, + CURLOPT_USERAGENT => \sprintf(APP_USERAGENT, + Config::getParam('version'), + App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY) + ), + ]); + + $html = \curl_exec($curl); + + \curl_close($curl); + + if (!$html) { + throw new Exception('Failed to fetch remote URL', 404); + } + + $doc = new DOMDocument(); + $doc->strictErrorChecking = false; + @$doc->loadHTML($html); + + $links = $doc->getElementsByTagName('link'); + $outputHref = ''; + $outputExt = ''; + $space = 0; + + foreach ($links as $link) { /* @var $link DOMElement */ + $href = $link->getAttribute('href'); + $rel = $link->getAttribute('rel'); + $sizes = $link->getAttribute('sizes'); + $absolute = URLParse::unparse(\array_merge(\parse_url($url), \parse_url($href))); + + switch (\strtolower($rel)) { + case 'icon': + case 'shortcut icon': + //case 'apple-touch-icon': + $ext = \pathinfo(\parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION); + + switch ($ext) { + case 'ico': + case 'png': + case 'jpg': + case 'jpeg': + $size = \explode('x', \strtolower($sizes)); + + $sizeWidth = (isset($size[0])) ? (int) $size[0] : 0; + $sizeHeight = (isset($size[1])) ? (int) $size[1] : 0; + + if (($sizeWidth * $sizeHeight) >= $space) { + $space = $sizeWidth * $sizeHeight; + $outputHref = $absolute; + $outputExt = $ext; + } + + break; + } + + break; + } + } + + if (empty($outputHref) || empty($outputExt)) { + $default = \parse_url($url); + + $outputHref = $default['scheme'].'://'.$default['host'].'/favicon.ico'; + $outputExt = 'ico'; + } + + if ('ico' == $outputExt) { // Skip crop, Imagick isn\'t supporting icon files + $data = @\file_get_contents($outputHref, false); + + if (empty($data) || (\mb_substr($data, 0, 5) === 'save($key, $data); - echo $data; - - unset($resize); + $response + ->setContentType('image/x-icon') + ->addHeader('Expires', $date) + ->addHeader('X-Appwrite-Cache', 'miss') + ->send($data, 0) + ; } - ); + + $fetch = @\file_get_contents($outputHref, false); + + if (!$fetch) { + throw new Exception('Icon not found', 404); + } + + $resize = new Resize($fetch); + + $resize->crop((int) $width, (int) $height); + + $output = (empty($output)) ? $type : $output; + + $response + ->setContentType('image/png') + ->addHeader('Expires', $date) + ->addHeader('X-Appwrite-Cache', 'miss') + ->send('', null) + ; + + $data = $resize->output($output, $quality); + + $cache->save($key, $data); + + echo $data; + + unset($resize); + }, ['response']); App::get('/v1/avatars/qr') ->desc('Get QR Code') ->groups(['api', 'avatars']) - ->param('text', '', function () { return new Text(512); }, 'Plain text to be converted to QR code image.') - ->param('size', 400, function () { return new Range(0, 1000); }, 'QR code size. Pass an integer between 0 to 1000. Defaults to 400.', true) - ->param('margin', 1, function () { return new Range(0, 10); }, 'Margin from edge. Pass an integer between 0 to 10. Defaults to 1.', true) - ->param('download', false, function () { return new Boolean(true); }, 'Return resulting image with \'Content-Disposition: attachment \' headers for the browser to start downloading it. Pass 0 for no header, or 1 for otherwise. Default value is set to 0.', true) ->label('scope', 'avatars.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getQR') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-qr.md') - ->action( - function ($text, $size, $margin, $download) use ($response) { - $download = ($download === '1' || $download === 'true' || $download === 1 || $download === true); + ->param('text', '', function () { return new Text(512); }, 'Plain text to be converted to QR code image.') + ->param('size', 400, function () { return new Range(0, 1000); }, 'QR code size. Pass an integer between 0 to 1000. Defaults to 400.', true) + ->param('margin', 1, function () { return new Range(0, 10); }, 'Margin from edge. Pass an integer between 0 to 10. Defaults to 1.', true) + ->param('download', false, function () { return new Boolean(true); }, 'Return resulting image with \'Content-Disposition: attachment \' headers for the browser to start downloading it. Pass 0 for no header, or 1 for otherwise. Default value is set to 0.', true) + ->action(function ($text, $size, $margin, $download, $response) { + /** @var Utopia\Response $response */ - $renderer = new ImageRenderer( - new RendererStyle($size, $margin), - new ImagickImageBackEnd('png', 100) - ); + $download = ($download === '1' || $download === 'true' || $download === 1 || $download === true); - $writer = new Writer($renderer); + $renderer = new ImageRenderer( + new RendererStyle($size, $margin), + new ImagickImageBackEnd('png', 100) + ); - if ($download) { - $response->addHeader('Content-Disposition', 'attachment; filename="qr.png"'); - } + $writer = new Writer($renderer); - $response - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache - ->setContentType('image/png') - ->send($writer->writeString($text)) - ; + if ($download) { + $response->addHeader('Content-Disposition', 'attachment; filename="qr.png"'); } - ); + + $response + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache + ->setContentType('image/png') + ->send($writer->writeString($text)) + ; + }, ['response']); App::get('/v1/avatars/initials') ->desc('Get User Initials') ->groups(['api', 'avatars']) - ->param('name', '', function () { return new Text(512); }, 'Full Name. When empty, current user name or email will be used.', true) - ->param('width', 500, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('height', 500, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('color', '', function () { return new HexColor(); }, 'Changes text color. By default a random color will be picked and stay will persistent to the given name.', true) - ->param('background', '', function () { return new HexColor(); }, 'Changes background color. By default a random color will be picked and stay will persistent to the given name.', true) ->label('scope', 'avatars.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getInitials') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-initials.md') - ->action( - function ($name, $width, $height, $color, $background) use ($response, $user) { - $themes = [ - ['color' => '#27005e', 'background' => '#e1d2f6'], // VIOLET - ['color' => '#5e2700', 'background' => '#f3d9c6'], // ORANGE - ['color' => '#006128', 'background' => '#c9f3c6'], // GREEN - ['color' => '#580061', 'background' => '#f2d1f5'], // FUSCHIA - ['color' => '#00365d', 'background' => '#c6e1f3'], // BLUE - ['color' => '#00075c', 'background' => '#d2d5f6'], // INDIGO - ['color' => '#610038', 'background' => '#f5d1e6'], // PINK - ['color' => '#386100', 'background' => '#dcf1bd'], // LIME - ['color' => '#615800', 'background' => '#f1ecba'], // YELLOW - ['color' => '#610008', 'background' => '#f6d2d5'] // RED - ]; + ->param('name', '', function () { return new Text(512); }, 'Full Name. When empty, current user name or email will be used.', true) + ->param('width', 500, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('height', 500, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('color', '', function () { return new HexColor(); }, 'Changes text color. By default a random color will be picked and stay will persistent to the given name.', true) + ->param('background', '', function () { return new HexColor(); }, 'Changes background color. By default a random color will be picked and stay will persistent to the given name.', true) + ->action(function ($name, $width, $height, $color, $background, $response, $user) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ - $rand = \rand(0, \count($themes)-1); + $themes = [ + ['color' => '#27005e', 'background' => '#e1d2f6'], // VIOLET + ['color' => '#5e2700', 'background' => '#f3d9c6'], // ORANGE + ['color' => '#006128', 'background' => '#c9f3c6'], // GREEN + ['color' => '#580061', 'background' => '#f2d1f5'], // FUSCHIA + ['color' => '#00365d', 'background' => '#c6e1f3'], // BLUE + ['color' => '#00075c', 'background' => '#d2d5f6'], // INDIGO + ['color' => '#610038', 'background' => '#f5d1e6'], // PINK + ['color' => '#386100', 'background' => '#dcf1bd'], // LIME + ['color' => '#615800', 'background' => '#f1ecba'], // YELLOW + ['color' => '#610008', 'background' => '#f6d2d5'] // RED + ]; - $name = (!empty($name)) ? $name : $user->getAttribute('name', $user->getAttribute('email', '')); - $words = \explode(' ', \strtoupper($name)); - $initials = null; - $code = 0; + $rand = \rand(0, \count($themes)-1); - foreach ($words as $key => $w) { - $initials .= (isset($w[0])) ? $w[0] : ''; - $code += (isset($w[0])) ? \ord($w[0]) : 0; + $name = (!empty($name)) ? $name : $user->getAttribute('name', $user->getAttribute('email', '')); + $words = \explode(' ', \strtoupper($name)); + $initials = null; + $code = 0; - if ($key == 1) { - break; - } + foreach ($words as $key => $w) { + $initials .= (isset($w[0])) ? $w[0] : ''; + $code += (isset($w[0])) ? \ord($w[0]) : 0; + + if ($key == 1) { + break; } - - $length = \count($words); - $rand = \substr($code,-1); - $background = (!empty($background)) ? '#'.$background : $themes[$rand]['background']; - $color = (!empty($color)) ? '#'.$color : $themes[$rand]['color']; - - $image = new \Imagick(); - $draw = new \ImagickDraw(); - $fontSize = \min($width, $height) / 2; - - $draw->setFont(__DIR__."/../../../public/fonts/poppins-v9-latin-500.ttf"); - $image->setFont(__DIR__."/../../../public/fonts/poppins-v9-latin-500.ttf"); - - $draw->setFillColor(new \ImagickPixel($color)); - $draw->setFontSize($fontSize); - - $draw->setTextAlignment(\Imagick::ALIGN_CENTER); - $draw->annotation($width / 1.97, ($height / 2) + ($fontSize / 3), $initials); - - $image->newImage($width, $height, $background); - $image->setImageFormat("png"); - $image->drawImage($draw); - - //$image->setImageCompressionQuality(9 - round(($quality / 100) * 9)); - - $response - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache - ->setContentType('image/png') - ->send($image->getImageBlob()) - ; } - ); \ No newline at end of file + + $length = \count($words); + $rand = \substr($code,-1); + $background = (!empty($background)) ? '#'.$background : $themes[$rand]['background']; + $color = (!empty($color)) ? '#'.$color : $themes[$rand]['color']; + + $image = new \Imagick(); + $draw = new \ImagickDraw(); + $fontSize = \min($width, $height) / 2; + + $draw->setFont(__DIR__."/../../../public/fonts/poppins-v9-latin-500.ttf"); + $image->setFont(__DIR__."/../../../public/fonts/poppins-v9-latin-500.ttf"); + + $draw->setFillColor(new \ImagickPixel($color)); + $draw->setFontSize($fontSize); + + $draw->setTextAlignment(\Imagick::ALIGN_CENTER); + $draw->annotation($width / 1.97, ($height / 2) + ($fontSize / 3), $initials); + + $image->newImage($width, $height, $background); + $image->setImageFormat("png"); + $image->drawImage($draw); + + //$image->setImageCompressionQuality(9 - round(($quality / 100) * 9)); + + $response + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache + ->setContentType('image/png') + ->send($image->getImageBlob()) + ; + }, ['response', 'user']); \ No newline at end of file diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index fc505bcf0..b3fcfc3c7 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -1,7 +1,5 @@ desc('Create Collection') ->groups(['api', 'database']) @@ -38,67 +33,70 @@ App::post('/v1/database/collections') ->param('name', '', function () { return new Text(256); }, 'Collection name.') ->param('read', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') ->param('write', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') - ->param('rules', [], function () use ($projectDB) { return new ArrayList(new Collection($projectDB, [Database::SYSTEM_COLLECTION_RULES], ['$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => ['read' => [], 'write' => []]])); }, 'Array of [rule objects](/docs/rules). Each rule define a collection field name, data type and validation.') - ->action( - function ($name, $read, $write, $rules) use ($response, $projectDB, $webhook, $audit) { - $parsedRules = []; + ->param('rules', [], function ($projectDB) { return new ArrayList(new Collection($projectDB, [Database::SYSTEM_COLLECTION_RULES], ['$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => ['read' => [], 'write' => []]])); }, 'Array of [rule objects](/docs/rules). Each rule define a collection field name, data type and validation.', false, ['projectDB']) + ->action(function ($name, $read, $write, $rules, $response, $projectDB, $webhook, $audit) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhook */ + /** @var Appwrite\Event\Event $audit */ - foreach ($rules as &$rule) { - $parsedRules[] = \array_merge([ - '$collection' => Database::SYSTEM_COLLECTION_RULES, - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - ], $rule); - } + $parsedRules = []; - try { - $data = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, - 'name' => $name, - 'dateCreated' => \time(), - 'dateUpdated' => \time(), - 'structure' => true, - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - 'rules' => $parsedRules, - ]); - } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized action', 401); - } catch (StructureException $exception) { - throw new Exception('Bad structure. '.$exception->getMessage(), 400); - } catch (\Exception $exception) { - throw new Exception('Failed saving document to DB', 500); - } - - if (false === $data) { - throw new Exception('Failed saving collection to DB', 500); - } - - $data = $data->getArrayCopy(); - - $webhook - ->setParam('payload', $data) - ; - - $audit - ->setParam('event', 'database.collections.create') - ->setParam('resource', 'database/collection/'.$data['$id']) - ->setParam('data', $data) - ; - - /* - * View - */ - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($data) - ; + foreach ($rules as &$rule) { + $parsedRules[] = \array_merge([ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + '$permissions' => [ + 'read' => $read, + 'write' => $write, + ], + ], $rule); } - ); + + try { + $data = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, + 'name' => $name, + 'dateCreated' => \time(), + 'dateUpdated' => \time(), + 'structure' => true, + '$permissions' => [ + 'read' => $read, + 'write' => $write, + ], + 'rules' => $parsedRules, + ]); + } catch (AuthorizationException $exception) { + throw new Exception('Unauthorized action', 401); + } catch (StructureException $exception) { + throw new Exception('Bad structure. '.$exception->getMessage(), 400); + } catch (\Exception $exception) { + throw new Exception('Failed saving document to DB', 500); + } + + if (false === $data) { + throw new Exception('Failed saving collection to DB', 500); + } + + $data = $data->getArrayCopy(); + + $webhook + ->setParam('payload', $data) + ; + + $audit + ->setParam('event', 'database.collections.create') + ->setParam('resource', 'database/collection/'.$data['$id']) + ->setParam('data', $data) + ; + + /* + * View + */ + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($data) + ; + }, ['response', 'projectDB', 'webhook', 'audit']); App::get('/v1/database/collections') ->desc('List Collections') @@ -112,42 +110,24 @@ App::get('/v1/database/collections') ->param('limit', 25, function () { return new Range(0, 100); }, 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true) ->param('offset', 0, function () { return new Range(0, 40000); }, 'Results offset. The default value is 0. Use this param to manage pagination.', true) ->param('orderType', 'ASC', function () { return new WhiteList(['ASC', 'DESC']); }, 'Order result by ASC or DESC order.', true) - ->action( - function ($search, $limit, $offset, $orderType) use ($response, $projectDB) { - /*$vl = new Structure($projectDB); + ->action(function ($search, $limit, $offset, $orderType, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - var_dump($vl->isValid(new Document([ - '$collection' => Database::SYSTEM_COLLECTION_RULES, - '$permissions' => [ - 'read' => ['*'], - 'write' => ['*'], - ], - 'label' => 'Platforms', - 'key' => 'platforms', - 'type' => 'document', - 'default' => [], - 'required' => false, - 'array' => true, - 'options' => [Database::SYSTEM_COLLECTION_PLATFORMS], - ]))); + $results = $projectDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => 'name', + 'orderType' => $orderType, + 'orderCast' => 'string', + 'search' => $search, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_COLLECTIONS, + ], + ]); - var_dump($vl->getDescription());*/ - - $results = $projectDB->getCollection([ - 'limit' => $limit, - 'offset' => $offset, - 'orderField' => 'name', - 'orderType' => $orderType, - 'orderCast' => 'string', - 'search' => $search, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_COLLECTIONS, - ], - ]); - - $response->json(['sum' => $projectDB->getSum(), 'collections' => $results]); - } - ); + $response->json(['sum' => $projectDB->getSum(), 'collections' => $results]); + }, ['response', 'projectDB']); App::get('/v1/database/collections/:collectionId') ->desc('Get Collection') @@ -158,17 +138,18 @@ App::get('/v1/database/collections/:collectionId') ->label('sdk.method', 'getCollection') ->label('sdk.description', '/docs/references/database/get-collection.md') ->param('collectionId', '', function () { return new UID(); }, 'Collection unique ID.') - ->action( - function ($collectionId) use ($response, $projectDB) { - $collection = $projectDB->getDocument($collectionId, false); + ->action(function ($collectionId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + + $collection = $projectDB->getDocument($collectionId, false); - if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } - - $response->json($collection->getArrayCopy()); + if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + throw new Exception('Collection not found', 404); } - ); + + $response->json($collection->getArrayCopy()); + }, ['response', 'projectDB']); // App::get('/v1/database/collections/:collectionId/logs') // ->desc('Get Collection Logs') @@ -249,64 +230,67 @@ App::put('/v1/database/collections/:collectionId') ->param('read', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions(/docs/permissions) and get a full list of available permissions.') ->param('write', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') ->param('rules', [], function () use ($projectDB) { return new ArrayList(new Collection($projectDB, [Database::SYSTEM_COLLECTION_RULES], ['$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => ['read' => [], 'write' => []]])); }, 'Array of [rule objects](/docs/rules). Each rule define a collection field name, data type and validation.', true) - ->action( - function ($collectionId, $name, $read, $write, $rules) use ($response, $projectDB, $webhook, $audit) { - $collection = $projectDB->getDocument($collectionId, false); + ->action(function ($collectionId, $name, $read, $write, $rules, $response, $projectDB, $webhook, $audit) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhook */ + /** @var Appwrite\Event\Event $audit */ - if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } + $collection = $projectDB->getDocument($collectionId, false); - $parsedRules = []; - - foreach ($rules as &$rule) { - $parsedRules[] = \array_merge([ - '$collection' => Database::SYSTEM_COLLECTION_RULES, - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - ], $rule); - } - - try { - $collection = $projectDB->updateDocument(\array_merge($collection->getArrayCopy(), [ - 'name' => $name, - 'structure' => true, - 'dateUpdated' => \time(), - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - 'rules' => $parsedRules, - ])); - } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized action', 401); - } catch (StructureException $exception) { - throw new Exception('Bad structure. '.$exception->getMessage(), 400); - } catch (\Exception $exception) { - throw new Exception('Failed saving document to DB', 500); - } - - if (false === $collection) { - throw new Exception('Failed saving collection to DB', 500); - } - - $data = $collection->getArrayCopy(); - - $webhook - ->setParam('payload', $data) - ; - - $audit - ->setParam('event', 'database.collections.update') - ->setParam('resource', 'database/collections/'.$data['$id']) - ->setParam('data', $data) - ; - - $response->json($collection->getArrayCopy()); + if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + throw new Exception('Collection not found', 404); } - ); + + $parsedRules = []; + + foreach ($rules as &$rule) { + $parsedRules[] = \array_merge([ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + '$permissions' => [ + 'read' => $read, + 'write' => $write, + ], + ], $rule); + } + + try { + $collection = $projectDB->updateDocument(\array_merge($collection->getArrayCopy(), [ + 'name' => $name, + 'structure' => true, + 'dateUpdated' => \time(), + '$permissions' => [ + 'read' => $read, + 'write' => $write, + ], + 'rules' => $parsedRules, + ])); + } catch (AuthorizationException $exception) { + throw new Exception('Unauthorized action', 401); + } catch (StructureException $exception) { + throw new Exception('Bad structure. '.$exception->getMessage(), 400); + } catch (\Exception $exception) { + throw new Exception('Failed saving document to DB', 500); + } + + if (false === $collection) { + throw new Exception('Failed saving collection to DB', 500); + } + + $data = $collection->getArrayCopy(); + + $webhook + ->setParam('payload', $data) + ; + + $audit + ->setParam('event', 'database.collections.update') + ->setParam('resource', 'database/collections/'.$data['$id']) + ->setParam('data', $data) + ; + + $response->json($collection->getArrayCopy()); + }, ['response', 'projectDB', 'webhook', 'audit']); App::delete('/v1/database/collections/:collectionId') ->desc('Delete Collection') @@ -318,33 +302,36 @@ App::delete('/v1/database/collections/:collectionId') ->label('sdk.method', 'deleteCollection') ->label('sdk.description', '/docs/references/database/delete-collection.md') ->param('collectionId', '', function () { return new UID(); }, 'Collection unique ID.') - ->action( - function ($collectionId) use ($response, $projectDB, $webhook, $audit) { - $collection = $projectDB->getDocument($collectionId, false); + ->action(function ($collectionId, $response, $projectDB, $webhook, $audit) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhook */ + /** @var Appwrite\Event\Event $audit */ - if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } + $collection = $projectDB->getDocument($collectionId, false); - if (!$projectDB->deleteDocument($collectionId)) { - throw new Exception('Failed to remove collection from DB', 500); - } - - $data = $collection->getArrayCopy(); - - $webhook - ->setParam('payload', $data) - ; - - $audit - ->setParam('event', 'database.collections.delete') - ->setParam('resource', 'database/collections/'.$data['$id']) - ->setParam('data', $data) - ; - - $response->noContent(); + if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + throw new Exception('Collection not found', 404); } - ); + + if (!$projectDB->deleteDocument($collectionId)) { + throw new Exception('Failed to remove collection from DB', 500); + } + + $data = $collection->getArrayCopy(); + + $webhook + ->setParam('payload', $data) + ; + + $audit + ->setParam('event', 'database.collections.delete') + ->setParam('resource', 'database/collections/'.$data['$id']) + ->setParam('data', $data) + ; + + $response->noContent(); + }, ['response', 'projectDB', 'webhook', 'audit']); App::post('/v1/database/collections/:collectionId/documents') ->desc('Create Document') @@ -362,109 +349,112 @@ App::post('/v1/database/collections/:collectionId/documents') ->param('parentDocument', '', function () { return new UID(); }, 'Parent document unique ID. Use when you want your new document to be a child of a parent document.', true) ->param('parentProperty', '', function () { return new Key(); }, 'Parent document property name. Use when you want your new document to be a child of a parent document.', true) ->param('parentPropertyType', Document::SET_TYPE_ASSIGN, function () { return new WhiteList([Document::SET_TYPE_ASSIGN, Document::SET_TYPE_APPEND, Document::SET_TYPE_PREPEND]); }, 'Parent document property connection type. You can set this value to **assign**, **append** or **prepend**, default value is assign. Use when you want your new document to be a child of a parent document.', true) - ->action( - function ($collectionId, $data, $read, $write, $parentDocument, $parentProperty, $parentPropertyType) use ($response, $projectDB, $webhook, $audit) { - $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array + ->action(function ($collectionId, $data, $read, $write, $parentDocument, $parentProperty, $parentPropertyType, $response, $projectDB, $webhook, $audit) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhook */ + /** @var Appwrite\Event\Event $audit */ + + $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array - if (empty($data)) { - throw new Exception('Missing payload', 400); + if (empty($data)) { + throw new Exception('Missing payload', 400); + } + + if (isset($data['$id'])) { + throw new Exception('$id is not allowed for creating new documents, try update instead', 400); + } + + $collection = $projectDB->getDocument($collectionId, false); + + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + throw new Exception('Collection not found', 404); + } + + $data['$collection'] = $collectionId; // Adding this param to make API easier for developers + $data['$permissions'] = [ + 'read' => $read, + 'write' => $write, + ]; + + // Read parent document + validate not 404 + validate read / write permission like patch method + // Add payload to parent document property + if ((!empty($parentDocument)) && (!empty($parentProperty))) { + $parentDocument = $projectDB->getDocument($parentDocument, false); + + if (empty($parentDocument->getArrayCopy())) { // Check empty + throw new Exception('No parent document found', 404); } - if (isset($data['$id'])) { - throw new Exception('$id is not allowed for creating new documents, try update instead', 400); - } - - $collection = $projectDB->getDocument($collectionId, false); - - if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } - - $data['$collection'] = $collectionId; // Adding this param to make API easier for developers - $data['$permissions'] = [ - 'read' => $read, - 'write' => $write, - ]; - - // Read parent document + validate not 404 + validate read / write permission like patch method - // Add payload to parent document property - if ((!empty($parentDocument)) && (!empty($parentProperty))) { - $parentDocument = $projectDB->getDocument($parentDocument, false); - - if (empty($parentDocument->getArrayCopy())) { // Check empty - throw new Exception('No parent document found', 404); - } - - /* - * 1. Check child has valid structure, - * 2. Check user have write permission for parent document - * 3. Assign parent data (including child) to $data - * 4. Validate the combined result has valid structure (inside $projectDB->createDocument method) - */ - - $new = new Document($data); - - $structure = new Structure($projectDB); - - if (!$structure->isValid($new)) { - throw new Exception('Invalid data structure: '.$structure->getDescription(), 400); - } - - $authorization = new Authorization($parentDocument, 'write'); - - if (!$authorization->isValid($new->getPermissions())) { - throw new Exception('Unauthorized action', 401); - } - - $parentDocument - ->setAttribute($parentProperty, $data, $parentPropertyType); - - $data = $parentDocument->getArrayCopy(); - } - - /** - * Set default collection values - */ - foreach ($collection->getAttribute('rules') as $key => $rule) { - $key = (isset($rule['key'])) ? $rule['key'] : ''; - $default = (isset($rule['default'])) ? $rule['default'] : null; - - if (!isset($data[$key])) { - $data[$key] = $default; - } - } - - try { - $data = $projectDB->createDocument($data); - } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized action', 401); - } catch (StructureException $exception) { - throw new Exception('Bad structure. '.$exception->getMessage(), 400); - } catch (\Exception $exception) { - throw new Exception('Failed saving document to DB'.$exception->getMessage(), 500); - } - - $data = $data->getArrayCopy(); - - $webhook - ->setParam('payload', $data) - ; - - $audit - ->setParam('event', 'database.documents.create') - ->setParam('resource', 'database/document/'.$data['$id']) - ->setParam('data', $data) - ; - /* - * View - */ - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($data) - ; + * 1. Check child has valid structure, + * 2. Check user have write permission for parent document + * 3. Assign parent data (including child) to $data + * 4. Validate the combined result has valid structure (inside $projectDB->createDocument method) + */ + + $new = new Document($data); + + $structure = new Structure($projectDB); + + if (!$structure->isValid($new)) { + throw new Exception('Invalid data structure: '.$structure->getDescription(), 400); + } + + $authorization = new Authorization($parentDocument, 'write'); + + if (!$authorization->isValid($new->getPermissions())) { + throw new Exception('Unauthorized action', 401); + } + + $parentDocument + ->setAttribute($parentProperty, $data, $parentPropertyType); + + $data = $parentDocument->getArrayCopy(); } - ); + + /** + * Set default collection values + */ + foreach ($collection->getAttribute('rules') as $key => $rule) { + $key = (isset($rule['key'])) ? $rule['key'] : ''; + $default = (isset($rule['default'])) ? $rule['default'] : null; + + if (!isset($data[$key])) { + $data[$key] = $default; + } + } + + try { + $data = $projectDB->createDocument($data); + } catch (AuthorizationException $exception) { + throw new Exception('Unauthorized action', 401); + } catch (StructureException $exception) { + throw new Exception('Bad structure. '.$exception->getMessage(), 400); + } catch (\Exception $exception) { + throw new Exception('Failed saving document to DB'.$exception->getMessage(), 500); + } + + $data = $data->getArrayCopy(); + + $webhook + ->setParam('payload', $data) + ; + + $audit + ->setParam('event', 'database.documents.create') + ->setParam('resource', 'database/document/'.$data['$id']) + ->setParam('data', $data) + ; + + /* + * View + */ + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($data) + ; + }, ['response', 'projectDB', 'webhook', 'audit']); App::get('/v1/database/collections/:collectionId/documents') ->desc('List Documents') @@ -482,49 +472,50 @@ App::get('/v1/database/collections/:collectionId/documents') ->param('orderType', 'ASC', function () { return new WhiteList(array('DESC', 'ASC')); }, 'Order direction. Possible values are DESC for descending order, or ASC for ascending order.', true) ->param('orderCast', 'string', function () { return new WhiteList(array('int', 'string', 'date', 'time', 'datetime')); }, 'Order field type casting. Possible values are int, string, date, time or datetime. The database will attempt to cast the order field to the value you pass here. The default value is a string.', true) ->param('search', '', function () { return new Text(256); }, 'Search query. Enter any free text search. The database will try to find a match against all document attributes and children.', true) - ->action( - function ($collectionId, $filters, $offset, $limit, $orderField, $orderType, $orderCast, $search) use ($response, $projectDB) { - $collection = $projectDB->getDocument($collectionId, false); + ->action(function ($collectionId, $filters, $offset, $limit, $orderField, $orderType, $orderCast, $search, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } + $collection = $projectDB->getDocument($collectionId, false); - $list = $projectDB->getCollection([ - 'limit' => $limit, - 'offset' => $offset, - 'orderField' => $orderField, - 'orderType' => $orderType, - 'orderCast' => $orderCast, - 'search' => $search, - 'filters' => \array_merge($filters, [ - '$collection='.$collectionId, - ]), - ]); - - if (App::isDevelopment()) { - $collection - ->setAttribute('debug', $projectDB->getDebug()) - ->setAttribute('limit', $limit) - ->setAttribute('offset', $offset) - ->setAttribute('orderField', $orderField) - ->setAttribute('orderType', $orderType) - ->setAttribute('orderCast', $orderCast) - ->setAttribute('filters', $filters) - ; - } - - $collection - ->setAttribute('sum', $projectDB->getSum()) - ->setAttribute('documents', $list) - ; - - /* - * View - */ - $response->json($collection->getArrayCopy(/*['$id', '$collection', 'name', 'documents']*/[], ['rules'])); + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + throw new Exception('Collection not found', 404); } - ); + + $list = $projectDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => $orderField, + 'orderType' => $orderType, + 'orderCast' => $orderCast, + 'search' => $search, + 'filters' => \array_merge($filters, [ + '$collection='.$collectionId, + ]), + ]); + + if (App::isDevelopment()) { + $collection + ->setAttribute('debug', $projectDB->getDebug()) + ->setAttribute('limit', $limit) + ->setAttribute('offset', $offset) + ->setAttribute('orderField', $orderField) + ->setAttribute('orderType', $orderType) + ->setAttribute('orderCast', $orderCast) + ->setAttribute('filters', $filters) + ; + } + + $collection + ->setAttribute('sum', $projectDB->getSum()) + ->setAttribute('documents', $list) + ; + + /* + * View + */ + $response->json($collection->getArrayCopy(/*['$id', '$collection', 'name', 'documents']*/[], ['rules'])); + }, ['response', 'projectDB']); App::get('/v1/database/collections/:collectionId/documents/:documentId') ->desc('Get Document') @@ -536,41 +527,43 @@ App::get('/v1/database/collections/:collectionId/documents/:documentId') ->label('sdk.description', '/docs/references/database/get-document.md') ->param('collectionId', null, function () { return new UID(); }, 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') ->param('documentId', null, function () { return new UID(); }, 'Document unique ID.') - ->action( - function ($collectionId, $documentId) use ($response, $request, $projectDB) { - $document = $projectDB->getDocument($documentId, false); - $collection = $projectDB->getDocument($collectionId, false); + ->action(function ($collectionId, $documentId, $request, $response, $projectDB) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($document->getArrayCopy()) || $document->getCollection() != $collection->getId()) { // Check empty + $document = $projectDB->getDocument($documentId, false); + $collection = $projectDB->getDocument($collectionId, false); + + if (empty($document->getArrayCopy()) || $document->getCollection() != $collection->getId()) { // Check empty + throw new Exception('No document found', 404); + } + + $output = $document->getArrayCopy(); + + $paths = \explode('/', $request->getParam('q', '')); + $paths = \array_slice($paths, 7, \count($paths)); + + if (\count($paths) > 0) { + if (\count($paths) % 2 == 1) { + $output = $document->getAttribute(\implode('.', $paths)); + } else { + $id = (int) \array_pop($paths); + $output = $document->search('$id', $id, $document->getAttribute(\implode('.', $paths))); + } + + $output = ($output instanceof Document) ? $output->getArrayCopy() : $output; + + if (!\is_array($output)) { throw new Exception('No document found', 404); } - - $output = $document->getArrayCopy(); - - $paths = \explode('/', $request->getParam('q', '')); - $paths = \array_slice($paths, 7, \count($paths)); - - if (\count($paths) > 0) { - if (\count($paths) % 2 == 1) { - $output = $document->getAttribute(\implode('.', $paths)); - } else { - $id = (int) \array_pop($paths); - $output = $document->search('$id', $id, $document->getAttribute(\implode('.', $paths))); - } - - $output = ($output instanceof Document) ? $output->getArrayCopy() : $output; - - if (!\is_array($output)) { - throw new Exception('No document found', 404); - } - } - - /* - * View - */ - $response->json($output); } - ); + + /* + * View + */ + $response->json($output); + }, ['request', 'response', 'projectDB']); App::patch('/v1/database/collections/:collectionId/documents/:documentId') ->desc('Update Document') @@ -586,71 +579,74 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId') ->param('data', [], function () { return new JSON(); }, 'Document data as JSON object.') ->param('read', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') ->param('write', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') - ->action( - function ($collectionId, $documentId, $data, $read, $write) use ($response, $projectDB, $webhook, $audit) { - $collection = $projectDB->getDocument($collectionId, false); - $document = $projectDB->getDocument($documentId, false); + ->action(function ($collectionId, $documentId, $data, $read, $write, $response, $projectDB, $webhook, $audit) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhook */ + /** @var Appwrite\Event\Event $audit */ - $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array + $collection = $projectDB->getDocument($collectionId, false); + $document = $projectDB->getDocument($documentId, false); - if (!\is_array($data)) { - throw new Exception('Data param should be a valid JSON', 400); - } + $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array - if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } - - if (empty($document->getArrayCopy()) || $document->getCollection() != $collectionId) { // Check empty - throw new Exception('No document found', 404); - } - - //TODO check merge read write permissions - - if (!empty($read)) { // Overwrite permissions only when passed - $data['$permissions']['read'] = $read; - } - - if (!empty($write)) { // Overwrite permissions only when passed - $data['$permissions']['write'] = $read; - } - - $data = \array_merge($document->getArrayCopy(), $data); - - $data['$collection'] = $collection->getId(); // Make sure user don't switch collectionID - $data['$id'] = $document->getId(); // Make sure user don't switch document unique ID - - if (empty($data)) { - throw new Exception('Missing payload', 400); - } - try { - $data = $projectDB->updateDocument($data); - } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized action', 401); - } catch (StructureException $exception) { - throw new Exception('Bad structure. '.$exception->getMessage(), 400); - } catch (\Exception $exception) { - throw new Exception('Failed saving document to DB', 500); - } - - $data = $data->getArrayCopy(); - - $webhook - ->setParam('payload', $data) - ; - - $audit - ->setParam('event', 'database.documents.update') - ->setParam('resource', 'database/document/'.$data['$id']) - ->setParam('data', $data) - ; - - /* - * View - */ - $response->json($data); + if (!\is_array($data)) { + throw new Exception('Data param should be a valid JSON', 400); } - ); + + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + throw new Exception('Collection not found', 404); + } + + if (empty($document->getArrayCopy()) || $document->getCollection() != $collectionId) { // Check empty + throw new Exception('No document found', 404); + } + + //TODO check merge read write permissions + + if (!empty($read)) { // Overwrite permissions only when passed + $data['$permissions']['read'] = $read; + } + + if (!empty($write)) { // Overwrite permissions only when passed + $data['$permissions']['write'] = $read; + } + + $data = \array_merge($document->getArrayCopy(), $data); + + $data['$collection'] = $collection->getId(); // Make sure user don't switch collectionID + $data['$id'] = $document->getId(); // Make sure user don't switch document unique ID + + if (empty($data)) { + throw new Exception('Missing payload', 400); + } + try { + $data = $projectDB->updateDocument($data); + } catch (AuthorizationException $exception) { + throw new Exception('Unauthorized action', 401); + } catch (StructureException $exception) { + throw new Exception('Bad structure. '.$exception->getMessage(), 400); + } catch (\Exception $exception) { + throw new Exception('Failed saving document to DB', 500); + } + + $data = $data->getArrayCopy(); + + $webhook + ->setParam('payload', $data) + ; + + $audit + ->setParam('event', 'database.documents.update') + ->setParam('resource', 'database/document/'.$data['$id']) + ->setParam('data', $data) + ; + + /* + * View + */ + $response->json($data); + }, ['response', 'projectDB', 'webhook', 'audit']); App::delete('/v1/database/collections/:collectionId/documents/:documentId') ->desc('Delete Document') @@ -663,41 +659,44 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId') ->label('sdk.description', '/docs/references/database/delete-document.md') ->param('collectionId', null, function () { return new UID(); }, 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') ->param('documentId', null, function () { return new UID(); }, 'Document unique ID.') - ->action( - function ($collectionId, $documentId) use ($response, $projectDB, $audit, $webhook) { - $collection = $projectDB->getDocument($collectionId, false); - $document = $projectDB->getDocument($documentId, false); + ->action(function ($collectionId, $documentId, $response, $projectDB, $webhook, $audit) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhook */ + /** @var Appwrite\Event\Event $audit */ - if (empty($document->getArrayCopy()) || $document->getCollection() != $collectionId) { // Check empty - throw new Exception('No document found', 404); - } + $collection = $projectDB->getDocument($collectionId, false); + $document = $projectDB->getDocument($documentId, false); - if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } - - try { - $projectDB->deleteDocument($documentId); - } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized action', 401); - } catch (StructureException $exception) { - throw new Exception('Bad structure. '.$exception->getMessage(), 400); - } catch (\Exception $exception) { - throw new Exception('Failed to remove document from DB', 500); - } - - $data = $document->getArrayCopy(); - - $webhook - ->setParam('payload', $data) - ; - - $audit - ->setParam('event', 'database.documents.delete') - ->setParam('resource', 'database/document/'.$data['$id']) - ->setParam('data', $data) // Audit document in case of malicious or disastrous action - ; - - $response->noContent(); + if (empty($document->getArrayCopy()) || $document->getCollection() != $collectionId) { // Check empty + throw new Exception('No document found', 404); } - ); + + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + throw new Exception('Collection not found', 404); + } + + try { + $projectDB->deleteDocument($documentId); + } catch (AuthorizationException $exception) { + throw new Exception('Unauthorized action', 401); + } catch (StructureException $exception) { + throw new Exception('Bad structure. '.$exception->getMessage(), 400); + } catch (\Exception $exception) { + throw new Exception('Failed to remove document from DB', 500); + } + + $data = $document->getArrayCopy(); + + $webhook + ->setParam('payload', $data) + ; + + $audit + ->setParam('event', 'database.documents.delete') + ->setParam('resource', 'database/document/'.$data['$id']) + ->setParam('data', $data) // Audit document in case of malicious or disastrous action + ; + + $response->noContent(); + }, ['response', 'projectDB', 'webhook', 'audit']); \ No newline at end of file diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index e8c029619..a3477e398 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -1,7 +1,5 @@ label('sdk.namespace', 'health') ->label('sdk.method', 'get') ->label('sdk.description', '/docs/references/health/get.md') - ->action( - function () use ($response) { - $response->json(['status' => 'OK']); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ + + $response->json(['status' => 'OK']); + }, ['response']); App::get('/v1/health/version') ->desc('Get Version') ->groups(['api', 'health']) ->label('scope', 'public') - ->action( - function () use ($response) { - $response->json(['version' => APP_VERSION_STABLE]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ + + $response->json(['version' => APP_VERSION_STABLE]); + }, ['response']); App::get('/v1/health/db') ->desc('Get DB') @@ -40,13 +38,14 @@ App::get('/v1/health/db') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getDB') ->label('sdk.description', '/docs/references/health/get-db.md') - ->action( - function () use ($response, $register) { - $register->get('db'); /* @var $db PDO */ + ->action(function ($response, $register) { + /** @var Utopia\Response $response */ + /** @var Utopia\Registry\Registry $register */ - $response->json(['status' => 'OK']); - } - ); + $register->get('db'); /* @var $db PDO */ + + $response->json(['status' => 'OK']); + }, ['response', 'register']); App::get('/v1/health/cache') ->desc('Get Cache') @@ -56,13 +55,13 @@ App::get('/v1/health/cache') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getCache') ->label('sdk.description', '/docs/references/health/get-cache.md') - ->action( - function () use ($response, $register) { - $register->get('cache'); /* @var $cache Predis\Client */ + ->action(function ($response, $register) { + /** @var Utopia\Response $response */ + /** @var Utopia\Registry\Registry $register */ + $register->get('cache'); /* @var $cache Predis\Client */ - $response->json(['status' => 'OK']); - } - ); + $response->json(['status' => 'OK']); + }, ['response']); App::get('/v1/health/time') ->desc('Get Time') @@ -72,45 +71,45 @@ App::get('/v1/health/time') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getTime') ->label('sdk.description', '/docs/references/health/get-time.md') - ->action( - function () use ($response) { - /* - * Code from: @see https://www.beliefmedia.com.au/query-ntp-time-server - */ - $host = 'time.google.com'; // https://developers.google.com/time/ - $gap = 60; // Allow [X] seconds gap + ->action(function ($response) { + /** @var Utopia\Response $response */ - /* Create a socket and connect to NTP server */ - $sock = \socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + /* + * Code from: @see https://www.beliefmedia.com.au/query-ntp-time-server + */ + $host = 'time.google.com'; // https://developers.google.com/time/ + $gap = 60; // Allow [X] seconds gap - \socket_connect($sock, $host, 123); + /* Create a socket and connect to NTP server */ + $sock = \socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); - /* Send request */ - $msg = "\010".\str_repeat("\0", 47); + \socket_connect($sock, $host, 123); - \socket_send($sock, $msg, \strlen($msg), 0); + /* Send request */ + $msg = "\010".\str_repeat("\0", 47); - /* Receive response and close socket */ - \socket_recv($sock, $recv, 48, MSG_WAITALL); - \socket_close($sock); + \socket_send($sock, $msg, \strlen($msg), 0); - /* Interpret response */ - $data = \unpack('N12', $recv); - $timestamp = \sprintf('%u', $data[9]); + /* Receive response and close socket */ + \socket_recv($sock, $recv, 48, MSG_WAITALL); + \socket_close($sock); - /* NTP is number of seconds since 0000 UT on 1 January 1900 - Unix time is seconds since 0000 UT on 1 January 1970 */ - $timestamp -= 2208988800; + /* Interpret response */ + $data = \unpack('N12', $recv); + $timestamp = \sprintf('%u', $data[9]); - $diff = ($timestamp - \time()); + /* NTP is number of seconds since 0000 UT on 1 January 1900 + Unix time is seconds since 0000 UT on 1 January 1970 */ + $timestamp -= 2208988800; - if ($diff > $gap || $diff < ($gap * -1)) { - throw new Exception('Server time gaps detected'); - } + $diff = ($timestamp - \time()); - $response->json(['remote' => $timestamp, 'local' => \time(), 'diff' => $diff]); + if ($diff > $gap || $diff < ($gap * -1)) { + throw new Exception('Server time gaps detected'); } - ); + + $response->json(['remote' => $timestamp, 'local' => \time(), 'diff' => $diff]); + }, ['response']); App::get('/v1/health/queue/webhooks') ->desc('Get Webhooks Queue') @@ -120,11 +119,11 @@ App::get('/v1/health/queue/webhooks') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getQueueWebhooks') ->label('sdk.description', '/docs/references/health/get-queue-webhooks.md') - ->action( - function () use ($response) { - $response->json(['size' => Resque::size('v1-webhooks')]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ + + $response->json(['size' => Resque::size('v1-webhooks')]); + }, ['response']); App::get('/v1/health/queue/tasks') ->desc('Get Tasks Queue') @@ -134,11 +133,11 @@ App::get('/v1/health/queue/tasks') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getQueueTasks') ->label('sdk.description', '/docs/references/health/get-queue-tasks.md') - ->action( - function () use ($response) { - $response->json(['size' => Resque::size('v1-tasks')]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ + + $response->json(['size' => Resque::size('v1-tasks')]); + }, ['response']); App::get('/v1/health/queue/logs') ->desc('Get Logs Queue') @@ -148,11 +147,11 @@ App::get('/v1/health/queue/logs') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getQueueLogs') ->label('sdk.description', '/docs/references/health/get-queue-logs.md') - ->action( - function () use ($response) { - $response->json(['size' => Resque::size('v1-audit')]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ + + $response->json(['size' => Resque::size('v1-audit')]); + }, ['response']); App::get('/v1/health/queue/usage') ->desc('Get Usage Queue') @@ -162,11 +161,11 @@ App::get('/v1/health/queue/usage') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getQueueUsage') ->label('sdk.description', '/docs/references/health/get-queue-usage.md') - ->action( - function () use ($response) { - $response->json(['size' => Resque::size('v1-usage')]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ + + $response->json(['size' => Resque::size('v1-usage')]); + }, ['response']); App::get('/v1/health/queue/certificates') ->desc('Get Certificate Queue') @@ -176,11 +175,11 @@ App::get('/v1/health/queue/certificates') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getQueueCertificates') ->label('sdk.description', '/docs/references/health/get-queue-certificates.md') - ->action( - function () use ($response) { - $response->json(['size' => Resque::size('v1-certificates')]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ + + $response->json(['size' => Resque::size('v1-certificates')]); + }, ['response']); App::get('/v1/health/queue/functions') ->desc('Get Functions Queue') @@ -190,11 +189,11 @@ App::get('/v1/health/queue/functions') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getQueueFunctions') ->label('sdk.description', '/docs/references/health/get-queue-functions.md') - ->action( - function () use ($response) { - $response->json(['size' => Resque::size('v1-functions')]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ + + $response->json(['size' => Resque::size('v1-functions')]); + }, ['response']); App::get('/v1/health/storage/local') ->desc('Get Local Storage') @@ -204,28 +203,28 @@ App::get('/v1/health/storage/local') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getStorageLocal') ->label('sdk.description', '/docs/references/health/get-storage-local.md') - ->action( - function () use ($response) { - foreach ([ - 'Uploads' => APP_STORAGE_UPLOADS, - 'Cache' => APP_STORAGE_CACHE, - 'Config' => APP_STORAGE_CONFIG, - 'Certs' => APP_STORAGE_CERTIFICATES - ] as $key => $volume) { - $device = new Local($volume); + ->action(function ($response) { + /** @var Utopia\Response $response */ - if (!\is_readable($device->getRoot())) { - throw new Exception('Device '.$key.' dir is not readable'); - } + foreach ([ + 'Uploads' => APP_STORAGE_UPLOADS, + 'Cache' => APP_STORAGE_CACHE, + 'Config' => APP_STORAGE_CONFIG, + 'Certs' => APP_STORAGE_CERTIFICATES + ] as $key => $volume) { + $device = new Local($volume); - if (!\is_writable($device->getRoot())) { - throw new Exception('Device '.$key.' dir is not writable'); - } + if (!\is_readable($device->getRoot())) { + throw new Exception('Device '.$key.' dir is not readable'); } - $response->json(['status' => 'OK']); + if (!\is_writable($device->getRoot())) { + throw new Exception('Device '.$key.' dir is not writable'); + } } - ); + + $response->json(['status' => 'OK']); + }, ['response']); App::get('/v1/health/anti-virus') ->desc('Get Anti virus') @@ -235,20 +234,20 @@ App::get('/v1/health/anti-virus') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getAntiVirus') ->label('sdk.description', '/docs/references/health/get-storage-anti-virus.md') - ->action( - function () use ($response) { - if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'disabled') { // Check if scans are enabled - throw new Exception('Anitvirus is disabled'); - } + ->action(function ($response) { + /** @var Utopia\Response $response */ - $antiVirus = new Network('clamav', 3310); - - $response->json([ - 'status' => (@$antiVirus->ping()) ? 'online' : 'offline', - 'version' => @$antiVirus->version(), - ]); + if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'disabled') { // Check if scans are enabled + throw new Exception('Anitvirus is disabled'); } - ); + + $antiVirus = new Network('clamav', 3310); + + $response->json([ + 'status' => (@$antiVirus->ping()) ? 'online' : 'offline', + 'version' => @$antiVirus->version(), + ]); + }, ['response']); App::get('/v1/health/stats') // Currently only used internally ->desc('Get System Stats') @@ -258,34 +257,35 @@ App::get('/v1/health/stats') // Currently only used internally // ->label('sdk.namespace', 'health') // ->label('sdk.method', 'getStats') ->label('docs', false) - ->action( - function () use ($response, $register) { - $device = Storage::getDevice('local'); - $cache = $register->get('cache'); + ->action(function ($response, $register) { + /** @var Utopia\Response $response */ + /** @var Utopia\Registry\Registry $register */ - $cacheStats = $cache->info(); + $device = Storage::getDevice('local'); + $cache = $register->get('cache'); - $response - ->json([ - 'server' => [ - 'name' => 'nginx', - 'version' => \shell_exec('nginx -v 2>&1'), - ], - 'storage' => [ - 'used' => Storage::human($device->getDirectorySize($device->getRoot().'/')), - 'partitionTotal' => Storage::human($device->getPartitionTotalSpace()), - 'partitionFree' => Storage::human($device->getPartitionFreeSpace()), - ], - 'cache' => [ - 'uptime' => (isset($cacheStats['uptime_in_seconds'])) ? $cacheStats['uptime_in_seconds'] : 0, - 'clients' => (isset($cacheStats['connected_clients'])) ? $cacheStats['connected_clients'] : 0, - 'hits' => (isset($cacheStats['keyspace_hits'])) ? $cacheStats['keyspace_hits'] : 0, - 'misses' => (isset($cacheStats['keyspace_misses'])) ? $cacheStats['keyspace_misses'] : 0, - 'memory_used' => (isset($cacheStats['used_memory'])) ? $cacheStats['used_memory'] : 0, - 'memory_used_human' => (isset($cacheStats['used_memory_human'])) ? $cacheStats['used_memory_human'] : 0, - 'memory_used_peak' => (isset($cacheStats['used_memory_peak'])) ? $cacheStats['used_memory_peak'] : 0, - 'memory_used_peak_human' => (isset($cacheStats['used_memory_peak_human'])) ? $cacheStats['used_memory_peak_human'] : 0, - ], - ]); - } - ); + $cacheStats = $cache->info(); + + $response + ->json([ + 'server' => [ + 'name' => 'nginx', + 'version' => \shell_exec('nginx -v 2>&1'), + ], + 'storage' => [ + 'used' => Storage::human($device->getDirectorySize($device->getRoot().'/')), + 'partitionTotal' => Storage::human($device->getPartitionTotalSpace()), + 'partitionFree' => Storage::human($device->getPartitionFreeSpace()), + ], + 'cache' => [ + 'uptime' => (isset($cacheStats['uptime_in_seconds'])) ? $cacheStats['uptime_in_seconds'] : 0, + 'clients' => (isset($cacheStats['connected_clients'])) ? $cacheStats['connected_clients'] : 0, + 'hits' => (isset($cacheStats['keyspace_hits'])) ? $cacheStats['keyspace_hits'] : 0, + 'misses' => (isset($cacheStats['keyspace_misses'])) ? $cacheStats['keyspace_misses'] : 0, + 'memory_used' => (isset($cacheStats['used_memory'])) ? $cacheStats['used_memory'] : 0, + 'memory_used_human' => (isset($cacheStats['used_memory_human'])) ? $cacheStats['used_memory_human'] : 0, + 'memory_used_peak' => (isset($cacheStats['used_memory_peak'])) ? $cacheStats['used_memory_peak'] : 0, + 'memory_used_peak_human' => (isset($cacheStats['used_memory_peak_human'])) ? $cacheStats['used_memory_peak_human'] : 0, + ], + ]); + }, ['response', 'register']); diff --git a/app/controllers/api/locale.php b/app/controllers/api/locale.php index 0bbc12f4c..2ae098abb 100644 --- a/app/controllers/api/locale.php +++ b/app/controllers/api/locale.php @@ -1,9 +1,6 @@ label('sdk.namespace', 'locale') ->label('sdk.method', 'get') ->label('sdk.description', '/docs/references/locale/get-locale.md') - ->action( - function () use ($response, $request) { - $eu = include __DIR__.'/../../config/eu.php'; - $currencies = include __DIR__.'/../../config/currencies.php'; - $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); - $output = []; - $ip = $request->getIP(); - $time = (60 * 60 * 24 * 45); // 45 days cache - $countries = Locale::getText('countries'); - $continents = Locale::getText('continents'); + ->action(function ($request, $response, $locale) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Utopia\Locale\Locale $locale */ - if (!App::isProduction()) { - $ip = '79.177.241.94'; - } + $eu = include __DIR__.'/../../config/eu.php'; + $currencies = include __DIR__.'/../../config/currencies.php'; + $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); + $output = []; + $ip = $request->getIP(); + $time = (60 * 60 * 24 * 45); // 45 days cache + $countries = $locale->getText('countries'); + $continents = $locale->getText('continents'); - $output['ip'] = $ip; - - $currency = null; - - try { - $record = $reader->country($ip); - $output['countryCode'] = $record->country->isoCode; - $output['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown'); - //$output['countryTimeZone'] = DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, $record->country->isoCode); - $output['continent'] = (isset($continents[$record->continent->code])) ? $continents[$record->continent->code] : Locale::getText('locale.country.unknown'); - $output['continentCode'] = $record->continent->code; - $output['eu'] = (\in_array($record->country->isoCode, $eu)) ? true : false; - - foreach ($currencies as $code => $element) { - if (isset($element['locations']) && isset($element['code']) && \in_array($record->country->isoCode, $element['locations'])) { - $currency = $element['code']; - } - } - - $output['currency'] = $currency; - } catch (\Exception $e) { - $output['countryCode'] = '--'; - $output['country'] = Locale::getText('locale.country.unknown'); - $output['continent'] = Locale::getText('locale.country.unknown'); - $output['continentCode'] = '--'; - $output['eu'] = false; - $output['currency'] = $currency; - } - - $response - ->addHeader('Cache-Control', 'public, max-age='.$time) - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time).' GMT') // 45 days cache - ->json($output); + if (!App::isProduction()) { + $ip = '79.177.241.94'; } - ); + + $output['ip'] = $ip; + + $currency = null; + + try { + $record = $reader->country($ip); + $output['countryCode'] = $record->country->isoCode; + $output['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : $locale->getText('locale.country.unknown'); + //$output['countryTimeZone'] = DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, $record->country->isoCode); + $output['continent'] = (isset($continents[$record->continent->code])) ? $continents[$record->continent->code] : $locale->getText('locale.country.unknown'); + $output['continentCode'] = $record->continent->code; + $output['eu'] = (\in_array($record->country->isoCode, $eu)) ? true : false; + + foreach ($currencies as $code => $element) { + if (isset($element['locations']) && isset($element['code']) && \in_array($record->country->isoCode, $element['locations'])) { + $currency = $element['code']; + } + } + + $output['currency'] = $currency; + } catch (\Exception $e) { + $output['countryCode'] = '--'; + $output['country'] = $locale->getText('locale.country.unknown'); + $output['continent'] = $locale->getText('locale.country.unknown'); + $output['continentCode'] = '--'; + $output['eu'] = false; + $output['currency'] = $currency; + } + + $response + ->addHeader('Cache-Control', 'public, max-age='.$time) + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time).' GMT') // 45 days cache + ->json($output); + }, ['request', 'response', 'locale']); App::get('/v1/locale/countries') ->desc('List Countries') @@ -73,15 +72,16 @@ App::get('/v1/locale/countries') ->label('sdk.namespace', 'locale') ->label('sdk.method', 'getCountries') ->label('sdk.description', '/docs/references/locale/get-countries.md') - ->action( - function () use ($response) { - $list = Locale::getText('countries'); /* @var $list array */ + ->action(function ($response, $locale) { + /** @var Utopia\Response $response */ + /** @var Utopia\Locale\Locale $locale */ - \asort($list); + $list = $locale->getText('countries'); /* @var $list array */ - $response->json($list); - } - ); + \asort($list); + + $response->json($list); + }, ['response', 'locale']); App::get('/v1/locale/countries/eu') ->desc('List EU Countries') @@ -91,23 +91,24 @@ App::get('/v1/locale/countries/eu') ->label('sdk.namespace', 'locale') ->label('sdk.method', 'getCountriesEU') ->label('sdk.description', '/docs/references/locale/get-countries-eu.md') - ->action( - function () use ($response) { - $countries = Locale::getText('countries'); /* @var $countries array */ - $eu = include __DIR__.'/../../config/eu.php'; - $list = []; + ->action(function ($response, $locale) { + /** @var Utopia\Response $response */ + /** @var Utopia\Locale\Locale $locale */ - foreach ($eu as $code) { - if (\array_key_exists($code, $countries)) { - $list[$code] = $countries[$code]; - } + $countries = $locale->getText('countries'); /* @var $countries array */ + $eu = include __DIR__.'/../../config/eu.php'; + $list = []; + + foreach ($eu as $code) { + if (\array_key_exists($code, $countries)) { + $list[$code] = $countries[$code]; } - - \asort($list); - - $response->json($list); } - ); + + \asort($list); + + $response->json($list); + }, ['response', 'locale']); App::get('/v1/locale/countries/phones') ->desc('List Countries Phone Codes') @@ -117,23 +118,24 @@ App::get('/v1/locale/countries/phones') ->label('sdk.namespace', 'locale') ->label('sdk.method', 'getCountriesPhones') ->label('sdk.description', '/docs/references/locale/get-countries-phones.md') - ->action( - function () use ($response) { - $list = include __DIR__.'/../../config/phones.php'; /* @var $list array */ + ->action(function ($response, $locale) { + /** @var Utopia\Response $response */ + /** @var Utopia\Locale\Locale $locale */ - $countries = Locale::getText('countries'); /* @var $countries array */ + $list = include __DIR__.'/../../config/phones.php'; /* @var $list array */ - foreach ($list as $code => $name) { - if (\array_key_exists($code, $countries)) { - $list[$code] = '+'.$list[$code]; - } + $countries = $locale->getText('countries'); /* @var $countries array */ + + foreach ($list as $code => $name) { + if (\array_key_exists($code, $countries)) { + $list[$code] = '+'.$list[$code]; } - - \asort($list); - - $response->json($list); } - ); + + \asort($list); + + $response->json($list); + }, ['response', 'locale']); App::get('/v1/locale/continents') ->desc('List Continents') @@ -143,15 +145,16 @@ App::get('/v1/locale/continents') ->label('sdk.namespace', 'locale') ->label('sdk.method', 'getContinents') ->label('sdk.description', '/docs/references/locale/get-continents.md') - ->action( - function () use ($response) { - $list = Locale::getText('continents'); /* @var $list array */ + ->action(function ($response, $locale) { + /** @var Utopia\Response $response */ + /** @var Utopia\Locale\Locale $locale */ - \asort($list); + $list = $locale->getText('continents'); /* @var $list array */ - $response->json($list); - } - ); + \asort($list); + + $response->json($list); + }, ['response', 'locale']); App::get('/v1/locale/currencies') @@ -162,13 +165,13 @@ App::get('/v1/locale/currencies') ->label('sdk.namespace', 'locale') ->label('sdk.method', 'getCurrencies') ->label('sdk.description', '/docs/references/locale/get-currencies.md') - ->action( - function () use ($response) { - $currencies = include __DIR__.'/../../config/currencies.php'; + ->action(function ($response) { + /** @var Utopia\Response $response */ - $response->json($currencies); - } - ); + $currencies = include __DIR__.'/../../config/currencies.php'; + + $response->json($currencies); + }, ['response']); App::get('/v1/locale/languages') @@ -179,10 +182,10 @@ App::get('/v1/locale/languages') ->label('sdk.namespace', 'locale') ->label('sdk.method', 'getLanguages') ->label('sdk.description', '/docs/references/locale/get-languages.md') - ->action( - function () use ($response) { - $languages = include __DIR__.'/../../config/languages.php'; + ->action(function ($response) { + /** @var Utopia\Response $response */ - $response->json($languages); - } - ); \ No newline at end of file + $languages = include __DIR__.'/../../config/languages.php'; + + $response->json($languages); + }, ['response']); \ No newline at end of file diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 4b366efa2..ab3dd7762 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1,7 +1,5 @@ __DIR__.'/../../config/files/none.png', - - // Video Files - 'video/mp4' => __DIR__.'/../../config/files/video.png', - 'video/x-flv' => __DIR__.'/../../config/files/video.png', - 'application/x-mpegURL' => __DIR__.'/../../config/files/video.png', - 'video/MP2T' => __DIR__.'/../../config/files/video.png', - 'video/3gpp' => __DIR__.'/../../config/files/video.png', - 'video/quicktime' => __DIR__.'/../../config/files/video.png', - 'video/x-msvideo' => __DIR__.'/../../config/files/video.png', - 'video/x-ms-wmv' => __DIR__.'/../../config/files/video.png', - - // // Microsoft Word - 'application/msword' => __DIR__.'/../../config/files/word.png', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => __DIR__.'/../../config/files/word.png', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => __DIR__.'/../../config/files/word.png', - 'application/vnd.ms-word.document.macroEnabled.12' => __DIR__.'/../../config/files/word.png', - - // // Microsoft Excel - 'application/vnd.ms-excel' => __DIR__.'/../../config/files/excel.png', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => __DIR__.'/../../config/files/excel.png', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => __DIR__.'/../../config/files/excel.png', - 'application/vnd.ms-excel.sheet.macroEnabled.12' => __DIR__.'/../../config/files/excel.png', - 'application/vnd.ms-excel.template.macroEnabled.12' => __DIR__.'/../../config/files/excel.png', - 'application/vnd.ms-excel.addin.macroEnabled.12' => __DIR__.'/../../config/files/excel.png', - 'application/vnd.ms-excel.sheet.binary.macroEnabled.12' => __DIR__.'/../../config/files/excel.png', - - // // Microsoft Power Point - 'application/vnd.ms-powerpoint' => __DIR__.'/../../config/files/ppt.png', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => __DIR__.'/../../config/files/ppt.png', - 'application/vnd.openxmlformats-officedocument.presentationml.template' => __DIR__.'/../../config/files/ppt.png', - 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => __DIR__.'/../../config/files/ppt.png', - 'application/vnd.ms-powerpoint.addin.macroEnabled.12' => __DIR__.'/../../config/files/ppt.png', - 'application/vnd.ms-powerpoint.presentation.macroEnabled.12' => __DIR__.'/../../config/files/ppt.png', - 'application/vnd.ms-powerpoint.template.macroEnabled.12' => __DIR__.'/../../config/files/ppt.png', - 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12' => __DIR__.'/../../config/files/ppt.png', - - // Adobe PDF - 'application/pdf' => __DIR__.'/../../config/files/pdf.png', -]; +use Utopia\Config\Config; $inputs = [ 'jpg' => 'image/jpeg', @@ -81,58 +38,9 @@ $outputs = [ 'webp' => 'image/webp', ]; -$mimes = [ - 'image/jpeg', - 'image/jpeg', - 'image/gif', - 'image/png', - 'image/webp', - - // Video Files - 'video/mp4', - 'video/x-flv', - 'application/x-mpegURL', - 'video/MP2T', - 'video/3gpp', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - - // Microsoft Word - 'application/msword', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', - 'application/vnd.ms-word.document.macroEnabled.12', - - // Microsoft Excel - 'application/vnd.ms-excel', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', - 'application/vnd.ms-excel.sheet.macroEnabled.12', - 'application/vnd.ms-excel.template.macroEnabled.12', - 'application/vnd.ms-excel.addin.macroEnabled.12', - 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', - - // Microsoft Power Point - 'application/vnd.ms-powerpoint', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/vnd.openxmlformats-officedocument.presentationml.template', - 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', - 'application/vnd.ms-powerpoint.addin.macroEnabled.12', - 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', - 'application/vnd.ms-powerpoint.template.macroEnabled.12', - 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', - - // Microsoft Access - 'application/vnd.ms-access', - - // Adobe PDF - 'application/pdf', -]; - -App::init(function () use ($project) { +App::init(function ($project) { Storage::addDevice('local', new Local(APP_STORAGE_UPLOADS.'/app-'.$project->getId())); -}, 'storage'); +}, ['project'], 'storage'); App::post('/v1/storage/files') ->desc('Create File') @@ -148,128 +56,133 @@ App::post('/v1/storage/files') ->param('file', [], function () { return new File(); }, 'Binary File.', false) ->param('read', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') ->param('write', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') - // ->param('folderId', '', function () { return new UID(); }, 'Folder to associate files with.', true) - ->action( - function ($file, $read, $write, $folderId = '') use ($request, $response, $user, $projectDB, $webhook, $audit, $usage) { - $file = $request->getFiles('file'); - $read = (empty($read)) ? ['user:'.$user->getId()] : $read; - $write = (empty($write)) ? ['user:'.$user->getId()] : $write; + ->action(function ($file, $read, $write, $request, $response, $user, $projectDB, $webhook, $audit, $usage) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhook */ + /** @var Appwrite\Event\Event $audit */ + /** @var Appwrite\Event\Event $usage */ - /* - * Validators - */ - //$fileType = new FileType(array(FileType::FILE_TYPE_PNG, FileType::FILE_TYPE_GIF, FileType::FILE_TYPE_JPEG)); - $fileSize = new FileSize(App::getEnv('_APP_STORAGE_LIMIT', 0)); - $upload = new Upload(); + $file = $request->getFiles('file'); + $read = (empty($read)) ? ['user:'.$user->getId()] : $read; + $write = (empty($write)) ? ['user:'.$user->getId()] : $write; - if (empty($file)) { - throw new Exception('No file sent', 400); - } + /* + * Validators + */ + //$fileType = new FileType(array(FileType::FILE_TYPE_PNG, FileType::FILE_TYPE_GIF, FileType::FILE_TYPE_JPEG)); + $fileSize = new FileSize(App::getEnv('_APP_STORAGE_LIMIT', 0)); + $upload = new Upload(); - // Make sure we handle a single file and multiple files the same way - $file['name'] = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name']; - $file['tmp_name'] = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name']; - $file['size'] = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size']; + if (empty($file)) { + throw new Exception('No file sent', 400); + } - // Check if file type is allowed (feature for project settings?) - //if (!$fileType->isValid($file['tmp_name'])) { - //throw new Exception('File type not allowed', 400); - //} + // Make sure we handle a single file and multiple files the same way + $file['name'] = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name']; + $file['tmp_name'] = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name']; + $file['size'] = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size']; + + // Check if file type is allowed (feature for project settings?) + //if (!$fileType->isValid($file['tmp_name'])) { + //throw new Exception('File type not allowed', 400); + //} + + // Check if file size is exceeding allowed limit + if (!$fileSize->isValid($file['size'])) { + throw new Exception('File size not allowed', 400); + } + + /* + * Models + */ + $device = Storage::getDevice('local'); + + if (!$upload->isValid($file['tmp_name'])) { + throw new Exception('Invalid file', 403); + } + + // Save to storage + $size = $device->getFileSize($file['tmp_name']); + $path = $device->getPath(\uniqid().'.'.\pathinfo($file['name'], PATHINFO_EXTENSION)); + + if (!$device->upload($file['tmp_name'], $path)) { // TODO deprecate 'upload' and replace with 'move' + throw new Exception('Failed moving file', 500); + } + + $mimeType = $device->getFileMimeType($path); // Get mime-type before compression and encryption + + if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled') { // Check if scans are enabled + $antiVirus = new Network('clamav', 3310); // Check if file size is exceeding allowed limit - if (!$fileSize->isValid($file['size'])) { - throw new Exception('File size not allowed', 400); - } - - /* - * Models - */ - $device = Storage::getDevice('local'); - - if (!$upload->isValid($file['tmp_name'])) { + if (!$antiVirus->fileScan($path)) { + $device->delete($path); throw new Exception('Invalid file', 403); } - - // Save to storage - $size = $device->getFileSize($file['tmp_name']); - $path = $device->getPath(\uniqid().'.'.\pathinfo($file['name'], PATHINFO_EXTENSION)); - - if (!$device->upload($file['tmp_name'], $path)) { // TODO deprecate 'upload' and replace with 'move' - throw new Exception('Failed moving file', 500); - } - - $mimeType = $device->getFileMimeType($path); // Get mime-type before compression and encryption - - if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled') { // Check if scans are enabled - $antiVirus = new Network('clamav', 3310); - - // Check if file size is exceeding allowed limit - if (!$antiVirus->fileScan($path)) { - $device->delete($path); - throw new Exception('Invalid file', 403); - } - } - - // Compression - $compressor = new GZIP(); - $data = $device->read($path); - $data = $compressor->compress($data); - $key = App::getEnv('_APP_OPENSSL_KEY_V1'); - $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); - $data = OpenSSL::encrypt($data, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag); - - if (!$device->write($path, $data)) { - throw new Exception('Failed to save file', 500); - } - - $sizeActual = $device->getFileSize($path); - - $file = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_FILES, - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - 'dateCreated' => \time(), - 'folderId' => $folderId, - 'name' => $file['name'], - 'path' => $path, - 'signature' => $device->getFileHash($path), - 'mimeType' => $mimeType, - 'sizeOriginal' => $size, - 'sizeActual' => $sizeActual, - 'algorithm' => $compressor->getName(), - 'token' => \bin2hex(\random_bytes(64)), - 'comment' => '', - 'fileOpenSSLVersion' => '1', - 'fileOpenSSLCipher' => OpenSSL::CIPHER_AES_128_GCM, - 'fileOpenSSLTag' => \bin2hex($tag), - 'fileOpenSSLIV' => \bin2hex($iv), - ]); - - if (false === $file) { - throw new Exception('Failed saving file to DB', 500); - } - - $webhook - ->setParam('payload', $file->getArrayCopy()) - ; - - $audit - ->setParam('event', 'storage.files.create') - ->setParam('resource', 'storage/files/'.$file->getId()) - ; - - $usage - ->setParam('storage', $sizeActual) - ; - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($file->getArrayCopy()) - ; } - ); + + // Compression + $compressor = new GZIP(); + $data = $device->read($path); + $data = $compressor->compress($data); + $key = App::getEnv('_APP_OPENSSL_KEY_V1'); + $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); + $data = OpenSSL::encrypt($data, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag); + + if (!$device->write($path, $data)) { + throw new Exception('Failed to save file', 500); + } + + $sizeActual = $device->getFileSize($path); + + $file = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_FILES, + '$permissions' => [ + 'read' => $read, + 'write' => $write, + ], + 'dateCreated' => \time(), + 'folderId' => '', + 'name' => $file['name'], + 'path' => $path, + 'signature' => $device->getFileHash($path), + 'mimeType' => $mimeType, + 'sizeOriginal' => $size, + 'sizeActual' => $sizeActual, + 'algorithm' => $compressor->getName(), + 'token' => \bin2hex(\random_bytes(64)), + 'comment' => '', + 'fileOpenSSLVersion' => '1', + 'fileOpenSSLCipher' => OpenSSL::CIPHER_AES_128_GCM, + 'fileOpenSSLTag' => \bin2hex($tag), + 'fileOpenSSLIV' => \bin2hex($iv), + ]); + + if (false === $file) { + throw new Exception('Failed saving file to DB', 500); + } + + $webhook + ->setParam('payload', $file->getArrayCopy()) + ; + + $audit + ->setParam('event', 'storage.files.create') + ->setParam('resource', 'storage/files/'.$file->getId()) + ; + + $usage + ->setParam('storage', $sizeActual) + ; + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($file->getArrayCopy()) + ; + }, ['request', 'response', 'user', 'projectDB', 'webhook', 'audit', 'usage']); App::get('/v1/storage/files') ->desc('List Files') @@ -283,27 +196,28 @@ App::get('/v1/storage/files') ->param('limit', 25, function () { return new Range(0, 100); }, 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true) ->param('offset', 0, function () { return new Range(0, 2000); }, 'Results offset. The default value is 0. Use this param to manage pagination.', true) ->param('orderType', 'ASC', function () { return new WhiteList(['ASC', 'DESC']); }, 'Order result by ASC or DESC order.', true) - ->action( - function ($search, $limit, $offset, $orderType) use ($response, $projectDB) { - $results = $projectDB->getCollection([ - 'limit' => $limit, - 'offset' => $offset, - 'orderField' => 'dateCreated', - 'orderType' => $orderType, - 'orderCast' => 'int', - 'search' => $search, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_FILES, - ], - ]); + ->action(function ($search, $limit, $offset, $orderType, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - $results = \array_map(function ($value) { /* @var $value \Database\Document */ - return $value->getArrayCopy(['$id', '$permissions', 'name', 'dateCreated', 'signature', 'mimeType', 'sizeOriginal']); - }, $results); + $results = $projectDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => 'dateCreated', + 'orderType' => $orderType, + 'orderCast' => 'int', + 'search' => $search, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_FILES, + ], + ]); - $response->json(['sum' => $projectDB->getSum(), 'files' => $results]); - } - ); + $results = \array_map(function ($value) { /* @var $value \Database\Document */ + return $value->getArrayCopy(['$id', '$permissions', 'name', 'dateCreated', 'signature', 'mimeType', 'sizeOriginal']); + }, $results); + + $response->json(['sum' => $projectDB->getSum(), 'files' => $results]); + }, ['response', 'projectDB']); App::get('/v1/storage/files/:fileId') ->desc('Get File') @@ -314,17 +228,18 @@ App::get('/v1/storage/files/:fileId') ->label('sdk.method', 'getFile') ->label('sdk.description', '/docs/references/storage/get-file.md') ->param('fileId', '', function () { return new UID(); }, 'File unique ID.') - ->action( - function ($fileId) use ($response, $projectDB) { - $file = $projectDB->getDocument($fileId); + ->action(function ($fileId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { - throw new Exception('File not found', 404); - } + $file = $projectDB->getDocument($fileId); - $response->json($file->getArrayCopy(['$id', '$permissions', 'name', 'dateCreated', 'signature', 'mimeType', 'sizeOriginal'])); + if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { + throw new Exception('File not found', 404); } - ); + + $response->json($file->getArrayCopy(['$id', '$permissions', 'name', 'dateCreated', 'signature', 'mimeType', 'sizeOriginal'])); + }, ['response', 'projectDB']); App::get('/v1/storage/files/:fileId/preview') ->desc('Get File Preview') @@ -343,7 +258,7 @@ App::get('/v1/storage/files/:fileId/preview') ->param('background', '', function () { return new HexColor(); }, 'Preview image background color. Only works with transparent images (png). Use a valid HEX color, no # is needed for prefix.', true) ->param('output', null, function () use ($outputs) { return new WhiteList(\array_merge(\array_keys($outputs), [null])); }, 'Output format type (jpeg, jpg, png, gif and webp).', true) ->action( - function ($fileId, $width, $height, $quality, $background, $output) use ($request, $response, $projectDB, $project, $inputs, $outputs, $fileLogos) { + function ($fileId, $width, $height, $quality, $background, $output) use ($request, $response, $projectDB, $project, $inputs, $outputs) { $storage = 'local'; if (!\extension_loaded('imagick')) { @@ -372,6 +287,7 @@ App::get('/v1/storage/files/:fileId/preview') $algorithm = $file->getAttribute('algorithm'); $cipher = $file->getAttribute('fileOpenSSLCipher'); $mime = $file->getAttribute('mimeType'); + $fileLogos = Config::getParam('storage-logos'); if (!\in_array($mime, $inputs)) { $path = (\array_key_exists($mime, $fileLogos)) ? $fileLogos[$mime] : $fileLogos['default']; @@ -516,8 +432,9 @@ App::get('/v1/storage/files/:fileId/view') ->param('fileId', '', function () { return new UID(); }, 'File unique ID.') ->param('as', '', function () { return new WhiteList(['pdf', /*'html',*/ 'text']); }, 'Choose a file format to convert your file to. Currently you can only convert word and pdf files to pdf or txt. This option is currently experimental only, use at your own risk.', true) ->action( - function ($fileId, $as) use ($response, $projectDB, $mimes) { - $file = $projectDB->getDocument($fileId); + function ($fileId, $as) use ($response, $projectDB) { + $file = $projectDB->getDocument($fileId); + $mimes = Config::getParam('storage-mimes'); if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { throw new Exception('File not found', 404); diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 76a61aef3..c63e52550 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1,7 +1,5 @@ desc('Mock a get request for SDK tests') ->label('scope', 'public') @@ -335,7 +333,8 @@ App::get('/v1/mock/tests/general/oauth2/failure') } ); -App::shutdown(function() use ($response, $request, &$result, $utopia) { +App::shutdown(function($response, $request, $utopia) { + $result = []; $route = $utopia->match($request); $path = APP_STORAGE_CACHE.'/tests.json'; $tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : []; @@ -353,4 +352,4 @@ App::shutdown(function() use ($response, $request, &$result, $utopia) { } $response->json(['result' => $route->getMethod() . ':' . $route->getURL() . ':passed']); -}, 'mock'); \ No newline at end of file +}, ['response', 'request', 'utopia'], 'mock'); \ No newline at end of file diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index b7d984524..38c40498c 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -7,7 +7,7 @@ use Utopia\Abuse\Adapters\TimeLimit; global $utopia, $request, $response, $register, $user, $project; -App::init(function () use ($utopia, $request, $response, $register, $user, $project) { +App::init(function ($utopia, $request, $response, $register, $user, $project) { $route = $utopia->match($request); if (empty($project->getId()) && $route->getLabel('abuse-limit', 0) > 0) { // Abuse limit requires an active project scope @@ -47,4 +47,4 @@ App::init(function () use ($utopia, $request, $response, $register, $user, $proj if ($abuse->check() && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') { throw new Exception('Too many requests', 429); } -}, 'api'); \ No newline at end of file +}, ['utopia', 'request', 'response', 'register', 'user', 'project'], 'api'); \ No newline at end of file diff --git a/app/controllers/shared/web.php b/app/controllers/shared/web.php index 1a15890c8..9ff361229 100644 --- a/app/controllers/shared/web.php +++ b/app/controllers/shared/web.php @@ -4,9 +4,7 @@ use Utopia\App; use Utopia\View; use Utopia\Config\Config; -$layout = new View(__DIR__.'/../../views/layouts/default.phtml'); - -App::init(function () use ($utopia, $response, $request, $layout) { +App::init(function ($utopia, $response, $request, $layout) { /* AJAX check */ if (!empty($request->getQuery('version', ''))) { @@ -29,7 +27,6 @@ App::init(function () use ($utopia, $response, $request, $layout) { ; $time = (60 * 60 * 24 * 45); // 45 days cache - $isDev = (\Utopia\App::MODE_TYPE_DEVELOPMENT == Config::getParam('env')); $response ->addHeader('Cache-Control', 'public, max-age='.$time) @@ -40,7 +37,7 @@ App::init(function () use ($utopia, $response, $request, $layout) { $scope = $route->getLabel('scope', ''); $layout ->setParam('version', Config::getParam('version')) - ->setParam('isDev', $isDev) + ->setParam('isDev', App::isDevelopment()) ->setParam('class', $scope) ; -}, 'web'); +}, ['utopia', 'response', 'request', 'layout'], 'web'); diff --git a/app/controllers/web/console.php b/app/controllers/web/console.php index d3d28c52a..3369d7011 100644 --- a/app/controllers/web/console.php +++ b/app/controllers/web/console.php @@ -11,14 +11,19 @@ use Appwrite\Database\Validator\Authorization; use Appwrite\Database\Validator\UID; use Appwrite\Storage\Storage; -App::init(function () use ($layout) { +App::init(function ($layout) { + /** @var Utopia\View $layout */ + $layout ->setParam('description', 'Appwrite Console allows you to easily manage, monitor, and control your entire backend API and tools.') ->setParam('analytics', 'UA-26264668-5') ; -}, 'console'); +}, ['layout'], 'console'); + +App::shutdown(function ($response, $layout) { + /** @var Utopia\Response $response */ + /** @var Utopia\View $layout */ -App::shutdown(function () use ($response, $layout) { $header = new View(__DIR__.'/../../views/console/comps/header.phtml'); $footer = new View(__DIR__.'/../../views/console/comps/footer.phtml'); @@ -33,14 +38,16 @@ App::shutdown(function () use ($response, $layout) { ; $response->send($layout->render()); -}, 'console'); +}, ['response', 'layout'], 'console'); App::get('/error/:code') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'home') ->param('code', null, new \Utopia\Validator\Numeric(), 'Valid status code number', false) - ->action(function ($code) use ($layout) { + ->action(function ($code, $layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/error.phtml'); $page @@ -50,13 +57,15 @@ App::get('/error/:code') $layout ->setParam('title', APP_NAME.' - Error') ->setParam('body', $page); - }); + }, ['layout']); App::get('/console') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/index.phtml'); $page @@ -66,13 +75,15 @@ App::get('/console') $layout ->setParam('title', APP_NAME.' - Console') ->setParam('body', $page); - }); + }, ['layout']); App::get('/console/account') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/account/index.phtml'); $cc = new View(__DIR__.'/../../views/console/forms/credit-card.phtml'); @@ -84,37 +95,43 @@ App::get('/console/account') $layout ->setParam('title', 'Account - '.APP_NAME) ->setParam('body', $page); - }); + }, ['layout']); App::get('/console/notifications') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/v1/console/notifications/index.phtml'); $layout ->setParam('title', APP_NAME.' - Notifications') ->setParam('body', $page); - }); + }, ['layout']); App::get('/console/home') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/home/index.phtml'); $layout ->setParam('title', APP_NAME.' - Console') ->setParam('body', $page); - }); + }, ['layout']); App::get('/console/settings') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', '')); $page = new View(__DIR__.'/../../views/console/settings/index.phtml'); @@ -127,13 +144,15 @@ App::get('/console/settings') $layout ->setParam('title', APP_NAME.' - Settings') ->setParam('body', $page); - }); + }, ['layout']); App::get('/console/webhooks') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/webhooks/index.phtml'); $page @@ -143,13 +162,15 @@ App::get('/console/webhooks') $layout ->setParam('title', APP_NAME.' - Webhooks') ->setParam('body', $page); - }); + }, ['layout']); App::get('/console/keys') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $scopes = include __DIR__.'/../../../app/config/scopes.php'; $page = new View(__DIR__.'/../../views/console/keys/index.phtml'); @@ -158,38 +179,46 @@ App::get('/console/keys') $layout ->setParam('title', APP_NAME.' - API Keys') ->setParam('body', $page); - }); + }, ['layout']); App::get('/console/tasks') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/tasks/index.phtml'); $layout ->setParam('title', APP_NAME.' - Tasks') ->setParam('body', $page); - }); + }, ['layout']); App::get('/console/database') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/database/index.phtml'); $layout ->setParam('title', APP_NAME.' - Database') ->setParam('body', $page); - }); + }, ['layout']); App::get('/console/database/collection') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->param('id', '', function () { return new UID(); }, 'Collection unique ID.') - ->action(function ($id) use ($response, $layout, $projectDB) { + ->action(function ($id, $response, $layout, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Utopia\View $layout */ + /** @var Appwrite\Database\Database $projectDB */ + Authorization::disable(); $collection = $projectDB->getDocument($id, false); Authorization::reset(); @@ -214,14 +243,17 @@ App::get('/console/database/collection') ->addHeader('Expires', 0) ->addHeader('Pragma', 'no-cache') ; - }); + }, ['response', 'layout', 'projectDB']); App::get('/console/database/document') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->param('collection', '', function () { return new UID(); }, 'Collection unique ID.') - ->action(function ($collection) use ($layout, $projectDB) { + ->action(function ($collection, $layout, $projectDB) { + /** @var Utopia\View $layout */ + /** @var Appwrite\Database\Database $projectDB */ + Authorization::disable(); $collection = $projectDB->getDocument($collection, false); Authorization::reset(); @@ -244,13 +276,14 @@ App::get('/console/database/document') $layout ->setParam('title', APP_NAME.' - Database Document') ->setParam('body', $page); - }); + }, ['layout', 'projectDB']); App::get('/console/storage') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($request, $layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ $page = new View(__DIR__.'/../../views/console/storage/index.phtml'); $page @@ -262,13 +295,15 @@ App::get('/console/storage') $layout ->setParam('title', APP_NAME.' - Storage') ->setParam('body', $page); - }); + }, ['layout']); App::get('/console/users') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/users/index.phtml'); $page->setParam('providers', Config::getParam('providers')); @@ -276,28 +311,32 @@ App::get('/console/users') $layout ->setParam('title', APP_NAME.' - Users') ->setParam('body', $page); - }); + }, ['layout']); App::get('/console/users/user') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/users/user.phtml'); $layout ->setParam('title', APP_NAME.' - User') ->setParam('body', $page); - }); + }, ['layout']); App::get('/console/users/teams/team') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->action(function () use ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/users/team.phtml'); $layout ->setParam('title', APP_NAME.' - Team') ->setParam('body', $page); - }); + }, ['layout']); diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index 977da411e..b9e75ca52 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -8,7 +8,7 @@ use Utopia\Config\Config; use Utopia\Validator\WhiteList; use Utopia\Validator\Range; -App::init(function () use ($layout) { +App::init(function ($layout) { $header = new View(__DIR__.'/../../views/home/comps/header.phtml'); $footer = new View(__DIR__.'/../../views/home/comps/footer.phtml'); @@ -24,11 +24,11 @@ App::init(function () use ($layout) { ->setParam('header', [$header]) ->setParam('footer', [$footer]) ; -}, 'home'); +}, ['layout'], 'home'); -App::shutdown(function () use ($response, $layout) { +App::shutdown(function ($response, $layout) { $response->send($layout->render()); -}, 'home'); +}, ['response', 'layout'], 'home'); App::get('/') ->groups(['web', 'home']) diff --git a/app/init.php b/app/init.php index 077a85457..695e1db0d 100644 --- a/app/init.php +++ b/app/init.php @@ -60,6 +60,8 @@ Config::load('services', __DIR__.'/../app/config/services.php'); // List of ser Config::load('avatar-browsers', __DIR__.'/../app/config/avatars/browsers.php'); Config::load('avatar-credit-cards', __DIR__.'/../app/config/avatars/credit-cards.php'); Config::load('avatar-flags', __DIR__.'/../app/config/avatars/flags.php'); +Config::load('storage-logos', __DIR__.'/../app/config/storage/logos.php'); +Config::load('storage-mimes', __DIR__.'/../app/config/storage/mimes.php'); Resque::setBackend(App::getEnv('_APP_REDIS_HOST', '') .':'.App::getEnv('_APP_REDIS_PORT', '')); @@ -141,10 +143,10 @@ $register->set('smtp', function () { return $mail; }); -$register->set('queue-webhooks', function () { +$register->set('queue-webhook', function () { return new Event('v1-webhooks', 'WebhooksV1'); }); -$register->set('queue-audits', function () { +$register->set('queue-audit', function () { return new Event('v1-audits', 'AuditsV1'); }); $register->set('queue-usage', function () { @@ -208,8 +210,6 @@ Locale::setLanguage('vi', include __DIR__.'/config/locales/vi.php'); Locale::setLanguage('zh-cn', include __DIR__.'/config/locales/zh-cn.php'); Locale::setLanguage('zh-tw', include __DIR__.'/config/locales/zh-tw.php'); -Locale::setDefault('en'); - \stream_context_set_default([ // Set global user agent and http settings 'http' => [ 'method' => 'GET', diff --git a/app/views/layouts/default.phtml b/app/views/layouts/default.phtml index 211a9e93a..1c60c3bf2 100644 --- a/app/views/layouts/default.phtml +++ b/app/views/layouts/default.phtml @@ -11,6 +11,7 @@ $litespeed = $this->getParam('litespeed', true); $analytics = $this->getParam('analytics', 'UA-26264668-9'); $env = $this->getParam('env', ''); $canonical = $this->getParam('canonical', ''); +$locale = $this->getParam('locale', null); if(!empty($platforms)) { $platforms = array_map(function($platform) { @@ -30,14 +31,14 @@ if(!empty($platforms)) { } ?> +--> <?php echo $this->getParam('title', ''); ?> - + @@ -76,7 +77,7 @@ if(!empty($platforms)) { API: '/v1', PROJECT: 'console', PLATFORMS: , - LOCALE: 'escape(Locale::getText('settings.locale')); ?>', + LOCALE: 'escape($locale->getText('settings.locale')); ?>', PREFIX: 'escape($this->getParam('prefix')); ?>', ROLES: getParam('roles', [])); ?>, PAGING_LIMIT: diff --git a/public/index.php b/public/index.php index 00966df97..ef4d11d2a 100644 --- a/public/index.php +++ b/public/index.php @@ -14,5 +14,6 @@ ini_set('display_errors', 0); ini_set('display_errors', 1); ini_set('display_startup_errors', 1); error_reporting(E_ALL); +trigger_error('hide errors in prod', E_USER_NOTICE); include __DIR__ . '/../app/app.php';