Merge branch 'master' into feat-implement-transfers

This commit is contained in:
Bradley Schofield
2023-04-16 16:55:30 +09:00
3048 changed files with 59599 additions and 18864 deletions
@@ -0,0 +1,75 @@
<?php
namespace Appwrite\Auth\Validator;
/**
* Password.
*
* Validates user password string
*/
class PasswordDictionary extends Password
{
protected array $dictionary;
protected bool $enabled;
public function __construct(array $dictionary, bool $enabled = false)
{
$this->dictionary = $dictionary;
$this->enabled = $enabled;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return 'Password must be at least 8 characters and should not be one of the commonly used password.';
}
/**
* Is valid.
*
* @param mixed $value
*
* @return bool
*/
public function isValid($value): bool
{
if (!parent::isValid($value)) {
return false;
}
if ($this->enabled && array_key_exists($value, $this->dictionary)) {
return false;
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}
@@ -0,0 +1,77 @@
<?php
namespace Appwrite\Auth\Validator;
use Appwrite\Auth\Auth;
/**
* Password.
*
* Validates user password string
*/
class PasswordHistory extends Password
{
protected array $history;
protected string $algo;
protected array $algoOptions;
public function __construct(array $history, string $algo, array $algoOptions = [])
{
$this->history = $history;
$this->algo = $algo;
$this->algoOptions = $algoOptions;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return 'Password shouldn\'t be in the history.';
}
/**
* Is valid.
*
* @param mixed $value
*
* @return bool
*/
public function isValid($value): bool
{
foreach ($this->history as $hash) {
if (Auth::passwordVerify($value, $hash, $this->algo, $this->algoOptions)) {
return false;
}
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}
+5
View File
@@ -66,6 +66,7 @@ class Exception extends \Exception
public const USER_ANONYMOUS_CONSOLE_PROHIBITED = 'user_anonymous_console_prohibited';
public const USER_SESSION_ALREADY_EXISTS = 'user_session_already_exists';
public const USER_NOT_FOUND = 'user_not_found';
public const USER_PASSWORD_RECENTLY_USED = 'password_recently_used';
public const USER_EMAIL_ALREADY_EXISTS = 'user_email_already_exists';
public const USER_PASSWORD_MISMATCH = 'user_password_mismatch';
public const USER_SESSION_NOT_FOUND = 'user_session_not_found';
@@ -135,6 +136,8 @@ class Exception extends \Exception
public const DOCUMENT_INVALID_STRUCTURE = 'document_invalid_structure';
public const DOCUMENT_MISSING_PAYLOAD = 'document_missing_payload';
public const DOCUMENT_ALREADY_EXISTS = 'document_already_exists';
public const DOCUMENT_UPDATE_CONFLICT = 'document_update_conflict';
public const DOCUMENT_DELETE_RESTRICTED = 'document_delete_restricted';
/** Attribute */
public const ATTRIBUTE_NOT_FOUND = 'attribute_not_found';
@@ -145,6 +148,7 @@ class Exception extends \Exception
public const ATTRIBUTE_ALREADY_EXISTS = 'attribute_already_exists';
public const ATTRIBUTE_LIMIT_EXCEEDED = 'attribute_limit_exceeded';
public const ATTRIBUTE_VALUE_INVALID = 'attribute_value_invalid';
public const ATTRIBUTE_TYPE_INVALID = 'attribute_type_invalid';
/** Indexes */
public const INDEX_NOT_FOUND = 'index_not_found';
@@ -178,6 +182,7 @@ class Exception extends \Exception
public const DOMAIN_NOT_FOUND = 'domain_not_found';
public const DOMAIN_ALREADY_EXISTS = 'domain_already_exists';
public const DOMAIN_VERIFICATION_FAILED = 'domain_verification_failed';
public const DOMAIN_TARGET_INVALID = 'domain_target_invalid';
/** GraphqQL */
public const GRAPHQL_NO_QUERY = 'graphql_no_query';
+10 -4
View File
@@ -11,6 +11,7 @@ use GraphQL\Type\Definition\UnionType;
use Utopia\App;
use Utopia\Route;
use Utopia\Validator;
use Utopia\Validator\Nullable;
class Mapper
{
@@ -109,9 +110,6 @@ class Mapper
'type' => $parameterType,
'description' => $parameter['description'],
];
if ($parameter['optional']) {
$params[$name]['defaultValue'] = $parameter['default'];
}
}
$field = [
@@ -224,6 +222,12 @@ class Mapper
? \call_user_func_array($validator, $utopia->getResources($injections))
: $validator;
$isNullable = $validator instanceof Nullable;
if ($isNullable) {
$validator = $validator->getValidator();
}
switch ((!empty($validator)) ? $validator::class : '') {
case 'Appwrite\Network\Validator\CNAME':
case 'Appwrite\Task\Validator\Cron':
@@ -294,7 +298,7 @@ class Mapper
break;
}
if ($required) {
if ($required && !$isNullable) {
$type = Type::nonNull($type);
}
@@ -404,6 +408,8 @@ class Mapper
return static::model('AttributeBoolean');
case 'datetime':
return static::model('AttributeDatetime');
case 'relationship':
return static::model('AttributeRelationship');
}
throw new Exception('Unknown attribute implementation');
+30
View File
@@ -37,6 +37,8 @@ abstract class Migration
*/
protected Database $consoleDB;
protected \PDO $pdo;
/**
* @var array
*/
@@ -50,6 +52,8 @@ abstract class Migration
'1.1.2' => 'V16',
'1.2.0' => 'V17',
'1.2.1' => 'V17',
'1.3.0' => 'V18',
'1.3.1' => 'V18',
];
/**
@@ -98,6 +102,13 @@ abstract class Migration
return $this;
}
public function setPDO(\PDO $pdo): self
{
$this->pdo = $pdo;
return $this;
}
/**
* Iterates through every document.
*
@@ -331,6 +342,25 @@ abstract class Migration
);
}
/**
* Change a collection attribute's internal type
*
* @param string $collection
* @param string $attribute
* @param string $type
* @return void
*/
protected function changeAttributeInternalType(string $collection, string $attribute, string $type): void
{
$stmt = $this->pdo->prepare("ALTER TABLE `{$this->projectDB->getDefaultDatabase()}`.`_{$this->project->getInternalId()}_{$collection}` MODIFY `$attribute` $type;");
try {
$stmt->execute();
} catch (\Exception $e) {
Console::warning($e->getMessage());
}
}
/**
* Executes migration for set project.
*/
-5
View File
@@ -17,11 +17,6 @@ use Utopia\Database\Helpers\Role;
class V15 extends Migration
{
/**
* @var \PDO $pdo
*/
private $pdo;
/**
* @var array<string>
*/
+171
View File
@@ -0,0 +1,171 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
class V18 extends Migration
{
public function execute(): void
{
/**
* Disable SubQueries for Performance.
*/
foreach (['subQueryIndexes', 'subQueryPlatforms', 'subQueryDomains', 'subQueryKeys', 'subQueryWebhooks', 'subQuerySessions', 'subQueryTokens', 'subQueryMemberships', 'subQueryVariables'] as $name) {
Database::addFilter(
$name,
fn () => null,
fn () => []
);
}
Console::log('Migrating Project: ' . $this->project->getAttribute('name') . ' (' . $this->project->getId() . ')');
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
Console::info('Migrating Databases');
$this->migrateDatabases();
Console::info('Migrating Collections');
$this->migrateCollections();
Console::info('Migrating Documents');
$this->forEachDocument([$this, 'fixDocument']);
}
/**
* Migrate all Databases.
*
* @return void
* @throws \Exception
*/
private function migrateDatabases(): void
{
foreach ($this->documentsIterator('databases') as $database) {
$databaseTable = "database_{$database->getInternalId()}";
Console::info("Migrating Collections of {$database->getId()} ({$database->getAttribute('name')})");
foreach ($this->documentsIterator($databaseTable) as $collection) {
$collectionTable = "{$databaseTable}_collection_{$collection->getInternalId()}";
foreach ($collection['attributes'] ?? [] as $attribute) {
if ($attribute['type'] !== Database::VAR_FLOAT) {
continue;
}
$this->changeAttributeInternalType($collectionTable, $attribute['key'], 'DOUBLE');
}
}
}
}
/**
* Migrate all Collections.
*
* @return void
*/
private function migrateCollections(): void
{
foreach ($this->collections as $collection) {
$id = $collection['$id'];
Console::log("Migrating Collection \"{$id}\"");
foreach ($collection['attributes'] ?? [] as $attribute) {
if ($attribute['type'] !== Database::VAR_FLOAT) {
continue;
}
$this->changeAttributeInternalType($id, $attribute['$id'], 'DOUBLE');
}
switch ($id) {
case 'users':
try {
/**
* Create 'passwordHistory' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'passwordHistory');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'passwordHistory' from {$id}: {$th->getMessage()}");
}
break;
case 'teams':
try {
/**
* Create 'prefs' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'prefs');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'prefs' from {$id}: {$th->getMessage()}");
}
break;
case 'attributes':
try {
/**
* Create 'options' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'options');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'options' from {$id}: {$th->getMessage()}");
}
break;
default:
break;
}
usleep(50000);
}
}
/**
* Fix run on each document
*
* @param Document $document
* @return Document
*/
protected function fixDocument(Document $document): Document
{
switch ($document->getCollection()) {
case 'projects':
/**
* Bump version number.
*/
$document->setAttribute('version', '1.3.0');
/**
* Set default passwordHistory
*/
$document->setAttribute('auths', array_merge($document->getAttribute('auths', []), [
'passwordHistory' => 0,
'passwordDictionary' => false,
]));
break;
case 'users':
/**
* Default Password history
*/
$document->setAttribute('passwordHistory', []);
break;
case 'teams':
/**
* Default prefs
*/
$document->setAttribute('prefs', new \stdClass());
break;
case 'attributes':
/**
* Default options
*/
$document->setAttribute('options', new \stdClass());
break;
}
return $document;
}
}
@@ -8,6 +8,7 @@ use Appwrite\Utopia\Response\Model;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Validator;
use Utopia\Validator\Nullable;
class OpenAPI3 extends Format
{
@@ -170,6 +171,9 @@ class OpenAPI3 extends Format
'scope' => $route->getLabel('scope', ''),
'platforms' => $sdkPlatforms,
'packaging' => $route->getLabel('sdk.packaging', false),
'offline-model' => $route->getLabel('sdk.offline.model', ''),
'offline-key' => $route->getLabel('sdk.offline.key', ''),
'offline-response-key' => $route->getLabel('sdk.offline.response.key', '$id'),
],
];
@@ -281,6 +285,13 @@ class OpenAPI3 extends Format
}
}
$isNullable = $validator instanceof Nullable;
if ($isNullable) {
/** @var Nullable $validator */
$validator = $validator->getValidator();
}
switch ((!empty($validator)) ? \get_class($validator) : '') {
case 'Utopia\Validator\Text':
$node['schema']['type'] = $validator->getType();
@@ -446,6 +457,10 @@ class OpenAPI3 extends Format
if ($node['x-global'] ?? false) {
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-global'] = true;
}
if ($isNullable) {
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-nullable'] = true;
}
}
$url = \str_replace(':' . $name, '{' . $name . '}', $url);
@@ -8,6 +8,7 @@ use Appwrite\Utopia\Response\Model;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Validator;
use Utopia\Validator\Nullable;
class Swagger2 extends Format
{
@@ -171,6 +172,9 @@ class Swagger2 extends Format
'scope' => $route->getLabel('scope', ''),
'platforms' => $sdkPlatforms,
'packaging' => $route->getLabel('sdk.packaging', false),
'offline-model' => $route->getLabel('sdk.offline.model', ''),
'offline-key' => $route->getLabel('sdk.offline.key', ''),
'offline-response-key' => $route->getLabel('sdk.offline.response.key', '$id'),
],
];
@@ -282,6 +286,13 @@ class Swagger2 extends Format
}
}
$isNullable = $validator instanceof Nullable;
if ($isNullable) {
/** @var Nullable $validator */
$validator = $validator->getValidator();
}
switch ((!empty($validator)) ? \get_class($validator) : '') {
case 'Utopia\Validator\Text':
$node['type'] = $validator->getType();
@@ -445,6 +456,10 @@ class Swagger2 extends Format
$body['schema']['properties'][$name]['x-global'] = true;
}
if ($isNullable) {
$body['schema']['properties'][$name]['x-nullable'] = true;
}
if (\array_key_exists('items', $node)) {
$body['schema']['properties'][$name]['items'] = $node['items'];
}
@@ -10,26 +10,26 @@ use Utopia\Database\Query;
class IndexedQueries extends Queries
{
/**
* @var Document[]
* @var array<Document>
*/
protected $attributes = [];
protected array $attributes = [];
/**
* @var Document[]
* @var array<Document>
*/
protected $indexes = [];
protected array $indexes = [];
/**
* Expression constructor
*
* This Queries Validator filters indexes for only available indexes
*
* @param Document[] $attributes
* @param Document[] $indexes
* @param array<Document> $attributes
* @param array<Document> $indexes
* @param Base ...$validators
* @param bool $strict
* @throws \Exception
*/
public function __construct($attributes = [], $indexes = [], Base ...$validators)
public function __construct(array $attributes = [], array $indexes = [], Base ...$validators)
{
$this->attributes = $attributes;
@@ -55,33 +55,6 @@ class IndexedQueries extends Queries
parent::__construct(...$validators);
}
/**
* Check if indexed array $indexes matches $queries
*
* @param array $indexes
* @param array $queries
*
* @return bool
*/
protected function arrayMatch(array $indexes, array $queries): bool
{
// Check the count of indexes first for performance
if (count($queries) !== count($indexes)) {
return false;
}
// Sort them for comparison, the order is not important here anymore.
sort($indexes, SORT_STRING);
sort($queries, SORT_STRING);
// Only matching arrays will have equal diffs in both directions
if (array_diff_assoc($indexes, $queries) !== array_diff_assoc($queries, $indexes)) {
return false;
}
return true;
}
/**
* Is valid.
*
@@ -111,41 +84,26 @@ class IndexedQueries extends Queries
}
$grouped = Query::groupByType($queries);
/** @var Query[] */ $filters = $grouped['filters'];
/** @var string[] */ $orderAttributes = $grouped['orderAttributes'];
$filters = $grouped['filters'];
// Check filter queries for exact index match
if (count($filters) > 0) {
$filtersByAttribute = [];
foreach ($filters as $filter) {
$filtersByAttribute[$filter->getAttribute()] = $filter->getMethod();
}
foreach ($filters as $filter) {
if ($filter->getMethod() === Query::TYPE_SEARCH) {
$matched = false;
$found = null;
foreach ($this->indexes as $index) {
if (
$index->getAttribute('type') === Database::INDEX_FULLTEXT
&& $index->getAttribute('attributes') === [$filter->getAttribute()]
) {
$matched = true;
}
}
foreach ($this->indexes as $index) {
if ($this->arrayMatch($index->getAttribute('attributes'), array_keys($filtersByAttribute))) {
$found = $index;
if (!$matched) {
$this->message = "Searching by attribute \"{$filter->getAttribute()}\" requires a fulltext index.";
return false;
}
}
if (!$found) {
$this->message = 'Index not found: ' . implode(",", array_keys($filtersByAttribute));
return false;
}
// search method requires fulltext index
if (in_array(Query::TYPE_SEARCH, array_values($filtersByAttribute)) && $found['type'] !== Database::INDEX_FULLTEXT) {
$this->message = 'Search method requires fulltext index: ' . implode(",", array_keys($filtersByAttribute));
return false;
}
}
// Check order attributes for exact index match
$validator = new OrderAttributes($this->attributes, $this->indexes, true);
if (count($orderAttributes) > 0 && !$validator->isValid($orderAttributes)) {
$this->message = $validator->getDescription();
return false;
}
return true;
@@ -11,12 +11,12 @@ class Queries extends Validator
/**
* @var string
*/
protected $message = 'Invalid queries';
protected string $message = 'Invalid queries';
/**
* @var Base[]
* @var array<Base>
*/
protected $validators;
protected array $validators;
/**
* Queries constructor
@@ -57,41 +57,35 @@ class Queries extends Validator
if (!$query instanceof Query) {
try {
$query = Query::parse($query);
} catch (\Throwable $th) {
$this->message = 'Invalid query: ${query}';
} catch (\Throwable) {
$this->message = "Invalid query: {$query}";
return false;
}
}
$method = $query->getMethod();
$methodType = '';
switch ($method) {
case Query::TYPE_LIMIT:
$methodType = Base::METHOD_TYPE_LIMIT;
break;
case Query::TYPE_OFFSET:
$methodType = Base::METHOD_TYPE_OFFSET;
break;
case Query::TYPE_CURSORAFTER:
case Query::TYPE_CURSORBEFORE:
$methodType = Base::METHOD_TYPE_CURSOR;
break;
case Query::TYPE_ORDERASC:
case Query::TYPE_ORDERDESC:
$methodType = Base::METHOD_TYPE_ORDER;
break;
case Query::TYPE_EQUAL:
case Query::TYPE_NOTEQUAL:
case Query::TYPE_LESSER:
case Query::TYPE_LESSEREQUAL:
case Query::TYPE_GREATER:
case Query::TYPE_GREATEREQUAL:
case Query::TYPE_SEARCH:
$methodType = Base::METHOD_TYPE_FILTER;
break;
default:
break;
}
$methodType = match ($method) {
Query::TYPE_SELECT => Base::METHOD_TYPE_SELECT,
Query::TYPE_LIMIT => Base::METHOD_TYPE_LIMIT,
Query::TYPE_OFFSET => Base::METHOD_TYPE_OFFSET,
Query::TYPE_CURSORAFTER,
Query::TYPE_CURSORBEFORE => Base::METHOD_TYPE_CURSOR,
Query::TYPE_ORDERASC,
Query::TYPE_ORDERDESC => Base::METHOD_TYPE_ORDER,
Query::TYPE_EQUAL,
Query::TYPE_NOTEQUAL,
Query::TYPE_LESSER,
Query::TYPE_LESSEREQUAL,
Query::TYPE_GREATER,
Query::TYPE_GREATEREQUAL,
Query::TYPE_SEARCH,
Query::TYPE_IS_NULL,
Query::TYPE_IS_NOT_NULL,
Query::TYPE_BETWEEN,
Query::TYPE_STARTS_WITH,
Query::TYPE_ENDS_WITH => Base::METHOD_TYPE_FILTER,
default => '',
};
$methodIsValid = false;
foreach ($this->validators as $validator) {
@@ -8,6 +8,7 @@ use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Database\Validator\Query\Cursor;
use Appwrite\Utopia\Database\Validator\Query\Filter;
use Appwrite\Utopia\Database\Validator\Query\Order;
use Appwrite\Utopia\Database\Validator\Query\Select;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -19,6 +20,7 @@ class Base extends Queries
*
* @param string $collection
* @param string[] $allowedAttributes
* @throws \Exception
*/
public function __construct(string $collection, array $allowedAttributes)
{
@@ -65,6 +67,7 @@ class Base extends Queries
new Cursor(),
new Filter($attributes),
new Order($attributes),
new Select($attributes),
];
parent::__construct(...$validators);
@@ -0,0 +1,41 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Query\Select;
use Utopia\Database\Database;
class Document extends Queries
{
/**
* Expression constructor
*
* @param array $attributes
* @throws \Exception
*/
public function __construct(array $attributes)
{
$attributes[] = new \Utopia\Database\Document([
'key' => '$id',
'type' => Database::VAR_STRING,
'array' => false,
]);
$attributes[] = new \Utopia\Database\Document([
'key' => '$createdAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]);
$attributes[] = new \Utopia\Database\Document([
'key' => '$updatedAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]);
$validators = [
new Select($attributes),
];
parent::__construct(...$validators);
}
}
@@ -3,11 +3,12 @@
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\IndexedQueries;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Database\Validator\Query\Cursor;
use Appwrite\Utopia\Database\Validator\Query\Filter;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Database\Validator\Query\Order;
use Appwrite\Utopia\Database\Validator\Query\Select;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -17,7 +18,7 @@ class Documents extends IndexedQueries
* Expression constructor
*
* @param Document[] $attributes
* @param Document[] $indexes
* @throws \Exception
*/
public function __construct(array $attributes, array $indexes)
{
@@ -43,6 +44,7 @@ class Documents extends IndexedQueries
new Cursor(),
new Filter($attributes),
new Order($attributes),
new Select($attributes),
];
parent::__construct($attributes, $indexes, ...$validators);
@@ -12,6 +12,7 @@ abstract class Base extends Validator
public const METHOD_TYPE_CURSOR = 'cursor';
public const METHOD_TYPE_ORDER = 'order';
public const METHOD_TYPE_FILTER = 'filter';
public const METHOD_TYPE_SELECT = 'select';
/**
* @var string
@@ -18,6 +18,8 @@ class Filter extends Base
*/
protected $schema = [];
private int $maxValuesCount;
/**
* Query constructor
*
@@ -34,6 +36,18 @@ class Filter extends Base
protected function isValidAttribute($attribute): bool
{
if (\str_contains($attribute, '.')) {
// For relationships, just validate the top level.
// Utopia will validate each nested level during the recursive calls.
$attribute = \explode('.', $attribute)[0];
// TODO: Remove this when nested queries are supported
if (isset($this->schema[$attribute])) {
$this->message = 'Cannot query nested attribute on: ' . $attribute;
return false;
}
}
// Search for attribute in schema
if (!isset($this->schema[$attribute])) {
$this->message = 'Attribute not found in schema: ' . $attribute;
@@ -49,6 +63,12 @@ class Filter extends Base
return false;
}
if (\str_contains($attribute, '.')) {
// For relationships, just validate the top level.
// Utopia will validate each nested level during the recursive calls.
$attribute = \explode('.', $attribute)[0];
}
$attributeSchema = $this->schema[$attribute];
if (count($values) > $this->maxValuesCount) {
@@ -61,7 +81,9 @@ class Filter extends Base
foreach ($values as $value) {
$condition = match ($attributeType) {
Database::VAR_RELATIONSHIP => true,
Database::VAR_DATETIME => gettype($value) === Database::VAR_STRING,
Database::VAR_FLOAT => (gettype($value) === Database::VAR_FLOAT || gettype($value) === Database::VAR_INTEGER),
default => gettype($value) === $attributeType
};
@@ -99,6 +121,11 @@ class Filter extends Base
case Query::TYPE_GREATER:
case Query::TYPE_GREATEREQUAL:
case Query::TYPE_SEARCH:
case Query::TYPE_STARTS_WITH:
case Query::TYPE_ENDS_WITH:
case Query::TYPE_BETWEEN:
case Query::TYPE_IS_NULL:
case Query::TYPE_IS_NOT_NULL:
$values = $query->getValues();
return $this->isValidAttributeAndValues($attribute, $values);
@@ -15,7 +15,7 @@ class Limit extends Base
*
* @param int $maxLimit
*/
public function __construct(int $maxLimit = 100)
public function __construct(int $maxLimit = PHP_INT_MAX)
{
$this->maxLimit = $maxLimit;
}
@@ -2,7 +2,6 @@
namespace Appwrite\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Utopia\Database\Query;
use Utopia\Validator\Range;
@@ -15,7 +14,7 @@ class Offset extends Base
*
* @param int $maxOffset
*/
public function __construct(int $maxOffset = 5000)
public function __construct(int $maxOffset = PHP_INT_MAX)
{
$this->maxOffset = $maxOffset;
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Query;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
class Select extends Base
{
protected array $schema = [];
/**
* Query constructor
*
*/
public function __construct(array $attributes = [])
{
foreach ($attributes as $attribute) {
$this->schema[$attribute->getAttribute('key')] = $attribute->getArrayCopy();
}
}
/**
* Is valid.
*
* Returns true if method is TYPE_SELECT selections are valid
*
* Otherwise, returns false
*
* @param $query
* @return bool
*/
public function isValid($query): bool
{
/* @var $query Query */
if ($query->getMethod() !== Query::TYPE_SELECT) {
return false;
}
foreach ($query->getValues() as $attribute) {
if (\str_contains($attribute, '.')) {
// For relationships, just validate the top level.
// Utopia will validate each nested level during the recursive calls.
$attribute = \explode('.', $attribute)[0];
}
if (!isset($this->schema[$attribute]) && $attribute !== '*') {
$this->message = 'Attribute not found in schema: ' . $attribute;
return false;
}
}
return true;
}
public function getMethodType(): string
{
return self::METHOD_TYPE_SELECT;
}
}
+8
View File
@@ -30,6 +30,7 @@ use Appwrite\Utopia\Response\Model\AttributeEnum;
use Appwrite\Utopia\Response\Model\AttributeIP;
use Appwrite\Utopia\Response\Model\AttributeURL;
use Appwrite\Utopia\Response\Model\AttributeDatetime;
use Appwrite\Utopia\Response\Model\AttributeRelationship;
use Appwrite\Utopia\Response\Model\BaseList;
use Appwrite\Utopia\Response\Model\Collection;
use Appwrite\Utopia\Response\Model\Database;
@@ -44,6 +45,7 @@ use Appwrite\Utopia\Response\Model\Execution;
use Appwrite\Utopia\Response\Model\Build;
use Appwrite\Utopia\Response\Model\File;
use Appwrite\Utopia\Response\Model\Bucket;
use Appwrite\Utopia\Response\Model\ConsoleVariables;
use Appwrite\Utopia\Response\Model\Func;
use Appwrite\Utopia\Response\Model\Index;
use Appwrite\Utopia\Response\Model\JWT;
@@ -138,6 +140,7 @@ class Response extends SwooleResponse
public const MODEL_ATTRIBUTE_IP = 'attributeIp';
public const MODEL_ATTRIBUTE_URL = 'attributeUrl';
public const MODEL_ATTRIBUTE_DATETIME = 'attributeDatetime';
public const MODEL_ATTRIBUTE_RELATIONSHIP = 'attributeRelationship';
// Users
public const MODEL_ACCOUNT = 'account';
@@ -219,6 +222,9 @@ class Response extends SwooleResponse
public const MODEL_HEALTH_TIME = 'healthTime';
public const MODEL_HEALTH_ANTIVIRUS = 'healthAntivirus';
// Console
public const MODEL_CONSOLE_VARIABLES = 'consoleVariables';
// Transfers
public const MODEL_TRANSFER = 'transfer';
public const MODEL_TRANSFER_LIST = 'transferList';
@@ -308,6 +314,7 @@ class Response extends SwooleResponse
->setModel(new AttributeIP())
->setModel(new AttributeURL())
->setModel(new AttributeDatetime())
->setModel(new AttributeRelationship())
->setModel(new Index())
->setModel(new ModelDocument())
->setModel(new Log())
@@ -361,6 +368,7 @@ class Response extends SwooleResponse
->setModel(new UsageFunctions())
->setModel(new UsageFunction())
->setModel(new UsageProject())
->setModel(new ConsoleVariables())
->setModel(new Transfer())
->setModel(new Destination())
->setModel(new DestinationValidation())
+7 -2
View File
@@ -13,6 +13,7 @@ abstract class Model
public const TYPE_JSON = 'json';
public const TYPE_DATETIME = 'datetime';
public const TYPE_DATETIME_EXAMPLE = '2020-10-15T06:38:00.000+00:00';
public const TYPE_RELATIONSHIP = 'relationship';
/**
* @var bool
@@ -80,6 +81,7 @@ abstract class Model
*
* @param string $key
* @param array $options
* @return Model
*/
protected function addRule(string $key, array $options): self
{
@@ -98,7 +100,7 @@ abstract class Model
* If rule exists, it will be removed
*
* @param string $key
* @param array $options
* @return Model
*/
protected function removeRule(string $key): self
{
@@ -109,7 +111,10 @@ abstract class Model
return $this;
}
public function getRequired()
/**
* @return array
*/
public function getRequired(): array
{
$list = [];
@@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeBoolean extends Attribute
{
@@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeEmail extends Attribute
{
@@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeEnum extends Attribute
{
@@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeFloat extends Attribute
{
@@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeIP extends Attribute
{
@@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeInteger extends Attribute
{
@@ -4,7 +4,6 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Document;
class AttributeList extends Model
{
@@ -27,6 +26,7 @@ class AttributeList extends Model
Response::MODEL_ATTRIBUTE_URL,
Response::MODEL_ATTRIBUTE_IP,
Response::MODEL_ATTRIBUTE_DATETIME,
Response::MODEL_ATTRIBUTE_RELATIONSHIP,
Response::MODEL_ATTRIBUTE_STRING // needs to be last, since its condition would dominate any other string attribute
],
'description' => 'List of attributes.',
@@ -0,0 +1,76 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class AttributeRelationship extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('relatedCollection', [
'type' => self::TYPE_STRING,
'description' => 'The ID of the related collection.',
'default' => null,
'example' => 'collection',
])
->addRule('relationType', [
'type' => self::TYPE_STRING,
'description' => 'The type of the relationship.',
'default' => '',
'example' => 'oneToOne|oneToMany|manyToOne|manyToMany',
])
->addRule('twoWay', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Is the relationship two-way?',
'default' => false,
'example' => false,
])
->addRule('twoWayKey', [
'type' => self::TYPE_STRING,
'description' => 'The key of the two-way relationship.',
'default' => '',
'example' => 'string',
])
->addRule('onDelete', [
'type' => self::TYPE_STRING,
'description' => 'How deleting the parent document will propagate to child documents.',
'default' => 'restrict',
'example' => 'restrict|cascade|setNull',
])
->addRule('side', [
'type' => self::TYPE_STRING,
'description' => 'Whether this is the parent or child side of the relationship',
'default' => '',
'example' => 'parent|child',
])
;
}
public array $conditions = [
'type' => self::TYPE_RELATIONSHIP,
];
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AttributeRelationship';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ATTRIBUTE_RELATIONSHIP;
}
}
@@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeString extends Attribute
{
@@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeURL extends Attribute
{
@@ -69,6 +69,7 @@ class Collection extends Model
Response::MODEL_ATTRIBUTE_URL,
Response::MODEL_ATTRIBUTE_IP,
Response::MODEL_ATTRIBUTE_DATETIME,
Response::MODEL_ATTRIBUTE_RELATIONSHIP,
Response::MODEL_ATTRIBUTE_STRING, // needs to be last, since its condition would dominate any other string attribute
],
'description' => 'Collection attributes.',
@@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class ConsoleVariables extends Model
{
public function __construct()
{
$this
->addRule('_APP_DOMAIN_TARGET', [
'type' => self::TYPE_STRING,
'description' => 'CNAME target for your Appwrite custom domains.',
'default' => '',
'example' => 'appwrite.io',
])
->addRule('_APP_STORAGE_LIMIT', [
'type' => self::TYPE_INTEGER,
'description' => 'Maximum file size allowed for file upload in bytes.',
'default' => '',
'example' => '30000000',
])
->addRule('_APP_FUNCTIONS_SIZE_LIMIT', [
'type' => self::TYPE_INTEGER,
'description' => 'Maximum file size allowed for deployment in bytes.',
'default' => '',
'example' => '30000000',
])
->addRule('_APP_USAGE_STATS', [
'type' => self::TYPE_STRING,
'description' => 'Defines if usage stats are enabled. This value is set to \'enabled\' by default, to disable the usage stats set the value to \'disabled\'.',
'default' => '',
'example' => 'enabled',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Console Variables';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_CONSOLE_VARIABLES;
}
}
@@ -74,6 +74,18 @@ class Document extends Any
$document->removeAttribute('$internalId');
$document->removeAttribute('$collection'); // $collection is the internal collection ID
foreach ($document->getAttributes() as $attribute) {
if (\is_array($attribute)) {
foreach ($attribute as $subAttribute) {
if ($subAttribute instanceof DatabaseDocument) {
$this->filter($subAttribute);
}
}
} elseif ($attribute instanceof DatabaseDocument) {
$this->filter($attribute);
}
}
return $document;
}
}
@@ -120,6 +120,18 @@ class Project extends Model
'default' => 10,
'example' => 10,
])
->addRule('authPasswordHistory', [
'type' => self::TYPE_INTEGER,
'description' => 'Max allowed passwords in the history list per user. Max passwords limit allowed in history is 20. Use 0 for disabling password history.',
'default' => 0,
'example' => 5,
])
->addRule('authPasswordDictionary', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether or not to check user\'s password against most commonly used passwords.',
'default' => false,
'example' => true,
])
->addRule('providers', [
'type' => Response::MODEL_PROVIDER,
'description' => 'List of Providers.',
@@ -240,6 +252,8 @@ class Project extends Model
$document->setAttribute('authLimit', $authValues['limit'] ?? 0);
$document->setAttribute('authDuration', $authValues['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG);
$document->setAttribute('authSessionsLimit', $authValues['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT);
$document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0);
$document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false);
foreach ($auth as $index => $method) {
$key = $method['key'];
@@ -40,6 +40,12 @@ class Team extends Model
'default' => 0,
'example' => 7,
])
->addRule('prefs', [
'type' => Response::MODEL_PREFERENCES,
'description' => 'Team preferences as a key-value object',
'default' => new \stdClass(),
'example' => ['theme' => 'pink', 'timezone' => 'UTC'],
])
;
}
@@ -38,12 +38,14 @@ class User extends Model
->addRule('password', [
'type' => self::TYPE_STRING,
'description' => 'Hashed user password.',
'required' => false,
'default' => '',
'example' => '$argon2id$v=19$m=2048,t=4,p=3$aUZjLnliVWRINmFNTWMudg$5S+x+7uA31xFnrHFT47yFwcJeaP0w92L/4LdgrVRXxE',
])
->addRule('hash', [
'type' => self::TYPE_STRING,
'description' => 'Password hashing algorithm.',
'required' => false,
'default' => '',
'example' => 'argon2',
])
@@ -58,6 +60,7 @@ class User extends Model
Response::MODEL_ALGO_MD5, // keep least secure at the bottom. this order will be used in docs
],
'description' => 'Password hashing algorithm configuration.',
'required' => false,
'default' => [],
'example' => new \stdClass(),
'array' => false,