diff --git a/app/config/cloud/contributors.json b/app/config/cloud/contributors.json deleted file mode 100644 index efa36c3cb..000000000 --- a/app/config/cloud/contributors.json +++ /dev/null @@ -1 +0,0 @@ -[1297371,1759475,6360216,20852629,5857008,19310830,9708641,26739219,23742426,22174310,77877486,1477010,29069505,62933155,45863583,42211,176163,58045728,7091609,31401437,1911066,66096031,22432834,43054051,79051850,22895284,100597998,91385411,49699333,7818620,11004008,54898623,27856297,33250853,49375670,30630364,50206,47822499,7481165,52557347,13681567,20492520,51369094,51821861,50256986,28431370,13692220,24373771,15938422,27148250,14805534,36137226,36071208,40193621,56179878,53281158,48085134,19358691,32528768,19733683,37348419,13719696,18309412,42106787,53618500,5306011,4408379,35486736,18586611,15062564,27011453,56090587,44623032,25107942,81188,33922418,16717633,32809211,69401139,25815659,4104127,20889958,3102249,12476526,2635185,28495651,50957556,2791280,31023616,20955511,835733,471907,69008866,44906587,45271396,76054330,45748739,41908747,23402178,47356149,8921,36632821,3668741,71702982,29725587,26303198,785830,51410502,60089135,2847349,43172716,48546075,8216525,41161981,51828039,4334997,80918302,38534289,47860497,80036766,41341387,49818988,58487637,29237374,46913894,5148229,4377199,29686102,26272249,75117692,4090256,27357868,33062368,38664231,46695441,743291,22633385,6368283,11593067,45097959,43381712,3284228,1972717,33012425,61755381,25405707,3144291,44156359,5497267,7423905,20716175,28586681,5975506,23518097,22187384,24191952,7768078,971530,51240166,55633427,34207400,77061285,11719476,35950229,66742927,34406802,802933,50047839,39148877,26602940,9693472,44273767,19362725,31209978,30521594,686298,6237394,35039730,42580581,36671793,8502129,8466918,81866614,54903252,28373606,13381361,72331432,30694270,5355510,8209163,86675510,9453522,42496309,56145786,2149381,393945,22084723,52621436,8872447,5575392,29619660,5547479,8852116,11151445,4717349,17725274,65615065,18537755,29292618,53044263,26597930,10313411,55998629,77529288,17404636,33729848,19422168,17916404,66111735,10329006,33502846,398230,81643826,105039167,47522632,91655303,9774614,10603631,284924,60857954,22885912,116552306,36103454,794606,27729549,1754457,36594527,13899668,78664749,47406531,27698189,5305654,53345517,6756412,29176704,77790497,47504894,37251540,52361778,52200375,1351177,66022861,73975409,25745396,31433638,37118134,43210805,20317665,11923975,47187468,16362381,36751163,14959876,32362757,65529384,52352285,74085816,3628535,43902034,75667593,26132902,466713,617558,96806061,33605526,11290524,43621940,12446314,17146935,55018955,56096559,79797000,40014186,34449936,58387964,23368207,42414965,44056349,33743031,12294525,58251592,33755729,9021747,932084,11428067,97121933,80122730,60894542,58583793,56051809,32243289,9934371,90936802,74638775,65399526,77604,64524822,47782249,43633955,42793632,55969597,72334601,82395440,92818577,60866204,65016769,23725091,45892107,55308895,86314140,82756460,47685349,63562160,73419211,1613216,50882624,91469717,46166258,60927324,41763158,83607556,2171717,50497814,39427312,61322830,40076195,39419448,29397545,55090719,53259730,20885012,64558515,69677883,55741087,72426535,46033036,68477507,30376878,73700530,25518600,29922887,36229969,47573417,40424087,49054503,16880385,22801227,72848513,64347914,814402,49149679,55017867,49481876,67067955,31439735,63878173,80322286,43746210,17332970,22702905,62476876,89888292,75736952,54059881,90782137,63588969,57111920,63330165,70258211,46371923,17837758,59364507,52203828,60147326,18481195,74822422,9803078,67309607,60410049,47360939,19922556,90848252,24698014,58886915,63579762,96648934,68523530,60518745,37345795,3929651,54993657,52061363,43019989,5787917,94674993,71593494,17143469,10288548,1830380,71510505,59124772,2335145,70798495,46474346,49263351,52062536,63151043,65248303,26071571,53626355,43992469,60785452,63467479,71837281,19490891,58628586,38250310,7271718,1110414,57227290,11625672,85063520,88965873,70096901,42029519,85363195,64471630,69353350,66922161,2221746,100430077,12299813,62690310,68282006,99184676,2450,22989561,22212661,59973863,11232940,76688923,22321353,77732479,84286404,32268377,34828782,23068019,57074509,24620969,20735983,26173690,75809937,49760818,86646105,52617262] \ No newline at end of file diff --git a/app/config/cloud/employees.json b/app/config/cloud/employees.json deleted file mode 100644 index 1575a0588..000000000 --- a/app/config/cloud/employees.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "eldad@appwrite.io": { "memberSince": "2020-10-15", "spot": "0", "gitHub": "eldadfux" }, - "christy@appwrite.io": { "memberSince": "2020-12-01", "spot": "1", "gitHub": "christyjacob4" }, - "torsten@appwrite.io": { "memberSince": "2020-12-28", "spot": "2", "gitHub": "torstendittmann" }, - "damodar@appwrite.io": { "memberSince": "2021-01-02", "spot": "3", "gitHub": "lohanidamodar" }, - "bradley@appwrite.io": { "memberSince": "2021-05-21", "spot": "5", "gitHub": "PineappleIOnic" }, - "jake@appwrite.io": { "memberSince": "2021-06-28", "spot": "6", "gitHub": "abnegate" }, - "sara@appwrite.io": { "memberSince": "2021-08-16", "spot": "7", "gitHub": "sarakaandorp" }, - "matej@appwrite.io": { "memberSince": "2021-08-23", "spot": "8", "gitHub": "meldiron" }, - "aditya@appwrite.io": { "memberSince": "2021-09-01", "spot": "9", "gitHub": "adityaoberai" }, - "wess@appwrite.io": { "memberSince": "2021-11-08", "spot": "12", "gitHub": "wess" }, - "may@appwrite.io": { "memberSince": "2021-11-28", "spot": "14", "gitHub": "MayEnder" }, - "elad@appwrite.io": { "memberSince": "2021-12-19", "spot": "15", "gitHub": "elad2412" }, - "vincent@appwrite.io": { "memberSince": "2022-01-01", "spot": "16", "gitHub": "gewenyu99" }, - "haimantika@appwrite.io": { "memberSince": "2022-04-01", "spot": "18", "gitHub": "Haimantika" }, - "chen@appwrite.io": { "memberSince": "2022-01-24", "spot": "19", "gitHub": "chenparnasa" }, - "tessa@appwrite.io": { "memberSince": "2022-04-21", "spot": "20", "gitHub": "tessamero" }, - "shimon@appwrite.io": { "memberSince": "2022-05-01", "spot": "23", "gitHub": "shimonewman" }, - "shmuel@appwrite.io": { "memberSince": "2022-03-20", "spot": "24", "gitHub": "fogelito" }, - "arman@appwrite.io": { "memberSince": "2022-04-04", "spot": "25", "gitHub": "ArmanNik" }, - "carla@appwrite.io": { "memberSince": "2022-04-04", "spot": "26", "gitHub": "heyCarla" }, - "emma@appwrite.io": { "memberSince": "2022-05-08", "spot": "27", "gitHub": "emmacarpagnano1" }, - "dylan@appwrite.io": { "memberSince": "2022-05-09", "spot": "28", "gitHub": "DylanG-64" }, - "steven@appwrite.io": { "memberSince": "2022-07-01", "spot": "30", "gitHub": "stnguyen90" }, - "jyoti@appwrite.io": { "memberSince": "2022-10-24", "spot": "31", "gitHub": "joeyouss" }, - "jade@appwrite.io": { "memberSince": "2022-10-31", "spot": "32", "gitHub": "dajebp" }, - "khushboo@appwrite.io": { "memberSince": "2021-11-08", "spot": "13", "gitHub": "vermakhushboo" }, - "thomas@appwrite.io": { "memberSince": "2022-11-03", "spot": "34", "gitHub": "TGlide" }, - "holly@appwrite.io": { "memberSince": "2022-12-05", "spot": "35", "gitHub": "HollyBarclay" }, - "laura@appwrite.io": { "memberSince": "2023-01-25", "spot": "36", "gitHub": "LauraDuRy" }, - "caio@appwrite.io": { "memberSince": "2023-03-27", "spot": "37", "gitHub": "ariascaio" }, - "luke@appwrite.io": { "memberSince": "2023-05-04", "spot": "38", "gitHub": "loks0n" } -} diff --git a/app/config/cloud/heroes.json b/app/config/cloud/heroes.json deleted file mode 100644 index 3fc81b3b3..000000000 --- a/app/config/cloud/heroes.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "bishwajeet.techmaster@gmail.com": { "memberSince": "2023-02-07" }, - "lucasaudart@gmail.com": { "memberSince": "2023-02-07" }, - "tkarmakar27112000@gmail.com": { "memberSince": "2023-02-07" }, - "alves.mckl@gmail.com": { "memberSince": "2023-02-07" }, - "dpns_nampula@rnlay.com": { "memberSince": "2023-02-07" }, - "a.stephensimon@outlook.com": { "memberSince": "2023-02-07" }, - "hidianapham@gmail.com": { "memberSince": "2023-02-07" } -} diff --git a/app/config/errors.php b/app/config/errors.php index c1c1b4a67..ba28e3324 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -313,6 +313,11 @@ return [ ], /** Storage */ + Exception::STORAGE_FILE_ALREADY_EXISTS => [ + 'name' => Exception::STORAGE_FILE_ALREADY_EXISTS, + 'description' => 'A storage file with the requested ID already exists.', + 'code' => 409, + ], Exception::STORAGE_FILE_NOT_FOUND => [ 'name' => Exception::STORAGE_FILE_NOT_FOUND, 'description' => 'The requested file could not be found.', diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 60b635ef3..bf70cd44c 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -900,7 +900,9 @@ App::get('/v1/account/identities') $queries[] = Query::equal('userInternalId', [$user->getInternalId()]); // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index aa7999b84..f2f28ff2a 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -26,6 +26,7 @@ use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\Restricted as RestrictedException; use Utopia\Database\Exception\Structure as StructureException; +use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -356,7 +357,7 @@ function updateAttribute( ); } - $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key, $attribute); + $attribute = $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key, $attribute); $dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $collection->getId()); $events @@ -469,7 +470,9 @@ App::get('/v1/databases') } // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); $cursor = reset($cursor); if ($cursor) { $databaseId = $cursor->getValue(); @@ -718,7 +721,7 @@ App::post('/v1/databases/:databaseId/collections') $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); - if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } @@ -779,7 +782,7 @@ App::get('/v1/databases/:databaseId/collections') $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); - if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } @@ -790,7 +793,9 @@ App::get('/v1/databases/:databaseId/collections') } // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ @@ -833,7 +838,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId') $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); - if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } @@ -969,7 +974,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); - if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } @@ -1033,7 +1038,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId') $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); - if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } @@ -1645,11 +1650,18 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') $queries = Query::parseQueries($queries); - \array_push($queries, Query::equal('collectionId', [$collectionId]), Query::equal('databaseId', [$databaseId])); + \array_push( + $queries, + Query::equal('collectionId', [$collectionId]), + Query::equal('databaseId', [$databaseId]) + ); // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); - $cursor = reset($cursor); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); + + $cursor = \reset($cursor); if ($cursor) { $attributeId = $cursor->getValue(); @@ -1659,17 +1671,22 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') Query::equal('key', [$attributeId]), Query::limit(1), ])); + if (empty($cursorDocument) || $cursorDocument[0]->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Attribute '{$attributeId}' for the 'cursor' value not found."); } + $cursor->setValue($cursorDocument[0]); } - $filterQueries = Query::groupByType($queries)['filters']; + $filters = Query::groupByType($queries)['filters']; + + $attributes = $dbForProject->find('attributes', $queries); + $total = $dbForProject->count('attributes', $filters, APP_LIMIT_COUNT); $response->dynamic(new Document([ - 'total' => $dbForProject->count('attributes', $filterQueries, APP_LIMIT_COUNT), - 'attributes' => $dbForProject->find('attributes', $queries), + 'attributes' => $attributes, + 'total' => $total, ]), Response::MODEL_ATTRIBUTE_LIST); }); @@ -2474,7 +2491,9 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') \array_push($queries, Query::equal('collectionId', [$collectionId]), Query::equal('databaseId', [$databaseId])); // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); $cursor = reset($cursor); if ($cursor) { @@ -2647,16 +2666,17 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); - if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); - if ($collection->isEmpty() || !$collection->getAttribute('enabled')) { - if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } + if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); } $allowedPermissions = [ @@ -2679,8 +2699,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') } // Users can only manage their own roles, API keys and Admin users can manage any - $roles = Authorization::getRoles(); - if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) { + if (!$isAPIKey && !$isPrivilegedUser) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { $permission = Permission::parse($permission); @@ -2693,7 +2712,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') $permission->getDimension() ))->toString(); if (!Authorization::isRole($role)) { - throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); + throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', Authorization::getRoles()) . ')'); } } } @@ -2861,40 +2880,30 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') ->inject('dbForProject') ->inject('mode') ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode) { - $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); - if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); - if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { - if (!$collection->getAttribute('documentSecurity', false)) { - $validator = new Authorization(Database::PERMISSION_READ); - if (!$validator->isValid($collection->getRead())) { - $collection = new Document(); - } - } - } - - if ($collection->isEmpty() || !$collection->getAttribute('enabled')) { + if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } - // Validate queries - $queriesValidator = new Documents($collection->getAttribute('attributes'), $collection->getAttribute('indexes')); - $validQueries = $queriesValidator->isValid($queries); - if (!$validQueries) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $queriesValidator->getDescription()); - } - $queries = Query::parseQueries($queries); // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); - $cursor = reset($cursor); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); + + $cursor = \reset($cursor); + if ($cursor) { $documentId = $cursor->getValue(); @@ -2907,13 +2916,19 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') $cursor->setValue($cursorDocument); } - $filterQueries = Query::groupByType($queries)['filters']; + $filters = Query::groupByType($queries)['filters']; - $documents = $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries); - $total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $filterQueries, APP_LIMIT_COUNT); + try { + $documents = $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries); + $total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $filters, APP_LIMIT_COUNT); + } catch (AuthorizationException) { + throw new Exception(Exception::USER_UNAUTHORIZED); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $e->getMessage()); + } // Add $collectionId and $databaseId for all documents - $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database): bool { + $processDocument = (function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database): bool { if ($document->isEmpty()) { return false; } @@ -2958,12 +2973,11 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') } return true; - }; + }); - // The linter is forcing this indentation - foreach ($documents as $document) { - $processDocument($collection, $document); - } + foreach ($documents as $document) { + $processDocument($collection, $document); + } $response->dynamic(new Document([ 'total' => $total, @@ -2993,31 +3007,30 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->inject('dbForProject') ->inject('mode') ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode) { - $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); - if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); - if ($collection->isEmpty() || !$collection->getAttribute('enabled')) { - if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } - } - - // Validate queries - $queriesValidator = new DocumentQueriesValidator($collection->getAttribute('attributes')); - $validQueries = $queriesValidator->isValid($queries); - if (!$validQueries) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $queriesValidator->getDescription()); + if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); } $queries = Query::parseQueries($queries); - $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId, $queries); + try { + $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId, $queries); + } catch (AuthorizationException) { + throw new Exception(Exception::USER_UNAUTHORIZED); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $e->getMessage()); + } if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); @@ -3204,16 +3217,17 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); - if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); - if ($collection->isEmpty() || !$collection->getAttribute('enabled')) { - if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } + if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); } // Read permission should not be required for update @@ -3233,7 +3247,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum // Users can only manage their own roles, API keys and Admin users can manage any $roles = Authorization::getRoles(); - if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles) && !\is_null($permissions)) { + if (!$isAPIKey && !$isPrivilegedUser && !\is_null($permissions)) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { $permission = Permission::parse($permission); @@ -3256,10 +3270,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum $permissions = $document->getPermissions() ?? []; } - $data = \array_merge($document->getArrayCopy(), $data); // Merge existing data with new data - $data['$collection'] = $document->getAttribute('$collection'); // Make sure user doesn't switch collectionID - $data['$createdAt'] = $document->getCreatedAt(); // Make sure user doesn't switch createdAt - $data['$id'] = $document->getId(); // Make sure user doesn't switch document unique ID + $data['$id'] = $documentId; $data['$permissions'] = $permissions; $newDocument = new Document($data); @@ -3422,19 +3433,19 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->inject('deletes') ->inject('mode') ->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $events, Delete $deletes, string $mode) { - $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); - if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); - if ($collection->isEmpty() || !$collection->getAttribute('enabled')) { - if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } + if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); } // Read permission should not be required for delete @@ -3444,68 +3455,18 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu throw new Exception(Exception::DOCUMENT_NOT_FOUND); } - $checkPermissions = function (Document $collection, Document $document) use (&$checkPermissions, $dbForProject, $database) { - $documentSecurity = $collection->getAttribute('documentSecurity', false); - $validator = new Authorization(Database::PERMISSION_DELETE); - - $valid = $validator->isValid($collection->getDelete()); - if (!$documentSecurity && !$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } - - $valid = $valid || $validator->isValid($document->getDelete()); - if ($documentSecurity && !$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } - - $relationships = \array_filter( - $collection->getAttribute('attributes', []), - fn($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP - ); - - foreach ($relationships as $relationship) { - $related = $document->getAttribute($relationship->getAttribute('key')); - - if (empty($related)) { - continue; - } - if (!\is_array($related)) { - $related = [$related]; - } - - $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( - fn() => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) - ); - - foreach ($related as $relation) { - if ( - $relation instanceof Document - && $relationship->getAttribute('onDelete') === Database::RELATION_MUTATE_CASCADE - ) { - $checkPermissions($relatedCollection, $relation); - } - } - } - }; - - $checkPermissions($collection, $document); - - Authorization::skip(fn() => $dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $database, $collection, $documentId) { + $dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $database, $collection, $documentId) { try { $dbForProject->deleteDocument( 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId ); + } catch (AuthorizationException) { + throw new Exception(Exception::USER_UNAUTHORIZED); } catch (RestrictedException) { throw new Exception(Exception::DOCUMENT_DELETE_RESTRICTED); } - })); - - $dbForProject->deleteCachedDocument( - 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), - $documentId - ); + }); // Add $collectionId and $databaseId for all documents $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 81500f1d2..9aa7c326b 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -138,7 +138,9 @@ App::get('/v1/functions') } // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ @@ -778,7 +780,9 @@ App::get('/v1/functions/:functionId/deployments') $queries[] = Query::equal('resourceType', ['functions']); // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ @@ -998,13 +1002,13 @@ App::post('/v1/functions/:functionId/executions') ->inject('queueForFunctions') ->inject('queueForUsage') ->action(function (string $functionId, string $data, bool $async, Response $response, Document $project, Database $dbForProject, Document $user, Event $events, string $mode, Func $queueForFunctions, Usage $queueForUsage) { - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - if ($function->isEmpty() || !$function->getAttribute('enabled')) { - if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); } $runtimes = Config::getParam('runtimes', []); @@ -1192,13 +1196,13 @@ App::get('/v1/functions/:functionId/executions') ->inject('dbForProject') ->inject('mode') ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - if ($function->isEmpty() || !$function->getAttribute('enabled')) { - if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); } $queries = Query::parseQueries($queries); @@ -1211,7 +1215,9 @@ App::get('/v1/functions/:functionId/executions') $queries[] = Query::equal('functionId', [$function->getId()]); // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ @@ -1264,13 +1270,13 @@ App::get('/v1/functions/:functionId/executions/:executionId') ->inject('dbForProject') ->inject('mode') ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, string $mode) { - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - if ($function->isEmpty() || !$function->getAttribute('enabled')) { - if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); } $execution = $dbForProject->getDocument('executions', $executionId); diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 8d961a045..841946311 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -380,7 +380,9 @@ App::get('/v1/migrations') } // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index aa70e389d..71ca84f7d 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -245,7 +245,9 @@ App::get('/v1/projects') } // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index c34515b5e..3e656b9eb 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -1,6 +1,7 @@ getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ @@ -354,10 +357,12 @@ App::post('/v1/storage/buckets/:bucketId/files') ->inject('deviceFiles') ->inject('deviceLocal') ->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } @@ -490,13 +495,20 @@ App::post('/v1/storage/buckets/:bucketId/files') $metadata = ['content_type' => $deviceLocal->getFileMimeType($fileTmpName)]; if (!$file->isEmpty()) { $chunks = $file->getAttribute('chunksTotal', 1); + $uploaded = $file->getAttribute('chunksUploaded', 0); $metadata = $file->getAttribute('metadata', []); + if ($chunk === -1) { $chunk = $chunks; } + + if ($uploaded === $chunks) { + throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS); + } } $chunksUploaded = $deviceFiles->upload($fileTmpName, $path, $chunk, $chunks, $metadata); + if (empty($chunksUploaded)) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed uploading file'); } @@ -682,10 +694,12 @@ App::get('/v1/storage/buckets/:bucketId/files') ->inject('dbForProject') ->inject('mode') ->action(function (string $bucketId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } @@ -703,7 +717,9 @@ App::get('/v1/storage/buckets/:bucketId/files') } // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ @@ -756,10 +772,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId') ->inject('dbForProject') ->inject('mode') ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } @@ -826,7 +844,10 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } @@ -970,7 +991,10 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } @@ -1105,10 +1129,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') ->inject('mode') ->inject('deviceFiles') ->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, string $mode, Device $deviceFiles) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } @@ -1262,10 +1288,12 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') ->inject('mode') ->inject('events') ->action(function (string $bucketId, string $fileId, ?string $name, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $events) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } @@ -1368,7 +1396,10 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $events, string $mode, Device $deviceFiles, Delete $deletes) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index e03e481d7..99783d566 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -153,7 +153,9 @@ App::get('/v1/teams') } // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ @@ -649,7 +651,9 @@ App::get('/v1/teams/:teamId/memberships') $queries[] = Query::equal('teamId', [$teamId]); // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index e3fcca958..03ac414cd 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -396,7 +396,9 @@ App::get('/v1/users') } // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ @@ -656,7 +658,9 @@ App::get('/v1/users/identities') } // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + }); $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 2bbac4181..f35234113 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -282,7 +282,10 @@ App::init() $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } diff --git a/app/init.php b/app/init.php index 9741c72ed..b0f9e7fd5 100644 --- a/app/init.php +++ b/app/init.php @@ -286,7 +286,7 @@ Database::addFilter( return $value; }, function (mixed $value, Document $attribute) { - $formatOptions = json_decode($attribute->getAttribute('formatOptions', '[]'), true); + $formatOptions = \json_decode($attribute->getAttribute('formatOptions', '[]'), true); if (isset($formatOptions['elements'])) { $attribute->setAttribute('elements', $formatOptions['elements']); } @@ -356,7 +356,7 @@ Database::addFilter( ->find('indexes', [ Query::equal('collectionInternalId', [$document->getInternalId()]), Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), - Query::limit(64), + Query::limit($database->getLimitForIndexes()), ]); } ); @@ -1292,22 +1292,22 @@ App::setResource('schema', function ($utopia, $dbForProject) { }, ['utopia', 'dbForProject']); App::setResource('contributors', function () { - $path = 'app/config/cloud/contributors.json'; + $path = 'app/config/contributors.json'; $list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; return $list; -}, []); +}); App::setResource('employees', function () { - $path = 'app/config/cloud/employees.json'; + $path = 'app/config/employees.json'; $list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; return $list; -}, []); +}); App::setResource('heroes', function () { - $path = 'app/config/cloud/heroes.json'; + $path = 'app/config/heroes.json'; $list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; return $list; -}, []); +}); App::setResource('requestTimestamp', function ($request) { //TODO: Move this to the Request class itself diff --git a/composer.json b/composer.json index 2fd93cf51..4918b68bb 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", "utopia-php/database": "0.42.*", - "utopia-php/domains": "1.1.*", + "utopia-php/domains": "0.3.*", "utopia-php/dsn": "0.1.*", "utopia-php/framework": "0.28.*", "utopia-php/image": "0.5.*", diff --git a/composer.lock b/composer.lock index 5aed40268..2427078eb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2098172fc4b71eb0d41dcdbfea2f5061", + "content-hash": "3c637b7058e55050f09ff2af72c11fdb", "packages": [ { "name": "adhocore/jwt", @@ -1557,19 +1557,20 @@ }, { "name": "utopia-php/database", - "version": "0.42.1", + "version": "0.42.3", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "9ff69a9b9eadc581771798833d423829c9d8cc90" + "reference": "ab0e2f8ad46884f69b354cd8ee84a1a75fee26d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/9ff69a9b9eadc581771798833d423829c9d8cc90", - "reference": "9ff69a9b9eadc581771798833d423829c9d8cc90", + "url": "https://api.github.com/repos/utopia-php/database/zipball/ab0e2f8ad46884f69b354cd8ee84a1a75fee26d1", + "reference": "ab0e2f8ad46884f69b354cd8ee84a1a75fee26d1", "shasum": "" }, "require": { + "ext-mbstring": "*", "ext-pdo": "*", "php": ">=8.0", "utopia-php/cache": "0.8.*", @@ -1607,29 +1608,31 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.42.1" + "source": "https://github.com/utopia-php/database/tree/0.42.3" }, - "time": "2023-08-14T16:09:09+00:00" + "time": "2023-08-22T02:15:28+00:00" }, { "name": "utopia-php/domains", - "version": "v1.1.0", + "version": "0.3.2", "source": { "type": "git", "url": "https://github.com/utopia-php/domains.git", - "reference": "1665e1d9932afa3be63b5c1e0dcfe01fe77d8e73" + "reference": "aaa8c9a96c69ccb397997b1f4f2299c66f77eefb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/domains/zipball/1665e1d9932afa3be63b5c1e0dcfe01fe77d8e73", - "reference": "1665e1d9932afa3be63b5c1e0dcfe01fe77d8e73", + "url": "https://api.github.com/repos/utopia-php/domains/zipball/aaa8c9a96c69ccb397997b1f4f2299c66f77eefb", + "reference": "aaa8c9a96c69ccb397997b1f4f2299c66f77eefb", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.0", + "utopia-php/framework": "0.*.*" }, "require-dev": { - "phpunit/phpunit": "^7.0" + "laravel/pint": "1.2.*", + "phpunit/phpunit": "^9.3" }, "type": "library", "autoload": { @@ -1645,6 +1648,10 @@ { "name": "Eldad Fux", "email": "eldad@appwrite.io" + }, + { + "name": "Wess Cope", + "email": "wess@appwrite.io" } ], "description": "Utopia Domains library is simple and lite library for parsing web domains. This library is aiming to be as simple and easy to learn and use.", @@ -1661,9 +1668,9 @@ ], "support": { "issues": "https://github.com/utopia-php/domains/issues", - "source": "https://github.com/utopia-php/domains/tree/master" + "source": "https://github.com/utopia-php/domains/tree/0.3.2" }, - "time": "2020-02-23T07:40:02+00:00" + "time": "2023-07-19T16:39:24+00:00" }, { "name": "utopia-php/dsn", @@ -5377,5 +5384,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 159ec0ea1..c49f4f569 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -105,6 +105,7 @@ class Exception extends \Exception public const AVATAR_ICON_NOT_FOUND = 'avatar_icon_not_found'; /** Storage */ + public const STORAGE_FILE_ALREADY_EXISTS = 'storage_file_already_exists'; public const STORAGE_FILE_NOT_FOUND = 'storage_file_not_found'; public const STORAGE_DEVICE_NOT_FOUND = 'storage_device_not_found'; public const STORAGE_FILE_EMPTY = 'storage_file_empty'; diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index c2c1c70bd..f982d25cc 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -344,6 +344,63 @@ trait DatabasesBase $this->assertEquals(400, $response['headers']['status-code']); } + public function testUpdateAttributeEnum(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Test Database 2' + ]); + + $players = $this->client->call(Client::METHOD_POST, '/databases/' . $database['body']['$id'] . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Players', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + ], + ]); + + // Create enum attribute + $attribute = $this->client->call(Client::METHOD_POST, '/databases/' . $database['body']['$id'] . '/collections/' . $players['body']['$id'] . '/attributes/enum', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'key' => 'position', + 'elements' => ['goalkeeper', 'defender', 'midfielder', 'forward'], + 'required' => true, + 'array' => false, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + $this->assertEquals($attribute['body']['key'], 'position'); + $this->assertEquals($attribute['body']['elements'], ['goalkeeper', 'defender', 'midfielder', 'forward']); + + \sleep(2); + + // Update enum attribute + $attribute = $this->client->call(Client::METHOD_PATCH, '/databases/' . $database['body']['$id'] . '/collections/' . $players['body']['$id'] . '/attributes/enum/' . $attribute['body']['key'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'elements' => ['goalkeeper', 'defender', 'midfielder', 'forward', 'coach'], + 'required' => true, + 'default' => null + ]); + + $this->assertEquals(200, $attribute['headers']['status-code']); + $this->assertEquals($attribute['body']['elements'], ['goalkeeper', 'defender', 'midfielder', 'forward', 'coach']); + } + /** * @depends testCreateAttributes */ @@ -2862,8 +2919,6 @@ trait DatabasesBase $collectionId = $collection['body']['$id']; - sleep(2); - $attribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -2877,8 +2932,7 @@ trait DatabasesBase $this->assertEquals(202, $attribute['headers']['status-code'], 202); $this->assertEquals('attribute', $attribute['body']['key']); - // wait for db to add attribute - sleep(2); + \sleep(2); $index = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/indexes', array_merge([ 'content-type' => 'application/json', @@ -2893,8 +2947,7 @@ trait DatabasesBase $this->assertEquals(202, $index['headers']['status-code']); $this->assertEquals('key_attribute', $index['body']['key']); - // wait for db to add attribute - sleep(2); + \sleep(2); $document1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ 'content-type' => 'application/json', @@ -2993,7 +3046,7 @@ trait DatabasesBase 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session2, ]); - // Current user has no collection permissions and document permissions are disabled + // other2 has no collection permissions and document permissions are disabled $this->assertEquals(404, $document3GetWithDocumentRead['headers']['status-code']); $documentsUser2 = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ @@ -3003,8 +3056,8 @@ trait DatabasesBase 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session2, ]); - // Current user has no collection permissions and document permissions are disabled - $this->assertEquals(404, $documentsUser2['headers']['status-code']); + // other2 has no collection permissions and document permissions are disabled + $this->assertEquals(401, $documentsUser2['headers']['status-code']); // Enable document permissions $collection = $this->client->call(CLient::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $collectionId, [ diff --git a/tests/e2e/Services/Databases/DatabasesConsoleClientTest.php b/tests/e2e/Services/Databases/DatabasesConsoleClientTest.php index 17059adf8..7bf089625 100644 --- a/tests/e2e/Services/Databases/DatabasesConsoleClientTest.php +++ b/tests/e2e/Services/Databases/DatabasesConsoleClientTest.php @@ -29,6 +29,7 @@ class DatabasesConsoleClientTest extends Scope $this->assertTrue($database['body']['enabled']); $databaseId = $database['body']['$id']; + /** * Test for SUCCESS */ @@ -51,7 +52,7 @@ class DatabasesConsoleClientTest extends Scope $this->assertEquals($movies['body']['name'], 'Movies'); /** - * Test When database is disabled but can still create collections + * Test when database is disabled but can still create collections */ $database = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId, array_merge([ 'content-type' => 'application/json', @@ -78,6 +79,17 @@ class DatabasesConsoleClientTest extends Scope 'documentSecurity' => true, ]); + /** + * Test when collection is disabled but can still modify collections + */ + $database = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $movies['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Movies', + 'enabled' => false, + ]); + $this->assertEquals(201, $tvShows['headers']['status-code']); $this->assertEquals($tvShows['body']['name'], 'TvShows'); @@ -87,11 +99,12 @@ class DatabasesConsoleClientTest extends Scope /** * @depends testCreateCollection * @param array $data + * @throws \Exception */ public function testListCollection(array $data) { /** - * Test When database is disabled but can still call list collections + * Test when database is disabled but can still call list collections */ $databaseId = $data['databaseId']; @@ -108,6 +121,8 @@ class DatabasesConsoleClientTest extends Scope /** * @depends testCreateCollection * @param array $data + * @throws \Exception + * @throws \Exception */ public function testGetCollection(array $data) { @@ -115,7 +130,7 @@ class DatabasesConsoleClientTest extends Scope $moviesCollectionId = $data['moviesId']; /** - * Test When database is disabled but can still call get collection + * Test when database and collection are disabled but can still call get collection */ $collection = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $moviesCollectionId, array_merge([ 'content-type' => 'application/json', @@ -125,12 +140,14 @@ class DatabasesConsoleClientTest extends Scope $this->assertEquals(200, $collection['headers']['status-code']); $this->assertEquals('Movies', $collection['body']['name']); $this->assertEquals($moviesCollectionId, $collection['body']['$id']); - $this->assertTrue($collection['body']['enabled']); + $this->assertFalse($collection['body']['enabled']); } /** * @depends testCreateCollection * @param array $data + * @throws \Exception + * @throws \Exception */ public function testUpdateCollection(array $data) { @@ -138,7 +155,7 @@ class DatabasesConsoleClientTest extends Scope $moviesCollectionId = $data['moviesId']; /** - * Test When database is disabled but can still call update collection + * Test When database and collection are disabled but can still call update collection */ $collection = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $moviesCollectionId, array_merge([ 'content-type' => 'application/json', @@ -157,6 +174,8 @@ class DatabasesConsoleClientTest extends Scope /** * @depends testCreateCollection * @param array $data + * @throws \Exception + * @throws \Exception */ public function testDeleteCollection(array $data) { @@ -164,7 +183,7 @@ class DatabasesConsoleClientTest extends Scope $tvShowsId = $data['tvShowsId']; /** - * Test When database is disabled but can still call Delete collection + * Test when database and collection are disabled but can still call delete collection */ $response = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $tvShowsId, array_merge([ 'content-type' => 'application/json', diff --git a/tests/e2e/Services/Databases/DatabasesPermissionsTeamTest.php b/tests/e2e/Services/Databases/DatabasesPermissionsTeamTest.php index dcbf3e4bf..8377b9c80 100644 --- a/tests/e2e/Services/Databases/DatabasesPermissionsTeamTest.php +++ b/tests/e2e/Services/Databases/DatabasesPermissionsTeamTest.php @@ -176,7 +176,7 @@ class DatabasesPermissionsTeamTest extends Scope if ($success) { $this->assertCount(1, $documents['body']['documents']); } else { - $this->assertEquals(404, $documents['headers']['status-code']); + $this->assertEquals(401, $documents['headers']['status-code']); } } diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index b19fe49ed..dbd9521f4 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -316,6 +316,54 @@ trait StorageBase return ['bucketId' => $bucketId]; } + public function testCreateBucketFileNoCollidingId(): void + { + $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'bucketId' => ID::unique(), + 'name' => 'Test Bucket', + 'maximumFileSize' => 2000000, //2MB + 'allowedFileExtensions' => ["jpg", "png"], + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $bucket['headers']['status-code']); + $this->assertNotEmpty($bucket['body']['$id']); + + $bucketId = $bucket['body']['$id']; + + $fileId = ID::unique(); + + $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => $fileId, + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'), + ]); + + $this->assertEquals(201, $file['headers']['status-code']); + $this->assertEquals($fileId, $file['body']['$id']); + + $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => $fileId, + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/file.png'), 'image/png', 'file.png'), + ]); + + $this->assertEquals(409, $file['headers']['status-code']); + } + /** * @depends testCreateBucketFile */