mirror of
https://github.com/appwrite/appwrite
synced 2026-05-06 06:48:22 +00:00
Add more validation cases
This commit is contained in:
parent
7d26131019
commit
4de4b475a1
3 changed files with 217 additions and 40 deletions
|
|
@ -23,6 +23,7 @@ use Utopia\Database\Exception\Structure as StructureException;
|
|||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Index as IndexValidator;
|
||||
use Utopia\Database\Validator\Permissions;
|
||||
use Utopia\Database\Validator\Structure;
|
||||
use Utopia\Database\Validator\UID;
|
||||
|
|
@ -135,11 +136,17 @@ class Create extends Action
|
|||
throw $e;
|
||||
}
|
||||
|
||||
$indexLimit = $dbForProject->getLimitForIndexes();
|
||||
if (\count($indexes) > $indexLimit) {
|
||||
$dbForProject->deleteDocument($databaseKey, $collection->getId());
|
||||
throw new Exception($this->getLimitException(), "Cannot create more than $indexLimit indexes for a collection");
|
||||
}
|
||||
|
||||
$collectionIndexes = [];
|
||||
$indexDocuments = [];
|
||||
try {
|
||||
foreach ($indexes as $indexDef) {
|
||||
$idxDoc = $this->buildIndexDocument($database, $collection, $indexDef, $collectionAttributes);
|
||||
$idxDoc = $this->buildIndexDocument($database, $collection, $indexDef, $collectionAttributes, $dbForProject);
|
||||
$collectionIndexes[] = $idxDoc['collection'];
|
||||
$indexDocuments[] = $idxDoc['document'];
|
||||
}
|
||||
|
|
@ -260,6 +267,12 @@ class Create extends Action
|
|||
throw new Exception($this->getDefaultUnsupportedException(), 'Cannot set default value for array ' . $this->getContext() . 's');
|
||||
}
|
||||
|
||||
if (\in_array($type, Database::SPATIAL_TYPES)) {
|
||||
if (!$dbForProject->getAdapter()->getSupportForSpatialIndex()) {
|
||||
throw new Exception($this->getFormatUnsupportedException(), "Spatial attributes are not supported by the current database");
|
||||
}
|
||||
}
|
||||
|
||||
if ($type === Database::VAR_RELATIONSHIP) {
|
||||
$options['side'] = Database::RELATION_SIDE_PARENT;
|
||||
$relatedCollection = $dbForProject->getDocument('database_' . $database->getSequence(), $options['relatedCollection'] ?? '');
|
||||
|
|
@ -315,7 +328,7 @@ class Create extends Action
|
|||
*
|
||||
* @return array{collection: Document, document: Document}
|
||||
*/
|
||||
protected function buildIndexDocument(Document $database, Document $collection, array $indexDef, array $attributeDocuments): array
|
||||
protected function buildIndexDocument(Document $database, Document $collection, array $indexDef, array $attributeDocuments, Database $dbForProject): array
|
||||
{
|
||||
$key = $indexDef['key'];
|
||||
$type = $indexDef['type'];
|
||||
|
|
@ -380,6 +393,24 @@ class Create extends Action
|
|||
'orders' => $orders,
|
||||
]);
|
||||
|
||||
$indexValidator = new IndexValidator(
|
||||
$attributeDocuments,
|
||||
[],
|
||||
$dbForProject->getAdapter()->getMaxIndexLength(),
|
||||
$dbForProject->getAdapter()->getInternalIndexesKeys(),
|
||||
$dbForProject->getAdapter()->getSupportForIndexArray(),
|
||||
$dbForProject->getAdapter()->getSupportForSpatialIndexNull(),
|
||||
$dbForProject->getAdapter()->getSupportForSpatialIndexOrder(),
|
||||
$dbForProject->getAdapter()->getSupportForVectors(),
|
||||
$dbForProject->getAdapter()->getSupportForAttributes(),
|
||||
$dbForProject->getAdapter()->getSupportForMultipleFulltextIndexes(),
|
||||
$dbForProject->getAdapter()->getSupportForIdenticalIndexes()
|
||||
);
|
||||
|
||||
if (!$indexValidator->isValid($collectionDoc)) {
|
||||
throw new Exception($this->getInvalidIndexException(), $indexValidator->getDescription());
|
||||
}
|
||||
|
||||
return [
|
||||
'collection' => $collectionDoc,
|
||||
'document' => $document,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,12 @@
|
|||
namespace Appwrite\Utopia\Database\Validator;
|
||||
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Validator;
|
||||
use Utopia\Validator\Boolean as BooleanValidator;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Attributes extends Validator
|
||||
{
|
||||
|
|
@ -65,12 +69,12 @@ class Attributes extends Validator
|
|||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
if (!\is_array($value)) {
|
||||
$this->message = 'Attributes must be an array';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($value) > $this->maxAttributes) {
|
||||
if (\count($value) > $this->maxAttributes) {
|
||||
$this->message = 'Maximum of ' . $this->maxAttributes . ' attributes allowed';
|
||||
return false;
|
||||
}
|
||||
|
|
@ -79,7 +83,7 @@ class Attributes extends Validator
|
|||
$keys = [];
|
||||
|
||||
foreach ($value as $index => $attribute) {
|
||||
if (!is_array($attribute)) {
|
||||
if (!\is_array($attribute)) {
|
||||
$this->message = "Attribute at index $index must be an object";
|
||||
return false;
|
||||
}
|
||||
|
|
@ -108,6 +112,13 @@ class Attributes extends Validator
|
|||
}
|
||||
$keys[] = $attribute['key'];
|
||||
|
||||
// Check for reserved keys
|
||||
$reservedKeys = ['$id', '$createdAt', '$updatedAt', '$permissions', '$collection'];
|
||||
if (in_array($attribute['key'], $reservedKeys)) {
|
||||
$this->message = "Attribute key '" . $attribute['key'] . "' is reserved and cannot be used";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate type
|
||||
if (!in_array($attribute['type'], $this->supportedTypes)) {
|
||||
$this->message = "Invalid type for attribute '" . $attribute['key'] . "': " . $attribute['type'];
|
||||
|
|
@ -148,6 +159,12 @@ class Attributes extends Validator
|
|||
return false;
|
||||
}
|
||||
|
||||
// Validate signed only for integer/float types
|
||||
if (isset($attribute['signed']) && !in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_FLOAT])) {
|
||||
$this->message = "Attribute '" . $attribute['key'] . "': 'signed' can only be used with integer or float types";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate required and default conflict
|
||||
if (isset($attribute['required']) && $attribute['required'] === true && isset($attribute['default']) && $attribute['default'] !== null) {
|
||||
$this->message = "Attribute '" . $attribute['key'] . "' cannot have a default value when required is true";
|
||||
|
|
@ -160,20 +177,137 @@ class Attributes extends Validator
|
|||
return false;
|
||||
}
|
||||
|
||||
// Validate min/max range for integer/float
|
||||
if (isset($attribute['min']) && isset($attribute['max'])) {
|
||||
if (!in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_FLOAT])) {
|
||||
$this->message = "Attribute '" . $attribute['key'] . "': min/max can only be used with integer or float types";
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($attribute['min'] > $attribute['max']) {
|
||||
$this->message = "Attribute '" . $attribute['key'] . "': minimum value must be less than or equal to maximum value";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate default value matches attribute type
|
||||
if (isset($attribute['default'])) {
|
||||
switch ($attribute['type']) {
|
||||
case Database::VAR_STRING:
|
||||
if (!is_string($attribute['default'])) {
|
||||
$this->message = "Default value for string attribute '" . $attribute['key'] . "' must be a string";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate string size
|
||||
$size = $attribute['size'] ?? 0;
|
||||
if ($size > 0) {
|
||||
$textValidator = new Text($size, 0);
|
||||
if (!$textValidator->isValid($attribute['default'])) {
|
||||
$this->message = "Default value for attribute '" . $attribute['key'] . "' exceeds maximum size of $size characters";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Database::VAR_INTEGER:
|
||||
if (!is_int($attribute['default'])) {
|
||||
$this->message = "Default value for integer attribute '" . $attribute['key'] . "' must be an integer";
|
||||
return false;
|
||||
}
|
||||
// Validate within range if min/max specified
|
||||
if (isset($attribute['min']) || isset($attribute['max'])) {
|
||||
$min = $attribute['min'] ?? \PHP_INT_MIN;
|
||||
$max = $attribute['max'] ?? \PHP_INT_MAX;
|
||||
$rangeValidator = new Range($min, $max, Database::VAR_INTEGER);
|
||||
if (!$rangeValidator->isValid($attribute['default'])) {
|
||||
$this->message = "Default value for integer attribute '" . $attribute['key'] . "' must be between $min and $max";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Database::VAR_FLOAT:
|
||||
if (!is_float($attribute['default']) && !is_int($attribute['default'])) {
|
||||
$this->message = "Default value for float attribute '" . $attribute['key'] . "' must be a number";
|
||||
return false;
|
||||
}
|
||||
// Validate within range if min/max specified
|
||||
if (isset($attribute['min']) || isset($attribute['max'])) {
|
||||
$min = $attribute['min'] ?? -\PHP_FLOAT_MAX;
|
||||
$max = $attribute['max'] ?? \PHP_FLOAT_MAX;
|
||||
$rangeValidator = new Range($min, $max, Database::VAR_FLOAT);
|
||||
if (!$rangeValidator->isValid((float)$attribute['default'])) {
|
||||
$this->message = "Default value for float attribute '" . $attribute['key'] . "' must be between $min and $max";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Database::VAR_BOOLEAN:
|
||||
if (!is_bool($attribute['default'])) {
|
||||
$this->message = "Default value for boolean attribute '" . $attribute['key'] . "' must be a boolean";
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Database::VAR_DATETIME:
|
||||
if (!is_string($attribute['default'])) {
|
||||
$this->message = "Default value for datetime attribute '" . $attribute['key'] . "' must be a string in ISO 8601 format";
|
||||
return false;
|
||||
}
|
||||
// Basic datetime format validation
|
||||
$datetimeValidator = new DatetimeValidator();
|
||||
if (!$datetimeValidator->isValid($attribute['default'])) {
|
||||
$this->message = "Default value for datetime attribute '" . $attribute['key'] . "' must be in valid ISO 8601 format";
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate enum elements if format is enum
|
||||
if (isset($attribute['format']) && $attribute['format'] === APP_DATABASE_ATTRIBUTE_ENUM) {
|
||||
if (!isset($attribute['elements']) || !is_array($attribute['elements']) || empty($attribute['elements'])) {
|
||||
$this->message = "Attribute '" . $attribute['key'] . "' with enum format must have 'elements' array";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate each enum element
|
||||
foreach ($attribute['elements'] as $elementIndex => $element) {
|
||||
if (!is_string($element) || empty($element)) {
|
||||
$this->message = "Enum element at index $elementIndex for attribute '" . $attribute['key'] . "' must be a non-empty string";
|
||||
return false;
|
||||
}
|
||||
if (strlen($element) > Database::LENGTH_KEY) {
|
||||
$this->message = "Enum element at index $elementIndex for attribute '" . $attribute['key'] . "' exceeds maximum length of " . Database::LENGTH_KEY . " characters";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate default exists in elements
|
||||
if (isset($attribute['default']) && $attribute['default'] !== null) {
|
||||
if (!in_array($attribute['default'], $attribute['elements'], true)) {
|
||||
$this->message = "Default value for enum attribute '" . $attribute['key'] . "' must be one of the provided elements";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate relationship options
|
||||
if ($attribute['type'] === Database::VAR_RELATIONSHIP) {
|
||||
if (!isset($attribute['relatedCollection']) || empty($attribute['relatedCollection'])) {
|
||||
// Validate array cannot be true for relationship
|
||||
if (isset($attribute['array']) && $attribute['array'] === true) {
|
||||
$this->message = "Relationship attribute '" . $attribute['key'] . "' cannot be an array type";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate required fields for relationship
|
||||
if (empty($attribute['relatedCollection'])) {
|
||||
$this->message = "Relationship attribute '" . $attribute['key'] . "' must have 'relatedCollection'";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($attribute['relationType']) || !in_array($attribute['relationType'], [
|
||||
Database::RELATION_ONE_TO_ONE,
|
||||
Database::RELATION_ONE_TO_MANY,
|
||||
|
|
@ -191,7 +325,7 @@ class Attributes extends Validator
|
|||
}
|
||||
|
||||
// Validate twoWayKey if provided
|
||||
if (isset($attribute['twoWayKey']) && !empty($attribute['twoWayKey'])) {
|
||||
if (!empty($attribute['twoWayKey'])) {
|
||||
if (!$keyValidator->isValid($attribute['twoWayKey'])) {
|
||||
$this->message = "Invalid 'twoWayKey' for relationship attribute '" . $attribute['key'] . "': " . $keyValidator->getDescription();
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -70,95 +70,107 @@ class Indexes extends Validator
|
|||
$keyValidator = new Key();
|
||||
$keys = [];
|
||||
|
||||
foreach ($value as $index => $indexDef) {
|
||||
if (!is_array($indexDef)) {
|
||||
$this->message = "Index at position $index must be an object";
|
||||
foreach ($value as $i => $index) {
|
||||
if (!is_array($index)) {
|
||||
$this->message = "Index at position $i must be an object";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!isset($indexDef['key'])) {
|
||||
$this->message = "Index at position $index is missing required field 'key'";
|
||||
if (!isset($index['key'])) {
|
||||
$this->message = "Index at position $i is missing required field 'key'";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($indexDef['type'])) {
|
||||
$this->message = "Index at position $index is missing required field 'type'";
|
||||
if (!isset($index['type'])) {
|
||||
$this->message = "Index at position $i is missing required field 'type'";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($indexDef['attributes']) || !is_array($indexDef['attributes'])) {
|
||||
$this->message = "Index at position $index is missing required field 'attributes' (must be an array)";
|
||||
if (!isset($index['attributes']) || !is_array($index['attributes'])) {
|
||||
$this->message = "Index at position $i is missing required field 'attributes' (must be an array)";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate key
|
||||
if (!$keyValidator->isValid($indexDef['key'])) {
|
||||
$this->message = "Invalid key for index at position $index: " . $keyValidator->getDescription();
|
||||
if (!$keyValidator->isValid($index['key'])) {
|
||||
$this->message = "Invalid key for index at position $i: " . $keyValidator->getDescription();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for duplicate keys
|
||||
if (in_array($indexDef['key'], $keys)) {
|
||||
$this->message = "Duplicate index key: " . $indexDef['key'];
|
||||
if (in_array($index['key'], $keys)) {
|
||||
$this->message = "Duplicate index key: " . $index['key'];
|
||||
return false;
|
||||
}
|
||||
$keys[] = $indexDef['key'];
|
||||
$keys[] = $index['key'];
|
||||
|
||||
// Validate type
|
||||
if (!in_array($indexDef['type'], $this->supportedTypes)) {
|
||||
$this->message = "Invalid type for index '" . $indexDef['key'] . "': " . $indexDef['type'];
|
||||
if (!in_array($index['type'], $this->supportedTypes)) {
|
||||
$this->message = "Invalid type for index '" . $index['key'] . "': " . $index['type'];
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate attributes array
|
||||
if (empty($indexDef['attributes'])) {
|
||||
$this->message = "Index '" . $indexDef['key'] . "' must have at least one attribute";
|
||||
if (empty($index['attributes'])) {
|
||||
$this->message = "Index '" . $index['key'] . "' must have at least one attribute";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($indexDef['attributes']) > APP_LIMIT_ARRAY_PARAMS_SIZE) {
|
||||
$this->message = "Index '" . $indexDef['key'] . "' cannot have more than " . APP_LIMIT_ARRAY_PARAMS_SIZE . " attributes";
|
||||
if (count($index['attributes']) > APP_LIMIT_ARRAY_PARAMS_SIZE) {
|
||||
$this->message = "Index '" . $index['key'] . "' cannot have more than " . APP_LIMIT_ARRAY_PARAMS_SIZE . " attributes";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate each attribute in the index
|
||||
foreach ($indexDef['attributes'] as $attrIndex => $attr) {
|
||||
foreach ($index['attributes'] as $attrIndex => $attr) {
|
||||
if (!is_string($attr)) {
|
||||
$this->message = "Invalid attribute at position $attrIndex in index '" . $indexDef['key'] . "': must be a string";
|
||||
$this->message = "Invalid attribute at position $attrIndex in index '" . $index['key'] . "': must be a string";
|
||||
return false;
|
||||
}
|
||||
if (!$keyValidator->isValid($attr) && !in_array($attr, ['$id', '$createdAt', '$updatedAt'])) {
|
||||
$this->message = "Invalid attribute name '$attr' in index '" . $indexDef['key'] . "'";
|
||||
$this->message = "Invalid attribute name '$attr' in index '" . $index['key'] . "'";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate orders if provided
|
||||
if (isset($indexDef['orders'])) {
|
||||
if (!is_array($indexDef['orders'])) {
|
||||
$this->message = "Index '" . $indexDef['key'] . "' orders must be an array";
|
||||
if (isset($index['orders'])) {
|
||||
if (!is_array($index['orders'])) {
|
||||
$this->message = "Index '" . $index['key'] . "' orders must be an array";
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($indexDef['orders'] as $order) {
|
||||
// Validate orders array length matches attributes length
|
||||
if (count($index['orders']) !== count($index['attributes'])) {
|
||||
$this->message = "Index '" . $index['key'] . "': orders array length (" . count($index['orders']) . ") must match attributes array length (" . count($index['attributes']) . ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($index['orders'] as $order) {
|
||||
if ($order !== null && $order !== '' && !in_array($order, $this->supportedOrders)) {
|
||||
$this->message = "Invalid order '$order' in index '" . $indexDef['key'] . "'. Must be 'ASC' or 'DESC'";
|
||||
$this->message = "Invalid order '$order' in index '" . $index['key'] . "'. Must be 'ASC' or 'DESC'";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate lengths if provided
|
||||
if (isset($indexDef['lengths'])) {
|
||||
if (!is_array($indexDef['lengths'])) {
|
||||
$this->message = "Index '" . $indexDef['key'] . "' lengths must be an array";
|
||||
if (isset($index['lengths'])) {
|
||||
if (!is_array($index['lengths'])) {
|
||||
$this->message = "Index '" . $index['key'] . "' lengths must be an array";
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($indexDef['lengths'] as $length) {
|
||||
// MAJOR-7: Validate lengths array length matches attributes length
|
||||
if (count($index['lengths']) !== count($index['attributes'])) {
|
||||
$this->message = "Index '" . $index['key'] . "': lengths array length (" . count($index['lengths']) . ") must match attributes array length (" . count($index['attributes']) . ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($index['lengths'] as $length) {
|
||||
if ($length !== null && (!is_int($length) || $length < 0)) {
|
||||
$this->message = "Invalid length in index '" . $indexDef['key'] . "': must be a non-negative integer or null";
|
||||
$this->message = "Invalid length in index '" . $index['key'] . "': must be a non-negative integer or null";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue