From 8f24d121f5752abebf4e5a097a0be19dc7385495 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Mon, 16 Aug 2021 19:21:00 -0400 Subject: [PATCH] Validate attributes and responses --- app/controllers/api/database.php | 186 ++++++++++++++++++++----------- 1 file changed, 120 insertions(+), 66 deletions(-) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 75eab37142..f3e766341e 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -1,6 +1,5 @@ getCollection(); $attributeId = $attribute->getId(); $type = $attribute->getAttribute('type', ''); $size = $attribute->getAttribute('size', 0); $required = $attribute->getAttribute('required', true); $default = $attribute->getAttribute('default', null); - $min = $attribute->getAttribute('min', null); - $max = $attribute->getAttribute('max', null); $signed = $attribute->getAttribute('signed', true); // integers are signed by default $array = $attribute->getAttribute('array', false); $format = $attribute->getAttribute('format', null); @@ -51,15 +58,6 @@ $attributesCallback = function ($attribute, $response, $dbForExternal, $database throw new Exception('Collection not found', 404); } - // TODO@kodumbeats how to depend on $size for Text validator length - // Ensure attribute default is within required size - if ($size > 0 && !\is_null($default)) { - $validator = new Text($size); - if (!$validator->isValid($default)) { - throw new Exception('Length of default attribute exceeds attribute size', 400); - } - } - if (!\is_null($format)) { $name = \json_decode($format, true)['name']; if (!Structure::hasFormat($name, $type)) { @@ -67,23 +65,6 @@ $attributesCallback = function ($attribute, $response, $dbForExternal, $database } } - if (!is_null($min) || !is_null($max)) { // Add range validator if either $min or $max is provided - switch ($type) { - case Database::VAR_INTEGER: - $min = (is_null($min)) ? -INF : \intval($min); - $max = (is_null($max)) ? INF : \intval($max); - $format = 'int-range'; - break; - case Database::VAR_FLOAT: - $min = (is_null($min)) ? -INF : \floatval($min); - $max = (is_null($max)) ? INF : \floatval($max); - $format = 'float-range'; - break; - default: - throw new Exception("Format range not available for {$type} attributes.", 400); - } - } - $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, $format, $filters); // Database->addAttributeInQueue() does not return a document @@ -97,8 +78,6 @@ $attributesCallback = function ($attribute, $response, $dbForExternal, $database 'size' => $size, 'required' => $required, 'default' => $default, - 'min' => $min, - 'max' => $max, 'signed' => $signed, 'array' => $array, 'format' => $format, @@ -117,7 +96,8 @@ $attributesCallback = function ($attribute, $response, $dbForExternal, $database ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic($attribute, Response::MODEL_ATTRIBUTE); + + return $attribute; }; App::post('/v1/database/collections') @@ -344,13 +324,19 @@ App::post('/v1/database/collections/:collectionId/attributes/string') ->inject('dbForExternal') ->inject('database') ->inject('audits') - ->action(function ($collectionId, $attributeId, $size, $required, $default, $array, $response, $dbForExternal, $database, $audits) use ($attributesCallback) { + ->action(function ($collectionId, $attributeId, $size, $required, $default, $array, $response, $dbForExternal, $database, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForExternal*/ /** @var Appwrite\Event\Event $database */ /** @var Appwrite\Event\Event $audits */ - return $attributesCallback(new Document([ + // Ensure attribute default is within required size + $validator = new Text($size); + if (!is_null($default) && !$validator->isValid($default)) { + throw new Exception($validator->getDescription(), 400); + } + + $attribute = attributesCallback(new Document([ '$collection' => $collectionId, '$id' => $attributeId, 'type' => Database::VAR_STRING, @@ -359,6 +345,8 @@ App::post('/v1/database/collections/:collectionId/attributes/string') 'default' => $default, 'array' => $array, ]), $response, $dbForExternal, $database, $audits); + + $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_STRING); }); App::post('/v1/database/collections/:collectionId/attributes/email') @@ -382,13 +370,19 @@ App::post('/v1/database/collections/:collectionId/attributes/email') ->inject('dbForExternal') ->inject('database') ->inject('audits') - ->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForExternal, $database, $audits) use ($attributesCallback) { + ->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForExternal, $database, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForExternal*/ /** @var Appwrite\Event\Event $database */ /** @var Appwrite\Event\Event $audits */ - return $attributesCallback(new Document([ + // Ensure attribute default is valid email + $validator = new Email(); + if (!is_null($default) && !$validator->isValid($default)) { + throw new Exception($validator->getDescription(), 400); + } + + $attribute = attributesCallback(new Document([ '$collection' => $collectionId, '$id' => $attributeId, 'type' => Database::VAR_STRING, @@ -396,8 +390,10 @@ App::post('/v1/database/collections/:collectionId/attributes/email') 'required' => $required, 'default' => $default, 'array' => $array, - 'format' => \json_encode(['name'=>'email']), + 'format' => \json_encode(['name'=> APP_DATABASE_ATTRIBUTE_EMAIL]), ]), $response, $dbForExternal, $database, $audits); + + $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_EMAIL); }); App::post('/v1/database/collections/:collectionId/attributes/ip') @@ -421,13 +417,19 @@ App::post('/v1/database/collections/:collectionId/attributes/ip') ->inject('dbForExternal') ->inject('database') ->inject('audits') - ->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForExternal, $database, $audits) use ($attributesCallback) { + ->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForExternal, $database, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForExternal*/ /** @var Appwrite\Event\Event $database */ /** @var Appwrite\Event\Event $audits */ - return $attributesCallback(new Document([ + // Ensure attribute default is valid IP address + $validator = new IP(); + if (!is_null($default) && !$validator->isValid($default)) { + throw new Exception($validator->getDescription(), 400); + } + + $attribute = attributesCallback(new Document([ '$collection' => $collectionId, '$id' => $attributeId, 'type' => Database::VAR_STRING, @@ -435,8 +437,10 @@ App::post('/v1/database/collections/:collectionId/attributes/ip') 'required' => $required, 'default' => $default, 'array' => $array, - 'format' => \json_encode(['name'=>'ip']), + 'format' => \json_encode(['name'=> APP_DATABASE_ATTRIBUTE_IP]), ]), $response, $dbForExternal, $database, $audits); + + $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_IP); }); App::post('/v1/database/collections/:collectionId/attributes/url') @@ -461,13 +465,19 @@ App::post('/v1/database/collections/:collectionId/attributes/url') ->inject('dbForExternal') ->inject('database') ->inject('audits') - ->action(function ($collectionId, $attributeId, $size, $required, $default, $array, $response, $dbForExternal, $database, $audits) use ($attributesCallback) { + ->action(function ($collectionId, $attributeId, $size, $required, $default, $array, $response, $dbForExternal, $database, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForExternal*/ /** @var Appwrite\Event\Event $database */ /** @var Appwrite\Event\Event $audits */ - return $attributesCallback(new Document([ + // Ensure attribute default is valid URL + $validator = new URL(); + if (!is_null($default) && !$validator->isValid($default)) { + throw new Exception($validator->getDescription(), 400); + } + + $attribute = attributesCallback(new Document([ '$collection' => $collectionId, '$id' => $attributeId, 'type' => Database::VAR_STRING, @@ -475,8 +485,10 @@ App::post('/v1/database/collections/:collectionId/attributes/url') 'required' => $required, 'default' => $default, 'array' => $array, - 'format' => \json_encode(['name'=>'url']), + 'format' => \json_encode(['name'=> APP_DATABASE_ATTRIBUTE_URL]), ]), $response, $dbForExternal, $database, $audits); + + $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_URL); }); App::post('/v1/database/collections/:collectionId/attributes/integer') @@ -502,13 +514,23 @@ App::post('/v1/database/collections/:collectionId/attributes/integer') ->inject('dbForExternal') ->inject('database') ->inject('audits') - ->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForExternal, $database, $audits) use ($attributesCallback) { + ->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForExternal, $database, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForExternal*/ /** @var Appwrite\Event\Event $database */ /** @var Appwrite\Event\Event $audits */ - return $attributesCallback(new Document([ + // Ensure attribute default is within range + $format = (\is_null($min) || \is_null($max)); // whether to apply range format + $min = \is_null($min) ? -INF : \intval($min); + $max = \is_null($max) ? INF : \intval($max); + $validator = new Range($min, $max, Database::VAR_INTEGER); + + if (!is_null($default) && !$validator->isValid($default)) { + throw new Exception($validator->getDescription(), 400); + } + + $attribute = attributesCallback(new Document([ '$collection' => $collectionId, '$id' => $attributeId, 'type' => Database::VAR_INTEGER, @@ -516,12 +538,14 @@ App::post('/v1/database/collections/:collectionId/attributes/integer') 'required' => $required, 'default' => $default, 'array' => $array, - 'format' => \json_encode([ - 'name'=>'int-range', + 'format' => ($format) ? \json_encode([ + 'name'=> APP_DATABASE_ATTRIBUTE_INT_RANGE, 'min' => $min, 'max' => $max, - ]), + ]) : null, ]), $response, $dbForExternal, $database, $audits); + + $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_INTEGER); }); App::post('/v1/database/collections/:collectionId/attributes/float') @@ -547,13 +571,23 @@ App::post('/v1/database/collections/:collectionId/attributes/float') ->inject('dbForExternal') ->inject('database') ->inject('audits') - ->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForExternal, $database, $audits) use ($attributesCallback) { + ->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForExternal, $database, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForExternal*/ /** @var Appwrite\Event\Event $database */ /** @var Appwrite\Event\Event $audits */ - return $attributesCallback(new Document([ + // Ensure attribute default is within range + $format = (\is_null($min) || \is_null($max)); // whether to apply range format + $min = \is_null($min) ? -INF : \floatval($min); + $max = \is_null($max) ? INF : \floatval($max); + $validator = new Range($min, $max, Database::VAR_FLOAT); + + if (!is_null($default) && !$validator->isValid($default)) { + throw new Exception($validator->getDescription(), 400); + } + + $attribute = attributesCallback(new Document([ '$collection' => $collectionId, '$id' => $attributeId, 'type' => Database::VAR_FLOAT, @@ -561,12 +595,14 @@ App::post('/v1/database/collections/:collectionId/attributes/float') 'size' => 0, 'default' => $default, 'array' => $array, - 'format' => \json_encode([ - 'name'=>'float-range', + 'format' => ($format) ? \json_encode([ + 'name'=> APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, 'min' => $min, 'max' => $max, - ]), + ]) : null, ]), $response, $dbForExternal, $database, $audits); + + $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_FLOAT); }); App::post('/v1/database/collections/:collectionId/attributes/boolean') @@ -590,13 +626,13 @@ App::post('/v1/database/collections/:collectionId/attributes/boolean') ->inject('dbForExternal') ->inject('database') ->inject('audits') - ->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForExternal, $database, $audits) use ($attributesCallback) { + ->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForExternal, $database, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForExternal*/ /** @var Appwrite\Event\Event $database */ /** @var Appwrite\Event\Event $audits */ - return $attributesCallback(new Document([ + $attribute = attributesCallback(new Document([ '$collection' => $collectionId, '$id' => $attributeId, 'type' => Database::VAR_BOOLEAN, @@ -605,6 +641,8 @@ App::post('/v1/database/collections/:collectionId/attributes/boolean') 'default' => $default, 'array' => $array, ]), $response, $dbForExternal, $database, $audits); + + $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_BOOLEAN); }); App::get('/v1/database/collections/:collectionId/attributes') @@ -682,8 +720,24 @@ App::get('/v1/database/collections/:collectionId/attributes/:attributeId') $attribute = new Document([\array_merge($attributes[$attributeIndex], [ 'collectionId' => $collectionId, ])]); + + $type = $attribute->getAttribute('type'); + $format = json_decode($attribute->getAttribute('format'), true); + + $model = match($type) { + Database::VAR_BOOLEAN => Response::MODEL_ATTRIBUTE_BOOLEAN, + Database::VAR_INTEGER => Response::MODEL_ATTRIBUTE_INTEGER, + Database::VAR_FLOAT => Response::MODEL_ATTRIBUTE_FLOAT, + Database::VAR_STRING => match($format['name']) { + APP_DATABASE_ATTRIBUTE_URL => Response::MODEL_ATTRIBUTE_EMAIL, + APP_DATABASE_ATTRIBUTE_IP => Response::MODEL_ATTRIBUTE_IP, + APP_DATABASE_ATTRIBUTE_URL => Response::MODEL_ATTRIBUTE_URL, + default => Response::MODEL_ATTRIBUTE_STRING, + }, + default => Response::MODEL_ATTRIBUTE, + }; - $response->dynamic($attribute, Response::MODEL_ATTRIBUTE); + $response->dynamic($attribute, $model); }); App::delete('/v1/database/collections/:collectionId/attributes/:attributeId')