From 6824cf560fc59640718c1de09e876c91014c5be3 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 30 Jun 2020 14:09:28 +0300 Subject: [PATCH] Added env vars and new request methods --- app/app.php | 27 +- app/controllers/api/account.php | 45 +- app/controllers/api/avatars.php | 2 +- app/controllers/api/storage.php | 501 +++++++++--------- app/controllers/api/teams.php | 893 ++++++++++++++++---------------- app/controllers/api/users.php | 717 ++++++++++++------------- app/controllers/shared/web.php | 12 +- app/controllers/web/console.php | 2 +- app/controllers/web/home.php | 800 ++++++++++++++-------------- app/init.php | 2 +- app/workers/tasks.php | 2 +- app/workers/usage.php | 4 +- app/workers/webhooks.php | 3 +- composer.json | 2 +- composer.lock | 22 +- 15 files changed, 1561 insertions(+), 1473 deletions(-) diff --git a/app/app.php b/app/app.php index f1d29c60f3..fd9188ac8e 100644 --- a/app/app.php +++ b/app/app.php @@ -17,7 +17,6 @@ use Appwrite\Network\Validator\Origin; // 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)); @@ -131,10 +130,22 @@ use Appwrite\Network\Validator\Origin; // return false; // })))); -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', '')); +App::init(function ($utopia, $request, $response, $console, $project, $user, $locale, $webhooks, $audits, $usage, $clients) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $console */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Document $user */ + /** @var Utopia\Locale\Locale $locale */ + /** @var Appwrite\Event\Event $webhook */ + /** @var Appwrite\Event\Event $audit */ + /** @var Appwrite\Event\Event $usage */ + /** @var Appwrite\Event\Event $mail */ + /** @var Appwrite\Event\Event $deletes */ + /** @var bool $mode */ + /** @var array $clients */ + + $localeParam = (string)$request->getParam('locale', $request->getHeader('X-Appwrite-Locale', '')); if (\in_array($localeParam, Config::getParam('locale-codes'))) { $locale->setDefault($localeParam); @@ -175,7 +186,7 @@ App::init(function ($utopia, $request, $response, $user, $project, $console, $we * @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers */ if (App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS - if(Config::getParam('protocol') !== 'https') { + if($request->getProtocol() !== 'https') { return $response->redirect('https://' . Config::getParam('domain').$request->getServer('REQUEST_URI')); } @@ -317,7 +328,7 @@ App::init(function ($utopia, $request, $response, $user, $project, $console, $we ->setParam('response', 0) ->setParam('storage', 0) ; -}, ['utopia', 'request', 'response', 'user', 'project', 'console', 'webhook', 'audit', 'usage', 'clients', 'locale']); +}, ['utopia', 'request', 'response', 'console', 'project', 'user', 'locale', 'webhook', 'audit', 'usage', 'clients']); App::shutdown(function ($utopia, $response, $request, $webhook, $audit, $usage, $deletes, $mode, $project) { /* @@ -363,7 +374,7 @@ App::options(function ($request, $response) { App::error(function ($error, $utopia, $request, $response, $project) { /** @var Exception $error */ - $version = Config::getParam('version'); + $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); switch ($error->getCode()) { case 400: // Error allowed publicly diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index fac11c5d8f..6596bc7537 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -167,7 +167,7 @@ App::post('/v1/account/sessions') /** @var Appwrite\Event\Event $webhook */ /** @var Appwrite\Event\Event $audit */ - $protocol = Config::getParam('protocol'); + $protocol = $request->getProtocol(); $profile = $projectDB->getCollectionFirst([ // Get user by email address 'limit' => 1, 'filters' => [ @@ -264,7 +264,7 @@ App::get('/v1/account/sessions/oauth2/:provider') /** @var Utopia\Response $response */ /** @var Appwrite\Database\Document $project */ - $protocol = Config::getParam('protocol'); + $protocol = $request->getProtocol(); $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', '{}'); @@ -304,16 +304,19 @@ 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, $response) { + ->action(function ($projectId, $provider, $code, $state, $request, $response) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + $domain = Config::getParam('domain'); - $protocol = Config::getParam('protocol'); + $protocol = $request->getProtocol(); $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']); + }, ['request', 'response']); App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->desc('OAuth2 Callback') @@ -326,16 +329,19 @@ 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, $response) { + ->action(function ($projectId, $provider, $code, $state, $request, $response) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + $domain = Config::getParam('domain'); - $protocol = Config::getParam('protocol'); + $protocol = $request->getProtocol(); $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']); + }, ['request', 'response']); App::get('/v1/account/sessions/oauth2/:provider/redirect') ->desc('OAuth2 Redirect') @@ -357,7 +363,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') /** @var Appwrite\Database\Database $projectDB */ /** @var Appwrite\Event\Event $audit */ - $protocol = Config::getParam('protocol'); + $protocol = $request->getProtocol(); $callback = $protocol.'://'.$request->getServer('HTTP_HOST').'/v1/account/sessions/oauth2/callback/'.$provider.'/'.$project->getId(); $defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => '']; $validateURL = new URL(); @@ -923,14 +929,15 @@ App::delete('/v1/account') ->label('sdk.namespace', 'account') ->label('sdk.method', 'delete') ->label('sdk.description', '/docs/references/account/delete.md') - ->action(function ($response, $user, $projectDB, $audit, $webhook) { + ->action(function ($request, $response, $user, $projectDB, $audit, $webhook) { + /** @var Utopia\Request $request */ /** @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'); + $protocol = $request->getProtocol(); $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ 'status' => Auth::USER_STATUS_BLOCKED, ])); @@ -972,7 +979,7 @@ App::delete('/v1/account') ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) ->noContent() ; - }, ['response', 'user', 'projectDB', 'audit', 'webhook']); + }, ['request', 'response', 'user', 'projectDB', 'audit', 'webhook']); App::delete('/v1/account/sessions/:sessionId') ->desc('Delete Account Session') @@ -985,14 +992,15 @@ 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, $response, $user, $projectDB, $audit, $webhook) { + ->action(function ($sessionId, $request, $response, $user, $projectDB, $audit, $webhook) { + /** @var Utopia\Request $request */ /** @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'); + $protocol = $request->getProtocol(); $sessionId = ($sessionId === 'current') ? Auth::tokenVerify($user->getAttribute('tokens'), Auth::TOKEN_TYPE_LOGIN, Auth::$secret) : $sessionId; @@ -1036,7 +1044,7 @@ App::delete('/v1/account/sessions/:sessionId') } throw new Exception('Session not found', 404); - }, ['response', 'user', 'projectDB', 'audit', 'webhook']); + }, ['request', 'response', 'user', 'projectDB', 'audit', 'webhook']); App::delete('/v1/account/sessions') ->desc('Delete All Account Sessions') @@ -1048,14 +1056,15 @@ App::delete('/v1/account/sessions') ->label('sdk.method', 'deleteSessions') ->label('sdk.description', '/docs/references/account/delete-sessions.md') ->label('abuse-limit', 100) - ->action(function ($response, $user, $projectDB, $audit, $webhook) { + ->action(function ($request, $response, $user, $projectDB, $audit, $webhook) { + /** @var Utopia\Request $request */ /** @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'); + $protocol = $request->getProtocol(); $tokens = $user->getAttribute('tokens', []); foreach ($tokens as $token) { /* @var $token Document */ @@ -1091,7 +1100,7 @@ App::delete('/v1/account/sessions') } $response->noContent(); - }, ['response', 'user', 'projectDB', 'audit', 'webhook']); + }, ['request', 'response', 'user', 'projectDB', 'audit', 'webhook']); App::post('/v1/account/recovery') ->desc('Create Password Recovery') diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 850a3a3ed2..9972cfa169 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -246,7 +246,7 @@ App::get('/v1/avatars/favicon') CURLOPT_MAXREDIRS => 3, CURLOPT_URL => $url, CURLOPT_USERAGENT => \sprintf(APP_USERAGENT, - Config::getParam('version'), + App::getEnv('_APP_VERSION', 'UNKNOWN'), App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY) ), ]); diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index aba5663fa6..656bed297b 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -242,116 +242,119 @@ App::get('/v1/storage/files/:fileId/preview') ->param('quality', 100, function () { return new Range(0, 100); }, 'Preview image quality. Pass an integer between 0 to 100. Defaults to 100.', true) ->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 () { return new WhiteList(\array_merge(\array_keys(Config::getParam('storage-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) { - $storage = 'local'; + ->action(function ($fileId, $width, $height, $quality, $background, $output, $request, $response, $project, $projectDB) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Database $projectDB */ - if (!\extension_loaded('imagick')) { - throw new Exception('Imagick extension is missing', 500); - } + $storage = 'local'; - if (!Storage::exists($storage)) { - throw new Exception('No such storage device', 400); - } + if (!\extension_loaded('imagick')) { + throw new Exception('Imagick extension is missing', 500); + } - if ((\strpos($request->getServer('HTTP_ACCEPT'), 'image/webp') === false) && ('webp' == $output)) { // Fallback webp to jpeg when no browser support - $output = 'jpg'; - } + if (!Storage::exists($storage)) { + throw new Exception('No such storage device', 400); + } - $inputs = Config::getParam('storage-inputs'); - $outputs = Config::getParam('storage-outputs'); - $fileLogos = Config::getParam('storage-logos'); + if ((\strpos($request->getServer('HTTP_ACCEPT'), 'image/webp') === false) && ('webp' == $output)) { // Fallback webp to jpeg when no browser support + $output = 'jpg'; + } - $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache - $key = \md5($fileId.$width.$height.$quality.$background.$storage.$output); + $inputs = Config::getParam('storage-inputs'); + $outputs = Config::getParam('storage-outputs'); + $fileLogos = Config::getParam('storage-logos'); - $file = $projectDB->getDocument($fileId); + $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache + $key = \md5($fileId.$width.$height.$quality.$background.$storage.$output); - if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { - throw new Exception('File not found', 404); - } + $file = $projectDB->getDocument($fileId); - $path = $file->getAttribute('path'); + if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { + throw new Exception('File not found', 404); + } + + $path = $file->getAttribute('path'); + $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); + $algorithm = $file->getAttribute('algorithm'); + $cipher = $file->getAttribute('fileOpenSSLCipher'); + $mime = $file->getAttribute('mimeType'); + + if (!\in_array($mime, $inputs)) { + $path = (\array_key_exists($mime, $fileLogos)) ? $fileLogos[$mime] : $fileLogos['default']; + $algorithm = null; + $cipher = null; + $background = (empty($background)) ? 'eceff1' : $background; $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); - $algorithm = $file->getAttribute('algorithm'); - $cipher = $file->getAttribute('fileOpenSSLCipher'); - $mime = $file->getAttribute('mimeType'); + $key = \md5($path.$width.$height.$quality.$background.$storage.$output); + } - if (!\in_array($mime, $inputs)) { - $path = (\array_key_exists($mime, $fileLogos)) ? $fileLogos[$mime] : $fileLogos['default']; - $algorithm = null; - $cipher = null; - $background = (empty($background)) ? 'eceff1' : $background; - $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); - $key = \md5($path.$width.$height.$quality.$background.$storage.$output); - } + $compressor = new GZIP(); + $device = Storage::getDevice('local'); - $compressor = new GZIP(); - $device = Storage::getDevice('local'); + if (!\file_exists($path)) { + throw new Exception('File not found', 404); + } - if (!\file_exists($path)) { - throw new Exception('File not found', 404); - } - - $cache = new Cache(new Filesystem(APP_STORAGE_CACHE.'/app-'.$project->getId())); // Limit file number or size - $data = $cache->load($key, 60 * 60 * 24 * 30 * 3 /* 3 months */); - - if ($data) { - $output = (empty($output)) ? $type : $output; - - $response - ->setContentType((\in_array($output, $outputs)) ? $outputs[$output] : $outputs['jpg']) - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'hit') - ->send($data) - ; - - return; - } - - $source = $device->read($path); - - if (!empty($cipher)) { // Decrypt - $source = OpenSSL::decrypt( - $source, - $file->getAttribute('fileOpenSSLCipher'), - App::getEnv('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), - 0, - \hex2bin($file->getAttribute('fileOpenSSLIV')), - \hex2bin($file->getAttribute('fileOpenSSLTag')) - ); - } - - if (!empty($algorithm)) { - $source = $compressor->decompress($source); - } - - $resize = new Resize($source); - - $resize->crop((int) $width, (int) $height); - - if (!empty($background)) { - $resize->setBackground('#'.$background); - } + $cache = new Cache(new Filesystem(APP_STORAGE_CACHE.'/app-'.$project->getId())); // Limit file number or size + $data = $cache->load($key, 60 * 60 * 24 * 30 * 3 /* 3 months */); + if ($data) { $output = (empty($output)) ? $type : $output; $response - ->setContentType($outputs[$output]) + ->setContentType((\in_array($output, $outputs)) ? $outputs[$output] : $outputs['jpg']) ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'miss') - ->send('') + ->addHeader('X-Appwrite-Cache', 'hit') + ->send($data) ; - $data = $resize->output($output, $quality); - - $cache->save($key, $data); - - echo $data; - - unset($resize); + return; } - ); + + $source = $device->read($path); + + if (!empty($cipher)) { // Decrypt + $source = OpenSSL::decrypt( + $source, + $file->getAttribute('fileOpenSSLCipher'), + App::getEnv('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), + 0, + \hex2bin($file->getAttribute('fileOpenSSLIV')), + \hex2bin($file->getAttribute('fileOpenSSLTag')) + ); + } + + if (!empty($algorithm)) { + $source = $compressor->decompress($source); + } + + $resize = new Resize($source); + + $resize->crop((int) $width, (int) $height); + + if (!empty($background)) { + $resize->setBackground('#'.$background); + } + + $output = (empty($output)) ? $type : $output; + + $response + ->setContentType($outputs[$output]) + ->addHeader('Expires', $date) + ->addHeader('X-Appwrite-Cache', 'miss') + ->send('') + ; + + $data = $resize->output($output, $quality); + + $cache->save($key, $data); + + echo $data; + + unset($resize); + }, ['request', 'response', 'project', 'projectDB']); App::get('/v1/storage/files/:fileId/download') ->desc('Get File for Download') @@ -364,48 +367,49 @@ App::get('/v1/storage/files/:fileId/download') ->label('sdk.response.type', '*') ->label('sdk.methodType', 'location') ->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); - $path = $file->getAttribute('path', ''); - - if (!\file_exists($path)) { - throw new Exception('File not found in '.$path, 404); - } - - $compressor = new GZIP(); - $device = Storage::getDevice('local'); - - $source = $device->read($path); - - if (!empty($file->getAttribute('fileOpenSSLCipher'))) { // Decrypt - $source = OpenSSL::decrypt( - $source, - $file->getAttribute('fileOpenSSLCipher'), - App::getEnv('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), - 0, - \hex2bin($file->getAttribute('fileOpenSSLIV')), - \hex2bin($file->getAttribute('fileOpenSSLTag')) - ); - } - - $source = $compressor->decompress($source); - - // Response - $response - ->setContentType($file->getAttribute('mimeType')) - ->addHeader('Content-Disposition', 'attachment; filename="'.$file->getAttribute('name', '').'"') - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache - ->addHeader('X-Peak', \memory_get_peak_usage()) - ->send($source) - ; + if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { + throw new Exception('File not found', 404); } - ); + + $path = $file->getAttribute('path', ''); + + if (!\file_exists($path)) { + throw new Exception('File not found in '.$path, 404); + } + + $compressor = new GZIP(); + $device = Storage::getDevice('local'); + + $source = $device->read($path); + + if (!empty($file->getAttribute('fileOpenSSLCipher'))) { // Decrypt + $source = OpenSSL::decrypt( + $source, + $file->getAttribute('fileOpenSSLCipher'), + App::getEnv('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), + 0, + \hex2bin($file->getAttribute('fileOpenSSLIV')), + \hex2bin($file->getAttribute('fileOpenSSLTag')) + ); + } + + $source = $compressor->decompress($source); + + // Response + $response + ->setContentType($file->getAttribute('mimeType')) + ->addHeader('Content-Disposition', 'attachment; filename="'.$file->getAttribute('name', '').'"') + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache + ->addHeader('X-Peak', \memory_get_peak_usage()) + ->send($source) + ; + }, ['response', 'projectDB']); App::get('/v1/storage/files/:fileId/view') ->desc('Get File for View') @@ -419,65 +423,66 @@ App::get('/v1/storage/files/:fileId/view') ->label('sdk.methodType', 'location') ->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) { - $file = $projectDB->getDocument($fileId); - $mimes = Config::getParam('storage-mimes'); + ->action(function ($fileId, $as, $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); + $mimes = Config::getParam('storage-mimes'); - $path = $file->getAttribute('path', ''); - - if (!\file_exists($path)) { - throw new Exception('File not found in '.$path, 404); - } - - $compressor = new GZIP(); - $device = Storage::getDevice('local'); - - $contentType = 'text/plain'; - - if (\in_array($file->getAttribute('mimeType'), $mimes)) { - $contentType = $file->getAttribute('mimeType'); - } - - $source = $device->read($path); - - if (!empty($file->getAttribute('fileOpenSSLCipher'))) { // Decrypt - $source = OpenSSL::decrypt( - $source, - $file->getAttribute('fileOpenSSLCipher'), - App::getEnv('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), - 0, - \hex2bin($file->getAttribute('fileOpenSSLIV')), - \hex2bin($file->getAttribute('fileOpenSSLTag')) - ); - } - - $output = $compressor->decompress($source); - $fileName = $file->getAttribute('name', ''); - - $contentTypes = [ - 'pdf' => 'application/pdf', - 'text' => 'text/plain', - ]; - - $contentType = (\array_key_exists($as, $contentTypes)) ? $contentTypes[$as] : $contentType; - - // Response - $response - ->setContentType($contentType) - ->addHeader('Content-Security-Policy', 'script-src none;') - ->addHeader('X-Content-Type-Options', 'nosniff') - ->addHeader('Content-Disposition', 'inline; filename="'.$fileName.'"') - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache - ->addHeader('X-Peak', \memory_get_peak_usage()) - ->send($output) - ; + if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { + throw new Exception('File not found', 404); } - ); + + $path = $file->getAttribute('path', ''); + + if (!\file_exists($path)) { + throw new Exception('File not found in '.$path, 404); + } + + $compressor = new GZIP(); + $device = Storage::getDevice('local'); + + $contentType = 'text/plain'; + + if (\in_array($file->getAttribute('mimeType'), $mimes)) { + $contentType = $file->getAttribute('mimeType'); + } + + $source = $device->read($path); + + if (!empty($file->getAttribute('fileOpenSSLCipher'))) { // Decrypt + $source = OpenSSL::decrypt( + $source, + $file->getAttribute('fileOpenSSLCipher'), + App::getEnv('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), + 0, + \hex2bin($file->getAttribute('fileOpenSSLIV')), + \hex2bin($file->getAttribute('fileOpenSSLTag')) + ); + } + + $output = $compressor->decompress($source); + $fileName = $file->getAttribute('name', ''); + + $contentTypes = [ + 'pdf' => 'application/pdf', + 'text' => 'text/plain', + ]; + + $contentType = (\array_key_exists($as, $contentTypes)) ? $contentTypes[$as] : $contentType; + + // Response + $response + ->setContentType($contentType) + ->addHeader('Content-Security-Policy', 'script-src none;') + ->addHeader('X-Content-Type-Options', 'nosniff') + ->addHeader('Content-Disposition', 'inline; filename="'.$fileName.'"') + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache + ->addHeader('X-Peak', \memory_get_peak_usage()) + ->send($output) + ; + }, ['response', 'projectDB']); App::put('/v1/storage/files/:fileId') ->desc('Update File') @@ -491,39 +496,41 @@ App::put('/v1/storage/files/:fileId') ->param('fileId', '', function () { return new UID(); }, 'File unique ID.') ->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 ($fileId, $read, $write, $folderId = '') use ($response, $projectDB, $audit, $webhook) { - $file = $projectDB->getDocument($fileId); + ->action(function ($fileId, $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 */ - if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { - throw new Exception('File not found', 404); - } + $file = $projectDB->getDocument($fileId); - $file = $projectDB->updateDocument(\array_merge($file->getArrayCopy(), [ - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - 'folderId' => $folderId, - ])); - - if (false === $file) { - throw new Exception('Failed saving file to DB', 500); - } - - $webhook - ->setParam('payload', $file->getArrayCopy()) - ; - - $audit - ->setParam('event', 'storage.files.update') - ->setParam('resource', 'storage/files/'.$file->getId()) - ; - - $response->json($file->getArrayCopy()); + if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { + throw new Exception('File not found', 404); } - ); + + $file = $projectDB->updateDocument(\array_merge($file->getArrayCopy(), [ + '$permissions' => [ + 'read' => $read, + 'write' => $write, + ], + 'folderId' => '', + ])); + + if (false === $file) { + throw new Exception('Failed saving file to DB', 500); + } + + $webhook + ->setParam('payload', $file->getArrayCopy()) + ; + + $audit + ->setParam('event', 'storage.files.update') + ->setParam('resource', 'storage/files/'.$file->getId()) + ; + + $response->json($file->getArrayCopy()); + }, ['response', 'projectDB', 'webhook', 'audit']); App::delete('/v1/storage/files/:fileId') ->desc('Delete File') @@ -535,38 +542,42 @@ App::delete('/v1/storage/files/:fileId') ->label('sdk.method', 'deleteFile') ->label('sdk.description', '/docs/references/storage/delete-file.md') ->param('fileId', '', function () { return new UID(); }, 'File unique ID.') - ->action( - function ($fileId) use ($response, $projectDB, $webhook, $audit, $usage) { - $file = $projectDB->getDocument($fileId); + ->action(function ($fileId, $response, $projectDB, $webhook, $audit, $usage) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhook */ + /** @var Appwrite\Event\Event $audit */ + /** @var Appwrite\Event\Event $usage */ + + $file = $projectDB->getDocument($fileId); - if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { - throw new Exception('File not found', 404); - } - - $device = Storage::getDevice('local'); - - if ($device->delete($file->getAttribute('path', ''))) { - if (!$projectDB->deleteDocument($fileId)) { - throw new Exception('Failed to remove file from DB', 500); - } - } - - $webhook - ->setParam('payload', $file->getArrayCopy()) - ; - - $audit - ->setParam('event', 'storage.files.delete') - ->setParam('resource', 'storage/files/'.$file->getId()) - ; - - $usage - ->setParam('storage', $file->getAttribute('size', 0) * -1) - ; - - $response->noContent(); + if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { + throw new Exception('File not found', 404); } - ); + + $device = Storage::getDevice('local'); + + if ($device->delete($file->getAttribute('path', ''))) { + if (!$projectDB->deleteDocument($fileId)) { + throw new Exception('Failed to remove file from DB', 500); + } + } + + $webhook + ->setParam('payload', $file->getArrayCopy()) + ; + + $audit + ->setParam('event', 'storage.files.delete') + ->setParam('resource', 'storage/files/'.$file->getId()) + ; + + $usage + ->setParam('storage', $file->getAttribute('size', 0) * -1) + ; + + $response->noContent(); + }, ['fileId', 'response', 'projectDB', 'webhook', 'audit', 'usage']); // App::get('/v1/storage/files/:fileId/scan') // ->desc('Scan Storage') diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 250dfd0baa..4af413f62e 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -10,7 +10,6 @@ use Utopia\Validator\Host; use Utopia\Validator\Range; use Utopia\Validator\ArrayList; use Utopia\Validator\WhiteList; -use Utopia\Locale\Locale; use Appwrite\Auth\Auth; use Appwrite\Database\Database; use Appwrite\Database\Document; @@ -29,59 +28,62 @@ App::post('/v1/teams') ->label('sdk.description', '/docs/references/teams/create-team.md') ->param('name', null, function () { return new Text(100); }, 'Team name.') ->param('roles', ['owner'], function () { return new ArrayList(new Text(128)); }, 'Array of strings. Use this param to set the roles in the team for the user who created it. The default role is **owner**. A role can be any string. Learn more about [roles and permissions](/docs/permissions).', true) - ->action( - function ($name, $roles) use ($response, $projectDB, $user, $mode) { - Authorization::disable(); + ->action(function ($name, $roles, $response, $user, $projectDB, $mode) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var bool $mode */ - $team = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_TEAMS, + Authorization::disable(); + + $team = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_TEAMS, + '$permissions' => [ + 'read' => ['team:{self}'], + 'write' => ['team:{self}/owner'], + ], + 'name' => $name, + 'sum' => ($mode !== APP_MODE_ADMIN && $user->getId()) ? 1 : 0, + 'dateCreated' => \time(), + ]); + + Authorization::reset(); + + if (false === $team) { + throw new Exception('Failed saving team to DB', 500); + } + + if ($mode !== APP_MODE_ADMIN && $user->getId()) { // Don't add user on server mode + $membership = new Document([ + '$collection' => Database::SYSTEM_COLLECTION_MEMBERSHIPS, '$permissions' => [ - 'read' => ['team:{self}'], - 'write' => ['team:{self}/owner'], + 'read' => ['user:'.$user->getId(), 'team:'.$team->getId()], + 'write' => ['user:'.$user->getId(), 'team:'.$team->getId().'/owner'], ], - 'name' => $name, - 'sum' => ($mode !== APP_MODE_ADMIN && $user->getId()) ? 1 : 0, - 'dateCreated' => \time(), + 'userId' => $user->getId(), + 'teamId' => $team->getId(), + 'roles' => $roles, + 'invited' => \time(), + 'joined' => \time(), + 'confirm' => true, + 'secret' => '', ]); - Authorization::reset(); + // Attach user to team + $user->setAttribute('memberships', $membership, Document::SET_TYPE_APPEND); - if (false === $team) { - throw new Exception('Failed saving team to DB', 500); + $user = $projectDB->updateDocument($user->getArrayCopy()); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); } - - if ($mode !== APP_MODE_ADMIN && $user->getId()) { // Don't add user on server mode - $membership = new Document([ - '$collection' => Database::SYSTEM_COLLECTION_MEMBERSHIPS, - '$permissions' => [ - 'read' => ['user:'.$user->getId(), 'team:'.$team->getId()], - 'write' => ['user:'.$user->getId(), 'team:'.$team->getId().'/owner'], - ], - 'userId' => $user->getId(), - 'teamId' => $team->getId(), - 'roles' => $roles, - 'invited' => \time(), - 'joined' => \time(), - 'confirm' => true, - 'secret' => '', - ]); - - // Attach user to team - $user->setAttribute('memberships', $membership, Document::SET_TYPE_APPEND); - - $user = $projectDB->updateDocument($user->getArrayCopy()); - - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } - } - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($team->getArrayCopy()) - ; } - ); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($team->getArrayCopy()) + ; + }, ['response', 'user', 'projectDB', 'mode']); App::get('/v1/teams') ->desc('List Teams') @@ -95,23 +97,24 @@ App::get('/v1/teams') ->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_TEAMS, - ], - ]); + ->action(function ($search, $limit, $offset, $orderType, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - $response->json(['sum' => $projectDB->getSum(), 'teams' => $results]); - } - ); + $results = $projectDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => 'dateCreated', + 'orderType' => $orderType, + 'orderCast' => 'int', + 'search' => $search, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_TEAMS, + ], + ]); + + $response->json(['sum' => $projectDB->getSum(), 'teams' => $results]); + }, ['response', 'projectDB']); App::get('/v1/teams/:teamId') ->desc('Get Team') @@ -122,17 +125,18 @@ App::get('/v1/teams/:teamId') ->label('sdk.method', 'get') ->label('sdk.description', '/docs/references/teams/get-team.md') ->param('teamId', '', function () { return new UID(); }, 'Team unique ID.') - ->action( - function ($teamId) use ($response, $projectDB) { - $team = $projectDB->getDocument($teamId); + ->action(function ($teamId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { - throw new Exception('Team not found', 404); - } + $team = $projectDB->getDocument($teamId); - $response->json($team->getArrayCopy([])); + if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { + throw new Exception('Team not found', 404); } - ); + + $response->json($team->getArrayCopy([])); + }, ['response', 'projectDB']); App::put('/v1/teams/:teamId') ->desc('Update Team') @@ -144,25 +148,26 @@ App::put('/v1/teams/:teamId') ->label('sdk.description', '/docs/references/teams/update-team.md') ->param('teamId', '', function () { return new UID(); }, 'Team unique ID.') ->param('name', null, function () { return new Text(100); }, 'Team name.') - ->action( - function ($teamId, $name) use ($response, $projectDB) { - $team = $projectDB->getDocument($teamId); + ->action(function ($teamId, $name, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { - throw new Exception('Team not found', 404); - } + $team = $projectDB->getDocument($teamId); - $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ - 'name' => $name, - ])); - - if (false === $team) { - throw new Exception('Failed saving team to DB', 500); - } - - $response->json($team->getArrayCopy()); + if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { + throw new Exception('Team not found', 404); } - ); + + $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ + 'name' => $name, + ])); + + if (false === $team) { + throw new Exception('Failed saving team to DB', 500); + } + + $response->json($team->getArrayCopy()); + }, ['response', 'projectDB']); App::delete('/v1/teams/:teamId') ->desc('Delete Team') @@ -173,36 +178,37 @@ App::delete('/v1/teams/:teamId') ->label('sdk.method', 'delete') ->label('sdk.description', '/docs/references/teams/delete-team.md') ->param('teamId', '', function () { return new UID(); }, 'Team unique ID.') - ->action( - function ($teamId) use ($response, $projectDB) { - $team = $projectDB->getDocument($teamId); + ->action(function ($teamId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { - throw new Exception('Team not found', 404); - } + $team = $projectDB->getDocument($teamId); - $memberships = $projectDB->getCollection([ - 'limit' => 2000, // TODO add members limit - 'offset' => 0, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, - 'teamId='.$teamId, - ], - ]); - - foreach ($memberships as $member) { - if (!$projectDB->deleteDocument($member->getId())) { - throw new Exception('Failed to remove membership for team from DB', 500); - } - } - - if (!$projectDB->deleteDocument($teamId)) { - throw new Exception('Failed to remove team from DB', 500); - } - - $response->noContent(); + if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { + throw new Exception('Team not found', 404); } - ); + + $memberships = $projectDB->getCollection([ + 'limit' => 2000, // TODO add members limit + 'offset' => 0, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, + 'teamId='.$teamId, + ], + ]); + + foreach ($memberships as $member) { + if (!$projectDB->deleteDocument($member->getId())) { + throw new Exception('Failed to remove membership for team from DB', 500); + } + } + + if (!$projectDB->deleteDocument($teamId)) { + throw new Exception('Failed to remove team from DB', 500); + } + + $response->noContent(); + }, ['response', 'projectDB']); App::post('/v1/teams/:teamId/memberships') ->desc('Create Team Membership') @@ -216,169 +222,175 @@ App::post('/v1/teams/:teamId/memberships') ->param('email', '', function () { return new Email(); }, 'New team member email.') ->param('name', '', function () { return new Text(100); }, 'New team member name.', true) ->param('roles', [], function () { return new ArrayList(new Text(128)); }, 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions).') - ->param('url', '', function () use ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the invitation 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 our own built-in confirm page - ->action( - function ($teamId, $email, $name, $roles, $url) use ($response, $mail, $project, $user, $audit, $projectDB, $mode) { - $name = (empty($name)) ? $email : $name; - $team = $projectDB->getDocument($teamId); + ->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the invitation 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 our own built-in confirm page + ->action(function ($teamId, $email, $name, $roles, $url, $response, $project, $user, $projectDB, $locale, $audit, $mail, $mode) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audit */ + /** @var Appwrite\Event\Event $mail */ + /** @var bool $mode */ - if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { - throw new Exception('Team not found', 404); - } + $name = (empty($name)) ? $email : $name; + $team = $projectDB->getDocument($teamId); - $memberships = $projectDB->getCollection([ - 'limit' => 50, - 'offset' => 0, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, - 'teamId='.$team->getId(), - ], - ]); + if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { + throw new Exception('Team not found', 404); + } - $invitee = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'email='.$email, - ], - ]); + $memberships = $projectDB->getCollection([ + 'limit' => 50, + 'offset' => 0, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, + 'teamId='.$team->getId(), + ], + ]); - if (empty($invitee)) { // Create new user if no user with same email found + $invitee = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + 'email='.$email, + ], + ]); - Authorization::disable(); + if (empty($invitee)) { // Create new user if no user with same email found - try { - $invitee = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_USERS, - '$permissions' => [ - 'read' => ['user:{self}', '*'], - 'write' => ['user:{self}'], - ], - 'email' => $email, - 'emailVerification' => false, - 'status' => Auth::USER_STATUS_UNACTIVATED, - 'password' => Auth::passwordHash(Auth::passwordGenerator()), - 'password-update' => \time(), - 'registration' => \time(), - 'reset' => false, - 'name' => $name, - 'tokens' => [], - ], ['email' => $email]); - } catch (Duplicate $th) { - throw new Exception('Account already exists', 409); - } + Authorization::disable(); - Authorization::reset(); - - if (false === $invitee) { - throw new Exception('Failed saving user to DB', 500); - } - } - - $isOwner = false; - - foreach ($memberships as $member) { - if ($member->getAttribute('userId') == $invitee->getId()) { - throw new Exception('User has already been invited or is already a member of this team', 409); - } - - if ($member->getAttribute('userId') == $user->getId() && \in_array('owner', $member->getAttribute('roles', []))) { - $isOwner = true; - } - } - - if (!$isOwner && (APP_MODE_ADMIN !== $mode)) { - throw new Exception('User is not allowed to send invitations for this team', 401); - } - - $secret = Auth::tokenGenerator(); - - $membership = new Document([ - '$collection' => Database::SYSTEM_COLLECTION_MEMBERSHIPS, - '$permissions' => [ - 'read' => ['*'], - 'write' => ['user:'.$invitee->getId(), 'team:'.$team->getId().'/owner'], - ], - 'userId' => $invitee->getId(), - 'teamId' => $team->getId(), - 'roles' => $roles, - 'invited' => \time(), - 'joined' => 0, - 'confirm' => (APP_MODE_ADMIN === $mode), - 'secret' => Auth::hash($secret), - ]); - - if (APP_MODE_ADMIN === $mode) { // Allow admin to create membership - Authorization::disable(); - $membership = $projectDB->createDocument($membership->getArrayCopy()); - Authorization::reset(); - } else { - $membership = $projectDB->createDocument($membership->getArrayCopy()); - } - - if (false === $membership) { - throw new Exception('Failed saving membership to DB', 500); - } - - $url = Template::parseURL($url); - $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['inviteId' => $membership->getId(), 'teamId' => $team->getId(), 'userId' => $invitee->getId(), 'secret' => $secret, 'teamId' => $teamId]); - $url = Template::unParseURL($url); - - $body = new Template(__DIR__.'/../../config/locale/templates/email-base.tpl'); - $content = new Template(__DIR__.'/../../config/locale/translations/templates/'.Locale::getText('account.emails.invitation.body')); - $cta = new Template(__DIR__.'/../../config/locale/templates/email-cta.tpl'); - - $body - ->setParam('{{content}}', $content->render()) - ->setParam('{{cta}}', $cta->render()) - ->setParam('{{title}}', Locale::getText('account.emails.invitation.title')) - ->setParam('{{direction}}', Locale::getText('settings.direction')) - ->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]'])) - ->setParam('{{team}}', $team->getAttribute('name', '[TEAM-NAME]')) - ->setParam('{{owner}}', $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') - ; - - if (APP_MODE_ADMIN !== $mode) { // No need in comfirmation when in admin mode - $mail - ->setParam('event', 'teams.membership.create') - ->setParam('recipient', $email) - ->setParam('name', $name) - ->setParam('subject', \sprintf(Locale::getText('account.emails.invitation.title'), $team->getAttribute('name', '[TEAM-NAME]'), $project->getAttribute('name', ['[APP-NAME]']))) - ->setParam('body', $body->render()) - ->trigger(); - ; - } - - $audit - ->setParam('userId', $invitee->getId()) - ->setParam('event', 'teams.membership.create') - ->setParam('resource', 'teams/'.$teamId) - ; - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) // TODO change response of this endpoint - ->json(\array_merge($membership->getArrayCopy([ - '$id', - 'userId', - 'teamId', - 'roles', - 'invited', - 'joined', - 'confirm', - ]), [ + try { + $invitee = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_USERS, + '$permissions' => [ + 'read' => ['user:{self}', '*'], + 'write' => ['user:{self}'], + ], 'email' => $email, + 'emailVerification' => false, + 'status' => Auth::USER_STATUS_UNACTIVATED, + 'password' => Auth::passwordHash(Auth::passwordGenerator()), + 'password-update' => \time(), + 'registration' => \time(), + 'reset' => false, 'name' => $name, - ])) + 'tokens' => [], + ], ['email' => $email]); + } catch (Duplicate $th) { + throw new Exception('Account already exists', 409); + } + + Authorization::reset(); + + if (false === $invitee) { + throw new Exception('Failed saving user to DB', 500); + } + } + + $isOwner = false; + + foreach ($memberships as $member) { + if ($member->getAttribute('userId') == $invitee->getId()) { + throw new Exception('User has already been invited or is already a member of this team', 409); + } + + if ($member->getAttribute('userId') == $user->getId() && \in_array('owner', $member->getAttribute('roles', []))) { + $isOwner = true; + } + } + + if (!$isOwner && (APP_MODE_ADMIN !== $mode)) { + throw new Exception('User is not allowed to send invitations for this team', 401); + } + + $secret = Auth::tokenGenerator(); + + $membership = new Document([ + '$collection' => Database::SYSTEM_COLLECTION_MEMBERSHIPS, + '$permissions' => [ + 'read' => ['*'], + 'write' => ['user:'.$invitee->getId(), 'team:'.$team->getId().'/owner'], + ], + 'userId' => $invitee->getId(), + 'teamId' => $team->getId(), + 'roles' => $roles, + 'invited' => \time(), + 'joined' => 0, + 'confirm' => (APP_MODE_ADMIN === $mode), + 'secret' => Auth::hash($secret), + ]); + + if (APP_MODE_ADMIN === $mode) { // Allow admin to create membership + Authorization::disable(); + $membership = $projectDB->createDocument($membership->getArrayCopy()); + Authorization::reset(); + } else { + $membership = $projectDB->createDocument($membership->getArrayCopy()); + } + + if (false === $membership) { + throw new Exception('Failed saving membership to DB', 500); + } + + $url = Template::parseURL($url); + $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['inviteId' => $membership->getId(), 'teamId' => $team->getId(), 'userId' => $invitee->getId(), 'secret' => $secret, 'teamId' => $teamId]); + $url = Template::unParseURL($url); + + $body = new Template(__DIR__.'/../../config/locale/templates/email-base.tpl'); + $content = new Template(__DIR__.'/../../config/locale/translations/templates/'.$locale->getText('account.emails.invitation.body')); + $cta = new Template(__DIR__.'/../../config/locale/templates/email-cta.tpl'); + + $body + ->setParam('{{content}}', $content->render()) + ->setParam('{{cta}}', $cta->render()) + ->setParam('{{title}}', $locale->getText('account.emails.invitation.title')) + ->setParam('{{direction}}', $locale->getText('settings.direction')) + ->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]'])) + ->setParam('{{team}}', $team->getAttribute('name', '[TEAM-NAME]')) + ->setParam('{{owner}}', $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') + ; + + if (APP_MODE_ADMIN !== $mode) { // No need in comfirmation when in admin mode + $mail + ->setParam('event', 'teams.membership.create') + ->setParam('recipient', $email) + ->setParam('name', $name) + ->setParam('subject', \sprintf($locale->getText('account.emails.invitation.title'), $team->getAttribute('name', '[TEAM-NAME]'), $project->getAttribute('name', ['[APP-NAME]']))) + ->setParam('body', $body->render()) + ->trigger(); ; } - ); + + $audit + ->setParam('userId', $invitee->getId()) + ->setParam('event', 'teams.membership.create') + ->setParam('resource', 'teams/'.$teamId) + ; + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) // TODO change response of this endpoint + ->json(\array_merge($membership->getArrayCopy([ + '$id', + 'userId', + 'teamId', + 'roles', + 'invited', + 'joined', + 'confirm', + ]), [ + 'email' => $email, + 'name' => $name, + ])) + ; + }, ['response', 'project', 'user', 'projectDB', 'locale', 'audit', 'mail', 'mode']); App::get('/v1/teams/:teamId/memberships') ->desc('Get Team Memberships') @@ -393,50 +405,51 @@ App::get('/v1/teams/:teamId/memberships') ->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 ($teamId, $search, $limit, $offset, $orderType) use ($response, $projectDB) { - $team = $projectDB->getDocument($teamId); + ->action(function ($teamId, $search, $limit, $offset, $orderType, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { - throw new Exception('Team not found', 404); - } + $team = $projectDB->getDocument($teamId); - $memberships = $projectDB->getCollection([ - 'limit' => $limit, - 'offset' => $offset, - 'orderField' => 'joined', - 'orderType' => $orderType, - 'orderCast' => 'int', - 'search' => $search, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, - 'teamId='.$teamId, - ], - ]); - - $users = []; - - foreach ($memberships as $membership) { - if (empty($membership->getAttribute('userId', null))) { - continue; - } - - $temp = $projectDB->getDocument($membership->getAttribute('userId', null))->getArrayCopy(['email', 'name']); - - $users[] = \array_merge($temp, $membership->getArrayCopy([ - '$id', - 'userId', - 'teamId', - 'roles', - 'invited', - 'joined', - 'confirm', - ])); - } - - $response->json(['sum' => $projectDB->getSum(), 'memberships' => $users]); + if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { + throw new Exception('Team not found', 404); } - ); + + $memberships = $projectDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => 'joined', + 'orderType' => $orderType, + 'orderCast' => 'int', + 'search' => $search, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, + 'teamId='.$teamId, + ], + ]); + + $users = []; + + foreach ($memberships as $membership) { + if (empty($membership->getAttribute('userId', null))) { + continue; + } + + $temp = $projectDB->getDocument($membership->getAttribute('userId', null))->getArrayCopy(['email', 'name']); + + $users[] = \array_merge($temp, $membership->getArrayCopy([ + '$id', + 'userId', + 'teamId', + 'roles', + 'invited', + 'joined', + 'confirm', + ])); + } + + $response->json(['sum' => $projectDB->getSum(), 'memberships' => $users]); + }, ['response', 'projectDB']); App::patch('/v1/teams/:teamId/memberships/:inviteId/status') ->desc('Update Team Membership Status') @@ -450,125 +463,129 @@ App::patch('/v1/teams/:teamId/memberships/:inviteId/status') ->param('inviteId', '', function () { return new UID(); }, 'Invite unique ID.') ->param('userId', '', function () { return new UID(); }, 'User unique ID.') ->param('secret', '', function () { return new Text(256); }, 'Secret key.') - ->action( - function ($teamId, $inviteId, $userId, $secret) use ($response, $request, $user, $audit, $projectDB) { - $protocol = Config::getParam('protocol'); - $membership = $projectDB->getDocument($inviteId); + ->action(function ($teamId, $inviteId, $userId, $secret, $request, $response, $user, $projectDB, $audit) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audit */ - if (empty($membership->getId()) || Database::SYSTEM_COLLECTION_MEMBERSHIPS != $membership->getCollection()) { - throw new Exception('Invite not found', 404); - } + $protocol = $request->getProtocol(); + $membership = $projectDB->getDocument($inviteId); - if ($membership->getAttribute('teamId') !== $teamId) { - throw new Exception('Team IDs don\'t match', 404); - } + if (empty($membership->getId()) || Database::SYSTEM_COLLECTION_MEMBERSHIPS != $membership->getCollection()) { + throw new Exception('Invite not found', 404); + } - Authorization::disable(); + if ($membership->getAttribute('teamId') !== $teamId) { + throw new Exception('Team IDs don\'t match', 404); + } - $team = $projectDB->getDocument($teamId); - - Authorization::reset(); + Authorization::disable(); - if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { - throw new Exception('Team not found', 404); - } + $team = $projectDB->getDocument($teamId); + + Authorization::reset(); - if (Auth::hash($secret) !== $membership->getAttribute('secret')) { - throw new Exception('Secret key not valid', 401); - } + if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { + throw new Exception('Team not found', 404); + } - if ($userId != $membership->getAttribute('userId')) { - throw new Exception('Invite not belong to current user ('.$user->getAttribute('email').')', 401); - } + if (Auth::hash($secret) !== $membership->getAttribute('secret')) { + throw new Exception('Secret key not valid', 401); + } - if (empty($user->getId())) { - $user = $projectDB->getCollectionFirst([ // Get user - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - '$id='.$userId, - ], - ]); - } + if ($userId != $membership->getAttribute('userId')) { + throw new Exception('Invite not belong to current user ('.$user->getAttribute('email').')', 401); + } - if ($membership->getAttribute('userId') !== $user->getId()) { - throw new Exception('Invite not belong to current user ('.$user->getAttribute('email').')', 401); - } + if (empty($user->getId())) { + $user = $projectDB->getCollectionFirst([ // Get user + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + '$id='.$userId, + ], + ]); + } - $membership // Attach user to team - ->setAttribute('joined', \time()) - ->setAttribute('confirm', true) - ; + if ($membership->getAttribute('userId') !== $user->getId()) { + throw new Exception('Invite not belong to current user ('.$user->getAttribute('email').')', 401); + } - $user - ->setAttribute('emailVerification', true) - ->setAttribute('memberships', $membership, Document::SET_TYPE_APPEND) - ; + $membership // Attach user to team + ->setAttribute('joined', \time()) + ->setAttribute('confirm', true) + ; - // Log user in - $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; - $secret = Auth::tokenGenerator(); + $user + ->setAttribute('emailVerification', true) + ->setAttribute('memberships', $membership, Document::SET_TYPE_APPEND) + ; - $user->setAttribute('tokens', new Document([ - '$collection' => Database::SYSTEM_COLLECTION_TOKENS, - '$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->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(), - ]), Document::SET_TYPE_APPEND); + // Log user in + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $secret = Auth::tokenGenerator(); - Authorization::setRole('user:'.$userId); + $user->setAttribute('tokens', new Document([ + '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->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(), + ]), Document::SET_TYPE_APPEND); - $user = $projectDB->updateDocument($user->getArrayCopy()); + Authorization::setRole('user:'.$userId); - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } + $user = $projectDB->updateDocument($user->getArrayCopy()); - Authorization::disable(); + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } - $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ - 'sum' => $team->getAttribute('sum', 0) + 1, - ])); + Authorization::disable(); - Authorization::reset(); + $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ + 'sum' => $team->getAttribute('sum', 0) + 1, + ])); - if (false === $team) { - throw new Exception('Failed saving team to DB', 500); - } + Authorization::reset(); - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'teams.membership.update') - ->setParam('resource', 'teams/'.$teamId) - ; + if (false === $team) { + throw new Exception('Failed saving team to DB', 500); + } - if (!Config::getParam('domainVerification')) { - $response - ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) - ; - } + $audit + ->setParam('userId', $user->getId()) + ->setParam('event', 'teams.membership.update') + ->setParam('resource', 'teams/'.$teamId) + ; + if (!Config::getParam('domainVerification')) { $response - ->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) - ->json(\array_merge($membership->getArrayCopy([ - '$id', - 'userId', - 'teamId', - 'roles', - 'invited', - 'joined', - 'confirm', - ]), [ - 'email' => $user->getAttribute('email'), - 'name' => $user->getAttribute('name'), - ])) + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) ; } - ); + + $response + ->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) + ->json(\array_merge($membership->getArrayCopy([ + '$id', + 'userId', + 'teamId', + 'roles', + 'invited', + 'joined', + 'confirm', + ]), [ + 'email' => $user->getAttribute('email'), + 'name' => $user->getAttribute('name'), + ])) + ; + }, ['request', 'response', 'user', 'projectDB', 'audit']); App::delete('/v1/teams/:teamId/memberships/:inviteId') ->desc('Delete Team Membership') @@ -580,44 +597,46 @@ App::delete('/v1/teams/:teamId/memberships/:inviteId') ->label('sdk.description', '/docs/references/teams/delete-team-membership.md') ->param('teamId', '', function () { return new UID(); }, 'Team unique ID.') ->param('inviteId', '', function () { return new UID(); }, 'Invite unique ID.') - ->action( - function ($teamId, $inviteId) use ($response, $projectDB, $audit) { - $membership = $projectDB->getDocument($inviteId); + ->action(function ($teamId, $inviteId, $response, $projectDB, $audit) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audit */ - if (empty($membership->getId()) || Database::SYSTEM_COLLECTION_MEMBERSHIPS != $membership->getCollection()) { - throw new Exception('Invite not found', 404); - } + $membership = $projectDB->getDocument($inviteId); - if ($membership->getAttribute('teamId') !== $teamId) { - throw new Exception('Team IDs don\'t match', 404); - } - - $team = $projectDB->getDocument($teamId); - - if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { - throw new Exception('Team not found', 404); - } - - if (!$projectDB->deleteDocument($membership->getId())) { - throw new Exception('Failed to remove membership from DB', 500); - } - - if ($membership->getAttribute('confirm')) { // Count only confirmed members - $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ - 'sum' => $team->getAttribute('sum', 0) - 1, - ])); - } - - if (false === $team) { - throw new Exception('Failed saving team to DB', 500); - } - - $audit - ->setParam('userId', $membership->getAttribute('userId')) - ->setParam('event', 'teams.membership.delete') - ->setParam('resource', 'teams/'.$teamId) - ; - - $response->noContent(); + if (empty($membership->getId()) || Database::SYSTEM_COLLECTION_MEMBERSHIPS != $membership->getCollection()) { + throw new Exception('Invite not found', 404); } - ); + + if ($membership->getAttribute('teamId') !== $teamId) { + throw new Exception('Team IDs don\'t match', 404); + } + + $team = $projectDB->getDocument($teamId); + + if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { + throw new Exception('Team not found', 404); + } + + if (!$projectDB->deleteDocument($membership->getId())) { + throw new Exception('Failed to remove membership from DB', 500); + } + + if ($membership->getAttribute('confirm')) { // Count only confirmed members + $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ + 'sum' => $team->getAttribute('sum', 0) - 1, + ])); + } + + if (false === $team) { + throw new Exception('Failed saving team to DB', 500); + } + + $audit + ->setParam('userId', $membership->getAttribute('userId')) + ->setParam('event', 'teams.membership.delete') + ->setParam('resource', 'teams/'.$teamId) + ; + + $response->noContent(); + }, ['response', 'projectDB', 'audit']); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 31552e8548..18d6f314a5 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -11,7 +11,6 @@ use Utopia\Validator\Range; use Utopia\Audit\Audit; use Utopia\Audit\Adapters\MySQL as AuditAdapter; use Utopia\Config\Config; -use Utopia\Locale\Locale; use Appwrite\Auth\Auth; use Appwrite\Auth\Validator\Password; use Appwrite\Database\Database; @@ -31,64 +30,65 @@ App::post('/v1/users') ->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 ($response, $projectDB) { - $profile = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'email='.$email, - ], - ]); + ->action(function ($email, $password, $name, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (!empty($profile)) { - throw new Exception('User already registered', 409); - } + $profile = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + 'email='.$email, + ], + ]); - 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); - } - - $oauth2Keys = []; - - foreach (Config::getParam('providers') as $key => $provider) { - if (!$provider['enabled']) { - continue; - } - - $oauth2Keys[] = 'oauth2'.\ucfirst($key); - $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; - } - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json(\array_merge($user->getArrayCopy(\array_merge([ - '$id', - 'status', - 'email', - 'registration', - 'emailVerification', - 'name', - ], $oauth2Keys)), ['roles' => []])); + if (!empty($profile)) { + throw new Exception('User already registered', 409); } - ); - + + 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); + } + + $oauth2Keys = []; + + foreach (Config::getParam('providers') as $key => $provider) { + if (!$provider['enabled']) { + continue; + } + + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; + } + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json(\array_merge($user->getArrayCopy(\array_merge([ + '$id', + 'status', + 'email', + 'registration', + 'emailVerification', + 'name', + ], $oauth2Keys)), ['roles' => []])); + }, ['response', 'projectDB']); + App::get('/v1/users') ->desc('List Users') ->groups(['api', 'users']) @@ -101,48 +101,49 @@ App::get('/v1/users') ->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' => 'registration', - 'orderType' => $orderType, - 'orderCast' => 'int', - 'search' => $search, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - ], - ]); + ->action(function ($search, $limit, $offset, $orderType, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - $oauth2Keys = []; + $results = $projectDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => 'registration', + 'orderType' => $orderType, + 'orderCast' => 'int', + 'search' => $search, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + ], + ]); - foreach (Config::getParam('providers') as $key => $provider) { - if (!$provider['enabled']) { - continue; - } + $oauth2Keys = []; - $oauth2Keys[] = 'oauth2'.\ucfirst($key); - $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; + foreach (Config::getParam('providers') as $key => $provider) { + if (!$provider['enabled']) { + continue; } - $results = \array_map(function ($value) use ($oauth2Keys) { /* @var $value \Database\Document */ - return $value->getArrayCopy(\array_merge( - [ - '$id', - 'status', - 'email', - 'registration', - 'emailVerification', - 'name', - ], - $oauth2Keys - )); - }, $results); - - $response->json(['sum' => $projectDB->getSum(), 'users' => $results]); + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; } - ); + + $results = \array_map(function ($value) use ($oauth2Keys) { /* @var $value \Database\Document */ + return $value->getArrayCopy(\array_merge( + [ + '$id', + 'status', + 'email', + 'registration', + 'emailVerification', + 'name', + ], + $oauth2Keys + )); + }, $results); + + $response->json(['sum' => $projectDB->getSum(), 'users' => $results]); + }, ['response', 'projectDB']); App::get('/v1/users/:userId') ->desc('Get User') @@ -153,38 +154,39 @@ App::get('/v1/users/:userId') ->label('sdk.method', 'get') ->label('sdk.description', '/docs/references/users/get-user.md') ->param('userId', '', function () { return new UID(); }, 'User unique ID.') - ->action( - function ($userId) use ($response, $projectDB) { - $user = $projectDB->getDocument($userId); + ->action(function ($userId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } + $user = $projectDB->getDocument($userId); - $oauth2Keys = []; - - foreach (Config::getParam('providers') as $key => $provider) { - if (!$provider['enabled']) { - continue; - } - - $oauth2Keys[] = 'oauth2'.\ucfirst($key); - $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; - } - - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'status', - 'email', - 'registration', - 'emailVerification', - 'name', - ], - $oauth2Keys - )), ['roles' => []])); + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); } - ); + + $oauth2Keys = []; + + foreach (Config::getParam('providers') as $key => $provider) { + if (!$provider['enabled']) { + continue; + } + + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; + } + + $response->json(\array_merge($user->getArrayCopy(\array_merge( + [ + '$id', + 'status', + 'email', + 'registration', + 'emailVerification', + 'name', + ], + $oauth2Keys + )), ['roles' => []])); + }, ['response', 'projectDB']); App::get('/v1/users/:userId/prefs') ->desc('Get User Preferences') @@ -195,26 +197,27 @@ App::get('/v1/users/:userId/prefs') ->label('sdk.method', 'getPrefs') ->label('sdk.description', '/docs/references/users/get-user-prefs.md') ->param('userId', '', function () { return new UID(); }, 'User unique ID.') - ->action( - function ($userId) use ($response, $projectDB) { - $user = $projectDB->getDocument($userId); + ->action(function ($userId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } + $user = $projectDB->getDocument($userId); - $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 (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); } - ); + + $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', 'projectDB']); App::get('/v1/users/:userId/sessions') ->desc('Get User Sessions') @@ -225,60 +228,62 @@ App::get('/v1/users/:userId/sessions') ->label('sdk.method', 'getSessions') ->label('sdk.description', '/docs/references/users/get-user-sessions.md') ->param('userId', '', function () { return new UID(); }, 'User unique ID.') - ->action( - function ($userId) use ($response, $projectDB) { - $user = $projectDB->getDocument($userId); + ->action(function ($userId, $response, $projectDB, $locale) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Locale\Locale $locale */ - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } + $user = $projectDB->getDocument($userId); - $tokens = $user->getAttribute('tokens', []); - $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); - $sessions = []; - $index = 0; - $countries = Locale::getText('countries'); - - foreach ($tokens as $token) { /* @var $token Document */ - if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) { - continue; - } - - $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' => [], - ]; - - 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); + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); } - ); + + $tokens = $user->getAttribute('tokens', []); + $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); + $sessions = []; + $index = 0; + $countries = $locale->getText('countries'); + + foreach ($tokens as $token) { /* @var $token Document */ + if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) { + continue; + } + + $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' => [], + ]; + + 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', 'projectDB', 'locale']); App::get('/v1/users/:userId/logs') ->desc('Get User Logs') @@ -289,77 +294,81 @@ App::get('/v1/users/:userId/logs') ->label('sdk.method', 'getLogs') ->label('sdk.description', '/docs/references/users/get-user-logs.md') ->param('userId', '', function () { return new UID(); }, 'User unique ID.') - ->action( - function ($userId) use ($response, $register, $projectDB, $project) { - $user = $projectDB->getDocument($userId); + ->action(function ($userId, $response, $register, $project, $projectDB, $locale) { + /** @var Utopia\Response $response */ + /** @var Utopia\Registry\Registry $register */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Locale\Locale $locale */ + + $user = $projectDB->getDocument($userId); - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } - - $adapter = new AuditAdapter($register->get('db')); - $adapter->setNamespace('app_'.$project->getId()); - - $audit = new Audit($adapter); - - $countries = Locale::getText('countries'); - - $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', - ]); - - $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); - $output = []; - - foreach ($logs as $i => &$log) { - $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; - - $dd = new DeviceDetector($log['userAgent']); - - $dd->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - - $dd->parse(); - - $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); + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); } - ); + + $adapter = new AuditAdapter($register->get('db')); + $adapter->setNamespace('app_'.$project->getId()); + + $audit = new Audit($adapter); + + $countries = $locale->getText('countries'); + + $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', + ]); + + $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); + $output = []; + + foreach ($logs as $i => &$log) { + $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; + + $dd = new DeviceDetector($log['userAgent']); + + $dd->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + + $dd->parse(); + + $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', 'register', 'project', 'projectDB', 'locale']); App::patch('/v1/users/:userId/status') ->desc('Update User Status') @@ -371,44 +380,45 @@ App::patch('/v1/users/:userId/status') ->label('sdk.description', '/docs/references/users/update-user-status.md') ->param('userId', '', function () { return new UID(); }, 'User unique ID.') ->param('status', '', function () { return new WhiteList([Auth::USER_STATUS_ACTIVATED, Auth::USER_STATUS_BLOCKED, Auth::USER_STATUS_UNACTIVATED]); }, 'User Status code. To activate the user pass '.Auth::USER_STATUS_ACTIVATED.', to block the user pass '.Auth::USER_STATUS_BLOCKED.' and for disabling the user pass '.Auth::USER_STATUS_UNACTIVATED) - ->action( - function ($userId, $status) use ($response, $projectDB) { - $user = $projectDB->getDocument($userId); + ->action(function ($userId, $status, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } + $user = $projectDB->getDocument($userId); - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'status' => (int)$status, - ])); - - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } - - $oauth2Keys = []; - - foreach (Config::getParam('providers') as $key => $provider) { - if (!$provider['enabled']) { - continue; - } - - $oauth2Keys[] = 'oauth2'.\ucfirst($key); - $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; - } - - $response - ->json(\array_merge($user->getArrayCopy(\array_merge([ - '$id', - 'status', - 'email', - 'registration', - 'emailVerification', - 'name', - ], $oauth2Keys)), ['roles' => []])); + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); } - ); + + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'status' => (int)$status, + ])); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + $oauth2Keys = []; + + foreach (Config::getParam('providers') as $key => $provider) { + if (!$provider['enabled']) { + continue; + } + + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; + } + + $response + ->json(\array_merge($user->getArrayCopy(\array_merge([ + '$id', + 'status', + 'email', + 'registration', + 'emailVerification', + 'name', + ], $oauth2Keys)), ['roles' => []])); + }, ['response', 'projectDB']); App::patch('/v1/users/:userId/prefs') ->desc('Update User Preferences') @@ -420,37 +430,38 @@ App::patch('/v1/users/:userId/prefs') ->label('sdk.description', '/docs/references/users/update-user-prefs.md') ->param('userId', '', function () { return new UID(); }, 'User unique ID.') ->param('prefs', '', function () { return new Assoc();}, 'Prefs key-value JSON object.') - ->action( - function ($userId, $prefs) use ($response, $projectDB) { - $user = $projectDB->getDocument($userId); + ->action(function ($userId, $prefs, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } + $user = $projectDB->getDocument($userId); - $old = \json_decode($user->getAttribute('prefs', '{}'), true); - $old = ($old) ? $old : []; - - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'prefs' => \json_encode(\array_merge($old, $prefs)), - ])); - - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } - - $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 (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); } - ); + + $old = \json_decode($user->getAttribute('prefs', '{}'), true); + $old = ($old) ? $old : []; + + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'prefs' => \json_encode(\array_merge($old, $prefs)), + ])); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + $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', 'projectDB']); App::delete('/v1/users/:userId/sessions/:sessionId') @@ -464,27 +475,28 @@ App::delete('/v1/users/:userId/sessions/:sessionId') ->label('abuse-limit', 100) ->param('userId', '', function () { return new UID(); }, 'User unique ID.') ->param('sessionId', null, function () { return new UID(); }, 'User unique session ID.') - ->action( - function ($userId, $sessionId) use ($response, $request, $projectDB) { - $user = $projectDB->getDocument($userId); + ->action(function ($userId, $sessionId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } + $user = $projectDB->getDocument($userId); - $tokens = $user->getAttribute('tokens', []); + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); + } - foreach ($tokens as $token) { /* @var $token Document */ - if ($sessionId == $token->getId()) { - if (!$projectDB->deleteDocument($token->getId())) { - throw new Exception('Failed to remove token from DB', 500); - } + $tokens = $user->getAttribute('tokens', []); + + foreach ($tokens as $token) { /* @var $token Document */ + if ($sessionId == $token->getId()) { + if (!$projectDB->deleteDocument($token->getId())) { + throw new Exception('Failed to remove token from DB', 500); } } - - $response->json(array('result' => 'success')); } - ); + + $response->json(array('result' => 'success')); + }, ['response', 'projectDB']); App::delete('/v1/users/:userId/sessions') ->desc('Delete User Sessions') @@ -496,22 +508,23 @@ App::delete('/v1/users/:userId/sessions') ->label('sdk.description', '/docs/references/users/delete-user-sessions.md') ->label('abuse-limit', 100) ->param('userId', '', function () { return new UID(); }, 'User unique ID.') - ->action( - function ($userId) use ($response, $request, $projectDB) { - $user = $projectDB->getDocument($userId); + ->action(function ($userId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } + $user = $projectDB->getDocument($userId); - $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->json(array('result' => 'success')); + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); } - ); + + $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->json(array('result' => 'success')); + }, ['response', 'projectDB']); diff --git a/app/controllers/shared/web.php b/app/controllers/shared/web.php index 9ff3612290..e5eb16c489 100644 --- a/app/controllers/shared/web.php +++ b/app/controllers/shared/web.php @@ -4,7 +4,11 @@ use Utopia\App; use Utopia\View; use Utopia\Config\Config; -App::init(function ($utopia, $response, $request, $layout) { +App::init(function ($utopia, $request, $response, $layout) { + /** @var Utopia\App $utopia */ + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Utopia\View $layout */ /* AJAX check */ if (!empty($request->getQuery('version', ''))) { @@ -12,7 +16,7 @@ App::init(function ($utopia, $response, $request, $layout) { } $layout ->setParam('title', APP_NAME) - ->setParam('protocol', Config::getParam('protocol')) + ->setParam('protocol', $request->getProtocol()) ->setParam('domain', Config::getParam('domain')) ->setParam('home', App::getEnv('_APP_HOME')) ->setParam('setup', App::getEnv('_APP_SETUP')) @@ -36,8 +40,8 @@ App::init(function ($utopia, $response, $request, $layout) { $route = $utopia->match($request); $scope = $route->getLabel('scope', ''); $layout - ->setParam('version', Config::getParam('version')) + ->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN')) ->setParam('isDev', App::isDevelopment()) ->setParam('class', $scope) ; -}, ['utopia', 'response', 'request', 'layout'], 'web'); +}, ['utopia', 'request', 'response', 'layout'], 'web'); diff --git a/app/controllers/web/console.php b/app/controllers/web/console.php index 1c6db92975..962373bd53 100644 --- a/app/controllers/web/console.php +++ b/app/controllers/web/console.php @@ -29,7 +29,7 @@ App::shutdown(function ($response, $layout) { $footer ->setParam('home', App::getEnv('_APP_HOME', '')) - ->setParam('version', Config::getParam('version')) + ->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN')) ; $layout diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index b9e75ca52d..9cbfef4e47 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -9,11 +9,13 @@ use Utopia\Validator\WhiteList; use Utopia\Validator\Range; App::init(function ($layout) { + /** @var Utopia\View $layout */ + $header = new View(__DIR__.'/../../views/home/comps/header.phtml'); $footer = new View(__DIR__.'/../../views/home/comps/footer.phtml'); $footer - ->setParam('version', Config::getParam('version')) + ->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN')) ; $layout @@ -27,6 +29,9 @@ App::init(function ($layout) { }, ['layout'], 'home'); App::shutdown(function ($response, $layout) { + /** @var Utopia\Response $response */ + /** @var Utopia\View $layout */ + $response->send($layout->render()); }, ['response', 'layout'], 'home'); @@ -34,90 +39,102 @@ App::get('/') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action( - function () use ($response) { - $response->redirect('/auth/signin'); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ + + $response->redirect('/auth/signin'); + }, ['response']); App::get('/auth/signin') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/home/auth/signin.phtml'); $layout ->setParam('title', 'Sign In - '.APP_NAME) ->setParam('body', $page); - }); + }, ['layout']); App::get('/auth/signup') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ $page = new View(__DIR__.'/../../views/home/auth/signup.phtml'); $layout ->setParam('title', 'Sign Up - '.APP_NAME) ->setParam('body', $page); - }); + }, ['layout']); App::get('/auth/recovery') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($request, $layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/home/auth/recovery.phtml'); $layout ->setParam('title', 'Password Recovery - '.APP_NAME) ->setParam('body', $page); - }); + }, ['layout']); App::get('/auth/confirm') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/home/auth/confirm.phtml'); $layout ->setParam('title', 'Account Confirmation - '.APP_NAME) ->setParam('body', $page); - }); + }, ['layout']); App::get('/auth/join') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/home/auth/join.phtml'); $layout ->setParam('title', 'Invitation - '.APP_NAME) ->setParam('body', $page); - }); + }, ['layout']); App::get('/auth/recovery/reset') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/home/auth/recovery/reset.phtml'); $layout ->setParam('title', 'Password Reset - '.APP_NAME) ->setParam('body', $page); - }); - + }, ['layout']); App::get('/auth/oauth2/success') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/home/auth/oauth2.phtml'); $layout @@ -126,13 +143,15 @@ App::get('/auth/oauth2/success') ->setParam('header', []) ->setParam('footer', []) ; - }); + }, ['layout']); App::get('/auth/oauth2/failure') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/home/auth/oauth2.phtml'); $layout @@ -141,14 +160,16 @@ App::get('/auth/oauth2/failure') ->setParam('header', []) ->setParam('footer', []) ; - }); + }, ['layout']); App::get('/error/:code') ->groups(['web', 'home']) ->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 @@ -158,7 +179,7 @@ App::get('/error/:code') $layout ->setParam('title', 'Error'.' - '.APP_NAME) ->setParam('body', $page); - }); + }, ['layout']); App::get('/open-api-2.json') ->groups(['web', 'home']) @@ -167,401 +188,402 @@ App::get('/open-api-2.json') ->param('platform', APP_PLATFORM_CLIENT, function () {return new WhiteList([APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER, APP_PLATFORM_CONSOLE]);}, 'Choose target platform.', true) ->param('extensions', 0, function () {return new Range(0, 1);}, 'Show extra data.', true) ->param('tests', 0, function () {return new Range(0, 1);}, 'Include only test services.', true) - ->action( - function ($platform, $extensions, $tests) use ($response, $utopia) { - $services = Config::getParam('services', []); + ->action(function ($platform, $extensions, $tests, $utopia, $response) { + /** @var Utopia\App $utopia */ + /** @var Utopia\Response $response */ + + $services = Config::getParam('services', []); + + function fromCamelCase($input) + { + \preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches); + $ret = $matches[0]; + foreach ($ret as &$match) { + $match = $match == \strtoupper($match) ? \strtolower($match) : \lcfirst($match); + } + + return \implode('_', $ret); + } + + function fromCamelCaseToDash($input) + { + return \str_replace([' ', '_'], '-', \strtolower(\preg_replace('/([a-zA-Z])(?=[A-Z])/', '$1-', $input))); + } + + foreach ($services as $service) { /* @noinspection PhpIncludeInspection */ + if ($tests && !isset($service['tests'])) { + continue; + } + + if ($tests && !$service['tests']) { + continue; + } - function fromCamelCase($input) - { - \preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches); - $ret = $matches[0]; - foreach ($ret as &$match) { - $match = $match == \strtoupper($match) ? \strtolower($match) : \lcfirst($match); - } + if (!$tests && !$service['sdk']) { + continue; + } + + /** @noinspection PhpIncludeInspection */ + include_once \realpath(__DIR__.'/../../'.$service['controller']); + } - return \implode('_', $ret); + $security = [ + APP_PLATFORM_CLIENT => ['Project' => []], + APP_PLATFORM_SERVER => ['Project' => [], 'Key' => []], + APP_PLATFORM_CONSOLE => ['Project' => [], 'Key' => []], + ]; + + $platforms = [ + 'client' => APP_PLATFORM_CLIENT, + 'server' => APP_PLATFORM_SERVER, + 'all' => APP_PLATFORM_CONSOLE, + ]; + + $keys = [ + APP_PLATFORM_CLIENT => [ + 'Project' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Project', + 'description' => 'Your project ID', + 'in' => 'header', + ], + 'Locale' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Locale', + 'description' => '', + 'in' => 'header', + ], + ], + APP_PLATFORM_SERVER => [ + 'Project' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Project', + 'description' => 'Your project ID', + 'in' => 'header', + ], + 'Key' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Key', + 'description' => 'Your secret API key', + 'in' => 'header', + ], + 'Locale' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Locale', + 'description' => '', + 'in' => 'header', + ], + ], + APP_PLATFORM_CONSOLE => [ + 'Project' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Project', + 'description' => 'Your project ID', + 'in' => 'header', + ], + 'Key' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Key', + 'description' => 'Your secret API key', + 'in' => 'header', + ], + 'Locale' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Locale', + 'description' => '', + 'in' => 'header', + ], + 'Mode' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Mode', + 'description' => '', + 'in' => 'header', + ], + ], + ]; + + /* + * Specifications (v3.0.0): + * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md + */ + $output = [ + 'swagger' => '2.0', + 'info' => [ + 'version' => APP_VERSION_STABLE, + 'title' => APP_NAME, + 'description' => 'Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)', + 'termsOfService' => 'https://appwrite.io/policy/terms', + 'contact' => [ + 'name' => 'Appwrite Team', + 'url' => 'https://appwrite.io/support', + 'email' => App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), + ], + 'license' => [ + 'name' => 'BSD-3-Clause', + 'url' => 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', + ], + ], + 'host' => \parse_url(App::getEnv('_APP_HOME', Config::getParam('domain')), PHP_URL_HOST), + 'basePath' => '/v1', + 'schemes' => ['https'], + 'consumes' => ['application/json', 'multipart/form-data'], + 'produces' => ['application/json'], + 'securityDefinitions' => $keys[$platform], + 'paths' => [], + 'definitions' => [ + // 'Pet' => [ + // 'required' => ['id', 'name'], + // 'properties' => [ + // 'id' => [ + // 'type' => 'integer', + // 'format' => 'int64', + // ], + // 'name' => [ + // 'type' => 'string', + // ], + // 'tag' => [ + // 'type' => 'string', + // ], + // ], + // ], + // 'Pets' => array( + // 'type' => 'array', + // 'items' => array( + // '$ref' => '#/definitions/Pet', + // ), + // ), + 'Error' => array( + 'required' => array( + 0 => 'code', + 1 => 'message', + ), + 'properties' => array( + 'code' => array( + 'type' => 'integer', + 'format' => 'int32', + ), + 'message' => array( + 'type' => 'string', + ), + ), + ), + ], + 'externalDocs' => [ + 'description' => 'Full API docs, specs and tutorials', + 'url' => Config::getParam('protocol').'://'.Config::getParam('domain').'/docs', + ], + ]; + + if ($extensions) { + if (isset($output['securityDefinitions']['Project'])) { + $output['securityDefinitions']['Project']['extensions'] = ['demo' => '5df5acd0d48c2']; + } + + if (isset($output['securityDefinitions']['Key'])) { + $output['securityDefinitions']['Key']['extensions'] = ['demo' => '919c2d18fb5d4...a2ae413da83346ad2']; + } + + if (isset($output['securityDefinitions']['Locale'])) { + $output['securityDefinitions']['Locale']['extensions'] = ['demo' => 'en']; } - function fromCamelCaseToDash($input) - { - return \str_replace([' ', '_'], '-', \strtolower(\preg_replace('/([a-zA-Z])(?=[A-Z])/', '$1-', $input))); + if (isset($output['securityDefinitions']['Mode'])) { + $output['securityDefinitions']['Mode']['extensions'] = ['demo' => '']; } + } - foreach ($services as $service) { /* @noinspection PhpIncludeInspection */ - if ($tests && !isset($service['tests'])) { + foreach ($utopia->getRoutes() as $key => $method) { + foreach ($method as $route) { /* @var $route \Utopia\Route */ + if (!$route->getLabel('docs', true)) { continue; } - if ($tests && !$service['tests']) { + if (empty($route->getLabel('sdk.namespace', null))) { continue; } - - if (!$tests && !$service['sdk']) { + + if ($platform !== APP_PLATFORM_CONSOLE && !\in_array($platforms[$platform], $route->getLabel('sdk.platform', []))) { continue; } - - /** @noinspection PhpIncludeInspection */ - include_once \realpath(__DIR__.'/../../'.$service['controller']); - } - $security = [ - APP_PLATFORM_CLIENT => ['Project' => []], - APP_PLATFORM_SERVER => ['Project' => [], 'Key' => []], - APP_PLATFORM_CONSOLE => ['Project' => [], 'Key' => []], - ]; + $url = \str_replace('/v1', '', $route->getURL()); + $scope = $route->getLabel('scope', ''); + $hide = $route->getLabel('sdk.hide', false); + $consumes = ['application/json']; - $platforms = [ - 'client' => APP_PLATFORM_CLIENT, - 'server' => APP_PLATFORM_SERVER, - 'all' => APP_PLATFORM_CONSOLE, - ]; + if ($hide) { + continue; + } - $keys = [ - APP_PLATFORM_CLIENT => [ - 'Project' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Project', - 'description' => 'Your project ID', - 'in' => 'header', - ], - 'Locale' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Locale', - 'description' => '', - 'in' => 'header', - ], - ], - APP_PLATFORM_SERVER => [ - 'Project' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Project', - 'description' => 'Your project ID', - 'in' => 'header', - ], - 'Key' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Key', - 'description' => 'Your secret API key', - 'in' => 'header', - ], - 'Locale' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Locale', - 'description' => '', - 'in' => 'header', - ], - ], - APP_PLATFORM_CONSOLE => [ - 'Project' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Project', - 'description' => 'Your project ID', - 'in' => 'header', - ], - 'Key' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Key', - 'description' => 'Your secret API key', - 'in' => 'header', - ], - 'Locale' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Locale', - 'description' => '', - 'in' => 'header', - ], - 'Mode' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Mode', - 'description' => '', - 'in' => 'header', - ], - ], - ]; - - /* - * Specifications (v3.0.0): - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md - */ - $output = [ - 'swagger' => '2.0', - 'info' => [ - 'version' => APP_VERSION_STABLE, - 'title' => APP_NAME, - 'description' => 'Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)', - 'termsOfService' => 'https://appwrite.io/policy/terms', - 'contact' => [ - 'name' => 'Appwrite Team', - 'url' => 'https://appwrite.io/support', - 'email' => App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), - ], - 'license' => [ - 'name' => 'BSD-3-Clause', - 'url' => 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', - ], - ], - 'host' => \parse_url(App::getEnv('_APP_HOME', Config::getParam('domain')), PHP_URL_HOST), - 'basePath' => '/v1', - 'schemes' => ['https'], - 'consumes' => ['application/json', 'multipart/form-data'], - 'produces' => ['application/json'], - 'securityDefinitions' => $keys[$platform], - 'paths' => [], - 'definitions' => [ - // 'Pet' => [ - // 'required' => ['id', 'name'], - // 'properties' => [ - // 'id' => [ - // 'type' => 'integer', - // 'format' => 'int64', - // ], - // 'name' => [ - // 'type' => 'string', - // ], - // 'tag' => [ - // 'type' => 'string', + $desc = (!empty($route->getLabel('sdk.description', ''))) ? \realpath('../'.$route->getLabel('sdk.description', '')) : null; + + $temp = [ + 'summary' => $route->getDesc(), + 'operationId' => $route->getLabel('sdk.method', \uniqid()), + 'consumes' => [], + 'tags' => [$route->getLabel('sdk.namespace', 'default')], + 'description' => ($desc) ? \file_get_contents($desc) : '', + + // 'responses' => [ + // 200 => [ + // 'description' => 'An paged array of pets', + // 'schema' => [ + // '$ref' => '#/definitions/Pet', // ], // ], // ], - // 'Pets' => array( - // 'type' => 'array', - // 'items' => array( - // '$ref' => '#/definitions/Pet', - // ), - // ), - 'Error' => array( - 'required' => array( - 0 => 'code', - 1 => 'message', - ), - 'properties' => array( - 'code' => array( - 'type' => 'integer', - 'format' => 'int32', - ), - 'message' => array( - 'type' => 'string', - ), - ), - ), - ], - 'externalDocs' => [ - 'description' => 'Full API docs, specs and tutorials', - 'url' => Config::getParam('protocol').'://'.Config::getParam('domain').'/docs', - ], - ]; + ]; - if ($extensions) { - if (isset($output['securityDefinitions']['Project'])) { - $output['securityDefinitions']['Project']['extensions'] = ['demo' => '5df5acd0d48c2']; - } - - if (isset($output['securityDefinitions']['Key'])) { - $output['securityDefinitions']['Key']['extensions'] = ['demo' => '919c2d18fb5d4...a2ae413da83346ad2']; - } - - if (isset($output['securityDefinitions']['Locale'])) { - $output['securityDefinitions']['Locale']['extensions'] = ['demo' => 'en']; - } + if ($extensions) { + $platformList = $route->getLabel('sdk.platform', []); - if (isset($output['securityDefinitions']['Mode'])) { - $output['securityDefinitions']['Mode']['extensions'] = ['demo' => '']; - } - } - - foreach ($utopia->getRoutes() as $key => $method) { - foreach ($method as $route) { /* @var $route \Utopia\Route */ - if (!$route->getLabel('docs', true)) { - continue; - } - - if (empty($route->getLabel('sdk.namespace', null))) { - continue; - } - - if ($platform !== APP_PLATFORM_CONSOLE && !\in_array($platforms[$platform], $route->getLabel('sdk.platform', []))) { - continue; - } - - $url = \str_replace('/v1', '', $route->getURL()); - $scope = $route->getLabel('scope', ''); - $hide = $route->getLabel('sdk.hide', false); - $consumes = ['application/json']; - - if ($hide) { - continue; - } - - $desc = (!empty($route->getLabel('sdk.description', ''))) ? \realpath('../'.$route->getLabel('sdk.description', '')) : null; - - $temp = [ - 'summary' => $route->getDesc(), - 'operationId' => $route->getLabel('sdk.method', \uniqid()), - 'consumes' => [], - 'tags' => [$route->getLabel('sdk.namespace', 'default')], - 'description' => ($desc) ? \file_get_contents($desc) : '', - - // 'responses' => [ - // 200 => [ - // 'description' => 'An paged array of pets', - // 'schema' => [ - // '$ref' => '#/definitions/Pet', - // ], - // ], - // ], + $temp['extensions'] = [ + 'weight' => $route->getOrder(), + 'cookies' => $route->getLabel('sdk.cookies', false), + 'type' => $route->getLabel('sdk.methodType', ''), + 'demo' => 'docs/examples/'.fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')).'/'.fromCamelCaseToDash($temp['operationId']).'.md', + 'edit' => 'https://github.com/appwrite/appwrite/edit/master' . $route->getLabel('sdk.description', ''), + 'rate-limit' => $route->getLabel('abuse-limit', 0), + 'rate-time' => $route->getLabel('abuse-time', 3600), + 'rate-key' => $route->getLabel('abuse-key', 'url:{url},ip:{ip}'), + 'scope' => $route->getLabel('scope', ''), + 'platforms' => $platformList, ]; + } - if ($extensions) { - $platformList = $route->getLabel('sdk.platform', []); + if ((!empty($scope))) { // && 'public' != $scope + $temp['security'][] = $route->getLabel('sdk.security', $security[$platform]); + } - $temp['extensions'] = [ - 'weight' => $route->getOrder(), - 'cookies' => $route->getLabel('sdk.cookies', false), - 'type' => $route->getLabel('sdk.methodType', ''), - 'demo' => 'docs/examples/'.fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')).'/'.fromCamelCaseToDash($temp['operationId']).'.md', - 'edit' => 'https://github.com/appwrite/appwrite/edit/master' . $route->getLabel('sdk.description', ''), - 'rate-limit' => $route->getLabel('abuse-limit', 0), - 'rate-time' => $route->getLabel('abuse-time', 3600), - 'rate-key' => $route->getLabel('abuse-key', 'url:{url},ip:{ip}'), - 'scope' => $route->getLabel('scope', ''), - 'platforms' => $platformList, - ]; - } - - if ((!empty($scope))) { // && 'public' != $scope - $temp['security'][] = $route->getLabel('sdk.security', $security[$platform]); - } - - $requestBody = [ - 'content' => [ - 'application/x-www-form-urlencoded' => [ - 'schema' => [ - 'type' => 'object', - 'properties' => [], - ], - 'required' => [], + $requestBody = [ + 'content' => [ + 'application/x-www-form-urlencoded' => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [], ], + 'required' => [], ], + ], + ]; + + foreach ($route->getParams() as $name => $param) { + $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $utopia->getResources($param['resources'])) : $param['validator']; /* @var $validator \Utopia\Validator */ + + $node = [ + 'name' => $name, + 'description' => $param['description'], + 'required' => !$param['optional'], ]; - foreach ($route->getParams() as $name => $param) { - $validator = (\is_callable($param['validator'])) ? $param['validator']() : $param['validator']; /* @var $validator \Utopia\Validator */ - - $node = [ - 'name' => $name, - 'description' => $param['description'], - 'required' => !$param['optional'], - ]; - - switch ((!empty($validator)) ? \get_class($validator) : '') { - case 'Utopia\Validator\Text': - $node['type'] = 'string'; - $node['x-example'] = '['.\strtoupper(fromCamelCase($node['name'])).']'; - break; - case 'Utopia\Validator\Boolean': - $node['type'] = 'boolean'; - $node['x-example'] = false; - break; - case 'Appwrite\Database\Validator\UID': - $node['type'] = 'string'; - $node['x-example'] = '['.\strtoupper(fromCamelCase($node['name'])).']'; - break; - case 'Utopia\Validator\Email': - $node['type'] = 'string'; - $node['format'] = 'email'; - $node['x-example'] = 'email@example.com'; - break; - case 'Utopia\Validator\URL': - $node['type'] = 'string'; - $node['format'] = 'url'; - $node['x-example'] = 'https://example.com'; - break; - case 'Utopia\Validator\JSON': - case 'Utopia\Validator\Mock': - case 'Utopia\Validator\Assoc': - $node['type'] = 'object'; - $node['type'] = 'object'; - $node['x-example'] = '{}'; - //$node['format'] = 'json'; - break; - case 'Appwrite\Storage\Validator\File': - $consumes = ['multipart/form-data']; - $node['type'] = 'file'; - break; - case 'Utopia\Validator\ArrayList': - $node['type'] = 'array'; - $node['collectionFormat'] = 'multi'; - $node['items'] = [ - 'type' => 'string', - ]; - break; - case 'Appwrite\Auth\Validator\Password': - $node['type'] = 'string'; - $node['format'] = 'format'; - $node['x-example'] = 'password'; - break; - case 'Utopia\Validator\Range': /* @var $validator \Utopia\Validator\Range */ - $node['type'] = 'integer'; - $node['format'] = 'int32'; - $node['x-example'] = $validator->getMin(); - break; - case 'Utopia\Validator\Numeric': - $node['type'] = 'integer'; - $node['format'] = 'int32'; - break; - case 'Utopia\Validator\Length': - $node['type'] = 'string'; - break; - case 'Utopia\Validator\Host': - $node['type'] = 'string'; - $node['format'] = 'url'; - $node['x-example'] = 'https://example.com'; - break; - case 'Utopia\Validator\WhiteList': /* @var $validator \Utopia\Validator\WhiteList */ - $node['type'] = 'string'; - $node['x-example'] = $validator->getList()[0]; - break; - default: - $node['type'] = 'string'; - break; - } - - if ($param['optional'] && !\is_null($param['default'])) { // Param has default value - $node['default'] = $param['default']; - } - - if (false !== \strpos($url, ':'.$name)) { // Param is in URL path - $node['in'] = 'path'; - $temp['parameters'][] = $node; - } elseif ($key == 'GET') { // Param is in query - $node['in'] = 'query'; - $temp['parameters'][] = $node; - } else { // Param is in payload - $node['in'] = 'formData'; - $temp['parameters'][] = $node; - $requestBody['content']['application/x-www-form-urlencoded']['schema']['properties'][] = $node; - - if (!$param['optional']) { - $requestBody['content']['application/x-www-form-urlencoded']['required'][] = $name; - } - } - - $url = \str_replace(':'.$name, '{'.$name.'}', $url); + switch ((!empty($validator)) ? \get_class($validator) : '') { + case 'Utopia\Validator\Text': + $node['type'] = 'string'; + $node['x-example'] = '['.\strtoupper(fromCamelCase($node['name'])).']'; + break; + case 'Utopia\Validator\Boolean': + $node['type'] = 'boolean'; + $node['x-example'] = false; + break; + case 'Appwrite\Database\Validator\UID': + $node['type'] = 'string'; + $node['x-example'] = '['.\strtoupper(fromCamelCase($node['name'])).']'; + break; + case 'Utopia\Validator\Email': + $node['type'] = 'string'; + $node['format'] = 'email'; + $node['x-example'] = 'email@example.com'; + break; + case 'Utopia\Validator\URL': + $node['type'] = 'string'; + $node['format'] = 'url'; + $node['x-example'] = 'https://example.com'; + break; + case 'Utopia\Validator\JSON': + case 'Utopia\Validator\Mock': + case 'Utopia\Validator\Assoc': + $node['type'] = 'object'; + $node['type'] = 'object'; + $node['x-example'] = '{}'; + //$node['format'] = 'json'; + break; + case 'Appwrite\Storage\Validator\File': + $consumes = ['multipart/form-data']; + $node['type'] = 'file'; + break; + case 'Utopia\Validator\ArrayList': + $node['type'] = 'array'; + $node['collectionFormat'] = 'multi'; + $node['items'] = [ + 'type' => 'string', + ]; + break; + case 'Appwrite\Auth\Validator\Password': + $node['type'] = 'string'; + $node['format'] = 'format'; + $node['x-example'] = 'password'; + break; + case 'Utopia\Validator\Range': /* @var $validator \Utopia\Validator\Range */ + $node['type'] = 'integer'; + $node['format'] = 'int32'; + $node['x-example'] = $validator->getMin(); + break; + case 'Utopia\Validator\Numeric': + $node['type'] = 'integer'; + $node['format'] = 'int32'; + break; + case 'Utopia\Validator\Length': + $node['type'] = 'string'; + break; + case 'Utopia\Validator\Host': + $node['type'] = 'string'; + $node['format'] = 'url'; + $node['x-example'] = 'https://example.com'; + break; + case 'Utopia\Validator\WhiteList': /* @var $validator \Utopia\Validator\WhiteList */ + $node['type'] = 'string'; + $node['x-example'] = $validator->getList()[0]; + break; + default: + $node['type'] = 'string'; + break; } - $temp['consumes'] = $consumes; + if ($param['optional'] && !\is_null($param['default'])) { // Param has default value + $node['default'] = $param['default']; + } - $output['paths'][$url][\strtolower($route->getMethod())] = $temp; + if (false !== \strpos($url, ':'.$name)) { // Param is in URL path + $node['in'] = 'path'; + $temp['parameters'][] = $node; + } elseif ($key == 'GET') { // Param is in query + $node['in'] = 'query'; + $temp['parameters'][] = $node; + } else { // Param is in payload + $node['in'] = 'formData'; + $temp['parameters'][] = $node; + $requestBody['content']['application/x-www-form-urlencoded']['schema']['properties'][] = $node; + + if (!$param['optional']) { + $requestBody['content']['application/x-www-form-urlencoded']['required'][] = $name; + } + } + + $url = \str_replace(':'.$name, '{'.$name.'}', $url); } + + $temp['consumes'] = $consumes; + + $output['paths'][$url][\strtolower($route->getMethod())] = $temp; } - - /*foreach ($consoleDB->getMocks() as $mock) { - var_dump($mock['name']); - }*/ - - \ksort($output['paths']); - - $response - ->json($output); } - ); \ No newline at end of file + + /*foreach ($consoleDB->getMocks() as $mock) { + var_dump($mock['name']); + }*/ + + \ksort($output['paths']); + + $response + ->json($output); + }, ['utopia', 'response']); \ No newline at end of file diff --git a/app/init.php b/app/init.php index 5683a5d127..d02ded1951 100644 --- a/app/init.php +++ b/app/init.php @@ -222,7 +222,7 @@ Locale::setLanguage('zh-tw', include __DIR__.'/config/locale/translations/zh-tw. 'http' => [ 'method' => 'GET', 'user_agent' => \sprintf(APP_USERAGENT, - Config::getParam('version'), + App::getEnv('_APP_VERSION', 'UNKNOWN'), App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)), 'timeout' => 2, ], diff --git a/app/workers/tasks.php b/app/workers/tasks.php index 625bcfed6f..4d65605bc7 100644 --- a/app/workers/tasks.php +++ b/app/workers/tasks.php @@ -96,7 +96,7 @@ class TasksV1 \curl_setopt($ch, CURLOPT_HEADER, 0); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); \curl_setopt($ch, CURLOPT_USERAGENT, \sprintf(APP_USERAGENT, - Config::getParam('version'), + App::getEnv('_APP_VERSION', 'UNKNOWN'), App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY) )); \curl_setopt( diff --git a/app/workers/usage.php b/app/workers/usage.php index 0d0811c33f..a53b261137 100644 --- a/app/workers/usage.php +++ b/app/workers/usage.php @@ -1,6 +1,6 @@ get('statsd', true); - $tags = ",project={$projectId},version=".Config::getParam('version').''; + $tags = ",project={$projectId},version=".App::getEnv('_APP_VERSION', 'UNKNOWN').''; // the global namespace is prepended to every key (optional) $statsd->setNamespace('appwrite.usage'); diff --git a/app/workers/webhooks.php b/app/workers/webhooks.php index ab861dfd46..f07818724f 100644 --- a/app/workers/webhooks.php +++ b/app/workers/webhooks.php @@ -6,7 +6,6 @@ require_once __DIR__.'/../init.php'; echo APP_NAME.' webhooks worker v1 has started'; -use Utopia\Config\Config; use Appwrite\Database\Database; use Appwrite\Database\Validator\Authorization; use Utopia\App; @@ -61,7 +60,7 @@ class WebhooksV1 \curl_setopt($ch, CURLOPT_HEADER, 0); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); \curl_setopt($ch, CURLOPT_USERAGENT, \sprintf(APP_USERAGENT, - Config::getParam('version'), + App::getEnv('_APP_VERSION', 'UNKNOWN'), App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY) )); \curl_setopt( diff --git a/composer.json b/composer.json index b0eec296e0..1608977bbd 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "appwrite/php-clamav": "1.0.*", - "utopia-php/framework": "0.7.1", + "utopia-php/framework": "0.7.2", "utopia-php/abuse": "0.2.*", "utopia-php/audit": "0.3.*", "utopia-php/cache": "0.2.*", diff --git a/composer.lock b/composer.lock index 2e2ad49cf8..c94e5ea63b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b8ee06f97c395bc83a05f92939679724", + "content-hash": "2693761ec4a5bb1305ac226bffd1555d", "packages": [ { "name": "appwrite/php-clamav", @@ -1596,16 +1596,16 @@ }, { "name": "utopia-php/framework", - "version": "0.7.1", + "version": "0.7.2", "source": { "type": "git", "url": "https://github.com/utopia-php/framework.git", - "reference": "3810789c1caf16a9ad7811fd38067a35249e75f8" + "reference": "e592b7bdea5eeb48a3bf7ae18bc6d3e622e54cf3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/framework/zipball/3810789c1caf16a9ad7811fd38067a35249e75f8", - "reference": "3810789c1caf16a9ad7811fd38067a35249e75f8", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/e592b7bdea5eeb48a3bf7ae18bc6d3e622e54cf3", + "reference": "e592b7bdea5eeb48a3bf7ae18bc6d3e622e54cf3", "shasum": "" }, "require": { @@ -1636,20 +1636,20 @@ "php", "upf" ], - "time": "2020-06-29T16:02:35+00:00" + "time": "2020-06-30T09:43:41+00:00" }, { "name": "utopia-php/locale", - "version": "0.3.0", + "version": "0.3.2", "source": { "type": "git", "url": "https://github.com/utopia-php/locale.git", - "reference": "32c32a3bf5c295f3de93569cead7f412fa29ad13" + "reference": "89c488fbff65fc87c048786c3d76b6003fbaa833" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/locale/zipball/32c32a3bf5c295f3de93569cead7f412fa29ad13", - "reference": "32c32a3bf5c295f3de93569cead7f412fa29ad13", + "url": "https://api.github.com/repos/utopia-php/locale/zipball/89c488fbff65fc87c048786c3d76b6003fbaa833", + "reference": "89c488fbff65fc87c048786c3d76b6003fbaa833", "shasum": "" }, "require": { @@ -1682,7 +1682,7 @@ "upf", "utopia" ], - "time": "2020-06-29T12:39:35+00:00" + "time": "2020-06-29T20:53:16+00:00" }, { "name": "utopia-php/registry",