diff --git a/.gitpod.yml b/.gitpod.yml index 5f8eac91e1..a63c07bad2 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -22,6 +22,7 @@ ports: vscode: extensions: - ms-azuretools.vscode-docker + - zobo.php-intellisense github: # https://www.gitpod.io/docs/prebuilds#github-specific-configuration diff --git a/app/config/providers.php b/app/config/providers.php index e4f01fdf84..205d07123b 100644 --- a/app/config/providers.php +++ b/app/config/providers.php @@ -31,6 +31,16 @@ return [ // Ordered by ABC. 'beta' => false, 'mock' => false, ], + 'authentik' => [ + 'name' => 'Authentik', + 'developers' => 'https://goauthentik.io/docs/', + 'icon' => 'icon-authentik', + 'enabled' => true, + 'sandbox' => false, + 'form' => 'authentik.phtml', + 'beta' => false, + 'mock' => false, + ], 'autodesk' => [ 'name' => 'Autodesk', 'developers' => 'https://forge.autodesk.com/en/docs/oauth/v2/developers_guide/overview/', @@ -91,6 +101,16 @@ return [ // Ordered by ABC. 'beta' => false, 'mock' => false, ], + 'disqus' => [ + 'name' => 'Disqus', + 'developers' => 'https://disqus.com/api/docs/auth/', + 'icon' => 'icon-disqus', + 'enabled' => true, + 'sandbox' => false, + 'form' => false, + 'beta' => false, + 'mock' => false, + ], 'dropbox' => [ 'name' => 'Dropbox', 'developers' => 'https://www.dropbox.com/developers/documentation', @@ -201,6 +221,16 @@ return [ // Ordered by ABC. 'beta' => false, 'mock' => false ], + 'podio' => [ + 'name' => 'Podio', + 'developers' => 'https://developers.podio.com/doc/oauth-authorization', + 'icon' => 'icon-podio', + 'enabled' => true, + 'sandbox' => false, + 'form' => false, + 'beta' => false, + 'mock' => false, + ], 'salesforce' => [ 'name' => 'Salesforce', 'developers' => 'https://developer.salesforce.com/docs/', diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index bfba82dc29..ec83f7006a 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -886,7 +886,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_STRING) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('size', null, new Range(1, APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH, Range::TYPE_INTEGER), 'Attribute size for text attributes, in number of characters.') ->param('required', null, new Boolean(), 'Is attribute required?') @@ -933,7 +933,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email' ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_EMAIL) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('default', null, new Email(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true) @@ -974,7 +974,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_ENUM) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('elements', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of elements in enumerated type. Uses length of longest element to determine size. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' elements are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.') ->param('required', null, new Boolean(), 'Is attribute required?') @@ -1031,7 +1031,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_IP) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('default', null, new IP(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true) @@ -1072,7 +1072,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_URL) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('default', null, new URL(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true) @@ -1113,7 +1113,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_INTEGER) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('min', null, new Integer(), 'Minimum value to enforce on new documents', true) @@ -1183,7 +1183,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float' ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_FLOAT) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('min', null, new FloatValidator(), 'Minimum value to enforce on new documents', true) @@ -1256,7 +1256,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_BOOLEAN) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('default', null, new Boolean(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true) @@ -1295,7 +1295,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_LIST) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->inject('response') ->inject('dbForProject') ->inject('usage') @@ -1345,7 +1345,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') Response::MODEL_ATTRIBUTE_IP, Response::MODEL_ATTRIBUTE_STRING,])// needs to be last, since its condition would dominate any other string attribute ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->inject('response') ->inject('dbForProject') @@ -1408,7 +1408,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) ->label('sdk.response.model', Response::MODEL_NONE) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->inject('response') ->inject('dbForProject') @@ -1503,7 +1503,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_INDEX) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', null, new Key(), 'Index Key.') ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE, Database::INDEX_SPATIAL, Database::INDEX_ARRAY]), 'Index type.') ->param('attributes', null, new ArrayList(new Key(true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.') @@ -1658,7 +1658,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_INDEX_LIST) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->inject('response') ->inject('dbForProject') ->inject('usage') @@ -1700,7 +1700,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_INDEX) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', null, new Key(), 'Index Key.') ->inject('response') ->inject('dbForProject') @@ -1751,7 +1751,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) ->label('sdk.response.model', Response::MODEL_NONE) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', '', new Key(), 'Index Key.') ->inject('response') ->inject('dbForProject') @@ -1828,7 +1828,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->label('sdk.response.model', Response::MODEL_DOCUMENT) ->param('databaseId', '', new UID(), 'Database ID.') ->param('documentId', '', new CustomId(), 'Document ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') - ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection). Make sure to define attributes before creating documents.') + ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection). Make sure to define attributes before creating documents.') ->param('data', [], new JSON(), 'Document data as JSON object.') ->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) ->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) @@ -1949,8 +1949,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DOCUMENT_LIST) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') - ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/database#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) ->param('limit', 25, new Range(0, 100), 'Maximum number of documents to return in response. By default will return maximum 25 results. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' results allowed per request.', true) ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true) ->param('cursor', '', new UID(), 'ID of the document used as the starting point for the query, excluding the document itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true) @@ -2063,7 +2063,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DOCUMENT) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('documentId', null, new UID(), 'Document ID.') ->inject('response') ->inject('dbForProject') @@ -2366,7 +2366,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) ->label('sdk.response.model', Response::MODEL_NONE) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('documentId', null, new UID(), 'Document ID.') ->inject('response') ->inject('dbForProject') diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 86996f4a58..214af8e440 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -31,12 +31,14 @@ use Utopia\Validator\Range; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; -App::init(function (Document $project) { - - if ($project->getId() !== 'console') { - throw new Exception('Access to this API is forbidden.', 401, Exception::GENERAL_ACCESS_FORBIDDEN); - } -}, ['project'], 'projects'); +App::init() + ->groups(['projects']) + ->inject('project') + ->action(function (Document $project) { + if ($project->getId() !== 'console') { + throw new Exception('Access to this API is forbidden.', 401, Exception::GENERAL_ACCESS_FORBIDDEN); + } + }); App::post('/v1/projects') ->desc('Create Project') diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index ff55243712..7234f47848 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -427,8 +427,8 @@ App::post('/v1/teams/:teamId/memberships') $response->dynamic( $membership ->setAttribute('teamName', $team->getAttribute('name')) - ->setAttribute('userName', $user->getAttribute('name')) - ->setAttribute('userEmail', $user->getAttribute('email')), + ->setAttribute('userName', $invitee->getAttribute('name')) + ->setAttribute('userEmail', $invitee->getAttribute('email')), Response::MODEL_MEMBERSHIP ); }); diff --git a/app/controllers/general.php b/app/controllers/general.php index 620f16f1c1..c87d53c290 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -35,485 +35,506 @@ Config::setParam('domainVerification', false); Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); -App::init(function (App $utopia, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $clients) { +App::init() + ->inject('utopia') + ->inject('request') + ->inject('response') + ->inject('console') + ->inject('project') + ->inject('dbForConsole') + ->inject('user') + ->inject('locale') + ->inject('clients') + ->action(function (App $utopia, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $clients) { + /* + * Request format + */ + $route = $utopia->match($request); + Request::setRoute($route); - /* - * Request format - */ - $route = $utopia->match($request); - Request::setRoute($route); - - $requestFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', '')); - if ($requestFormat) { - switch ($requestFormat) { - case version_compare($requestFormat, '0.12.0', '<'): - Request::setFilter(new RequestV12()); - break; - case version_compare($requestFormat, '0.13.0', '<'): - Request::setFilter(new RequestV13()); - break; - case version_compare($requestFormat, '0.14.0', '<'): - Request::setFilter(new RequestV14()); - break; - default: - Request::setFilter(null); - } - } else { - Request::setFilter(null); - } - - $domain = $request->getHostname(); - $domains = Config::getParam('domains', []); - if (!array_key_exists($domain, $domains)) { - $domain = new Domain(!empty($domain) ? $domain : ''); - - if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) { - $domains[$domain->get()] = false; - Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.'); - } elseif (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) { - Console::warning('Skipping SSL certificates generation on ACME challenge.'); - } else { - Authorization::disable(); - - $envDomain = App::getEnv('_APP_DOMAIN', ''); - $mainDomain = null; - if (!empty($envDomain) && $envDomain !== 'localhost') { - $mainDomain = $envDomain; - } else { - $domainDocument = $dbForConsole->findOne('domains', [], 0, ['_id'], ['ASC']); - $mainDomain = $domainDocument ? $domainDocument->getAttribute('domain') : $domain->get(); + $requestFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', '')); + if ($requestFormat) { + switch ($requestFormat) { + case version_compare($requestFormat, '0.12.0', '<'): + Request::setFilter(new RequestV12()); + break; + case version_compare($requestFormat, '0.13.0', '<'): + Request::setFilter(new RequestV13()); + break; + case version_compare($requestFormat, '0.14.0', '<'): + Request::setFilter(new RequestV14()); + break; + default: + Request::setFilter(null); } + } else { + Request::setFilter(null); + } - if ($mainDomain !== $domain->get()) { - Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.'); + $domain = $request->getHostname(); + $domains = Config::getParam('domains', []); + if (!array_key_exists($domain, $domains)) { + $domain = new Domain(!empty($domain) ? $domain : ''); + + if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) { + $domains[$domain->get()] = false; + Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.'); + } elseif (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) { + Console::warning('Skipping SSL certificates generation on ACME challenge.'); } else { - $domainDocument = $dbForConsole->findOne('domains', [ - new Query('domain', QUERY::TYPE_EQUAL, [$domain->get()]) - ]); + Authorization::disable(); - if (!$domainDocument) { - $domainDocument = new Document([ - 'domain' => $domain->get(), - 'tld' => $domain->getSuffix(), - 'registerable' => $domain->getRegisterable(), - 'verification' => false, - 'certificateId' => null, + $envDomain = App::getEnv('_APP_DOMAIN', ''); + $mainDomain = null; + if (!empty($envDomain) && $envDomain !== 'localhost') { + $mainDomain = $envDomain; + } else { + $domainDocument = $dbForConsole->findOne('domains', [], 0, ['_id'], ['ASC']); + $mainDomain = $domainDocument ? $domainDocument->getAttribute('domain') : $domain->get(); + } + + if ($mainDomain !== $domain->get()) { + Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.'); + } else { + $domainDocument = $dbForConsole->findOne('domains', [ + new Query('domain', QUERY::TYPE_EQUAL, [$domain->get()]) ]); - $domainDocument = $dbForConsole->createDocument('domains', $domainDocument); + if (!$domainDocument) { + $domainDocument = new Document([ + 'domain' => $domain->get(), + 'tld' => $domain->getSuffix(), + 'registerable' => $domain->getRegisterable(), + 'verification' => false, + 'certificateId' => null, + ]); - Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...'); + $domainDocument = $dbForConsole->createDocument('domains', $domainDocument); - (new Certificate()) - ->setDomain($domainDocument) - ->trigger(); + Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...'); + + (new Certificate()) + ->setDomain($domainDocument) + ->trigger(); + } } + $domains[$domain->get()] = true; + + Authorization::reset(); // ensure authorization is re-enabled } - $domains[$domain->get()] = true; - - Authorization::reset(); // ensure authorization is re-enabled - } - Config::setParam('domains', $domains); - } - - $localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', '')); - if (\in_array($localeParam, Config::getParam('locale-codes'))) { - $locale->setDefault($localeParam); - } - - if ($project->isEmpty()) { - throw new AppwriteException('Project not found', 404, AppwriteException::PROJECT_NOT_FOUND); - } - - if (!empty($route->getLabel('sdk.auth', [])) && $project->isEmpty() && ($route->getLabel('scope', '') !== 'public')) { - throw new AppwriteException('Missing or unknown project ID', 400, AppwriteException::PROJECT_UNKNOWN); - } - - $referrer = $request->getReferer(); - $origin = \parse_url($request->getOrigin($referrer), PHP_URL_HOST); - $protocol = \parse_url($request->getOrigin($referrer), PHP_URL_SCHEME); - $port = \parse_url($request->getOrigin($referrer), PHP_URL_PORT); - - $refDomainOrigin = 'localhost'; - $validator = new Hostname($clients); - if ($validator->isValid($origin)) { - $refDomainOrigin = $origin; - } - - $refDomain = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $refDomainOrigin . (!empty($port) ? ':' . $port : ''); - - $refDomain = (!$route->getLabel('origin', false)) // This route is publicly accessible - ? $refDomain - : (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : ''); - - $selfDomain = new Domain($request->getHostname()); - $endDomain = new Domain((string)$origin); - - Config::setParam( - 'domainVerification', - ($selfDomain->getRegisterable() === $endDomain->getRegisterable()) && - $endDomain->getRegisterable() !== '' - ); - - Config::setParam('cookieDomain', ( - $request->getHostname() === 'localhost' || - $request->getHostname() === 'localhost:' . $request->getPort() || - (\filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false) - ) - ? null - : '.' . $request->getHostname()); - - /* - * Response format - */ - $responseFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', '')); - if ($responseFormat) { - switch ($responseFormat) { - case version_compare($responseFormat, '0.11.2', '<='): - Response::setFilter(new ResponseV11()); - break; - case version_compare($responseFormat, '0.12.4', '<='): - Response::setFilter(new ResponseV12()); - break; - case version_compare($responseFormat, '0.13.4', '<='): - Response::setFilter(new ResponseV13()); - break; - case version_compare($responseFormat, '0.14.0', '<='): - Response::setFilter(new ResponseV14()); - break; - default: - Response::setFilter(null); - } - } else { - Response::setFilter(null); - } - - /* - * Security Headers - * - * As recommended at: - * @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers - */ - if (App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS - if ($request->getProtocol() !== 'https') { - if ($request->getMethod() !== Request::METHOD_GET) { - throw new AppwriteException('Method unsupported over HTTP.', 500, AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED); - } - - return $response->redirect('https://' . $request->getHostname() . $request->getURI()); + Config::setParam('domains', $domains); } - $response->addHeader('Strict-Transport-Security', 'max-age=' . (60 * 60 * 24 * 126)); // 126 days - } - - $response - ->addHeader('Server', 'Appwrite') - ->addHeader('X-Content-Type-Options', 'nosniff') - ->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE') - ->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, X-Appwrite-ID, Content-Range, Range, Cache-Control, Expires, Pragma') - ->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies') - ->addHeader('Access-Control-Allow-Origin', $refDomain) - ->addHeader('Access-Control-Allow-Credentials', 'true') - ; - - /* - * Validate Client Domain - Check to avoid CSRF attack - * Adding Appwrite API domains to allow XDOMAIN communication - * Skip this check for non-web platforms which are not required to send an origin header - */ - $origin = $request->getOrigin($request->getReferer('')); - $originValidator = new Origin(\array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', []))); - - if ( - !$originValidator->isValid($origin) - && \in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE]) - && $route->getLabel('origin', false) !== '*' - && empty($request->getHeader('x-appwrite-key', '')) - ) { - throw new AppwriteException($originValidator->getDescription(), 403, AppwriteException::GENERAL_UNKNOWN_ORIGIN); - } - - /* - * ACL Check - */ - $role = ($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER; - - // Add user roles - $memberships = $user->find('teamId', $project->getAttribute('teamId', null), 'memberships'); - - if ($memberships) { - foreach ($memberships->getAttribute('roles', []) as $memberRole) { - switch ($memberRole) { - case 'owner': - $role = Auth::USER_ROLE_OWNER; - break; - case 'admin': - $role = Auth::USER_ROLE_ADMIN; - break; - case 'developer': - $role = Auth::USER_ROLE_DEVELOPER; - break; - } + $localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', '')); + if (\in_array($localeParam, Config::getParam('locale-codes'))) { + $locale->setDefault($localeParam); } - } - $roles = Config::getParam('roles', []); - $scope = $route->getLabel('scope', 'none'); // Allowed scope for chosen route - $scopes = $roles[$role]['scopes']; // Allowed scopes for user role - - $authKey = $request->getHeader('x-appwrite-key', ''); - - if (!empty($authKey)) { // API Key authentication - // Check if given key match project API keys - $key = $project->find('secret', $authKey, 'keys'); - - /* - * Try app auth when we have project key and no user - * Mock user to app and grant API key scopes in addition to default app scopes - */ - if ($key && $user->isEmpty()) { - $user = new Document([ - '$id' => '', - 'status' => true, - 'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(), - 'password' => '', - 'name' => $project->getAttribute('name', 'Untitled'), - ]); - - $role = Auth::USER_ROLE_APP; - $scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', [])); - - $expire = $key->getAttribute('expire', 0); - - if (!empty($expire) && $expire < \time()) { - throw new AppwriteException('Project key expired', 401, AppwriteException:: PROJECT_KEY_EXPIRED); - } - - Authorization::setRole('role:' . Auth::USER_ROLE_APP); - Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys. - } - } - - Authorization::setRole('role:' . $role); - - foreach (Auth::getRoles($user) as $authRole) { - Authorization::setRole($authRole); - } - - $service = $route->getLabel('sdk.namespace', ''); - if (!empty($service)) { - if ( - array_key_exists($service, $project->getAttribute('services', [])) - && !$project->getAttribute('services', [])[$service] - && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) - ) { - throw new AppwriteException('Service is disabled', 503, AppwriteException::GENERAL_SERVICE_DISABLED); - } - } - - if (!\in_array($scope, $scopes)) { - if ($project->isEmpty()) { // Check if permission is denied because project is missing + if ($project->isEmpty()) { throw new AppwriteException('Project not found', 404, AppwriteException::PROJECT_NOT_FOUND); } - throw new AppwriteException($user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scope (' . $scope . ')', 401, AppwriteException::GENERAL_UNAUTHORIZED_SCOPE); - } - - if (false === $user->getAttribute('status')) { // Account is blocked - throw new AppwriteException('Invalid credentials. User is blocked', 401, AppwriteException::USER_BLOCKED); - } - - if ($user->getAttribute('reset')) { - throw new AppwriteException('Password reset is required', 412, AppwriteException::USER_PASSWORD_RESET_REQUIRED); - } -}, ['utopia', 'request', 'response', 'console', 'project', 'dbForConsole', 'user', 'locale', 'clients']); - -App::options(function (Request $request, Response $response) { - - $origin = $request->getOrigin(); - - $response - ->addHeader('Server', 'Appwrite') - ->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE') - ->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, X-Appwrite-ID, Content-Range, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies') - ->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies') - ->addHeader('Access-Control-Allow-Origin', $origin) - ->addHeader('Access-Control-Allow-Credentials', 'true') - ->noContent(); -}, ['request', 'response']); - -App::error(function (Throwable $error, App $utopia, Request $request, Response $response, View $layout, Document $project, ?Logger $logger, array $loggerBreadcrumbs) { - - $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); - $route = $utopia->match($request); - - /** Delegate PDO exceptions to the global handler so the database connection can be returned to the pool */ - if ($error instanceof PDOException) { - throw $error; - } - - if ($logger) { - if ($error->getCode() >= 500 || $error->getCode() === 0) { - try { - /** @var Utopia\Database\Document $user */ - $user = $utopia->getResource('user'); - } catch (\Throwable $th) { - // All good, user is optional information for logger - } - - $log = new Utopia\Logger\Log(); - - if (isset($user) && !$user->isEmpty()) { - $log->setUser(new User($user->getId())); - } - - $log->setNamespace("http"); - $log->setServer(\gethostname()); - $log->setVersion($version); - $log->setType(Log::TYPE_ERROR); - $log->setMessage($error->getMessage()); - - $log->addTag('method', $route->getMethod()); - $log->addTag('url', $route->getPath()); - $log->addTag('verboseType', get_class($error)); - $log->addTag('code', $error->getCode()); - $log->addTag('projectId', $project->getId()); - $log->addTag('hostname', $request->getHostname()); - $log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', ''))); - - $log->addExtra('file', $error->getFile()); - $log->addExtra('line', $error->getLine()); - $log->addExtra('trace', $error->getTraceAsString()); - $log->addExtra('detailedTrace', $error->getTrace()); - $log->addExtra('roles', Authorization::$roles); - - $action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD"); - $log->setAction($action); - - $isProduction = App::getEnv('_APP_ENV', 'development') === 'production'; - $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); - - foreach ($loggerBreadcrumbs as $loggerBreadcrumb) { - $log->addBreadcrumb($loggerBreadcrumb); - } - - $responseCode = $logger->addLog($log); - Console::info('Log pushed with status code: ' . $responseCode); - } - } - - $code = $error->getCode(); - $message = $error->getMessage(); - $file = $error->getFile(); - $line = $error->getLine(); - $trace = $error->getTrace(); - - if (php_sapi_name() === 'cli') { - Console::error('[Error] Timestamp: ' . date('c', time())); - - if ($route) { - Console::error('[Error] Method: ' . $route->getMethod()); - Console::error('[Error] URL: ' . $route->getPath()); + if (!empty($route->getLabel('sdk.auth', [])) && $project->isEmpty() && ($route->getLabel('scope', '') !== 'public')) { + throw new AppwriteException('Missing or unknown project ID', 400, AppwriteException::PROJECT_UNKNOWN); } - Console::error('[Error] Type: ' . get_class($error)); - Console::error('[Error] Message: ' . $message); - Console::error('[Error] File: ' . $file); - Console::error('[Error] Line: ' . $line); - } + $referrer = $request->getReferer(); + $origin = \parse_url($request->getOrigin($referrer), PHP_URL_HOST); + $protocol = \parse_url($request->getOrigin($referrer), PHP_URL_SCHEME); + $port = \parse_url($request->getOrigin($referrer), PHP_URL_PORT); - /** Handle Utopia Errors */ - if ($error instanceof Utopia\Exception) { - $error = new AppwriteException($message, $code, AppwriteException::GENERAL_UNKNOWN, $error); - switch ($code) { - case 400: - $error->setType(AppwriteException::GENERAL_ARGUMENT_INVALID); - break; - case 404: - $error->setType(AppwriteException::GENERAL_ROUTE_NOT_FOUND); - break; + $refDomainOrigin = 'localhost'; + $validator = new Hostname($clients); + if ($validator->isValid($origin)) { + $refDomainOrigin = $origin; } - } - /** Wrap all exceptions inside Appwrite\Extend\Exception */ - if (!($error instanceof AppwriteException)) { - $error = new AppwriteException($message, $code, AppwriteException::GENERAL_UNKNOWN, $error); - } + $refDomain = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $refDomainOrigin . (!empty($port) ? ':' . $port : ''); - switch ($code) { // Don't show 500 errors! - case 400: // Error allowed publicly - case 401: // Error allowed publicly - case 402: // Error allowed publicly - case 403: // Error allowed publicly - case 404: // Error allowed publicly - case 409: // Error allowed publicly - case 412: // Error allowed publicly - case 416: // Error allowed publicly - case 429: // Error allowed publicly - case 501: // Error allowed publicly - case 503: // Error allowed publicly - break; - default: - $code = 500; // All other errors get the generic 500 server error status code - $message = 'Server Error'; - } + $refDomain = (!$route->getLabel('origin', false)) // This route is publicly accessible + ? $refDomain + : (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : ''); - //$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised + $selfDomain = new Domain($request->getHostname()); + $endDomain = new Domain((string)$origin); - $type = $error->getType(); + Config::setParam( + 'domainVerification', + ($selfDomain->getRegisterable() === $endDomain->getRegisterable()) && + $endDomain->getRegisterable() !== '' + ); - $output = ((App::isDevelopment())) ? [ - 'message' => $message, - 'code' => $code, - 'file' => $file, - 'line' => $line, - 'trace' => $trace, - 'version' => $version, - 'type' => $type, - ] : [ - 'message' => $message, - 'code' => $code, - 'version' => $version, - 'type' => $type, - ]; + Config::setParam('cookieDomain', ( + $request->getHostname() === 'localhost' || + $request->getHostname() === 'localhost:' . $request->getPort() || + (\filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false) + ) + ? null + : '.' . $request->getHostname()); - $response - ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') - ->addHeader('Expires', '0') - ->addHeader('Pragma', 'no-cache') - ->setStatusCode($code) - ; + /* + * Response format + */ + $responseFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', '')); + if ($responseFormat) { + switch ($responseFormat) { + case version_compare($responseFormat, '0.11.2', '<='): + Response::setFilter(new ResponseV11()); + break; + case version_compare($responseFormat, '0.12.4', '<='): + Response::setFilter(new ResponseV12()); + break; + case version_compare($responseFormat, '0.13.4', '<='): + Response::setFilter(new ResponseV13()); + break; + case version_compare($responseFormat, '0.14.0', '<='): + Response::setFilter(new ResponseV14()); + break; + default: + Response::setFilter(null); + } + } else { + Response::setFilter(null); + } - $template = ($route) ? $route->getLabel('error', null) : null; + /* + * Security Headers + * + * As recommended at: + * @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers + */ + if (App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS + if ($request->getProtocol() !== 'https') { + if ($request->getMethod() !== Request::METHOD_GET) { + throw new AppwriteException('Method unsupported over HTTP.', 500, AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED); + } - if ($template) { - $comp = new View($template); + return $response->redirect('https://' . $request->getHostname() . $request->getURI()); + } - $comp - ->setParam('development', App::isDevelopment()) - ->setParam('projectName', $project->getAttribute('name')) - ->setParam('projectURL', $project->getAttribute('url')) - ->setParam('message', $error->getMessage()) - ->setParam('code', $code) - ->setParam('trace', $trace) + $response->addHeader('Strict-Transport-Security', 'max-age=' . (60 * 60 * 24 * 126)); // 126 days + } + + $response + ->addHeader('Server', 'Appwrite') + ->addHeader('X-Content-Type-Options', 'nosniff') + ->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE') + ->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, X-Appwrite-ID, Content-Range, Range, Cache-Control, Expires, Pragma') + ->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies') + ->addHeader('Access-Control-Allow-Origin', $refDomain) + ->addHeader('Access-Control-Allow-Credentials', 'true') ; - $layout - ->setParam('title', $project->getAttribute('name') . ' - Error') - ->setParam('description', 'No Description') - ->setParam('body', $comp) - ->setParam('version', $version) - ->setParam('litespeed', false) + /* + * Validate Client Domain - Check to avoid CSRF attack + * Adding Appwrite API domains to allow XDOMAIN communication + * Skip this check for non-web platforms which are not required to send an origin header + */ + $origin = $request->getOrigin($request->getReferer('')); + $originValidator = new Origin(\array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', []))); + + if ( + !$originValidator->isValid($origin) + && \in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE]) + && $route->getLabel('origin', false) !== '*' + && empty($request->getHeader('x-appwrite-key', '')) + ) { + throw new AppwriteException($originValidator->getDescription(), 403, AppwriteException::GENERAL_UNKNOWN_ORIGIN); + } + + /* + * ACL Check + */ + $role = ($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER; + + // Add user roles + $memberships = $user->find('teamId', $project->getAttribute('teamId', null), 'memberships'); + + if ($memberships) { + foreach ($memberships->getAttribute('roles', []) as $memberRole) { + switch ($memberRole) { + case 'owner': + $role = Auth::USER_ROLE_OWNER; + break; + case 'admin': + $role = Auth::USER_ROLE_ADMIN; + break; + case 'developer': + $role = Auth::USER_ROLE_DEVELOPER; + break; + } + } + } + + $roles = Config::getParam('roles', []); + $scope = $route->getLabel('scope', 'none'); // Allowed scope for chosen route + $scopes = $roles[$role]['scopes']; // Allowed scopes for user role + + $authKey = $request->getHeader('x-appwrite-key', ''); + + if (!empty($authKey)) { // API Key authentication + // Check if given key match project API keys + $key = $project->find('secret', $authKey, 'keys'); + + /* + * Try app auth when we have project key and no user + * Mock user to app and grant API key scopes in addition to default app scopes + */ + if ($key && $user->isEmpty()) { + $user = new Document([ + '$id' => '', + 'status' => true, + 'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(), + 'password' => '', + 'name' => $project->getAttribute('name', 'Untitled'), + ]); + + $role = Auth::USER_ROLE_APP; + $scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', [])); + + $expire = $key->getAttribute('expire', 0); + + if (!empty($expire) && $expire < \time()) { + throw new AppwriteException('Project key expired', 401, AppwriteException:: PROJECT_KEY_EXPIRED); + } + + Authorization::setRole('role:' . Auth::USER_ROLE_APP); + Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys. + } + } + + Authorization::setRole('role:' . $role); + + foreach (Auth::getRoles($user) as $authRole) { + Authorization::setRole($authRole); + } + + $service = $route->getLabel('sdk.namespace', ''); + if (!empty($service)) { + if ( + array_key_exists($service, $project->getAttribute('services', [])) + && !$project->getAttribute('services', [])[$service] + && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + ) { + throw new AppwriteException('Service is disabled', 503, AppwriteException::GENERAL_SERVICE_DISABLED); + } + } + + if (!\in_array($scope, $scopes)) { + if ($project->isEmpty()) { // Check if permission is denied because project is missing + throw new AppwriteException('Project not found', 404, AppwriteException::PROJECT_NOT_FOUND); + } + + throw new AppwriteException($user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scope (' . $scope . ')', 401, AppwriteException::GENERAL_UNAUTHORIZED_SCOPE); + } + + if (false === $user->getAttribute('status')) { // Account is blocked + throw new AppwriteException('Invalid credentials. User is blocked', 401, AppwriteException::USER_BLOCKED); + } + + if ($user->getAttribute('reset')) { + throw new AppwriteException('Password reset is required', 412, AppwriteException::USER_PASSWORD_RESET_REQUIRED); + } + }); + +App::options() + ->inject('request') + ->inject('response') + ->action(function (Request $request, Response $response) { + + $origin = $request->getOrigin(); + + $response + ->addHeader('Server', 'Appwrite') + ->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE') + ->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, X-Appwrite-ID, Content-Range, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies') + ->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies') + ->addHeader('Access-Control-Allow-Origin', $origin) + ->addHeader('Access-Control-Allow-Credentials', 'true') + ->noContent(); + }); + +App::error() + ->inject('error') + ->inject('utopia') + ->inject('request') + ->inject('response') + ->inject('layout') + ->inject('project') + ->inject('logger') + ->inject('loggerBreadcrumbs') + ->action(function (Throwable $error, App $utopia, Request $request, Response $response, View $layout, Document $project, ?Logger $logger, array $loggerBreadcrumbs) { + + $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); + $route = $utopia->match($request); + + /** Delegate PDO exceptions to the global handler so the database connection can be returned to the pool */ + if ($error instanceof PDOException) { + throw $error; + } + + if ($logger) { + if ($error->getCode() >= 500 || $error->getCode() === 0) { + try { + /** @var Utopia\Database\Document $user */ + $user = $utopia->getResource('user'); + } catch (\Throwable $th) { + // All good, user is optional information for logger + } + + $log = new Utopia\Logger\Log(); + + if (isset($user) && !$user->isEmpty()) { + $log->setUser(new User($user->getId())); + } + + $log->setNamespace("http"); + $log->setServer(\gethostname()); + $log->setVersion($version); + $log->setType(Log::TYPE_ERROR); + $log->setMessage($error->getMessage()); + + $log->addTag('method', $route->getMethod()); + $log->addTag('url', $route->getPath()); + $log->addTag('verboseType', get_class($error)); + $log->addTag('code', $error->getCode()); + $log->addTag('projectId', $project->getId()); + $log->addTag('hostname', $request->getHostname()); + $log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', ''))); + + $log->addExtra('file', $error->getFile()); + $log->addExtra('line', $error->getLine()); + $log->addExtra('trace', $error->getTraceAsString()); + $log->addExtra('detailedTrace', $error->getTrace()); + $log->addExtra('roles', Authorization::$roles); + + $action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD"); + $log->setAction($action); + + $isProduction = App::getEnv('_APP_ENV', 'development') === 'production'; + $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); + + foreach ($loggerBreadcrumbs as $loggerBreadcrumb) { + $log->addBreadcrumb($loggerBreadcrumb); + } + + $responseCode = $logger->addLog($log); + Console::info('Log pushed with status code: ' . $responseCode); + } + } + + $code = $error->getCode(); + $message = $error->getMessage(); + $file = $error->getFile(); + $line = $error->getLine(); + $trace = $error->getTrace(); + + if (php_sapi_name() === 'cli') { + Console::error('[Error] Timestamp: ' . date('c', time())); + + if ($route) { + Console::error('[Error] Method: ' . $route->getMethod()); + Console::error('[Error] URL: ' . $route->getPath()); + } + + Console::error('[Error] Type: ' . get_class($error)); + Console::error('[Error] Message: ' . $message); + Console::error('[Error] File: ' . $file); + Console::error('[Error] Line: ' . $line); + } + + /** Handle Utopia Errors */ + if ($error instanceof Utopia\Exception) { + $error = new AppwriteException($message, $code, AppwriteException::GENERAL_UNKNOWN, $error); + switch ($code) { + case 400: + $error->setType(AppwriteException::GENERAL_ARGUMENT_INVALID); + break; + case 404: + $error->setType(AppwriteException::GENERAL_ROUTE_NOT_FOUND); + break; + } + } + + /** Wrap all exceptions inside Appwrite\Extend\Exception */ + if (!($error instanceof AppwriteException)) { + $error = new AppwriteException($message, $code, AppwriteException::GENERAL_UNKNOWN, $error); + } + + switch ($code) { // Don't show 500 errors! + case 400: // Error allowed publicly + case 401: // Error allowed publicly + case 402: // Error allowed publicly + case 403: // Error allowed publicly + case 404: // Error allowed publicly + case 409: // Error allowed publicly + case 412: // Error allowed publicly + case 416: // Error allowed publicly + case 429: // Error allowed publicly + case 501: // Error allowed publicly + case 503: // Error allowed publicly + break; + default: + $code = 500; // All other errors get the generic 500 server error status code + $message = 'Server Error'; + } + + //$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised + + $type = $error->getType(); + + $output = ((App::isDevelopment())) ? [ + 'message' => $message, + 'code' => $code, + 'file' => $file, + 'line' => $line, + 'trace' => $trace, + 'version' => $version, + 'type' => $type, + ] : [ + 'message' => $message, + 'code' => $code, + 'version' => $version, + 'type' => $type, + ]; + + $response + ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') + ->addHeader('Expires', '0') + ->addHeader('Pragma', 'no-cache') + ->setStatusCode($code) ; - $response->html($layout->render()); - } + $template = ($route) ? $route->getLabel('error', null) : null; - $response->dynamic( - new Document($output), - $utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR - ); -}, ['error', 'utopia', 'request', 'response', 'layout', 'project', 'logger', 'loggerBreadcrumbs']); + if ($template) { + $comp = new View($template); + + $comp + ->setParam('development', App::isDevelopment()) + ->setParam('projectName', $project->getAttribute('name')) + ->setParam('projectURL', $project->getAttribute('url')) + ->setParam('message', $error->getMessage()) + ->setParam('code', $code) + ->setParam('trace', $trace) + ; + + $layout + ->setParam('title', $project->getAttribute('name') . ' - Error') + ->setParam('description', 'No Description') + ->setParam('body', $comp) + ->setParam('version', $version) + ->setParam('litespeed', false) + ; + + $response->html($layout->render()); + } + + $response->dynamic( + new Document($output), + $utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR + ); + }); App::get('/manifest.json') ->desc('Progressive app manifest file') diff --git a/app/controllers/mock.php b/app/controllers/mock.php index f681cb2482..d3b150a55f 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -214,7 +214,7 @@ App::get('/v1/mock/tests/general/download') ->addHeader('Content-Disposition', 'attachment; filename="test.txt"') ->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("Download test passed.") + ->send("GET:/v1/mock/tests/general/download:passed") ; }); @@ -558,24 +558,29 @@ App::get('/v1/mock/tests/general/oauth2/failure') ]); }); -App::shutdown(function (App $utopia, Response $response, Request $request) { +App::shutdown() + ->groups(['mock']) + ->inject('utopia') + ->inject('response') + ->inject('request') + ->action(function (App $utopia, Response $response, Request $request) { - $result = []; - $route = $utopia->match($request); - $path = APP_STORAGE_CACHE . '/tests.json'; - $tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : []; + $result = []; + $route = $utopia->match($request); + $path = APP_STORAGE_CACHE . '/tests.json'; + $tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : []; - if (!\is_array($tests)) { - throw new Exception('Failed to read results', 500, Exception::GENERAL_MOCK); - } + if (!\is_array($tests)) { + throw new Exception('Failed to read results', 500, Exception::GENERAL_MOCK); + } - $result[$route->getMethod() . ':' . $route->getPath()] = true; + $result[$route->getMethod() . ':' . $route->getPath()] = true; - $tests = \array_merge($tests, $result); + $tests = \array_merge($tests, $result); - if (!\file_put_contents($path, \json_encode($tests), LOCK_EX)) { - throw new Exception('Failed to save results', 500, Exception::GENERAL_MOCK); - } + if (!\file_put_contents($path, \json_encode($tests), LOCK_EX)) { + throw new Exception('Failed to save results', 500, Exception::GENERAL_MOCK); + } - $response->dynamic(new Document(['result' => $route->getMethod() . ':' . $route->getPath() . ':passed']), Response::MODEL_MOCK); -}, ['utopia', 'response', 'request'], 'mock'); + $response->dynamic(new Document(['result' => $route->getMethod() . ':' . $route->getPath() . ':passed']), Response::MODEL_MOCK); + }); diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 3f5f2277d4..2d27209562 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -19,234 +19,267 @@ use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\Registry\Registry; -App::init(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $events, Audit $audits, Mail $mails, Stats $usage, Delete $deletes, EventDatabase $database, Database $dbForProject, string $mode) { +App::init() + ->groups(['api']) + ->inject('utopia') + ->inject('request') + ->inject('response') + ->inject('project') + ->inject('user') + ->inject('events') + ->inject('audits') + ->inject('mails') + ->inject('usage') + ->inject('deletes') + ->inject('database') + ->inject('dbForProject') + ->inject('mode') + ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $events, Audit $audits, Mail $mails, Stats $usage, Delete $deletes, EventDatabase $database, Database $dbForProject, string $mode) { - $route = $utopia->match($request); + $route = $utopia->match($request); - if ($project->isEmpty() && $route->getLabel('abuse-limit', 0) > 0) { // Abuse limit requires an active project scope - throw new Exception('Missing or unknown project ID', 400, Exception::PROJECT_UNKNOWN); - } + if ($project->isEmpty() && $route->getLabel('abuse-limit', 0) > 0) { // Abuse limit requires an active project scope + throw new Exception('Missing or unknown project ID', 400, Exception::PROJECT_UNKNOWN); + } - /* - * Abuse Check - */ - $abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}'); - $timeLimitArray = []; + /* + * Abuse Check + */ + $abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}'); + $timeLimitArray = []; - $abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel; + $abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel; - foreach ($abuseKeyLabel as $abuseKey) { - $timeLimit = new TimeLimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), $dbForProject); - $timeLimit - ->setParam('{userId}', $user->getId()) - ->setParam('{userAgent}', $request->getUserAgent('')) - ->setParam('{ip}', $request->getIP()) - ->setParam('{url}', $request->getHostname() . $route->getPath()); - $timeLimitArray[] = $timeLimit; - } + foreach ($abuseKeyLabel as $abuseKey) { + $timeLimit = new TimeLimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), $dbForProject); + $timeLimit + ->setParam('{userId}', $user->getId()) + ->setParam('{userAgent}', $request->getUserAgent('')) + ->setParam('{ip}', $request->getIP()) + ->setParam('{url}', $request->getHostname() . $route->getPath()); + $timeLimitArray[] = $timeLimit; + } - $closestLimit = null; + $closestLimit = null; - $roles = Authorization::getRoles(); - $isPrivilegedUser = Auth::isPrivilegedUser($roles); - $isAppUser = Auth::isAppUser($roles); + $roles = Authorization::getRoles(); + $isPrivilegedUser = Auth::isPrivilegedUser($roles); + $isAppUser = Auth::isAppUser($roles); - foreach ($timeLimitArray as $timeLimit) { - foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys - if (!empty($value)) { - $timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value); + foreach ($timeLimitArray as $timeLimit) { + foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys + if (!empty($value)) { + $timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value); + } + } + + $abuse = new Abuse($timeLimit); + + if ($timeLimit->limit() && ($timeLimit->remaining() < $closestLimit || is_null($closestLimit))) { + $closestLimit = $timeLimit->remaining(); + $response + ->addHeader('X-RateLimit-Limit', $timeLimit->limit()) + ->addHeader('X-RateLimit-Remaining', $timeLimit->remaining()) + ->addHeader('X-RateLimit-Reset', $timeLimit->time() + $route->getLabel('abuse-time', 3600)) + ; + } + + if ( + (App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled' // Route is rate-limited + && $abuse->check()) // Abuse is not disabled + && (!$isAppUser && !$isPrivilegedUser) + ) { // User is not an admin or API key + throw new Exception('Too many requests', 429, Exception::GENERAL_RATE_LIMIT_EXCEEDED); } } - $abuse = new Abuse($timeLimit); - - if ($timeLimit->limit() && ($timeLimit->remaining() < $closestLimit || is_null($closestLimit))) { - $closestLimit = $timeLimit->remaining(); - $response - ->addHeader('X-RateLimit-Limit', $timeLimit->limit()) - ->addHeader('X-RateLimit-Remaining', $timeLimit->remaining()) - ->addHeader('X-RateLimit-Reset', $timeLimit->time() + $route->getLabel('abuse-time', 3600)) - ; - } - - if ( - (App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled' // Route is rate-limited - && $abuse->check()) // Abuse is not disabled - && (!$isAppUser && !$isPrivilegedUser) - ) { // User is not an admin or API key - throw new Exception('Too many requests', 429, Exception::GENERAL_RATE_LIMIT_EXCEEDED); - } - } - - /* - * Background Jobs - */ - $events - ->setEvent($route->getLabel('event', '')) - ->setProject($project) - ->setUser($user) - ; - - $mails - ->setProject($project) - ->setUser($user) - ; - - $audits - ->setMode($mode) - ->setUserAgent($request->getUserAgent('')) - ->setIP($request->getIP()) - ->setEvent($route->getLabel('event', '')) - ->setProject($project) - ->setUser($user) - ; - - $usage - ->setParam('projectId', $project->getId()) - ->setParam('httpRequest', 1) - ->setParam('httpUrl', $request->getHostname() . $request->getURI()) - ->setParam('httpMethod', $request->getMethod()) - ->setParam('httpPath', $route->getPath()) - ->setParam('networkRequestSize', 0) - ->setParam('networkResponseSize', 0) - ->setParam('storage', 0) - ; - - $deletes->setProject($project); - $database->setProject($project); -}, ['utopia', 'request', 'response', 'project', 'user', 'events', 'audits', 'mails', 'usage', 'deletes', 'database', 'dbForProject', 'mode'], 'api'); - -App::init(function (App $utopia, Request $request, Document $project) { - - $route = $utopia->match($request); - - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - $isAppUser = Auth::isAppUser(Authorization::getRoles()); - - if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs - return; - } - - $auths = $project->getAttribute('auths', []); - switch ($route->getLabel('auth.type', '')) { - case 'emailPassword': - if (($auths['emailPassword'] ?? true) === false) { - throw new Exception('Email / Password authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED); - } - break; - - case 'magic-url': - if ($project->getAttribute('usersAuthMagicURL', true) === false) { - throw new Exception('Magic URL authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED); - } - break; - - case 'anonymous': - if (($auths['anonymous'] ?? true) === false) { - throw new Exception('Anonymous authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED); - } - break; - - case 'invites': - if (($auths['invites'] ?? true) === false) { - throw new Exception('Invites authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED); - } - break; - - case 'jwt': - if (($auths['JWT'] ?? true) === false) { - throw new Exception('JWT authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED); - } - break; - - default: - throw new Exception('Unsupported authentication route', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED); - break; - } -}, ['utopia', 'request', 'project'], 'auth'); - -App::shutdown(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, string $mode, Database $dbForProject) { - - if (!empty($events->getEvent())) { - if (empty($events->getPayload())) { - $events->setPayload($response->getPayload()); - } - /** - * Trigger functions. - */ + /* + * Background Jobs + */ $events - ->setClass(Event::FUNCTIONS_CLASS_NAME) - ->setQueue(Event::FUNCTIONS_QUEUE_NAME) - ->trigger(); + ->setEvent($route->getLabel('event', '')) + ->setProject($project) + ->setUser($user) + ; - /** - * Trigger webhooks. - */ - $events - ->setClass(Event::WEBHOOK_CLASS_NAME) - ->setQueue(Event::WEBHOOK_QUEUE_NAME) - ->trigger(); + $mails + ->setProject($project) + ->setUser($user) + ; - /** - * Trigger realtime. - */ - if ($project->getId() !== 'console') { - $allEvents = Event::generateEvents($events->getEvent(), $events->getParams()); - $payload = new Document($events->getPayload()); + $audits + ->setMode($mode) + ->setUserAgent($request->getUserAgent('')) + ->setIP($request->getIP()) + ->setEvent($route->getLabel('event', '')) + ->setProject($project) + ->setUser($user) + ; - $db = $events->getContext('database'); - $collection = $events->getContext('collection'); - $bucket = $events->getContext('bucket'); - - $target = Realtime::fromPayload( - // Pass first, most verbose event pattern - event: $allEvents[0], - payload: $payload, - project: $project, - database: $db, - collection: $collection, - bucket: $bucket, - ); - - Realtime::send( - projectId: $target['projectId'] ?? $project->getId(), - payload: $events->getPayload(), - events: $allEvents, - channels: $target['channels'], - roles: $target['roles'], - options: [ - 'permissionsChanged' => $target['permissionsChanged'], - 'userId' => $events->getParam('userId') - ] - ); - } - } - - if (!empty($audits->getResource())) { - foreach ($events->getParams() as $key => $value) { - $audits->setParam($key, $value); - } - $audits->trigger(); - } - - if (!empty($deletes->getType())) { - $deletes->trigger(); - } - - if (!empty($database->getType())) { - $database->trigger(); - } - - $route = $utopia->match($request); - if ( - App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled' - && $project->getId() - && $mode !== APP_MODE_ADMIN // TODO: add check to make sure user is admin - && !empty($route->getLabel('sdk.namespace', null)) - ) { // Don't calculate console usage on admin mode $usage - ->setParam('networkRequestSize', $request->getSize() + $usage->getParam('storage')) - ->setParam('networkResponseSize', $response->getSize()) - ->submit(); - } -}, ['utopia', 'request', 'response', 'project', 'events', 'audits', 'usage', 'deletes', 'database', 'mode', 'dbForProject'], 'api'); + ->setParam('projectId', $project->getId()) + ->setParam('httpRequest', 1) + ->setParam('httpUrl', $request->getHostname() . $request->getURI()) + ->setParam('httpMethod', $request->getMethod()) + ->setParam('httpPath', $route->getPath()) + ->setParam('networkRequestSize', 0) + ->setParam('networkResponseSize', 0) + ->setParam('storage', 0) + ; + + $deletes->setProject($project); + $database->setProject($project); + }); + +App::init() + ->groups(['auth']) + ->inject('utopia') + ->inject('request') + ->inject('project') + ->action(function (App $utopia, Request $request, Document $project) { + + $route = $utopia->match($request); + + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAppUser = Auth::isAppUser(Authorization::getRoles()); + + if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs + return; + } + + $auths = $project->getAttribute('auths', []); + switch ($route->getLabel('auth.type', '')) { + case 'emailPassword': + if (($auths['emailPassword'] ?? true) === false) { + throw new Exception('Email / Password authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED); + } + break; + + case 'magic-url': + if ($project->getAttribute('usersAuthMagicURL', true) === false) { + throw new Exception('Magic URL authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED); + } + break; + + case 'anonymous': + if (($auths['anonymous'] ?? true) === false) { + throw new Exception('Anonymous authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED); + } + break; + + case 'invites': + if (($auths['invites'] ?? true) === false) { + throw new Exception('Invites authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED); + } + break; + + case 'jwt': + if (($auths['JWT'] ?? true) === false) { + throw new Exception('JWT authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED); + } + break; + + default: + throw new Exception('Unsupported authentication route', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED); + break; + } + }); + +App::shutdown() + ->groups(['api']) + ->inject('utopia') + ->inject('request') + ->inject('response') + ->inject('project') + ->inject('events') + ->inject('audits') + ->inject('usage') + ->inject('deletes') + ->inject('database') + ->inject('mode') + ->inject('dbForProject') + ->action(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, string $mode, Database $dbForProject) { + + if (!empty($events->getEvent())) { + if (empty($events->getPayload())) { + $events->setPayload($response->getPayload()); + } + /** + * Trigger functions. + */ + $events + ->setClass(Event::FUNCTIONS_CLASS_NAME) + ->setQueue(Event::FUNCTIONS_QUEUE_NAME) + ->trigger(); + + /** + * Trigger webhooks. + */ + $events + ->setClass(Event::WEBHOOK_CLASS_NAME) + ->setQueue(Event::WEBHOOK_QUEUE_NAME) + ->trigger(); + + /** + * Trigger realtime. + */ + if ($project->getId() !== 'console') { + $allEvents = Event::generateEvents($events->getEvent(), $events->getParams()); + $payload = new Document($events->getPayload()); + + $db = $events->getContext('database'); + $collection = $events->getContext('collection'); + $bucket = $events->getContext('bucket'); + + $target = Realtime::fromPayload( + // Pass first, most verbose event pattern + event: $allEvents[0], + payload: $payload, + project: $project, + database: $db, + collection: $collection, + bucket: $bucket, + ); + + Realtime::send( + projectId: $target['projectId'] ?? $project->getId(), + payload: $events->getPayload(), + events: $allEvents, + channels: $target['channels'], + roles: $target['roles'], + options: [ + 'permissionsChanged' => $target['permissionsChanged'], + 'userId' => $events->getParam('userId') + ] + ); + } + } + + if (!empty($audits->getResource())) { + foreach ($events->getParams() as $key => $value) { + $audits->setParam($key, $value); + } + $audits->trigger(); + } + + if (!empty($deletes->getType())) { + $deletes->trigger(); + } + + if (!empty($database->getType())) { + $database->trigger(); + } + + $route = $utopia->match($request); + if ( + App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled' + && $project->getId() + && $mode !== APP_MODE_ADMIN // TODO: add check to make sure user is admin + && !empty($route->getLabel('sdk.namespace', null)) + ) { // Don't calculate console usage on admin mode + $usage + ->setParam('networkRequestSize', $request->getSize() + $usage->getParam('storage')) + ->setParam('networkResponseSize', $response->getSize()) + ->submit(); + } + }); diff --git a/app/controllers/shared/web.php b/app/controllers/shared/web.php index a3f2df012c..5e1e0bd370 100644 --- a/app/controllers/shared/web.php +++ b/app/controllers/shared/web.php @@ -6,54 +6,59 @@ use Appwrite\Utopia\Response; use Appwrite\Utopia\Request; use Appwrite\Utopia\View; -App::init(function (App $utopia, Request $request, Response $response, View $layout) { +App::init() + ->groups(['web']) + ->inject('utopia') + ->inject('request') + ->inject('response') + ->inject('layout') + ->action(function (App $utopia, Request $request, Response $response, View $layout) { + /* AJAX check */ + if (!empty($request->getQuery('version', ''))) { + $layout->setPath(__DIR__ . '/../../views/layouts/empty.phtml'); + } - /* AJAX check */ - if (!empty($request->getQuery('version', ''))) { - $layout->setPath(__DIR__ . '/../../views/layouts/empty.phtml'); - } + $port = $request->getPort(); + $protocol = $request->getProtocol(); + $domain = $request->getHostname(); - $port = $request->getPort(); - $protocol = $request->getProtocol(); - $domain = $request->getHostname(); + $layout + ->setParam('title', APP_NAME) + ->setParam('protocol', $protocol) + ->setParam('domain', $domain) + ->setParam('endpoint', $protocol . '://' . $domain . ($port != 80 && $port != 443 ? ':' . $port : '')) + ->setParam('home', App::getEnv('_APP_HOME')) + ->setParam('setup', App::getEnv('_APP_SETUP')) + ->setParam('class', 'unknown') + ->setParam('icon', '/images/favicon.png') + ->setParam('roles', [ + ['type' => 'owner', 'label' => 'Owner'], + ['type' => 'developer', 'label' => 'Developer'], + ['type' => 'admin', 'label' => 'Admin'], + ]) + ->setParam('runtimes', Config::getParam('runtimes')) + ->setParam('mode', App::getMode()) + ; - $layout - ->setParam('title', APP_NAME) - ->setParam('protocol', $protocol) - ->setParam('domain', $domain) - ->setParam('endpoint', $protocol . '://' . $domain . ($port != 80 && $port != 443 ? ':' . $port : '')) - ->setParam('home', App::getEnv('_APP_HOME')) - ->setParam('setup', App::getEnv('_APP_SETUP')) - ->setParam('class', 'unknown') - ->setParam('icon', '/images/favicon.png') - ->setParam('roles', [ - ['type' => 'owner', 'label' => 'Owner'], - ['type' => 'developer', 'label' => 'Developer'], - ['type' => 'admin', 'label' => 'Admin'], - ]) - ->setParam('runtimes', Config::getParam('runtimes')) - ->setParam('mode', App::getMode()) - ; + $time = (60 * 60 * 24 * 45); // 45 days cache - $time = (60 * 60 * 24 * 45); // 45 days cache + $response + ->addHeader('Cache-Control', 'public, max-age=' . $time) + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time) . ' GMT') // 45 days cache + ->addHeader('X-Frame-Options', 'SAMEORIGIN') // Avoid console and homepage from showing in iframes + ->addHeader('X-XSS-Protection', '1; mode=block; report=/v1/xss?url=' . \urlencode($request->getURI())) + ->addHeader('X-UA-Compatible', 'IE=Edge') // Deny IE browsers from going into quirks mode + ; - $response - ->addHeader('Cache-Control', 'public, max-age=' . $time) - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time) . ' GMT') // 45 days cache - ->addHeader('X-Frame-Options', 'SAMEORIGIN') // Avoid console and homepage from showing in iframes - ->addHeader('X-XSS-Protection', '1; mode=block; report=/v1/xss?url=' . \urlencode($request->getURI())) - ->addHeader('X-UA-Compatible', 'IE=Edge') // Deny IE browsers from going into quirks mode - ; + $route = $utopia->match($request); - $route = $utopia->match($request); + $route->label('error', __DIR__ . '/../../views/general/error.phtml'); - $route->label('error', __DIR__ . '/../../views/general/error.phtml'); + $scope = $route->getLabel('scope', ''); - $scope = $route->getLabel('scope', ''); - - $layout - ->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN')) - ->setParam('isDev', App::isDevelopment()) - ->setParam('class', $scope) - ; -}, ['utopia', 'request', 'response', 'layout'], 'web'); + $layout + ->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN')) + ->setParam('isDev', App::isDevelopment()) + ->setParam('class', $scope) + ; + }); diff --git a/app/controllers/web/console.php b/app/controllers/web/console.php index 410be4d307..e627ff986e 100644 --- a/app/controllers/web/console.php +++ b/app/controllers/web/console.php @@ -9,31 +9,36 @@ use Utopia\Domains\Domain; use Utopia\Database\Validator\UID; use Utopia\Storage\Storage; -App::init(function (View $layout) { +App::init() + ->groups(['console']) + ->inject('layout') + ->action(function (View $layout) { + $layout + ->setParam('description', 'Appwrite Console allows you to easily manage, monitor, and control your entire backend API and tools.') + ->setParam('analytics', 'UA-26264668-5') + ; + }); - $layout - ->setParam('description', 'Appwrite Console allows you to easily manage, monitor, and control your entire backend API and tools.') - ->setParam('analytics', 'UA-26264668-5') - ; -}, ['layout'], 'console'); +App::shutdown() + ->groups(['console']) + ->inject('response') + ->inject('layout') + ->action(function (Response $response, View $layout) { + $header = new View(__DIR__ . '/../../views/console/comps/header.phtml'); + $footer = new View(__DIR__ . '/../../views/console/comps/footer.phtml'); -App::shutdown(function (Response $response, View $layout) { + $footer + ->setParam('home', App::getEnv('_APP_HOME', '')) + ->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN')) + ; - $header = new View(__DIR__ . '/../../views/console/comps/header.phtml'); - $footer = new View(__DIR__ . '/../../views/console/comps/footer.phtml'); + $layout + ->setParam('header', [$header]) + ->setParam('footer', [$footer]) + ; - $footer - ->setParam('home', App::getEnv('_APP_HOME', '')) - ->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN')) - ; - - $layout - ->setParam('header', [$header]) - ->setParam('footer', [$footer]) - ; - - $response->html($layout->render()); -}, ['response', 'layout'], 'console'); + $response->html($layout->render()); + }); App::get('/error/:code') ->groups(['web', 'console']) diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index b5bf366066..90828a1d54 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -7,29 +7,34 @@ use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; -App::init(function (View $layout) { +App::init() + ->groups(['home']) + ->inject('layout') + ->action(function (View $layout) { + $header = new View(__DIR__ . '/../../views/home/comps/header.phtml'); + $footer = new View(__DIR__ . '/../../views/home/comps/footer.phtml'); - $header = new View(__DIR__ . '/../../views/home/comps/header.phtml'); - $footer = new View(__DIR__ . '/../../views/home/comps/footer.phtml'); + $footer + ->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN')) + ; - $footer - ->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN')) - ; + $layout + ->setParam('title', APP_NAME) + ->setParam('description', '') + ->setParam('class', 'home') + ->setParam('platforms', Config::getParam('platforms')) + ->setParam('header', [$header]) + ->setParam('footer', [$footer]) + ; + }); - $layout - ->setParam('title', APP_NAME) - ->setParam('description', '') - ->setParam('class', 'home') - ->setParam('platforms', Config::getParam('platforms')) - ->setParam('header', [$header]) - ->setParam('footer', [$footer]) - ; -}, ['layout'], 'home'); - -App::shutdown(function (Response $response, View $layout) { - - $response->html($layout->render()); -}, ['response', 'layout'], 'home'); +App::shutdown() + ->groups(['home']) + ->inject('response') + ->inject('layout') + ->action(function (Response $response, View $layout) { + $response->html($layout->render()); + }); App::get('/') ->groups(['web', 'home']) diff --git a/app/executor.php b/app/executor.php index 7bf42c98db..22d8552635 100644 --- a/app/executor.php +++ b/app/executor.php @@ -581,57 +581,64 @@ App::setResource('orchestrationPool', fn() => $orchestrationPool); App::setResource('activeRuntimes', fn() => $activeRuntimes); /** Set callbacks */ -App::error(function ($utopia, $error, $request, $response) { - $route = $utopia->match($request); - logError($error, "httpError", $route); +App::error() + ->inject('utopia') + ->inject('error') + ->inject('request') + ->inject('response') + ->action(function (App $utopia, throwable $error, Request $request, Response $response) { + $route = $utopia->match($request); + logError($error, "httpError", $route); - switch ($error->getCode()) { - case 400: // Error allowed publicly - case 401: // Error allowed publicly - case 402: // Error allowed publicly - case 403: // Error allowed publicly - case 404: // Error allowed publicly - case 406: // Error allowed publicly - case 409: // Error allowed publicly - case 412: // Error allowed publicly - case 425: // Error allowed publicly - case 429: // Error allowed publicly - case 501: // Error allowed publicly - case 503: // Error allowed publicly - $code = $error->getCode(); - break; - default: - $code = 500; // All other errors get the generic 500 server error status code - } + switch ($error->getCode()) { + case 400: // Error allowed publicly + case 401: // Error allowed publicly + case 402: // Error allowed publicly + case 403: // Error allowed publicly + case 404: // Error allowed publicly + case 406: // Error allowed publicly + case 409: // Error allowed publicly + case 412: // Error allowed publicly + case 425: // Error allowed publicly + case 429: // Error allowed publicly + case 501: // Error allowed publicly + case 503: // Error allowed publicly + $code = $error->getCode(); + break; + default: + $code = 500; // All other errors get the generic 500 server error status code + } - $output = [ - 'message' => $error->getMessage(), - 'code' => $error->getCode(), - 'file' => $error->getFile(), - 'line' => $error->getLine(), - 'trace' => $error->getTrace(), - 'version' => App::getEnv('_APP_VERSION', 'UNKNOWN') - ]; + $output = [ + 'message' => $error->getMessage(), + 'code' => $error->getCode(), + 'file' => $error->getFile(), + 'line' => $error->getLine(), + 'trace' => $error->getTrace(), + 'version' => App::getEnv('_APP_VERSION', 'UNKNOWN') + ]; - $response - ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') - ->addHeader('Expires', '0') - ->addHeader('Pragma', 'no-cache') - ->setStatusCode($code); + $response + ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') + ->addHeader('Expires', '0') + ->addHeader('Pragma', 'no-cache') + ->setStatusCode($code); - $response->json($output); -}, ['utopia', 'error', 'request', 'response']); + $response->json($output); + }); -App::init(function ($request, $response) { - $secretKey = $request->getHeader('x-appwrite-executor-key', ''); - if (empty($secretKey)) { - throw new Exception('Missing executor key', 401); - } +App::init() + ->inject('request') + ->action(function (Request $request) { + $secretKey = $request->getHeader('x-appwrite-executor-key', ''); + if (empty($secretKey)) { + throw new Exception('Missing executor key', 401); + } - if ($secretKey !== App::getEnv('_APP_EXECUTOR_SECRET', '')) { - throw new Exception('Missing executor key', 401); - } -}, ['request', 'response']); + if ($secretKey !== App::getEnv('_APP_EXECUTOR_SECRET', '')) { + throw new Exception('Missing executor key', 401); + } + }); $http->on('start', function ($http) { diff --git a/app/views/console/users/oauth/authentik.phtml b/app/views/console/users/oauth/authentik.phtml new file mode 100644 index 0000000000..ba1119879c --- /dev/null +++ b/app/views/console/users/oauth/authentik.phtml @@ -0,0 +1,12 @@ +getParam('provider', ''); +?> + + + + + + + + + \ No newline at end of file diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 45628f5bcc..3da3fe4afe 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -595,7 +595,7 @@ services: - _APP_REDIS_PASS mariadb: - image: mariadb:10.7 # fix issues when upgrading using: mysql_upgrade -u root -p + image: mariadb:10.8.3 # fix issues when upgrading using: mysql_upgrade -u root -p container_name: appwrite-mariadb <<: *x-logging restart: unless-stopped @@ -611,7 +611,7 @@ services: command: 'mysqld --innodb-flush-method=fsync' redis: - image: redis:6.2-alpine + image: redis:7.0.4-alpine container_name: appwrite-redis <<: *x-logging restart: unless-stopped diff --git a/app/workers/deletes.php b/app/workers/deletes.php index ed818f8356..a4616f54c1 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -20,11 +20,6 @@ Console::success(APP_NAME . ' deletes worker v1 has started' . "\n"); class DeletesV1 extends Worker { - /** - * @var Database - */ - protected $consoleDB = null; - public function getName(): string { return "deletes"; diff --git a/composer.json b/composer.json index 4f26341669..67f473b452 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "autoload-dev": { "psr-4": { "Tests\\E2E\\": "tests/e2e", + "Tests\\Unit\\": "tests/unit", "Appwrite\\Tests\\": "tests/extensions" } }, @@ -42,7 +43,7 @@ "ext-sockets": "*", "appwrite/php-clamav": "1.1.*", "appwrite/php-runtimes": "0.10.*", - "utopia-php/framework": "0.19.*", + "utopia-php/framework": "0.20.*", "utopia-php/logger": "0.3.*", "utopia-php/abuse": "0.7.*", "utopia-php/analytics": "0.2.*", diff --git a/composer.lock b/composer.lock index 2e32ade987..903b7476a4 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": "7f417dc4a1f0d5ed97b3d6a88aa72adf", + "content-hash": "993486075710ab0cdbba6c33f0b09218", "packages": [ { "name": "adhocore/jwt", @@ -1894,16 +1894,16 @@ }, { "name": "utopia-php/cache", - "version": "0.6.0", + "version": "0.6.1", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "8ea1353a4bbab617e23c865a7c97b60d8074aee3" + "reference": "9889235a6d3da6cbb1f435201529da4d27c30e79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/8ea1353a4bbab617e23c865a7c97b60d8074aee3", - "reference": "8ea1353a4bbab617e23c865a7c97b60d8074aee3", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/9889235a6d3da6cbb1f435201529da4d27c30e79", + "reference": "9889235a6d3da6cbb1f435201529da4d27c30e79", "shasum": "" }, "require": { @@ -1941,9 +1941,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/0.6.0" + "source": "https://github.com/utopia-php/cache/tree/0.6.1" }, - "time": "2022-04-04T12:30:05+00:00" + "time": "2022-08-10T08:12:46+00:00" }, { "name": "utopia-php/cli", @@ -2169,16 +2169,16 @@ }, { "name": "utopia-php/framework", - "version": "0.19.21", + "version": "0.20.0", "source": { "type": "git", "url": "https://github.com/utopia-php/framework.git", - "reference": "3b7bd8e4acf84fd7d560ced8e0142221d302575d" + "reference": "beb5e861c7d0a6256a1272e6b9d70b060ca8629a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/framework/zipball/3b7bd8e4acf84fd7d560ced8e0142221d302575d", - "reference": "3b7bd8e4acf84fd7d560ced8e0142221d302575d", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/beb5e861c7d0a6256a1272e6b9d70b060ca8629a", + "reference": "beb5e861c7d0a6256a1272e6b9d70b060ca8629a", "shasum": "" }, "require": { @@ -2212,9 +2212,9 @@ ], "support": { "issues": "https://github.com/utopia-php/framework/issues", - "source": "https://github.com/utopia-php/framework/tree/0.19.21" + "source": "https://github.com/utopia-php/framework/tree/0.20.0" }, - "time": "2022-05-12T18:42:28+00:00" + "time": "2022-07-30T09:55:28+00:00" }, { "name": "utopia-php/image", @@ -5271,16 +5271,16 @@ }, { "name": "twig/twig", - "version": "v3.4.1", + "version": "v3.4.2", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "e939eae92386b69b49cfa4599dd9bead6bf4a342" + "reference": "e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/e939eae92386b69b49cfa4599dd9bead6bf4a342", - "reference": "e939eae92386b69b49cfa4599dd9bead6bf4a342", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077", + "reference": "e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077", "shasum": "" }, "require": { @@ -5331,7 +5331,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.4.1" + "source": "https://github.com/twigphp/Twig/tree/v3.4.2" }, "funding": [ { @@ -5343,7 +5343,7 @@ "type": "tidelift" } ], - "time": "2022-05-17T05:48:52+00:00" + "time": "2022-08-12T06:47:24+00:00" } ], "aliases": [], diff --git a/docker-compose.yml b/docker-compose.yml index 20d2c5c2f9..84ec637177 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -638,7 +638,7 @@ services: - _APP_REDIS_PASS mariadb: - image: mariadb:10.7 # fix issues when upgrading using: mysql_upgrade -u root -p + image: mariadb:10.8.3 # fix issues when upgrading using: mysql_upgrade -u root -p container_name: appwrite-mariadb <<: *x-logging networks: @@ -668,7 +668,7 @@ services: # - SMARTHOST_PORT=587 redis: - image: redis:6.2-alpine + image: redis:7.0.4-alpine <<: *x-logging container_name: appwrite-redis command: > diff --git a/docs/references/avatars/get-flag.md b/docs/references/avatars/get-flag.md index 04a8bbca64..4afdd8a133 100644 --- a/docs/references/avatars/get-flag.md +++ b/docs/references/avatars/get-flag.md @@ -1,3 +1,3 @@ -You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. +You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](http://en.wikipedia.org/wiki/ISO_3166-1) standard. When one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px. diff --git a/docs/references/databases/create-collection.md b/docs/references/databases/create-collection.md index 6f6da13d53..259ad987ee 100644 --- a/docs/references/databases/create-collection.md +++ b/docs/references/databases/create-collection.md @@ -1 +1 @@ -Create a new Collection. Before using this route, you should create a new database resource using either a [server integration](/docs/server/database#databaseCreateCollection) API or directly from your database console. \ No newline at end of file +Create a new Collection. Before using this route, you should create a new database resource using either a [server integration](/docs/server/databases#databasesCreateCollection) API or directly from your database console. \ No newline at end of file diff --git a/docs/references/databases/create-document.md b/docs/references/databases/create-document.md index 25d0b403cd..10898b7d4d 100644 --- a/docs/references/databases/create-document.md +++ b/docs/references/databases/create-document.md @@ -1 +1 @@ -Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](/docs/server/database#databaseCreateCollection) API or directly from your database console. \ No newline at end of file +Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](/docs/server/databases#databasesCreateCollection) API or directly from your database console. \ No newline at end of file diff --git a/docs/references/storage/create-file.md b/docs/references/storage/create-file.md index b577d1a05d..77e5cf2225 100644 --- a/docs/references/storage/create-file.md +++ b/docs/references/storage/create-file.md @@ -1,4 +1,4 @@ -Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](/docs/server/database#storageCreateBucket) API or directly from your Appwrite console. +Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](/docs/server/storage#storageCreateBucket) API or directly from your Appwrite console. Larger files should be uploaded using multiple requests with the [content-range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes. diff --git a/docs/services/databases.md b/docs/services/databases.md index e5359c18ff..00076dc8da 100644 --- a/docs/services/databases.md +++ b/docs/services/databases.md @@ -2,6 +2,6 @@ The Databases service allows you to create structured collections of documents, All data returned by the Databases service are represented as structured JSON documents. -The Databases service can contain multiple databases, each database can contain multiple collections. A collection is a group of similarly structured documents. The accepted structure of documents is defined by [collection attributes](/docs/database#attributes). The collection attributes help you ensure all your user-submitted data is validated and stored according to the collection structure. +The Databases service can contain multiple databases, each database can contain multiple collections. A collection is a group of similarly structured documents. The accepted structure of documents is defined by [collection attributes](/docs/databases#attributes). The collection attributes help you ensure all your user-submitted data is validated and stored according to the collection structure. Using Appwrite permissions architecture, you can assign read or write access to each collection or document in your project for either a specific user, team, user role, or even grant it with public access (`role:all`). You can learn more about [how Appwrite handles permissions and access control](/docs/permissions). \ No newline at end of file diff --git a/docs/services/locale.md b/docs/services/locale.md index e757d13a4d..d9a1c38d2d 100644 --- a/docs/services/locale.md +++ b/docs/services/locale.md @@ -1,3 +1,3 @@ -The Locale service allows you to customize your app based on your users' location. Using this service, you can get your users' location, IP address, list of countries and continents names, phone codes, currencies, and more. +The Locale service allows you to customize your app based on your users' location. Using this service, you can get your users' location, IP address, list of countries and continents names, phone codes, currencies, and more. Country codes returned follow the [ISO 3166-1](http://en.wikipedia.org/wiki/ISO_3166-1) standard. The user service supports multiple locales. This feature allows you to fetch countries and continents information in your app language. To switch locales, all you need to do is pass the 'X-Appwrite-Locale' header or set the 'setLocale' method using any of our available SDKs. [View here the list of available locales](https://github.com/appwrite/appwrite/blob/master/app/config/locale/codes.php). diff --git a/phpunit.xml b/phpunit.xml index cea67d60d9..5379852718 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -13,7 +13,7 @@ - ./tests/unit/ + ./tests/unit ./tests/e2e/Client.php diff --git a/public/dist/scripts/app-all.js b/public/dist/scripts/app-all.js index 3163702c6b..e90196253c 100644 --- a/public/dist/scripts/app-all.js +++ b/public/dist/scripts/app-all.js @@ -4142,7 +4142,7 @@ list["filters-"+filter.key]=params[key][i];}}}} return list;};let apply=function(params){let cached=container.get(name);cached=cached?cached.params:[];params=Object.assign(cached,params);container.set(name,{name:name,params:params,query:serialize(params),forward:parseInt(params.offset)+parseInt(params.limit),backward:parseInt(params.offset)-parseInt(params.limit),keys:flatten(params)},true,name);document.dispatchEvent(new CustomEvent(name+"-changed",{bubbles:false,cancelable:true}));};switch(element.tagName){case"INPUT":break;case"TEXTAREA":break;case"BUTTON":element.addEventListener("click",function(){apply(JSON.parse(expression.parse(element.dataset["params"]||"{}")));});break;case"FORM":element.addEventListener("input",function(){apply(form.toJson(element));});element.addEventListener("change",function(){apply(form.toJson(element));});element.addEventListener("reset",function(){setTimeout(function(){apply(form.toJson(element));},0);});events=events.trim().split(",");for(let y=0;y=distance)&&(distance>=0)){if(minLink){minLink.classList.remove('selected');} -console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-oauth-custom",controller:function(element){let providers={"Microsoft":{"clientSecret":"oauth2MicrosoftClientSecret","tenantID":"oauth2MicrosoftTenantId"},"Apple":{"keyID":"oauth2AppleKeyId","teamID":"oauth2AppleTeamId","p8":"oauth2AppleP8"},"Okta":{"clientSecret":"oauth2OktaClientSecret","oktaDomain":"oauth2OktaDomain","authorizationServerId":"oauth2OktaAuthorizationServerId"},"Auth0":{"clientSecret":"oauth2Auth0ClientSecret","auth0Domain":"oauth2Auth0Domain"},"Gitlab":{"endpoint":"oauth2GitlabEndpoint","clientSecret":"oauth2GitlabClientSecret",},} +console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-oauth-custom",controller:function(element){let providers={"Microsoft":{"clientSecret":"oauth2MicrosoftClientSecret","tenantID":"oauth2MicrosoftTenantId"},"Apple":{"keyID":"oauth2AppleKeyId","teamID":"oauth2AppleTeamId","p8":"oauth2AppleP8"},"Okta":{"clientSecret":"oauth2OktaClientSecret","oktaDomain":"oauth2OktaDomain","authorizationServerId":"oauth2OktaAuthorizationServerId"},"Auth0":{"clientSecret":"oauth2Auth0ClientSecret","auth0Domain":"oauth2Auth0Domain"},"Authentik":{"clientSecret":"oauth2AuthentikClientSecret","authentikDomain":"oauth2AuthentikDomain"},"Gitlab":{"endpoint":"oauth2GitlabEndpoint","clientSecret":"oauth2GitlabClientSecret",},} let provider=element.getAttribute("data-forms-oauth-custom");if(!provider||!providers.hasOwnProperty(provider)){console.error("Provider for custom form not set or unknown")} let config=providers[provider];element.addEventListener('change',sync);let elements={};for(const key in config){if(Object.hasOwnProperty.call(config,key)){elements[key]=document.getElementById(config[key]);elements[key].addEventListener('change',update);}} function update(){let json={};for(const key in elements){if(Object.hasOwnProperty.call(elements,key)){json[key]=elements[key].value}} diff --git a/public/dist/scripts/app.js b/public/dist/scripts/app.js index fadc1caf92..742e4e29f6 100644 --- a/public/dist/scripts/app.js +++ b/public/dist/scripts/app.js @@ -798,7 +798,7 @@ list["filters-"+filter.key]=params[key][i];}}}} return list;};let apply=function(params){let cached=container.get(name);cached=cached?cached.params:[];params=Object.assign(cached,params);container.set(name,{name:name,params:params,query:serialize(params),forward:parseInt(params.offset)+parseInt(params.limit),backward:parseInt(params.offset)-parseInt(params.limit),keys:flatten(params)},true,name);document.dispatchEvent(new CustomEvent(name+"-changed",{bubbles:false,cancelable:true}));};switch(element.tagName){case"INPUT":break;case"TEXTAREA":break;case"BUTTON":element.addEventListener("click",function(){apply(JSON.parse(expression.parse(element.dataset["params"]||"{}")));});break;case"FORM":element.addEventListener("input",function(){apply(form.toJson(element));});element.addEventListener("change",function(){apply(form.toJson(element));});element.addEventListener("reset",function(){setTimeout(function(){apply(form.toJson(element));},0);});events=events.trim().split(",");for(let y=0;y=distance)&&(distance>=0)){if(minLink){minLink.classList.remove('selected');} -console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-oauth-custom",controller:function(element){let providers={"Microsoft":{"clientSecret":"oauth2MicrosoftClientSecret","tenantID":"oauth2MicrosoftTenantId"},"Apple":{"keyID":"oauth2AppleKeyId","teamID":"oauth2AppleTeamId","p8":"oauth2AppleP8"},"Okta":{"clientSecret":"oauth2OktaClientSecret","oktaDomain":"oauth2OktaDomain","authorizationServerId":"oauth2OktaAuthorizationServerId"},"Auth0":{"clientSecret":"oauth2Auth0ClientSecret","auth0Domain":"oauth2Auth0Domain"},"Gitlab":{"endpoint":"oauth2GitlabEndpoint","clientSecret":"oauth2GitlabClientSecret",},} +console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-oauth-custom",controller:function(element){let providers={"Microsoft":{"clientSecret":"oauth2MicrosoftClientSecret","tenantID":"oauth2MicrosoftTenantId"},"Apple":{"keyID":"oauth2AppleKeyId","teamID":"oauth2AppleTeamId","p8":"oauth2AppleP8"},"Okta":{"clientSecret":"oauth2OktaClientSecret","oktaDomain":"oauth2OktaDomain","authorizationServerId":"oauth2OktaAuthorizationServerId"},"Auth0":{"clientSecret":"oauth2Auth0ClientSecret","auth0Domain":"oauth2Auth0Domain"},"Authentik":{"clientSecret":"oauth2AuthentikClientSecret","authentikDomain":"oauth2AuthentikDomain"},"Gitlab":{"endpoint":"oauth2GitlabEndpoint","clientSecret":"oauth2GitlabClientSecret",},} let provider=element.getAttribute("data-forms-oauth-custom");if(!provider||!providers.hasOwnProperty(provider)){console.error("Provider for custom form not set or unknown")} let config=providers[provider];element.addEventListener('change',sync);let elements={};for(const key in config){if(Object.hasOwnProperty.call(config,key)){elements[key]=document.getElementById(config[key]);elements[key].addEventListener('change',update);}} function update(){let json={};for(const key in elements){if(Object.hasOwnProperty.call(elements,key)){json[key]=elements[key].value}} diff --git a/public/images/users/authentik.png b/public/images/users/authentik.png new file mode 100644 index 0000000000..883e054221 Binary files /dev/null and b/public/images/users/authentik.png differ diff --git a/public/images/users/disqus.png b/public/images/users/disqus.png new file mode 100644 index 0000000000..29187942e1 Binary files /dev/null and b/public/images/users/disqus.png differ diff --git a/public/images/users/podio.png b/public/images/users/podio.png new file mode 100644 index 0000000000..0eb6b49ac9 Binary files /dev/null and b/public/images/users/podio.png differ diff --git a/public/scripts/views/forms/oauth-custom.js b/public/scripts/views/forms/oauth-custom.js index 66b81cba58..cbf6331077 100644 --- a/public/scripts/views/forms/oauth-custom.js +++ b/public/scripts/views/forms/oauth-custom.js @@ -26,6 +26,10 @@ "clientSecret": "oauth2Auth0ClientSecret", "auth0Domain": "oauth2Auth0Domain" }, + "Authentik": { + "clientSecret": "oauth2AuthentikClientSecret", + "authentikDomain": "oauth2AuthentikDomain" + }, "Gitlab": { "endpoint": "oauth2GitlabEndpoint", "clientSecret": "oauth2GitlabClientSecret", diff --git a/src/Appwrite/Auth/OAuth2/Authentik.php b/src/Appwrite/Auth/OAuth2/Authentik.php new file mode 100644 index 0000000000..16822e2c9f --- /dev/null +++ b/src/Appwrite/Auth/OAuth2/Authentik.php @@ -0,0 +1,227 @@ +getAuthentikDomain() . '/application/o/authorize?' . \http_build_query([ + 'client_id' => $this->appID, + 'redirect_uri' => $this->callback, + 'state' => \json_encode($this->state), + 'scope' => \implode(' ', $this->getScopes()), + 'response_type' => 'code' + ]); + } + + /** + * @param string $code + * + * @return array + */ + protected function getTokens(string $code): array + { + if (empty($this->tokens)) { + $headers = ['Content-Type: application/x-www-form-urlencoded']; + $this->tokens = \json_decode($this->request( + 'POST', + 'https://' . $this->getAuthentikDomain() . '/application/o/token/', + $headers, + \http_build_query([ + 'code' => $code, + 'client_id' => $this->appID, + 'client_secret' => $this->getClientSecret(), + 'redirect_uri' => $this->callback, + 'scope' => \implode(' ', $this->getScopes()), + 'grant_type' => 'authorization_code' + ]) + ), true); + } + return $this->tokens; + } + + + /** + * @param string $refreshToken + * + * @return array + */ + public function refreshTokens(string $refreshToken): array + { + $headers = ['Content-Type: application/x-www-form-urlencoded']; + $this->tokens = \json_decode($this->request( + 'POST', + 'https://' . $this->getAuthentikDomain() . '/application/o/token/', + $headers, + \http_build_query([ + 'refresh_token' => $refreshToken, + 'client_id' => $this->appID, + 'client_secret' => $this->getClientSecret(), + 'grant_type' => 'refresh_token' + ]) + ), true); + + if (empty($this->tokens['refresh_token'])) { + $this->tokens['refresh_token'] = $refreshToken; + } + + return $this->tokens; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserID(string $accessToken): string + { + $user = $this->getUser($accessToken); + + if (isset($user['sub'])) { + return $user['sub']; + } + + return ''; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserEmail(string $accessToken): string + { + $user = $this->getUser($accessToken); + + if (isset($user['email'])) { + return $user['email']; + } + + return ''; + } + + /** + * Check if the User email is verified + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + if ($user['email_verified'] ?? false) { + return true; + } + + return false; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserName(string $accessToken): string + { + $user = $this->getUser($accessToken); + + if (isset($user['name'])) { + return $user['name']; + } + + return ''; + } + + /** + * @param string $accessToken + * + * @return array + */ + protected function getUser(string $accessToken): array + { + if (empty($this->user)) { + $headers = ['Authorization: Bearer ' . \urlencode($accessToken)]; + $user = $this->request('GET', 'https://' . $this->getAuthentikDomain() . '/application/o/userinfo/', $headers); + $this->user = \json_decode($user, true); + } + + return $this->user; + } + + /** + * Extracts the Client Secret from the JSON stored in appSecret + * + * @return string + */ + protected function getClientSecret(): string + { + $secret = $this->getAppSecret(); + + return $secret['clientSecret'] ?? ''; + } + + /** + * Extracts the authentik Domain from the JSON stored in appSecret + * + * @return string + */ + protected function getAuthentikDomain(): string + { + $secret = $this->getAppSecret(); + return $secret['authentikDomain'] ?? ''; + } + + /** + * Decode the JSON stored in appSecret + * + * @return array + */ + protected function getAppSecret(): array + { + try { + $secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR); + } catch (\Throwable $th) { + throw new \Exception('Invalid secret'); + } + return $secret; + } +} diff --git a/src/Appwrite/Auth/OAuth2/Disqus.php b/src/Appwrite/Auth/OAuth2/Disqus.php new file mode 100644 index 0000000000..58b7f48914 --- /dev/null +++ b/src/Appwrite/Auth/OAuth2/Disqus.php @@ -0,0 +1,188 @@ +endpoint . 'oauth/2.0/authorize/?' . + \http_build_query([ + 'response_type' => 'code', + 'client_id' => $this->appID, + 'state' => \json_encode($this->state), + 'redirect_uri' => $this->callback, + 'scope' => \implode(',', $this->getScopes()) + ]); + + return $url; + } + + /** + * @param string $code + * + * @return array + */ + protected function getTokens(string $code): array + { + if (empty($this->tokens)) { + $this->tokens = \json_decode($this->request( + 'POST', + $this->endpoint . 'oauth/2.0/access_token/', + ['Content-Type: application/x-www-form-urlencoded'], + \http_build_query([ + 'grant_type' => 'authorization_code', + 'client_id' => $this->appID, + 'client_secret' => $this->appSecret, + 'redirect_uri' => $this->callback, + 'code' => $code, + 'scope' => \implode(' ', $this->getScopes()), + ]) + ), true); + } + return $this->tokens; + } + + /** + * @param string $refreshToken + * + * @return array + */ + public function refreshTokens(string $refreshToken): array + { + $this->tokens = \json_decode($this->request( + 'POST', + $this->endpoint . 'oauth/2.0/access_token/?', + ['Content-Type: application/x-www-form-urlencoded'], + \http_build_query([ + 'grant_type' => 'refresh_token', + 'refresh_token' => $refreshToken, + 'client_id' => $this->appID, + 'client_secret' => $this->appSecret, + ]) + ), true); + + if (empty($this->tokens['refresh_token'])) { + $this->tokens['refresh_token'] = $refreshToken; + } + return $this->tokens; + } + + /** + * @param string $token + * + * @return string + */ + public function getUserID(string $accessToken): string + { + $user = $this->getUser($accessToken); + + $userId = $user['id']; + + return $userId; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserEmail(string $accessToken): string + { + $user = $this->getUser($accessToken); + + $userEmail = $user['email']; + + return $userEmail; + } + + /** + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + + // Look out for the change in their enpoint. + // It's in Beta so they may provide a parameter in the future. + // https://disqus.com/api/docs/users/details/ + + return false; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserName(string $accessToken): string + { + $user = $this->getUser($accessToken); + + $username = $user['name'] ?? ''; + + return $username; + } + + /** + * @param string $accessToken + * + * @return array + */ + protected function getUser(string $accessToken): array + { + if (empty($this->user)) { + $user = $this->request( + 'GET', + $this->endpoint . '3.0/users/details.json?' . \http_build_query([ + 'access_token' => $accessToken, + 'api_key' => $this->appID, + 'api_secret' => $this->appSecret + ]), + ); + $this->user = \json_decode($user, true)['response']; + } + + return $this->user; + } +} diff --git a/src/Appwrite/Auth/OAuth2/Linkedin.php b/src/Appwrite/Auth/OAuth2/Linkedin.php index 8cf9b8aff7..340cab2df0 100644 --- a/src/Appwrite/Auth/OAuth2/Linkedin.php +++ b/src/Appwrite/Auth/OAuth2/Linkedin.php @@ -80,7 +80,6 @@ class Linkedin extends OAuth2 ]) ), true); } - return $this->tokens; } @@ -107,7 +106,6 @@ class Linkedin extends OAuth2 if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } - return $this->tokens; } diff --git a/src/Appwrite/Auth/OAuth2/Podio.php b/src/Appwrite/Auth/OAuth2/Podio.php new file mode 100644 index 0000000000..e4194238d1 --- /dev/null +++ b/src/Appwrite/Auth/OAuth2/Podio.php @@ -0,0 +1,199 @@ +endpoint . '/authorize?' . + \http_build_query([ + 'client_id' => $this->appID, + 'state' => \json_encode($this->state), + 'redirect_uri' => $this->callback + ]); + + return $url; + } + + /** + * @param string $code + * + * @return array + */ + protected function getTokens(string $code): array + { + if (empty($this->tokens)) { + $this->tokens = \json_decode($this->request( + 'POST', + $this->apiEndpoint . '/oauth/token', + ['Content-Type: application/x-www-form-urlencoded'], + \http_build_query([ + 'grant_type' => 'authorization_code', + 'code' => $code, + 'redirect_uri' => $this->callback, + 'client_id' => $this->appID, + 'client_secret' => $this->appSecret + ]) + ), true); + } + + return $this->tokens; + } + + /** + * @param string $refreshToken + * + * @return array + */ + public function refreshTokens(string $refreshToken): array + { + $this->tokens = \json_decode($this->request( + 'POST', + $this->apiEndpoint . '/oauth/token', + ['Content-Type: application/x-www-form-urlencoded'], + \http_build_query([ + 'grant_type' => 'refresh_token', + 'refresh_token' => $refreshToken, + 'client_id' => $this->appID, + 'client_secret' => $this->appSecret, + ]) + ), true); + + if (empty($this->tokens['refresh_token'])) { + $this->tokens['refresh_token'] = $refreshToken; + } + + return $this->tokens; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserID(string $accessToken): string + { + $user = $this->getUser($accessToken); + + return \strval($user['user_id']) ?? ''; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserEmail(string $accessToken): string + { + $user = $this->getUser($accessToken); + + return $user['mail'] ?? ''; + } + + /** + * Check if the OAuth email is verified + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + $mails = $user['mails']; + $mainMailIndex = \array_search($user['mail'], \array_map(fn($m) => $m['mail'], $mails)); + $mainMain = $mails[$mainMailIndex]; + + if ($mainMain['verified'] ?? false) { + return true; + } + + return false; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserName(string $accessToken): string + { + $user = $this->getUser($accessToken); + + return $user['name'] ?? ''; + } + + /** + * @param string $accessToken + * + * @return array + */ + protected function getUser(string $accessToken): array + { + if (empty($this->user)) { + $user = \json_decode($this->request( + 'GET', + $this->apiEndpoint . '/user', + ['Authorization: Bearer ' . \urlencode($accessToken)] + ), true); + + $profile = \json_decode($this->request( + 'GET', + $this->apiEndpoint . '/user/profile', + ['Authorization: Bearer ' . \urlencode($accessToken)] + ), true); + + $this->user = $user; + $this->user['name'] = $profile['name']; + } + + return $this->user; + } +} diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 3ca3dbb3c4..721c69981e 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -199,7 +199,7 @@ class Response extends SwooleResponse /** * @var array */ - protected $payload = []; + protected array $payload = []; /** * Response constructor. @@ -300,8 +300,7 @@ class Response extends SwooleResponse // Verification // Recovery // Tests (keep last) - ->setModel(new Mock()) - ; + ->setModel(new Mock()); parent::__construct($response); } @@ -391,12 +390,13 @@ class Response extends SwooleResponse if ($model->isAny()) { $this->payload = $document->getArrayCopy(); + return $this->payload; } foreach ($model->getRules() as $key => $rule) { - if (!$document->isSet($key) && $rule['require']) { // do not set attribute in response if not required - if (!is_null($rule['default'])) { + if (!$document->isSet($key) && $rule['required']) { // do not set attribute in response if not required + if (\array_key_exists('default', $rule)) { $document->setAttribute($key, $rule['default']); } else { throw new Exception('Model ' . $model->getName() . ' is missing response key: ' . $key); @@ -408,7 +408,7 @@ class Response extends SwooleResponse throw new Exception($key . ' must be an array of type ' . $rule['type']); } - foreach ($data[$key] as &$item) { + foreach ($data[$key] as $index => $item) { if ($item instanceof Document) { if (\is_array($rule['type'])) { foreach ($rule['type'] as $type) { @@ -432,9 +432,13 @@ class Response extends SwooleResponse throw new Exception('Missing model for rule: ' . $ruleType); } - $item = $this->output($item, $ruleType); + $data[$key][$index] = $this->output($item, $ruleType); } } + } else { + if ($data[$key] instanceof Document) { + $data[$key] = $this->output($data[$key], $rule['type']); + } } $output[$key] = $data[$key]; @@ -465,8 +469,7 @@ class Response extends SwooleResponse $this ->setContentType(Response::CONTENT_TYPE_YAML) - ->send(yaml_emit($data, YAML_UTF8_ENCODING)) - ; + ->send(yaml_emit($data, YAML_UTF8_ENCODING)); } /** diff --git a/src/Appwrite/Utopia/Response/Model.php b/src/Appwrite/Utopia/Response/Model.php index 6407c85118..b146259f11 100644 --- a/src/Appwrite/Utopia/Response/Model.php +++ b/src/Appwrite/Utopia/Response/Model.php @@ -15,22 +15,28 @@ abstract class Model /** * @var bool */ - protected $none = false; + protected bool $none = false; /** * @var bool */ - protected $any = false; + protected bool $any = false; /** * @var bool */ - protected $public = true; + protected bool $public = true; /** * @var array */ - protected $rules = []; + protected array $rules = []; + + /** + * @var array + */ + public array $conditions = []; + /** * Filter Document Structure @@ -76,12 +82,10 @@ abstract class Model protected function addRule(string $key, array $options): self { $this->rules[$key] = array_merge([ - 'require' => true, - 'type' => '', + 'required' => true, + 'array' => false, 'description' => '', - 'default' => null, - 'example' => '', - 'array' => false + 'example' => '' ], $options); return $this; @@ -92,7 +96,7 @@ abstract class Model $list = []; foreach ($this->rules as $key => $rule) { - if ($rule['require'] ?? false) { + if ($rule['required'] ?? false) { $list[] = $key; } } diff --git a/src/Appwrite/Utopia/Response/Model/Any.php b/src/Appwrite/Utopia/Response/Model/Any.php index 589dfd1eb4..634e0859b9 100644 --- a/src/Appwrite/Utopia/Response/Model/Any.php +++ b/src/Appwrite/Utopia/Response/Model/Any.php @@ -10,7 +10,7 @@ class Any extends Model /** * @var bool */ - protected $any = true; + protected bool $any = true; /** * Get Name diff --git a/src/Appwrite/Utopia/Response/Model/Attribute.php b/src/Appwrite/Utopia/Response/Model/Attribute.php index 274b0caa92..ee1b8628ed 100644 --- a/src/Appwrite/Utopia/Response/Model/Attribute.php +++ b/src/Appwrite/Utopia/Response/Model/Attribute.php @@ -39,7 +39,6 @@ class Attribute extends Model 'description' => 'Is attribute an array?', 'default' => false, 'example' => false, - 'require' => false ]) ; } diff --git a/src/Appwrite/Utopia/Response/Model/AttributeBoolean.php b/src/Appwrite/Utopia/Response/Model/AttributeBoolean.php index 0623406636..e31b517de6 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeBoolean.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeBoolean.php @@ -28,9 +28,7 @@ class AttributeBoolean extends Attribute 'type' => self::TYPE_BOOLEAN, 'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.', 'default' => null, - 'example' => false, - 'array' => false, - 'require' => false, + 'example' => false ]) ; } diff --git a/src/Appwrite/Utopia/Response/Model/AttributeEmail.php b/src/Appwrite/Utopia/Response/Model/AttributeEmail.php index 64fd5b42fe..e521c31a85 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeEmail.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeEmail.php @@ -37,8 +37,6 @@ class AttributeEmail extends Attribute 'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.', 'default' => null, 'example' => 'default@example.com', - 'array' => false, - 'require' => false, ]) ; } diff --git a/src/Appwrite/Utopia/Response/Model/AttributeEnum.php b/src/Appwrite/Utopia/Response/Model/AttributeEnum.php index 2c3875c005..3b111c27e1 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeEnum.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeEnum.php @@ -30,7 +30,6 @@ class AttributeEnum extends Attribute 'default' => null, 'example' => 'element', 'array' => true, - 'require' => true, ]) ->addRule('format', [ 'type' => self::TYPE_STRING, @@ -45,8 +44,6 @@ class AttributeEnum extends Attribute 'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.', 'default' => null, 'example' => 'element', - 'array' => false, - 'require' => false, ]) ; } diff --git a/src/Appwrite/Utopia/Response/Model/AttributeFloat.php b/src/Appwrite/Utopia/Response/Model/AttributeFloat.php index 5cc6f90649..e2d0594e7b 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeFloat.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeFloat.php @@ -29,24 +29,18 @@ class AttributeFloat extends Attribute 'description' => 'Minimum value to enforce for new documents.', 'default' => null, 'example' => 1.5, - 'array' => false, - 'require' => false, ]) ->addRule('max', [ 'type' => self::TYPE_FLOAT, 'description' => 'Maximum value to enforce for new documents.', 'default' => null, 'example' => 10.5, - 'array' => false, - 'require' => false, ]) ->addRule('default', [ 'type' => self::TYPE_FLOAT, 'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.', 'default' => null, 'example' => 2.5, - 'array' => false, - 'require' => false, ]) ; } diff --git a/src/Appwrite/Utopia/Response/Model/AttributeIP.php b/src/Appwrite/Utopia/Response/Model/AttributeIP.php index 6cd401fe35..14724144ec 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeIP.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeIP.php @@ -37,8 +37,6 @@ class AttributeIP extends Attribute 'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.', 'default' => null, 'example' => '192.0.2.0', - 'array' => false, - 'require' => false, ]) ; } diff --git a/src/Appwrite/Utopia/Response/Model/AttributeInteger.php b/src/Appwrite/Utopia/Response/Model/AttributeInteger.php index eefa5e39d7..35fd955a5e 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeInteger.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeInteger.php @@ -29,24 +29,18 @@ class AttributeInteger extends Attribute 'description' => 'Minimum value to enforce for new documents.', 'default' => null, 'example' => 1, - 'array' => false, - 'require' => false, ]) ->addRule('max', [ 'type' => self::TYPE_INTEGER, 'description' => 'Maximum value to enforce for new documents.', 'default' => null, 'example' => 10, - 'array' => false, - 'require' => false, ]) ->addRule('default', [ 'type' => self::TYPE_INTEGER, 'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.', 'default' => null, 'example' => 10, - 'array' => false, - 'require' => false, ]) ; } diff --git a/src/Appwrite/Utopia/Response/Model/AttributeString.php b/src/Appwrite/Utopia/Response/Model/AttributeString.php index c09ce0eea8..75c3a71019 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeString.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeString.php @@ -23,8 +23,6 @@ class AttributeString extends Attribute 'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.', 'default' => null, 'example' => 'default', - 'array' => false, - 'require' => false, ]) ; } diff --git a/src/Appwrite/Utopia/Response/Model/AttributeURL.php b/src/Appwrite/Utopia/Response/Model/AttributeURL.php index 46641bc283..d59a871072 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeURL.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeURL.php @@ -37,8 +37,6 @@ class AttributeURL extends Attribute 'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.', 'default' => null, 'example' => 'http://example.com', - 'array' => false, - 'require' => false, ]) ; } diff --git a/src/Appwrite/Utopia/Response/Model/BaseList.php b/src/Appwrite/Utopia/Response/Model/BaseList.php index ee8788e6a6..06a5249388 100644 --- a/src/Appwrite/Utopia/Response/Model/BaseList.php +++ b/src/Appwrite/Utopia/Response/Model/BaseList.php @@ -2,7 +2,6 @@ namespace Appwrite\Utopia\Response\Model; -use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model; class BaseList extends Model @@ -10,12 +9,12 @@ class BaseList extends Model /** * @var string */ - protected $name = ''; + protected string $name = ''; /** * @var string */ - protected $type = ''; + protected string $type = ''; /** * @param string $name diff --git a/src/Appwrite/Utopia/Response/Model/Deployment.php b/src/Appwrite/Utopia/Response/Model/Deployment.php index 5b1af1a2e2..31aa8ce63c 100644 --- a/src/Appwrite/Utopia/Response/Model/Deployment.php +++ b/src/Appwrite/Utopia/Response/Model/Deployment.php @@ -66,9 +66,9 @@ class Deployment extends Model ]) ->addRule('status', [ 'type' => self::TYPE_STRING, - 'description' => 'The deployment status.', + 'description' => 'The deployment status. Possible values are "processing", "building", "pending", "ready", and "failed".', 'default' => '', - 'example' => 'enabled', + 'example' => 'ready', ]) ->addRule('buildStdout', [ 'type' => self::TYPE_STRING, diff --git a/src/Appwrite/Utopia/Response/Model/Domain.php b/src/Appwrite/Utopia/Response/Model/Domain.php index 393342f359..964470e466 100644 --- a/src/Appwrite/Utopia/Response/Model/Domain.php +++ b/src/Appwrite/Utopia/Response/Model/Domain.php @@ -10,7 +10,7 @@ class Domain extends Model /** * @var bool */ - protected $public = false; + protected bool $public = false; public function __construct() { diff --git a/src/Appwrite/Utopia/Response/Model/ErrorDev.php b/src/Appwrite/Utopia/Response/Model/ErrorDev.php index b5450c67ad..9319397c77 100644 --- a/src/Appwrite/Utopia/Response/Model/ErrorDev.php +++ b/src/Appwrite/Utopia/Response/Model/ErrorDev.php @@ -9,7 +9,7 @@ class ErrorDev extends Error /** * @var bool */ - protected $public = false; + protected bool $public = false; public function __construct() { diff --git a/src/Appwrite/Utopia/Response/Model/Index.php b/src/Appwrite/Utopia/Response/Model/Index.php index b7ac626be9..9f27613669 100644 --- a/src/Appwrite/Utopia/Response/Model/Index.php +++ b/src/Appwrite/Utopia/Response/Model/Index.php @@ -41,7 +41,6 @@ class Index extends Model 'default' => [], 'example' => [], 'array' => true, - 'required' => false, ]) ; } diff --git a/src/Appwrite/Utopia/Response/Model/Key.php b/src/Appwrite/Utopia/Response/Model/Key.php index 5c79c28b55..9becc7fab0 100644 --- a/src/Appwrite/Utopia/Response/Model/Key.php +++ b/src/Appwrite/Utopia/Response/Model/Key.php @@ -10,7 +10,7 @@ class Key extends Model /** * @var bool */ - protected $public = false; + protected bool $public = false; public function __construct() { diff --git a/src/Appwrite/Utopia/Response/Model/Metric.php b/src/Appwrite/Utopia/Response/Model/Metric.php index 70706d6143..7fb1b98386 100644 --- a/src/Appwrite/Utopia/Response/Model/Metric.php +++ b/src/Appwrite/Utopia/Response/Model/Metric.php @@ -16,7 +16,7 @@ class Metric extends Model 'default' => -1, 'example' => 1, ]) - ->addRule('timestamp', [ + ->addRule('date', [ 'type' => self::TYPE_INTEGER, 'description' => 'The UNIX timestamp at which this metric was aggregated.', 'default' => 0, diff --git a/src/Appwrite/Utopia/Response/Model/None.php b/src/Appwrite/Utopia/Response/Model/None.php index ecc387498e..8f98ae892f 100644 --- a/src/Appwrite/Utopia/Response/Model/None.php +++ b/src/Appwrite/Utopia/Response/Model/None.php @@ -10,7 +10,7 @@ class None extends Model /** * @var bool */ - protected $none = true; + protected bool $none = true; /** * Get Name diff --git a/src/Appwrite/Utopia/Response/Model/Platform.php b/src/Appwrite/Utopia/Response/Model/Platform.php index e0052dbd00..865e3479ab 100644 --- a/src/Appwrite/Utopia/Response/Model/Platform.php +++ b/src/Appwrite/Utopia/Response/Model/Platform.php @@ -10,7 +10,7 @@ class Platform extends Model /** * @var bool */ - protected $public = false; + protected bool $public = false; public function __construct() { diff --git a/src/Appwrite/Utopia/Response/Model/Preferences.php b/src/Appwrite/Utopia/Response/Model/Preferences.php index ad4bf66132..379543660c 100644 --- a/src/Appwrite/Utopia/Response/Model/Preferences.php +++ b/src/Appwrite/Utopia/Response/Model/Preferences.php @@ -6,11 +6,6 @@ use Appwrite\Utopia\Response; class Preferences extends Any { - /** - * @var bool - */ - protected $any = true; - /** * Get Name * diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index 34430e9c4a..5a80898dea 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -12,7 +12,7 @@ class Project extends Model /** * @var bool */ - protected $public = false; + protected bool $public = false; public function __construct() { diff --git a/src/Appwrite/Utopia/Response/Model/Webhook.php b/src/Appwrite/Utopia/Response/Model/Webhook.php index b5726e88a0..73ab269609 100644 --- a/src/Appwrite/Utopia/Response/Model/Webhook.php +++ b/src/Appwrite/Utopia/Response/Model/Webhook.php @@ -10,7 +10,7 @@ class Webhook extends Model /** * @var bool */ - protected $public = false; + protected bool $public = false; public function __construct() { diff --git a/tests/e2e/Services/Teams/TeamsBaseClient.php b/tests/e2e/Services/Teams/TeamsBaseClient.php index bd9b9c1ae0..08e2ddc8c8 100644 --- a/tests/e2e/Services/Teams/TeamsBaseClient.php +++ b/tests/e2e/Services/Teams/TeamsBaseClient.php @@ -107,6 +107,8 @@ trait TeamsBaseClient $this->assertEquals(201, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); $this->assertNotEmpty($response['body']['userId']); + $this->assertEquals($name, $response['body']['userName']); + $this->assertEquals($email, $response['body']['userEmail']); $this->assertNotEmpty($response['body']['teamId']); $this->assertNotEmpty($response['body']['teamName']); $this->assertCount(2, $response['body']['roles']); diff --git a/tests/e2e/Services/Teams/TeamsBaseServer.php b/tests/e2e/Services/Teams/TeamsBaseServer.php index 9961cec247..f08664f4df 100644 --- a/tests/e2e/Services/Teams/TeamsBaseServer.php +++ b/tests/e2e/Services/Teams/TeamsBaseServer.php @@ -57,6 +57,8 @@ trait TeamsBaseServer $this->assertEquals(201, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); $this->assertNotEmpty($response['body']['userId']); + $this->assertEquals('Friend User', $response['body']['userName']); + $this->assertEquals($email, $response['body']['userEmail']); $this->assertNotEmpty($response['body']['teamId']); $this->assertCount(2, $response['body']['roles']); $this->assertIsInt($response['body']['joined']); diff --git a/tests/unit/Auth/AuthTest.php b/tests/unit/Auth/AuthTest.php index fd917d613b..1f137fc054 100644 --- a/tests/unit/Auth/AuthTest.php +++ b/tests/unit/Auth/AuthTest.php @@ -1,6 +1,6 @@ assertEquals(Auth::$cookieName, $name); } - public function testEncodeDecodeSession() + public function testEncodeDecodeSession(): void { $id = 'id'; $secret = 'secret'; @@ -40,13 +36,13 @@ class AuthTest extends TestCase $this->assertEquals(Auth::decodeSession($session), ['id' => $id, 'secret' => $secret]); } - public function testHash() + public function testHash(): void { $secret = 'secret'; $this->assertEquals(Auth::hash($secret), '2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b'); } - public function testPassword() + public function testPassword(): void { $secret = 'secret'; $static = '$2y$08$PDbMtV18J1KOBI9tIYabBuyUwBrtXPGhLxCy9pWP6xkldVOKLrLKy'; @@ -56,19 +52,19 @@ class AuthTest extends TestCase $this->assertEquals(Auth::passwordVerify($secret, $static), true); } - public function testPasswordGenerator() + public function testPasswordGenerator(): void { $this->assertEquals(\mb_strlen(Auth::passwordGenerator()), 40); $this->assertEquals(\mb_strlen(Auth::passwordGenerator(5)), 10); } - public function testTokenGenerator() + public function testTokenGenerator(): void { $this->assertEquals(\mb_strlen(Auth::tokenGenerator()), 256); $this->assertEquals(\mb_strlen(Auth::tokenGenerator(5)), 10); } - public function testSessionVerify() + public function testSessionVerify(): void { $secret = 'secret1'; $hash = Auth::hash($secret); @@ -112,7 +108,7 @@ class AuthTest extends TestCase $this->assertEquals(Auth::sessionVerify($tokens2, 'false-secret'), false); } - public function testTokenVerify() + public function testTokenVerify(): void { $secret = 'secret1'; $hash = Auth::hash($secret); @@ -169,7 +165,7 @@ class AuthTest extends TestCase $this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false); } - public function testIsPrivilegedUser() + public function testIsPrivilegedUser(): void { $this->assertEquals(false, Auth::isPrivilegedUser([])); $this->assertEquals(false, Auth::isPrivilegedUser(['role:' . Auth::USER_ROLE_GUEST])); @@ -186,7 +182,7 @@ class AuthTest extends TestCase $this->assertEquals(true, Auth::isPrivilegedUser(['role:' . Auth::USER_ROLE_OWNER, 'role:' . Auth::USER_ROLE_ADMIN, 'role:' . Auth::USER_ROLE_DEVELOPER])); } - public function testIsAppUser() + public function testIsAppUser(): void { $this->assertEquals(false, Auth::isAppUser([])); $this->assertEquals(false, Auth::isAppUser(['role:' . Auth::USER_ROLE_GUEST])); @@ -203,7 +199,7 @@ class AuthTest extends TestCase $this->assertEquals(false, Auth::isAppUser(['role:' . Auth::USER_ROLE_OWNER, 'role:' . Auth::USER_ROLE_ADMIN, 'role:' . Auth::USER_ROLE_DEVELOPER])); } - public function testGuestRoles() + public function testGuestRoles(): void { $user = new Document([ '$id' => '' @@ -214,7 +210,7 @@ class AuthTest extends TestCase $this->assertContains('role:guest', $roles); } - public function testUserRoles() + public function testUserRoles(): void { $user = new Document([ '$id' => '123', @@ -247,7 +243,7 @@ class AuthTest extends TestCase $this->assertContains('team:def/guest', $roles); } - public function testPrivilegedUserRoles() + public function testPrivilegedUserRoles(): void { Authorization::setRole('role:' . Auth::USER_ROLE_OWNER); $user = new Document([ @@ -281,7 +277,7 @@ class AuthTest extends TestCase $this->assertContains('team:def/guest', $roles); } - public function testAppUserRoles() + public function testAppUserRoles(): void { Authorization::setRole('role:' . Auth::USER_ROLE_APP); $user = new Document([ diff --git a/tests/unit/Auth/Validator/PasswordTest.php b/tests/unit/Auth/Validator/PasswordTest.php index 5a96941b10..0d84ff3573 100644 --- a/tests/unit/Auth/Validator/PasswordTest.php +++ b/tests/unit/Auth/Validator/PasswordTest.php @@ -1,27 +1,20 @@ object = new Password(); } - public function tearDown(): void - { - } - - public function testValues() + public function testValues(): void { $this->assertEquals($this->object->isValid(false), false); $this->assertEquals($this->object->isValid(null), false); diff --git a/tests/unit/Auth/Validator/PhoneTest.php b/tests/unit/Auth/Validator/PhoneTest.php index f6695918c3..7bfa8db477 100644 --- a/tests/unit/Auth/Validator/PhoneTest.php +++ b/tests/unit/Auth/Validator/PhoneTest.php @@ -1,6 +1,6 @@ object = new Phone(); } - public function tearDown(): void - { - } - - public function testValues() + public function testValues(): void { $this->assertEquals($this->object->isValid(false), false); $this->assertEquals($this->object->isValid(null), false); diff --git a/tests/unit/DSN/DSNTest.php b/tests/unit/DSN/DSNTest.php index be31a640b8..d1f5ba1197 100644 --- a/tests/unit/DSN/DSNTest.php +++ b/tests/unit/DSN/DSNTest.php @@ -1,20 +1,12 @@ expectException(\InvalidArgumentException::class); - $dsn = new DSN("mariadb://"); + new DSN("mariadb://"); } } diff --git a/tests/unit/Detector/DetectorTest.php b/tests/unit/Detector/DetectorTest.php index a70ad44cf6..12a803de8c 100644 --- a/tests/unit/Detector/DetectorTest.php +++ b/tests/unit/Detector/DetectorTest.php @@ -1,16 +1,13 @@ assertEquals($this->object->getOS(), [ 'osCode' => 'WIN', @@ -30,7 +27,7 @@ class DetectorTest extends TestCase ]); } - public function testGetClient() + public function testGetClient(): void { $this->assertEquals($this->object->getClient(), [ 'clientType' => 'browser', @@ -42,7 +39,7 @@ class DetectorTest extends TestCase ]); } - public function testGetDevice() + public function testGetDevice(): void { $this->assertEquals($this->object->getDevice(), [ 'deviceName' => 'desktop', diff --git a/tests/unit/Docker/ComposeTest.php b/tests/unit/Docker/ComposeTest.php index ad3f40ab54..9f974b5970 100644 --- a/tests/unit/Docker/ComposeTest.php +++ b/tests/unit/Docker/ComposeTest.php @@ -1,6 +1,6 @@ object = new Compose($data); } - public function tearDown(): void - { - } - - public function testVersion() + public function testVersion(): void { $this->assertEquals('3', $this->object->getVersion()); } - public function testServices() + public function testServices(): void { $this->assertCount(17, $this->object->getServices()); $this->assertEquals('appwrite-telegraf', $this->object->getService('telegraf')->getContainerName()); @@ -44,12 +36,12 @@ class ComposeTest extends TestCase $this->assertEquals(['2080' => '80', '2443' => '443', '8080' => '8080'], $this->object->getService('traefik')->getPorts()); } - public function testNetworks() + public function testNetworks(): void { $this->assertCount(2, $this->object->getNetworks()); } - public function testVolumes() + public function testVolumes(): void { $this->assertCount(9, $this->object->getVolumes()); $this->assertEquals('appwrite-mariadb', $this->object->getVolumes()[0]); diff --git a/tests/unit/Docker/EnvTest.php b/tests/unit/Docker/EnvTest.php index f436354efa..f085d62e56 100644 --- a/tests/unit/Docker/EnvTest.php +++ b/tests/unit/Docker/EnvTest.php @@ -1,6 +1,6 @@ object = new Env($data); } - public function tearDown(): void - { - } - - public function testVars() + public function testVars(): void { $this->object->setVar('_APP_TEST', 'value4'); @@ -39,7 +31,7 @@ class EnvTest extends TestCase $this->assertEquals('value4', $this->object->getVar('_APP_TEST')); } - public function testExport() + public function testExport(): void { $this->assertEquals("_APP_X=value1 _APP_Y=value2 diff --git a/tests/unit/Event/EventTest.php b/tests/unit/Event/EventTest.php index 8a8ff1e6d2..dee905638f 100644 --- a/tests/unit/Event/EventTest.php +++ b/tests/unit/Event/EventTest.php @@ -1,6 +1,6 @@ object = new Event($this->queue, 'TestsV1'); } - public function tearDown(): void - { - } - - public function testQueue() + public function testQueue(): void { $this->assertEquals($this->queue, $this->object->getQueue()); @@ -44,7 +33,7 @@ class EventTest extends TestCase $this->object->setQueue($this->queue); } - public function testClass() + public function testClass(): void { $this->assertEquals('TestsV1', $this->object->getClass()); @@ -55,7 +44,7 @@ class EventTest extends TestCase $this->object->setClass('TestsV1'); } - public function testParams() + public function testParams(): void { $this->object ->setParam('eventKey1', 'eventValue1') @@ -69,7 +58,7 @@ class EventTest extends TestCase $this->assertEquals(\Resque::size($this->queue), 1); } - public function testReset() + public function testReset(): void { $this->object ->setParam('eventKey1', 'eventValue1') @@ -85,7 +74,7 @@ class EventTest extends TestCase $this->assertEquals(null, $this->object->getParam('eventKey3')); } - public function testGenerateEvents() + public function testGenerateEvents(): void { $event = Event::generateEvents('users.[userId].create', [ 'userId' => 'torsten' diff --git a/tests/unit/Event/Validator/EventValidatorTest.php b/tests/unit/Event/Validator/EventValidatorTest.php index e40dcce05c..59a31f272d 100644 --- a/tests/unit/Event/Validator/EventValidatorTest.php +++ b/tests/unit/Event/Validator/EventValidatorTest.php @@ -1,6 +1,6 @@ collections = require('app/config/collections.php'); } - public function tearDown(): void - { - } - - public function testDuplicateRules() + public function testDuplicateRules(): void { foreach ($this->collections as $key => $collection) { if (array_key_exists('attributes', $collection)) { diff --git a/tests/unit/General/ExtensionsTest.php b/tests/unit/General/ExtensionsTest.php index 2a7c9d58e9..d638cfffa6 100644 --- a/tests/unit/General/ExtensionsTest.php +++ b/tests/unit/General/ExtensionsTest.php @@ -1,116 +1,77 @@ assertEquals(true, extension_loaded('redis')); } - public function testSwoole() + public function testSwoole(): void { $this->assertEquals(true, extension_loaded('swoole')); } - public function testYAML() + public function testYAML(): void { $this->assertEquals(true, extension_loaded('yaml')); } - public function testOPCache() + public function testOPCache(): void { $this->assertEquals(true, extension_loaded('Zend OPcache')); } - public function testDOM() + public function testDOM(): void { $this->assertEquals(true, extension_loaded('dom')); } - public function testPDO() + public function testPDO(): void { $this->assertEquals(true, extension_loaded('PDO')); } - public function testImagick() + public function testImagick(): void { $this->assertEquals(true, extension_loaded('imagick')); } - public function testJSON() + public function testJSON(): void { $this->assertEquals(true, extension_loaded('json')); } - public function testCURL() + public function testCURL(): void { $this->assertEquals(true, extension_loaded('curl')); } - public function testMBString() + public function testMBString(): void { $this->assertEquals(true, extension_loaded('mbstring')); } - public function testOPENSSL() + public function testOPENSSL(): void { $this->assertEquals(true, extension_loaded('openssl')); } - public function testZLIB() + public function testZLIB(): void { $this->assertEquals(true, extension_loaded('zlib')); } - public function testSockets() + public function testSockets(): void { $this->assertEquals(true, extension_loaded('sockets')); } - public function testMaxminddb() + public function testMaxminddb(): void { $this->assertEquals(true, extension_loaded('maxminddb')); } diff --git a/tests/unit/Messaging/MessagingChannelsTest.php b/tests/unit/Messaging/MessagingChannelsTest.php index e62c273cc8..833345680b 100644 --- a/tests/unit/Messaging/MessagingChannelsTest.php +++ b/tests/unit/Messaging/MessagingChannelsTest.php @@ -1,6 +1,6 @@ connectionsCount = 0; } - public function testSubscriptions() + public function testSubscriptions(): void { /** * Check for 1 project. @@ -148,7 +148,7 @@ class MessagingChannelsTest extends TestCase /** * Tests Wildcard (role:all) Permissions on every channel. */ - public function testWildcardPermission() + public function testWildcardPermission(): void { foreach ($this->allChannels as $index => $channel) { $event = [ @@ -177,7 +177,7 @@ class MessagingChannelsTest extends TestCase } } - public function testRolePermissions() + public function testRolePermissions(): void { $roles = ['role:guest', 'role:member']; foreach ($this->allChannels as $index => $channel) { @@ -211,7 +211,7 @@ class MessagingChannelsTest extends TestCase } } - public function testUserPermissions() + public function testUserPermissions(): void { foreach ($this->allChannels as $index => $channel) { $permissions = []; @@ -244,7 +244,7 @@ class MessagingChannelsTest extends TestCase } } - public function testTeamPermissions() + public function testTeamPermissions(): void { foreach ($this->allChannels as $index => $channel) { $permissions = []; diff --git a/tests/unit/Messaging/MessagingGuestTest.php b/tests/unit/Messaging/MessagingGuestTest.php index f6b5e22cda..ac5795b8a2 100644 --- a/tests/unit/Messaging/MessagingGuestTest.php +++ b/tests/unit/Messaging/MessagingGuestTest.php @@ -1,13 +1,13 @@ assertEmpty($realtime->subscriptions); } - public function testConvertChannelsGuest() + public function testConvertChannelsGuest(): void { $user = new Document([ '$id' => '' @@ -157,7 +157,7 @@ class MessagingTest extends TestCase $this->assertArrayNotHasKey('account.456', $channels); } - public function testConvertChannelsUser() + public function testConvertChannelsUser(): void { $user = new Document([ '$id' => '123', diff --git a/tests/unit/Migration/MigrationTest.php b/tests/unit/Migration/MigrationTest.php index 24aab8d1e3..d424c16bb7 100644 --- a/tests/unit/Migration/MigrationTest.php +++ b/tests/unit/Migration/MigrationTest.php @@ -1,6 +1,6 @@ assertArrayHasKey(APP_VERSION_STABLE, Migration::$versions); } - public function testHasDifference() + public function testHasDifference(): void { $this->assertFalse(Migration::hasDifference([], [])); $this->assertFalse(Migration::hasDifference([ diff --git a/tests/unit/Migration/MigrationV12Test.php b/tests/unit/Migration/MigrationV12Test.php index 57deb546f3..2a0a0512a2 100644 --- a/tests/unit/Migration/MigrationV12Test.php +++ b/tests/unit/Migration/MigrationV12Test.php @@ -1,6 +1,6 @@ method->setAccessible(true); } - public function testMigrationProjects() + public function testMigrationProjects(): void { $document = $this->fixDocument(new Document([ '$id' => 'project', @@ -30,7 +30,7 @@ class MigrationV12Test extends MigrationTest $this->assertEquals($document->getAttribute('search'), 'project Appwrite'); } - public function testMigrationUsers() + public function testMigrationUsers(): void { $document = $this->fixDocument(new Document([ '$id' => 'user', @@ -42,7 +42,7 @@ class MigrationV12Test extends MigrationTest $this->assertEquals($document->getAttribute('search'), 'user test@appwrite.io Torsten Dittmann'); } - public function testMigrationTeams() + public function testMigrationTeams(): void { $document = $this->fixDocument(new Document([ '$id' => 'team', @@ -53,7 +53,7 @@ class MigrationV12Test extends MigrationTest $this->assertEquals($document->getAttribute('search'), 'team Appwrite'); } - public function testMigrationFunctions() + public function testMigrationFunctions(): void { $document = $this->fixDocument(new Document([ '$id' => 'function', @@ -65,7 +65,7 @@ class MigrationV12Test extends MigrationTest $this->assertEquals($document->getAttribute('search'), 'function My Function php-8.0'); } - public function testMigrationExecutions() + public function testMigrationExecutions(): void { $document = $this->fixDocument(new Document([ '$id' => 'execution', diff --git a/tests/unit/Migration/MigrationV13Test.php b/tests/unit/Migration/MigrationV13Test.php index ff04403536..954aea71cf 100644 --- a/tests/unit/Migration/MigrationV13Test.php +++ b/tests/unit/Migration/MigrationV13Test.php @@ -1,8 +1,7 @@ method->setAccessible(true); } - public function testMigrateFunctions() + public function testMigrateFunctions(): void { $document = $this->fixDocument(new Document([ '$id' => 'func', @@ -28,7 +27,7 @@ class MigrationV13Test extends MigrationTest $this->assertEquals($document->getAttribute('events'), ['users.*.create']); } - public function testMigrationWebhooks() + public function testMigrationWebhooks(): void { $document = $this->fixDocument(new Document([ '$id' => 'webh', diff --git a/tests/unit/Migration/MigrationV14Test.php b/tests/unit/Migration/MigrationV14Test.php index ea839465af..1010a2c873 100644 --- a/tests/unit/Migration/MigrationV14Test.php +++ b/tests/unit/Migration/MigrationV14Test.php @@ -1,6 +1,6 @@ method->setAccessible(true); } - public function testMigrateProjects() + public function testMigrateProjects(): void { $document = $this->fixDocument(new Document([ '$id' => 'appwrite', @@ -28,7 +28,7 @@ class MigrationV14Test extends MigrationTest $this->assertEquals($document->getAttribute('version'), '0.15.0'); } - public function testMigrateKeys() + public function testMigrateKeys(): void { $document = $this->fixDocument(new Document([ '$id' => 'appwrite', @@ -39,7 +39,7 @@ class MigrationV14Test extends MigrationTest $this->assertEquals($document->getAttribute('expire'), 0); } - public function testMigrateWebhooks() + public function testMigrateWebhooks(): void { $document = $this->fixDocument(new Document([ '$id' => 'appwrite', @@ -50,7 +50,7 @@ class MigrationV14Test extends MigrationTest $this->assertEquals(strlen($document->getAttribute('signatureKey')), 128); } - public function testMigrateUsers() + public function testMigrateUsers(): void { $document = $this->fixDocument(new Document([ '$id' => 'appwrite', @@ -62,7 +62,7 @@ class MigrationV14Test extends MigrationTest $this->assertFalse($document->getAttribute('phoneVerification')); } - public function testMigratePlatforms() + public function testMigratePlatforms(): void { $document = $this->fixDocument(new Document([ '$id' => 'appwrite', @@ -77,7 +77,7 @@ class MigrationV14Test extends MigrationTest $this->assertEquals($document->getUpdatedAt(), 987654321); } - public function testMigrateFunctions() + public function testMigrateFunctions(): void { $document = $this->fixDocument(new Document([ '$id' => 'appwrite', @@ -92,7 +92,7 @@ class MigrationV14Test extends MigrationTest $this->assertEquals($document->getUpdatedAt(), 987654321); } - public function testMigrateDeployments() + public function testMigrateDeployments(): void { $document = $this->fixDocument(new Document([ '$id' => 'appwrite', @@ -104,7 +104,7 @@ class MigrationV14Test extends MigrationTest $this->assertEquals($document->getCreatedAt(), 123456789); } - public function testMigrateExecutions() + public function testMigrateExecutions(): void { $document = $this->fixDocument(new Document([ '$id' => 'appwrite', @@ -116,7 +116,7 @@ class MigrationV14Test extends MigrationTest $this->assertEquals($document->getCreatedAt(), 123456789); } - public function testMigrateTeams() + public function testMigrateTeams(): void { $document = $this->fixDocument(new Document([ '$id' => 'appwrite', @@ -128,7 +128,7 @@ class MigrationV14Test extends MigrationTest $this->assertEquals($document->getCreatedAt(), 123456789); } - public function testMigrateAudits() + public function testMigrateAudits(): void { $document = $this->fixDocument(new Document([ '$id' => 'appwrite', @@ -151,7 +151,7 @@ class MigrationV14Test extends MigrationTest $this->assertEquals($document->getAttribute('event'), 'databases.default.collections.movies.documents.avatar.create'); } - public function testMigrateStats() + public function testMigrateStats(): void { $document = $this->fixDocument(new Document([ '$id' => 'appwrite', diff --git a/tests/unit/Network/Validators/CNAMETest.php b/tests/unit/Network/Validators/CNAMETest.php index 5aa7a69db4..cbb07f19b5 100644 --- a/tests/unit/Network/Validators/CNAMETest.php +++ b/tests/unit/Network/Validators/CNAMETest.php @@ -1,16 +1,13 @@ assertEquals($this->object->isValid(''), false); $this->assertEquals($this->object->isValid(null), false); diff --git a/tests/unit/Network/Validators/DomainTest.php b/tests/unit/Network/Validators/DomainTest.php index eb77b19a3d..631ea10753 100644 --- a/tests/unit/Network/Validators/DomainTest.php +++ b/tests/unit/Network/Validators/DomainTest.php @@ -1,16 +1,13 @@ domain = null; } - public function testIsValid() + public function testIsValid(): void { // Assertions $this->assertEquals(true, $this->domain->isValid('example.com')); diff --git a/tests/unit/Network/Validators/EmailTest.php b/tests/unit/Network/Validators/EmailTest.php index ca73509c65..f629ed6ddc 100755 --- a/tests/unit/Network/Validators/EmailTest.php +++ b/tests/unit/Network/Validators/EmailTest.php @@ -12,16 +12,14 @@ * @license The MIT License (MIT) */ -namespace Appwrite\Network\Validator; +namespace Tests\Unit\Network\Validators; +use Appwrite\Network\Validator\Email; use PHPUnit\Framework\TestCase; class EmailTest extends TestCase { - /** - * @var Email - */ - protected $email = null; + protected ?Email $email = null; public function setUp(): void { @@ -33,9 +31,8 @@ class EmailTest extends TestCase $this->email = null; } - public function testIsValid() + public function testIsValid(): void { - // Assertions $this->assertEquals(true, $this->email->isValid('email@domain.com')); $this->assertEquals(true, $this->email->isValid('firstname.lastname@domain.com')); $this->assertEquals(true, $this->email->isValid('email@subdomain.domain.com')); diff --git a/tests/unit/Network/Validators/HostTest.php b/tests/unit/Network/Validators/HostTest.php index a23911dc76..7974bf86a1 100755 --- a/tests/unit/Network/Validators/HostTest.php +++ b/tests/unit/Network/Validators/HostTest.php @@ -12,16 +12,14 @@ * @license The MIT License (MIT) */ -namespace Appwrite\Network\Validator; +namespace Tests\Unit\Network\Validators; +use Appwrite\Network\Validator\Host; use PHPUnit\Framework\TestCase; class HostTest extends TestCase { - /** - * @var Host - */ - protected $host = null; + protected ?Host $host = null; public function setUp(): void { @@ -33,7 +31,7 @@ class HostTest extends TestCase $this->host = null; } - public function testIsValid() + public function testIsValid(): void { // Assertions $this->assertEquals($this->host->isValid('https://appwrite.io/link'), true); diff --git a/tests/unit/Network/Validators/IPTest.php b/tests/unit/Network/Validators/IPTest.php index 0f5fc63ce9..57e395111c 100755 --- a/tests/unit/Network/Validators/IPTest.php +++ b/tests/unit/Network/Validators/IPTest.php @@ -12,71 +12,76 @@ * @license The MIT License (MIT) */ -namespace Appwrite\Network\Validator; +namespace Tests\Unit\Network\Validators; +use Appwrite\Network\Validator\IP; use PHPUnit\Framework\TestCase; class IPTest extends TestCase { + protected ?IP $validator; + + public function setUp(): void + { + $this->validator = new IP(); + } + public function tearDown(): void { $this->validator = null; } - public function testIsValidIP() + public function testIsValidIP(): void { - $validator = new IP(); - - // Assertions - $this->assertEquals($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), true); - $this->assertEquals($validator->isValid('109.67.204.101'), true); - $this->assertEquals($validator->isValid(23.5), false); - $this->assertEquals($validator->isValid('23.5'), false); - $this->assertEquals($validator->isValid(null), false); - $this->assertEquals($validator->isValid(true), false); - $this->assertEquals($validator->isValid(false), false); - $this->assertEquals($validator->getType(), 'string'); + $this->assertEquals($this->validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), true); + $this->assertEquals($this->validator->isValid('109.67.204.101'), true); + $this->assertEquals($this->validator->isValid(23.5), false); + $this->assertEquals($this->validator->isValid('23.5'), false); + $this->assertEquals($this->validator->isValid(null), false); + $this->assertEquals($this->validator->isValid(true), false); + $this->assertEquals($this->validator->isValid(false), false); + $this->assertEquals($this->validator->getType(), 'string'); } - public function testIsValidIPALL() + public function testIsValidIPALL(): void { - $validator = new IP(IP::ALL); + $this->validator = new IP(IP::ALL); // Assertions - $this->assertEquals($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), true); - $this->assertEquals($validator->isValid('109.67.204.101'), true); - $this->assertEquals($validator->isValid(23.5), false); - $this->assertEquals($validator->isValid('23.5'), false); - $this->assertEquals($validator->isValid(null), false); - $this->assertEquals($validator->isValid(true), false); - $this->assertEquals($validator->isValid(false), false); + $this->assertEquals($this->validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), true); + $this->assertEquals($this->validator->isValid('109.67.204.101'), true); + $this->assertEquals($this->validator->isValid(23.5), false); + $this->assertEquals($this->validator->isValid('23.5'), false); + $this->assertEquals($this->validator->isValid(null), false); + $this->assertEquals($this->validator->isValid(true), false); + $this->assertEquals($this->validator->isValid(false), false); } - public function testIsValidIPV4() + public function testIsValidIPV4(): void { - $validator = new IP(IP::V4); + $this->validator = new IP(IP::V4); // Assertions - $this->assertEquals($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), false); - $this->assertEquals($validator->isValid('109.67.204.101'), true); - $this->assertEquals($validator->isValid(23.5), false); - $this->assertEquals($validator->isValid('23.5'), false); - $this->assertEquals($validator->isValid(null), false); - $this->assertEquals($validator->isValid(true), false); - $this->assertEquals($validator->isValid(false), false); + $this->assertEquals($this->validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), false); + $this->assertEquals($this->validator->isValid('109.67.204.101'), true); + $this->assertEquals($this->validator->isValid(23.5), false); + $this->assertEquals($this->validator->isValid('23.5'), false); + $this->assertEquals($this->validator->isValid(null), false); + $this->assertEquals($this->validator->isValid(true), false); + $this->assertEquals($this->validator->isValid(false), false); } - public function testIsValidIPV6() + public function testIsValidIPV6(): void { - $validator = new IP(IP::V6); + $this->validator = new IP(IP::V6); // Assertions - $this->assertEquals($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), true); - $this->assertEquals($validator->isValid('109.67.204.101'), false); - $this->assertEquals($validator->isValid(23.5), false); - $this->assertEquals($validator->isValid('23.5'), false); - $this->assertEquals($validator->isValid(null), false); - $this->assertEquals($validator->isValid(true), false); - $this->assertEquals($validator->isValid(false), false); + $this->assertEquals($this->validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), true); + $this->assertEquals($this->validator->isValid('109.67.204.101'), false); + $this->assertEquals($this->validator->isValid(23.5), false); + $this->assertEquals($this->validator->isValid('23.5'), false); + $this->assertEquals($this->validator->isValid(null), false); + $this->assertEquals($this->validator->isValid(true), false); + $this->assertEquals($this->validator->isValid(false), false); } } diff --git a/tests/unit/Network/Validators/OriginTest.php b/tests/unit/Network/Validators/OriginTest.php index 414b380465..50dc0ad10a 100644 --- a/tests/unit/Network/Validators/OriginTest.php +++ b/tests/unit/Network/Validators/OriginTest.php @@ -1,13 +1,13 @@ */ -namespace Appwrite\Network\Validator; +namespace Tests\Unit\Network\Validators; +use Appwrite\Network\Validator\URL; use PHPUnit\Framework\TestCase; class URLTest extends TestCase diff --git a/tests/unit/OpenSSL/OpenSSLTest.php b/tests/unit/OpenSSL/OpenSSLTest.php index e3d5e59691..82386aaae0 100644 --- a/tests/unit/OpenSSL/OpenSSLTest.php +++ b/tests/unit/OpenSSL/OpenSSLTest.php @@ -1,6 +1,6 @@ object->setNamespace('appwritetest.usage'); $this->assertEquals('appwritetest.usage', $this->object->getNamespace()); } - public function testParams() + public function testParams(): void { $this->object ->setParam('projectId', 'appwrite_test') @@ -50,7 +50,7 @@ class StatsTest extends TestCase $this->assertEquals(null, $this->object->getParam('networkRequestSize')); } - public function testReset() + public function testReset(): void { $this->object ->setParam('projectId', 'appwrite_test') diff --git a/tests/unit/Task/Validator/CronTest.php b/tests/unit/Task/Validator/CronTest.php index fc0cfff679..f18d8621ba 100644 --- a/tests/unit/Task/Validator/CronTest.php +++ b/tests/unit/Task/Validator/CronTest.php @@ -1,6 +1,6 @@ assertEquals($this->object->isValid('0 2 * * *'), true); // execute at 2am daily $this->assertEquals($this->object->isValid('0 5,17 * * *'), true); // execute twice a day diff --git a/tests/unit/Template/TemplateTest.php b/tests/unit/Template/TemplateTest.php index 1103cbc9a2..1ca1595ca3 100644 --- a/tests/unit/Template/TemplateTest.php +++ b/tests/unit/Template/TemplateTest.php @@ -1,6 +1,6 @@ assertEquals($this->object->render(), 'Hello WORLD'); } - public function testParseURL() + public function testParseURL(): void { $url = $this->object->parseURL('https://appwrite.io/demo'); @@ -38,7 +38,7 @@ class TemplateTest extends TestCase $this->assertEquals($url['path'], '/demo'); } - public function testUnParseURL() + public function testUnParseURL(): void { $url = $this->object->parseURL('https://appwrite.io/demo'); @@ -49,18 +49,18 @@ class TemplateTest extends TestCase $this->assertEquals($this->object->unParseURL($url), 'http://example.com/new'); } - public function testMergeQuery() + public function testMergeQuery(): void { $this->assertEquals($this->object->mergeQuery('key1=value1&key2=value2', ['key1' => 'value3', 'key4' => 'value4']), 'key1=value3&key2=value2&key4=value4'); } - public function testFromCamelCaseToSnake() + public function testFromCamelCaseToSnake(): void { $this->assertEquals('app_write', Template::fromCamelCaseToSnake('appWrite')); $this->assertEquals('app_write', Template::fromCamelCaseToSnake('App Write')); } - public function testFromCamelCaseToDash() + public function testFromCamelCaseToDash(): void { $this->assertEquals('app-write', Template::fromCamelCaseToDash('appWrite')); $this->assertEquals('app-write', Template::fromCamelCaseToDash('App Write')); diff --git a/tests/unit/URL/URLTest.php b/tests/unit/URL/URLTest.php index 0348153a4b..fecaf25bca 100644 --- a/tests/unit/URL/URLTest.php +++ b/tests/unit/URL/URLTest.php @@ -1,13 +1,13 @@ assertEquals('', $url['query']); } - public function testUnparse() + public function testUnparse(): void { $url = URL::unparse([ 'scheme' => 'https', @@ -88,7 +88,7 @@ class URLTest extends TestCase $this->assertEquals('https://eldad:fux@appwrite.io/#bottom', $url); } - public function testParseQuery() + public function testParseQuery(): void { $result = URL::parseQuery('param1=value1¶m2=value2'); @@ -96,7 +96,7 @@ class URLTest extends TestCase $this->assertEquals(['param1' => 'value1', 'param2' => 'value2'], $result); } - public function testUnParseQuery() + public function testUnParseQuery(): void { $result = URL::unparseQuery(['param1' => 'value1', 'param2' => 'value2']); diff --git a/tests/unit/Utopia/Database/Validator/CustomIdTest.php b/tests/unit/Utopia/Database/Validator/CustomIdTest.php index 8b0ddf737a..7989c4f555 100644 --- a/tests/unit/Utopia/Database/Validator/CustomIdTest.php +++ b/tests/unit/Utopia/Database/Validator/CustomIdTest.php @@ -1,6 +1,6 @@ assertEquals($this->object->isValid('unique()'), true); $this->assertEquals($this->object->isValid('unique)'), false); diff --git a/tests/unit/Utopia/Lists.php b/tests/unit/Utopia/Lists.php new file mode 100644 index 0000000000..8f003cc2ae --- /dev/null +++ b/tests/unit/Utopia/Lists.php @@ -0,0 +1,28 @@ +addRule('singles', [ + 'type' => 'single', + 'default' => '', + 'array' => true + ]); + } + + public function getName(): string + { + return 'Lists'; + } + + public function getType(): string + { + return 'lists'; + } +} diff --git a/tests/unit/Utopia/Nested.php b/tests/unit/Utopia/Nested.php new file mode 100644 index 0000000000..2be0e85762 --- /dev/null +++ b/tests/unit/Utopia/Nested.php @@ -0,0 +1,31 @@ +addRule('lists', [ + 'type' => 'lists', + 'default' => '', + ]) + ->addRule('single', [ + 'type' => 'single', + 'default' => '' + ]); + } + + public function getName(): string + { + return 'Nested'; + } + + public function getType(): string + { + return 'nested'; + } +} diff --git a/tests/unit/Utopia/ResponseTest.php b/tests/unit/Utopia/ResponseTest.php index 246380dee0..e4389b3953 100644 --- a/tests/unit/Utopia/ResponseTest.php +++ b/tests/unit/Utopia/ResponseTest.php @@ -1,33 +1,144 @@ object = new Response(new SwooleResponse()); + $this->response = new Response(new SwooleResponse()); + $this->response->setModel(new Single()); + $this->response->setModel(new Lists()); + $this->response->setModel(new Nested()); } - public function testSetFilter() + public function testSetFilter(): void { - $this->assertEquals($this->object->hasFilter(), false); - $this->assertEquals($this->object->getFilter(), null); + $this->assertEquals($this->response->hasFilter(), false); + $this->assertEquals($this->response->getFilter(), null); $filter = new V11(); - $this->object->setFilter($filter); + $this->response->setFilter($filter); - $this->assertEquals($this->object->hasFilter(), true); - $this->assertEquals($this->object->getFilter(), $filter); + $this->assertEquals($this->response->hasFilter(), true); + $this->assertEquals($this->response->getFilter(), $filter); + } + + public function testResponseModel(): void + { + $output = $this->response->output(new Document([ + 'string' => 'lorem ipsum', + 'integer' => 123, + 'boolean' => true, + 'hidden' => 'secret', + ]), 'single'); + + $this->assertArrayHasKey('string', $output); + $this->assertArrayHasKey('integer', $output); + $this->assertArrayHasKey('boolean', $output); + $this->assertArrayNotHasKey('hidden', $output); + } + + public function testResponseModelRequired(): void + { + $output = $this->response->output(new Document([ + 'string' => 'lorem ipsum', + 'integer' => 123, + 'boolean' => true, + ]), 'single'); + + $this->assertArrayHasKey('string', $output); + $this->assertArrayHasKey('integer', $output); + $this->assertArrayHasKey('boolean', $output); + $this->assertArrayHasKey('required', $output); + $this->assertEquals('default', $output['required']); + } + + public function testResponseModelRequiredException(): void + { + $this->expectException(Exception::class); + $this->response->output(new Document([ + 'integer' => 123, + 'boolean' => true, + ]), 'single'); + } + + public function testResponseModelLists(): void + { + $output = $this->response->output(new Document([ + 'singles' => [ + new Document([ + 'string' => 'lorem ipsum', + 'integer' => 123, + 'boolean' => true, + 'hidden' => 'secret' + ]) + ], + 'hidden' => 'secret', + ]), 'lists'); + + $this->assertArrayHasKey('singles', $output); + $this->assertArrayNotHasKey('hidden', $output); + $this->assertCount(1, $output['singles']); + + $single = $output['singles'][0]; + $this->assertArrayHasKey('string', $single); + $this->assertArrayHasKey('integer', $single); + $this->assertArrayHasKey('boolean', $single); + $this->assertArrayHasKey('required', $single); + $this->assertArrayNotHasKey('hidden', $single); + } + + public function testResponseModelNested(): void + { + $output = $this->response->output(new Document([ + 'lists' => new Document([ + 'singles' => [ + new Document([ + 'string' => 'lorem ipsum', + 'integer' => 123, + 'boolean' => true, + 'hidden' => 'secret' + ]) + ], + 'hidden' => 'secret', + ]), + 'single' => new Document([ + 'string' => 'lorem ipsum', + 'integer' => 123, + 'boolean' => true, + 'hidden' => 'secret' + ]), + 'hidden' => 'secret', + ]), 'nested'); + + $this->assertArrayHasKey('lists', $output); + $this->assertArrayHasKey('single', $output); + $this->assertArrayNotHasKey('hidden', $output); + $this->assertCount(1, $output['lists']['singles']); + + + $single = $output['single']; + $this->assertArrayHasKey('string', $single); + $this->assertArrayHasKey('integer', $single); + $this->assertArrayHasKey('boolean', $single); + $this->assertArrayHasKey('required', $single); + $this->assertArrayNotHasKey('hidden', $single); + + $singleFromArray = $output['lists']['singles'][0]; + $this->assertArrayHasKey('string', $singleFromArray); + $this->assertArrayHasKey('integer', $singleFromArray); + $this->assertArrayHasKey('boolean', $singleFromArray); + $this->assertArrayHasKey('required', $single); + $this->assertArrayNotHasKey('hidden', $singleFromArray); } } diff --git a/tests/unit/Utopia/Single.php b/tests/unit/Utopia/Single.php new file mode 100644 index 0000000000..3bd09ef6da --- /dev/null +++ b/tests/unit/Utopia/Single.php @@ -0,0 +1,43 @@ +addRule('string', [ + 'type' => self::TYPE_STRING, + 'example' => '5e5ea5c16897e', + 'required' => true + ]) + ->addRule('integer', [ + 'type' => self::TYPE_INTEGER, + 'default' => 0, + 'example' => 1592981250, + ]) + ->addRule('boolean', [ + 'type' => self::TYPE_BOOLEAN, + 'default' => true, + 'example' => true, + ]) + ->addRule('required', [ + 'type' => self::TYPE_STRING, + 'default' => 'default', + 'required' => true + ]); + } + + public function getName(): string + { + return 'Single'; + } + + public function getType(): string + { + return 'single'; + } +}