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/account.php b/app/controllers/api/account.php index 6c90441afb..b748730f94 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1443,7 +1443,7 @@ App::patch('/v1/account/name') $user = $dbForProject->updateDocument('users', $user->getId(), $user ->setAttribute('name', $name) - ->setAttribute('search', implode(' ', [$user->getId(), $name, $user->getAttribute('email')]))); + ->setAttribute('search', implode(' ', [$user->getId(), $name, $user->getAttribute('email', ''), $user->getAttribute('phone', '')]))); $audits ->setResource('user/' . $user->getId()) @@ -1539,7 +1539,7 @@ App::patch('/v1/account/email') ->setAttribute('password', $isAnonymousUser ? Auth::passwordHash($password) : $user->getAttribute('password', '')) ->setAttribute('email', $email) ->setAttribute('emailVerification', false) // After this user needs to confirm mail again - ->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name'), $user->getAttribute('email')])); + ->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $email, $user->getAttribute('phone', '')])); try { $user = $dbForProject->updateDocument('users', $user->getId(), $user); @@ -1592,7 +1592,7 @@ App::patch('/v1/account/phone') $user ->setAttribute('phone', $phone) ->setAttribute('phoneVerification', false) // After this user needs to confirm phone number again - ->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name'), $user->getAttribute('email')])); + ->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $user->getAttribute('email', ''), $phone])); try { $user = $dbForProject->updateDocument('users', $user->getId(), $user); diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 24304a126b..ec83f7006a 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -882,11 +882,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string ->label('sdk.namespace', 'databases') ->label('sdk.method', 'createStringAttribute') ->label('sdk.description', '/docs/references/databases/create-string-attribute.md') - ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->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?') @@ -915,6 +915,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string 'array' => $array, ]), $response, $dbForProject, $database, $audits, $events, $usage); + $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_STRING); }); @@ -928,11 +929,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email' ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createEmailAttribute') ->label('sdk.description', '/docs/references/databases/create-email-attribute.md') - ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->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) @@ -955,6 +956,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email' 'format' => APP_DATABASE_ATTRIBUTE_EMAIL, ]), $response, $dbForProject, $database, $audits, $events, $usage); + $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_EMAIL); }); @@ -968,11 +970,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createEnumAttribute') ->label('sdk.description', '/docs/references/databases/create-attribute-enum.md') - ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->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?') @@ -1011,6 +1013,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') 'formatOptions' => ['elements' => $elements], ]), $response, $dbForProject, $database, $audits, $events, $usage); + $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_ENUM); }); @@ -1024,11 +1027,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createIpAttribute') ->label('sdk.description', '/docs/references/databases/create-ip-attribute.md') - ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->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) @@ -1051,6 +1054,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') 'format' => APP_DATABASE_ATTRIBUTE_IP, ]), $response, $dbForProject, $database, $audits, $events, $usage); + $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_IP); }); @@ -1064,11 +1068,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createUrlAttribute') ->label('sdk.description', '/docs/references/databases/create-url-attribute.md') - ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->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) @@ -1091,6 +1095,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') 'format' => APP_DATABASE_ATTRIBUTE_URL, ]), $response, $dbForProject, $database, $audits, $events, $usage); + $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_URL); }); @@ -1104,11 +1109,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createIntegerAttribute') ->label('sdk.description', '/docs/references/databases/create-integer-attribute.md') - ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->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) @@ -1160,6 +1165,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege $attribute->setAttribute('max', \intval($formatOptions['max'])); } + $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_INTEGER); }); @@ -1173,11 +1179,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float' ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createFloatAttribute') ->label('sdk.description', '/docs/references/databases/create-float-attribute.md') - ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->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) @@ -1232,6 +1238,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float' $attribute->setAttribute('max', \floatval($formatOptions['max'])); } + $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_FLOAT); }); @@ -1245,11 +1252,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createBooleanAttribute') ->label('sdk.description', '/docs/references/databases/create-boolean-attribute.md') - ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->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) @@ -1271,6 +1278,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea 'array' => $array, ]), $response, $dbForProject, $database, $audits, $events, $usage); + $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_BOOLEAN); }); @@ -1287,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') @@ -1337,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') @@ -1400,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') @@ -1491,11 +1499,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->label('sdk.namespace', 'databases') ->label('sdk.method', 'createIndex') ->label('sdk.description', '/docs/references/databases/create-index.md') - ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->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.') @@ -1633,7 +1641,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->setPayload($index->getArrayCopy()) ; - $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($index, Response::MODEL_INDEX); }); @@ -1650,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') @@ -1692,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') @@ -1743,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') @@ -1820,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) @@ -1941,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) @@ -2055,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') @@ -2358,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/functions.php b/app/controllers/api/functions.php index 21b827042d..ac49636407 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -453,7 +453,7 @@ App::post('/v1/functions/:functionId/deployments') ->label('sdk.description', '/docs/references/functions/create-deployment.md') ->label('sdk.packaging', true) ->label('sdk.request.type', 'multipart/form-data') - ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DEPLOYMENT) ->param('functionId', '', new UID(), 'Function ID.') @@ -622,7 +622,7 @@ App::post('/v1/functions/:functionId/deployments') ->setParam('functionId', $function->getId()) ->setParam('deploymentId', $deployment->getId()); - $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($deployment, Response::MODEL_DEPLOYMENT); }); @@ -906,7 +906,7 @@ App::post('/v1/functions/:functionId/executions') $event->trigger(); - $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); return $response->dynamic($execution, Response::MODEL_EXECUTION); } 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 383bcac149..79e1ac3d18 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/api/users.php b/app/controllers/api/users.php index 8845db4810..3bcc665330 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -506,7 +506,7 @@ App::patch('/v1/users/:userId/name') $user ->setAttribute('name', $name) - ->setAttribute('search', \implode(' ', [$user->getId(), $user->getAttribute('email'), $name])); + ->setAttribute('search', \implode(' ', [$user->getId(), $user->getAttribute('email', ''), $name, $user->getAttribute('phone', '')])); ; $user = $dbForProject->updateDocument('users', $user->getId(), $user); @@ -596,7 +596,7 @@ App::patch('/v1/users/:userId/email') $user ->setAttribute('email', $email) ->setAttribute('emailVerification', false) - ->setAttribute('search', \implode(' ', [$user->getId(), $email, $user->getAttribute('name')])) + ->setAttribute('search', \implode(' ', [$user->getId(), $email, $user->getAttribute('name', ''), $user->getAttribute('phone', '')])) ; try { @@ -646,6 +646,7 @@ App::patch('/v1/users/:userId/phone') $user ->setAttribute('phone', $number) ->setAttribute('phoneVerification', false) + ->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $user->getAttribute('email', ''), $number])); ; try { 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/comps/header.phtml b/app/views/console/comps/header.phtml index a1efacac56..09fe421c79 100644 --- a/app/views/console/comps/header.phtml +++ b/app/views/console/comps/header.phtml @@ -123,7 +123,7 @@ data-analytics-category="console/navigation" data-analytics-label="Users Link"> - Users + Authentication