diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cebdc02163..509e1f5b29 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -223,7 +223,7 @@ jobs: -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ - appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots + appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group abuseEnabled,screenshots - name: Failure Logs if: failure() @@ -312,7 +312,7 @@ jobs: -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ - appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots + appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group abuseEnabled,screenshots - name: Failure Logs if: failure() @@ -322,8 +322,8 @@ jobs: echo "=== OpenRuntimes Executor Logs ===" docker compose logs openruntimes-executor - e2e_dev_keys: - name: E2E Service Test (Dev Keys) + e2e_abuse_enabled: + name: E2E Service Test (Abuse enabled) runs-on: ubuntu-latest needs: setup steps: @@ -344,7 +344,7 @@ jobs: docker compose up -d sleep 30 - - name: Run Projects tests with dev keys in dedicated table mode + - name: Run Projects tests in dedicated table mode run: | echo "Using project tables" export _APP_DATABASE_SHARED_TABLES= @@ -354,7 +354,7 @@ jobs: -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ - appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=devKeys + appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled - name: Failure Logs if: failure() @@ -364,8 +364,8 @@ jobs: echo "=== OpenRuntimes Executor Logs ===" docker compose logs openruntimes-executor - e2e_dev_keys_shared_mode: - name: E2E Shared Mode Service Test (Dev Keys) + e2e_abuse_enabled_shared_mode: + name: E2E Shared Mode Service Test (Abuse enabled) runs-on: ubuntu-latest needs: [ setup, check_database_changes ] if: needs.check_database_changes.outputs.database_changed == 'true' @@ -394,7 +394,7 @@ jobs: docker compose up -d sleep 30 - - name: Run Projects tests with dev keys in ${{ matrix.tables-mode }} table mode + - name: Run Projects tests in ${{ matrix.tables-mode }} table mode run: | if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then echo "Using shared tables V1" @@ -410,7 +410,7 @@ jobs: -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ - appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=devKeys + appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled - name: Failure Logs if: failure() @@ -420,7 +420,7 @@ jobs: echo "=== OpenRuntimes Executor Logs ===" docker compose logs openruntimes-executor - e2e_screenshots_keys: + e2e_screenshots: name: E2E Service Test (Site Screenshots) runs-on: ubuntu-latest needs: setup diff --git a/app/config/collections/platform.php b/app/config/collections/platform.php index e919df8e1a..8e214e7999 100644 --- a/app/config/collections/platform.php +++ b/app/config/collections/platform.php @@ -330,7 +330,18 @@ $platformCollections = [ 'default' => null, 'array' => false, 'filters' => ['datetime'], - ] + ], + [ + '$id' => ID::custom('labels'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], ], 'indexes' => [ [ @@ -1402,21 +1413,21 @@ $platformCollections = [ '$id' => '_key_type', 'type' => Database::INDEX_KEY, 'attributes' => ['type'], - 'lengths' => [32], + 'lengths' => [], 'orders' => [Database::ORDER_ASC], ], [ '$id' => '_key_trigger', 'type' => Database::INDEX_KEY, 'attributes' => ['trigger'], - 'lengths' => [32], + 'lengths' => [], 'orders' => [Database::ORDER_ASC], ], [ '$id' => '_key_deploymentResourceType', 'type' => Database::INDEX_KEY, 'attributes' => ['deploymentResourceType'], - 'lengths' => [32], + 'lengths' => [], 'orders' => [Database::ORDER_ASC], ], [ @@ -1458,23 +1469,23 @@ $platformCollections = [ '$id' => ID::custom('_key_owner'), 'type' => Database::INDEX_KEY, 'attributes' => ['owner'], - 'lengths' => [16], + 'lengths' => [], 'orders' => [Database::ORDER_ASC], ], [ - '$id' => ID::custom('_key_region'), - 'type' => Database::INDEX_KEY, - 'attributes' => ['region'], - 'lengths' => [16], - 'orders' => [Database::ORDER_ASC], - ], - [ - '$id' => ID::custom('_key_piid_riid_rt'), + '$id' => ID::custom('_key_piid_diid_drt'), 'type' => Database::INDEX_KEY, 'attributes' => ['projectInternalId', 'deploymentInternalId', 'deploymentResourceType'], 'lengths' => [], 'orders' => [], ], + [ + '$id' => '_key_region_status_createdAt', + 'type' => Database::INDEX_KEY, + 'attributes' => ['region', 'status', '$createdAt'], + 'lengths' => [], + 'orders' => [], + ], ], ], diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index d6b99f8855..2c481b500c 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -959,6 +959,7 @@ App::post('/v1/account/sessions/email') )) ->label('abuse-limit', 10) ->label('abuse-key', 'url:{url},email:{param-email}') + ->label('abuse-reset', [201]) ->param('email', '', new EmailValidator(), 'User email.') ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') ->inject('request') @@ -1257,6 +1258,7 @@ App::post('/v1/account/sessions/token') )) ->label('abuse-limit', 10) ->label('abuse-key', 'ip:{ip},userId:{param-userId}') + ->label('abuse-reset', [201]) ->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('secret', '', new Text(256), 'Secret of a token generated by login methods. For example, the `createMagicURLToken` or `createPhoneToken` methods.') ->inject('request') @@ -2645,6 +2647,7 @@ App::put('/v1/account/sessions/magic-url') )) ->label('abuse-limit', 10) ->label('abuse-key', 'ip:{ip},userId:{param-userId}') + ->label('abuse-reset', [201]) ->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('secret', '', new Text(256), 'Valid verification token.') ->inject('request') diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 4a97118853..b4f75a9ee5 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -697,7 +697,7 @@ App::get('/v1/avatars/screenshots') } $client = new Client(); - $client->setTimeout(30); + $client->setTimeout(30 * 1000); // 30 seconds $client->addHeader('content-type', Client::CONTENT_TYPE_APPLICATION_JSON); // Convert indexed array to empty array (should not happen due to Assoc validator) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 49c0003588..74f734a856 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -204,6 +204,7 @@ App::post('/v1/projects') 'accessedAt' => DateTime::now(), 'search' => implode(' ', [$projectId, $name]), 'database' => $dsn, + 'labels' => [], ])); } catch (Duplicate) { throw new Exception(Exception::PROJECT_ALREADY_EXISTS); @@ -1500,6 +1501,7 @@ App::post('/v1/projects/:projectId/keys') Permission::update(Role::any()), Permission::delete(Role::any()), ], + // TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column. 'projectInternalId' => $project->getSequence(), 'projectId' => $project->getId(), 'resourceInternalId' => $project->getSequence(), @@ -1552,13 +1554,8 @@ App::get('/v1/projects/:projectId/keys') } $keys = $dbForPlatform->find('keys', [ - Query::or([ - Query::equal('projectInternalId', [$project->getSequence()]), - Query::and([ - Query::equal('resourceType', ['projects']), - Query::equal('resourceInternalId', [$project->getSequence()]), - ]) - ]), + Query::equal('resourceType', ['projects']), + Query::equal('resourceInternalId', [$project->getSequence()]), Query::limit(5000), ]); @@ -1599,13 +1596,8 @@ App::get('/v1/projects/:projectId/keys/:keyId') $key = $dbForPlatform->findOne('keys', [ Query::equal('$id', [$keyId]), - Query::or([ - Query::equal('projectInternalId', [$project->getSequence()]), - Query::and([ - Query::equal('resourceType', ['projects']), - Query::equal('resourceInternalId', [$project->getSequence()]), - ]) - ]) + Query::equal('resourceType', ['projects']), + Query::equal('resourceInternalId', [$project->getSequence()]), ]); if ($key->isEmpty()) { @@ -1649,13 +1641,8 @@ App::put('/v1/projects/:projectId/keys/:keyId') $key = $dbForPlatform->findOne('keys', [ Query::equal('$id', [$keyId]), - Query::or([ - Query::equal('projectInternalId', [$project->getSequence()]), - Query::and([ - Query::equal('resourceType', ['projects']), - Query::equal('resourceInternalId', [$project->getSequence()]), - ]) - ]) + Query::equal('resourceType', ['projects']), + Query::equal('resourceInternalId', [$project->getSequence()]), ]); if ($key->isEmpty()) { @@ -1706,13 +1693,8 @@ App::delete('/v1/projects/:projectId/keys/:keyId') $key = $dbForPlatform->findOne('keys', [ Query::equal('$id', [$keyId]), - Query::or([ - Query::equal('projectInternalId', [$project->getSequence()]), - Query::and([ - Query::equal('resourceType', ['projects']), - Query::equal('resourceInternalId', [$project->getSequence()]), - ]) - ]) + Query::equal('resourceType', ['projects']), + Query::equal('resourceInternalId', [$project->getSequence()]), ]); if ($key->isEmpty()) { diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 6f092a5d19..fd7dae55b4 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -200,8 +200,12 @@ App::post('/v1/mock/api-key-unprefixed') Permission::update(Role::any()), Permission::delete(Role::any()), ], + // TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column. 'projectInternalId' => $project->getSequence(), 'projectId' => $project->getId(), + 'resourceInternalId' => $project->getSequence(), + 'resourceId' => $project->getId(), + 'resourceType' => 'projects', 'name' => 'Outdated key', 'scopes' => $scopes, 'expire' => null, diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 83b56f626a..05c08a2231 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -814,7 +814,8 @@ App::shutdown() ->inject('queueForWebhooks') ->inject('queueForRealtime') ->inject('dbForProject') - ->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) { + ->inject('timelimit') + ->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, callable $timelimit) use ($parseLabel) { $responsePayload = $response->getPayload(); @@ -848,6 +849,41 @@ App::shutdown() $route = $utopia->getRoute(); $requestParams = $route->getParamsValues(); + /** + * Abuse labels + */ + $abuseEnabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'; + $abuseResetCode = $route->getLabel('abuse-reset', []); + $abuseResetCode = \is_array($abuseResetCode) ? $abuseResetCode : [$abuseResetCode]; + + if ($abuseEnabled && \count($abuseResetCode) > 0 && \in_array($response->getStatusCode(), $abuseResetCode)) { + $abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}'); + $abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel; + + foreach ($abuseKeyLabel as $abuseKey) { + $start = $request->getContentRangeStart(); + $end = $request->getContentRangeEnd(); + $timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600)); + $timeLimit + ->setParam('{projectId}', $project->getId()) + ->setParam('{userId}', $user->getId()) + ->setParam('{userAgent}', $request->getUserAgent('')) + ->setParam('{ip}', $request->getIP()) + ->setParam('{url}', $request->getHostname() . $route->getPath()) + ->setParam('{method}', $request->getMethod()) + ->setParam('{chunkId}', (int)($start / ($end + 1 - $start))); + + foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys + if (!empty($value)) { + $timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value); + } + } + + $abuse = new Abuse($timeLimit); + $abuse->reset(); + } + } + /** * Audit labels */ diff --git a/app/init.php b/app/init.php index c32f1eb9a8..91a64a72ce 100644 --- a/app/init.php +++ b/app/init.php @@ -26,6 +26,7 @@ require_once __DIR__ . '/init/database/filters.php'; require_once __DIR__ . '/init/database/formats.php'; require_once __DIR__ . '/init/locales.php'; require_once __DIR__ . '/init/registers.php'; +require_once __DIR__ . '/init/models.php'; require_once __DIR__ . '/init/resources.php'; \stream_context_set_default([ // Set global user agent and http settings diff --git a/app/init/database/filters.php b/app/init/database/filters.php index 49c13c9a0b..c9ad3fce03 100644 --- a/app/init/database/filters.php +++ b/app/init/database/filters.php @@ -136,13 +136,8 @@ Database::addFilter( function (mixed $value, Document $document, Database $database) { return $database ->find('keys', [ - Query::or([ - Query::equal('projectInternalId', [$document->getSequence()]), - Query::and([ - Query::equal('resourceType', ['projects']), - Query::equal('resourceInternalId', [$document->getSequence()]), - ]) - ]), + Query::equal('resourceType', ['projects']), + Query::equal('resourceInternalId', [$document->getSequence()]), Query::limit(APP_LIMIT_SUBQUERY), ]); } diff --git a/app/init/models.php b/app/init/models.php new file mode 100644 index 0000000000..fdfa0271b4 --- /dev/null +++ b/app/init/models.php @@ -0,0 +1,344 @@ +set('smtp', function () { $mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', ''); $mail->SMTPAutoTLS = false; $mail->CharSet = 'UTF-8'; + $mail->Timeout = 10; /* Connection timeout */ + $mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */ $from = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')); $email = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); diff --git a/composer.json b/composer.json index 844a10d7e8..7d08c2bf32 100644 --- a/composer.json +++ b/composer.json @@ -45,9 +45,9 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.19.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "1.*", + "utopia-php/abuse": "1.*.*", "utopia-php/analytics": "0.10.*", - "utopia-php/audit": "2.0.2-rc1", + "utopia-php/audit": "2.0.2-rc3", "utopia-php/auth": "0.5.*", "utopia-php/cache": "0.13.*", "utopia-php/cli": "0.15.*", @@ -59,12 +59,12 @@ "utopia-php/dns": "1.4.*", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", - "utopia-php/fetch": "0.4.*", + "utopia-php/fetch": "0.5.*", "utopia-php/image": "0.8.*", "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", - "utopia-php/migration": "1.3.*", + "utopia-php/migration": "1.*.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.8.*", @@ -100,6 +100,12 @@ "provide": { "ext-phpiredis": "*" }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/utopia-php/migration.git" + } + ], "config": { "platform": { "php": "8.3" diff --git a/composer.lock b/composer.lock index c678d1c01e..996844994c 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": "b873febd2b03c32ec61a57b690cc44a2", + "content-hash": "f63c88303152af32cae4c800b8642540", "packages": [ { "name": "adhocore/jwt", @@ -69,16 +69,16 @@ }, { "name": "appwrite/appwrite", - "version": "15.1.0", + "version": "19.1.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b" + "reference": "8738e812062f899c85b2598eef43d6a247f08a56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/c438b3885071ac7c0329199dce5e6f6a24dd215b", - "reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/8738e812062f899c85b2598eef43d6a247f08a56", + "reference": "8738e812062f899c85b2598eef43d6a247f08a56", "shasum": "" }, "require": { @@ -87,7 +87,7 @@ "php": ">=7.1.0" }, "require-dev": { - "mockery/mockery": "^1.6.6", + "mockery/mockery": "^1.6.12", "phpunit/phpunit": "^10" }, "type": "library", @@ -104,10 +104,10 @@ "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/15.1.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/19.1.0", "url": "https://appwrite.io/support" }, - "time": "2025-08-01T04:50:51+00:00" + "time": "2025-12-18T08:07:43+00:00" }, { "name": "appwrite/php-clamav", @@ -3455,24 +3455,25 @@ }, { "name": "utopia-php/abuse", - "version": "1.0.2", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828" + "reference": "3339d057c6bb1fa3e5ac5b2598923f6938425ec2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/611fa66a97e87c0dbbc133a717d970da7a5ca828", - "reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/3339d057c6bb1fa3e5ac5b2598923f6938425ec2", + "reference": "3339d057c6bb1fa3e5ac5b2598923f6938425ec2", "shasum": "" }, "require": { + "appwrite/appwrite": "19.*.*", "ext-curl": "*", "ext-pdo": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "*" + "utopia-php/database": "3.*.*" }, "require-dev": { "laravel/pint": "1.*", @@ -3500,9 +3501,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/1.0.2" + "source": "https://github.com/utopia-php/abuse/tree/1.2.0" }, - "time": "2025-10-20T07:18:33+00:00" + "time": "2026-01-05T21:29:10+00:00" }, { "name": "utopia-php/analytics", @@ -3552,23 +3553,23 @@ }, { "name": "utopia-php/audit", - "version": "2.0.2-rc1", + "version": "2.0.2-rc3", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "7b35dab40bce66bda56eeeacd2bbcbf1e823f05f" + "reference": "f60a298b516300f56a328403b334b7d62a96e7e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/7b35dab40bce66bda56eeeacd2bbcbf1e823f05f", - "reference": "7b35dab40bce66bda56eeeacd2bbcbf1e823f05f", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/f60a298b516300f56a328403b334b7d62a96e7e7", + "reference": "f60a298b516300f56a328403b334b7d62a96e7e7", "shasum": "" }, "require": { "php": ">=8.0", "utopia-php/database": "3.*", - "utopia-php/fetch": "^0.4.2", - "utopia-php/validators": "^0.1.0" + "utopia-php/fetch": "0.5.*", + "utopia-php/validators": "0.1.*" }, "require-dev": { "laravel/pint": "1.*", @@ -3595,9 +3596,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/2.0.2-rc1" + "source": "https://github.com/utopia-php/audit/tree/2.0.2-rc3" }, - "time": "2025-12-24T01:20:43+00:00" + "time": "2026-01-06T15:32:52+00:00" }, { "name": "utopia-php/auth", @@ -4167,23 +4168,23 @@ }, { "name": "utopia-php/emails", - "version": "0.6.3", + "version": "0.6.4", "source": { "type": "git", "url": "https://github.com/utopia-php/emails.git", - "reference": "9524d7f7bd1651a06fef8a3d964f774b04fe2918" + "reference": "fb2bd5c428e88f645b0f7ede0dd29ac0d120ec52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/emails/zipball/9524d7f7bd1651a06fef8a3d964f774b04fe2918", - "reference": "9524d7f7bd1651a06fef8a3d964f774b04fe2918", + "url": "https://api.github.com/repos/utopia-php/emails/zipball/fb2bd5c428e88f645b0f7ede0dd29ac0d120ec52", + "reference": "fb2bd5c428e88f645b0f7ede0dd29ac0d120ec52", "shasum": "" }, "require": { "php": ">=8.0", "utopia-php/cli": "^0.15", "utopia-php/domains": "^0.9", - "utopia-php/fetch": "^0.4", + "utopia-php/fetch": "^0.5", "utopia-php/validators": "0.*" }, "require-dev": { @@ -4221,26 +4222,26 @@ ], "support": { "issues": "https://github.com/utopia-php/emails/issues", - "source": "https://github.com/utopia-php/emails/tree/0.6.3" + "source": "https://github.com/utopia-php/emails/tree/0.6.4" }, - "time": "2025-11-26T12:27:47+00:00" + "time": "2025-12-18T16:36:50+00:00" }, { "name": "utopia-php/fetch", - "version": "0.4.2", + "version": "0.5.1", "source": { "type": "git", "url": "https://github.com/utopia-php/fetch.git", - "reference": "83986d1be75a2fae4e684107fe70dd78a8e19b77" + "reference": "a96a010e1c273f3888765449687baf58cbc61fcd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/fetch/zipball/83986d1be75a2fae4e684107fe70dd78a8e19b77", - "reference": "83986d1be75a2fae4e684107fe70dd78a8e19b77", + "url": "https://api.github.com/repos/utopia-php/fetch/zipball/a96a010e1c273f3888765449687baf58cbc61fcd", + "reference": "a96a010e1c273f3888765449687baf58cbc61fcd", "shasum": "" }, "require": { - "php": ">=8.0" + "php": ">=8.1" }, "require-dev": { "laravel/pint": "^1.5.0", @@ -4260,9 +4261,9 @@ "description": "A simple library that provides an interface for making HTTP Requests.", "support": { "issues": "https://github.com/utopia-php/fetch/issues", - "source": "https://github.com/utopia-php/fetch/tree/0.4.2" + "source": "https://github.com/utopia-php/fetch/tree/0.5.1" }, - "time": "2025-04-25T13:48:02+00:00" + "time": "2025-12-18T16:25:10+00:00" }, { "name": "utopia-php/framework", @@ -4515,20 +4516,20 @@ }, { "name": "utopia-php/migration", - "version": "1.3.9", + "version": "1.3.11", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "c55ec67c74663190cda10fd79297422147be7e85" + "reference": "798f0976a1c14234c4b283b858b08c9afbcc1662" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/c55ec67c74663190cda10fd79297422147be7e85", - "reference": "c55ec67c74663190cda10fd79297422147be7e85", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/798f0976a1c14234c4b283b858b08c9afbcc1662", + "reference": "798f0976a1c14234c4b283b858b08c9afbcc1662", "shasum": "" }, "require": { - "appwrite/appwrite": "15.*", + "appwrite/appwrite": "19.*", "ext-curl": "*", "ext-openssl": "*", "php": ">=8.1", @@ -4550,7 +4551,25 @@ "Utopia\\Migration\\": "src/Migration" } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "Utopia\\Tests\\": "tests/Migration" + } + }, + "scripts": { + "test": [ + "./vendor/bin/phpunit" + ], + "lint": [ + "./vendor/bin/pint --test" + ], + "format": [ + "./vendor/bin/pint" + ], + "check": [ + "./vendor/bin/phpstan analyse --level 3 src tests --memory-limit 2G" + ] + }, "license": [ "MIT" ], @@ -4563,10 +4582,10 @@ "utopia" ], "support": { - "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.3.9" + "source": "https://github.com/utopia-php/migration/tree/1.3.11", + "issues": "https://github.com/utopia-php/migration/issues" }, - "time": "2025-12-08T08:45:09+00:00" + "time": "2026-01-06T12:07:07+00:00" }, { "name": "utopia-php/mongo", @@ -4837,23 +4856,23 @@ }, { "name": "utopia-php/queue", - "version": "0.11.2", + "version": "0.11.3", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "a854f7c4abc18e0eca55fc5608cd7088d71eb19f" + "reference": "f3b2623efe87595c9ed907b3efd587e77c622d3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/a854f7c4abc18e0eca55fc5608cd7088d71eb19f", - "reference": "a854f7c4abc18e0eca55fc5608cd7088d71eb19f", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/f3b2623efe87595c9ed907b3efd587e77c622d3d", + "reference": "f3b2623efe87595c9ed907b3efd587e77c622d3d", "shasum": "" }, "require": { "php": ">=8.3", "php-amqplib/php-amqplib": "^3.7", "utopia-php/cli": "0.15.*", - "utopia-php/fetch": "0.4.*", + "utopia-php/fetch": "0.5.*", "utopia-php/framework": "0.33.*", "utopia-php/pools": "0.8.*", "utopia-php/telemetry": "*" @@ -4897,9 +4916,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.11.2" + "source": "https://github.com/utopia-php/queue/tree/0.11.3" }, - "time": "2025-12-17T09:32:35+00:00" + "time": "2025-12-19T10:56:22+00:00" }, { "name": "utopia-php/registry", @@ -5438,16 +5457,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.8.6", + "version": "1.8.9", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "b6cc29d3bd247e193f3c06b4168dc69d884645f0" + "reference": "5fc210f7403f9ecfa068cd2a74210ec6e2a3cec1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/b6cc29d3bd247e193f3c06b4168dc69d884645f0", - "reference": "b6cc29d3bd247e193f3c06b4168dc69d884645f0", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/5fc210f7403f9ecfa068cd2a74210ec6e2a3cec1", + "reference": "5fc210f7403f9ecfa068cd2a74210ec6e2a3cec1", "shasum": "" }, "require": { @@ -5483,9 +5502,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.8.6" + "source": "https://github.com/appwrite/sdk-generator/tree/1.8.9" }, - "time": "2025-12-31T10:22:17+00:00" + "time": "2026-01-02T12:09:51+00:00" }, { "name": "doctrine/annotations", @@ -5566,30 +5585,29 @@ }, { "name": "doctrine/instantiator", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^8.4" }, "require-dev": { - "doctrine/coding-standard": "^11", + "doctrine/coding-standard": "^14", "ext-pdo": "*", "ext-phar": "*", "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.58" }, "type": "library", "autoload": { @@ -5616,7 +5634,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + "source": "https://github.com/doctrine/instantiator/tree/2.1.0" }, "funding": [ { @@ -5632,7 +5650,7 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:23:10+00:00" + "time": "2026-01-05T06:47:08+00:00" }, { "name": "doctrine/lexer", @@ -5713,16 +5731,16 @@ }, { "name": "laravel/pint", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f" + "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f", - "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f", + "url": "https://api.github.com/repos/laravel/pint/zipball/c67b4195b75491e4dfc6b00b1c78b68d86f54c90", + "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90", "shasum": "" }, "require": { @@ -5733,9 +5751,9 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.90.0", - "illuminate/view": "^12.40.1", - "larastan/larastan": "^3.8.0", + "friendsofphp/php-cs-fixer": "^3.92.4", + "illuminate/view": "^12.44.0", + "larastan/larastan": "^3.8.1", "laravel-zero/framework": "^12.0.4", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.3.3", @@ -5776,7 +5794,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-11-25T21:15:52+00:00" + "time": "2026-01-05T16:49:17+00:00" }, { "name": "matthiasmullie/minify", @@ -8562,16 +8580,16 @@ }, { "name": "symfony/process", - "version": "v8.0.0", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149" + "reference": "0cbbd88ec836f8757641c651bb995335846abb78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/a0a750500c4ce900d69ba4e9faf16f82c10ee149", - "reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149", + "url": "https://api.github.com/repos/symfony/process/zipball/0cbbd88ec836f8757641c651bb995335846abb78", + "reference": "0cbbd88ec836f8757641c651bb995335846abb78", "shasum": "" }, "require": { @@ -8603,7 +8621,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v8.0.0" + "source": "https://github.com/symfony/process/tree/v8.0.3" }, "funding": [ { @@ -8623,7 +8641,7 @@ "type": "tidelift" } ], - "time": "2025-10-16T16:25:44+00:00" + "time": "2025-12-19T10:01:18+00:00" }, { "name": "symfony/string", @@ -8971,5 +8989,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Appwrite/Platform/Action.php b/src/Appwrite/Platform/Action.php index 356209ef6f..ba93225a77 100644 --- a/src/Appwrite/Platform/Action.php +++ b/src/Appwrite/Platform/Action.php @@ -2,8 +2,6 @@ namespace Appwrite\Platform; -use Appwrite\Utopia\Request; -use Appwrite\Utopia\Response; use Swoole\Coroutine as Co; use Utopia\CLI\Console; use Utopia\Database\Database; @@ -161,50 +159,4 @@ class Action extends UtopiaAction Console::info("[" . DateTime::now() . "] " . $method . ' ' . $type . ' ' . $project->getSequence() . ' ' . $project->getId() . ' ' . $collectionId . ' ' . $log); } } - - - /** - * Helper to apply (request) select queries to response model. - * - * This prevents default values of rules to be presnet for not-selected attributes - * - * @param Request $request - * @param Document $document - * @return void - */ - public function applySelectQueries(Request $request, Response $response, string $model): void - { - $queries = $request->getParam('queries', []); - - $queries = Query::parseQueries($queries); - $selectQueries = Query::groupByType($queries)['selections'] ?? []; - - // No select queries means no filtering out - if (empty($selectQueries)) { - return; - } - - $attributes = []; - foreach ($selectQueries as $query) { - foreach ($query->getValues() as $attribute) { - $attributes[] = $attribute; - } - } - - // found a wildcard, return! - if (\in_array('*', $attributes)) { - return; - } - - $responseModel = $response->getModel($model); - foreach ($responseModel->getRules() as $ruleName => $rule) { - if (\str_starts_with($ruleName, '$')) { - continue; - } - - if (!\in_array($ruleName, $attributes)) { - $responseModel->removeRule($ruleName); - } - } - } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php index a3a1ea6ce8..53831f0fc5 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php @@ -177,6 +177,7 @@ class Decrement extends Action value: $value, min: $min ); + $document->setAttribute('$databaseId', $database->getId()); $document->setAttribute('$' . $this->getCollectionsEventsContext() . 'Id', $collectionId); } catch (ConflictException) { throw new Exception($this->getConflictException()); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php index 157c5ef2af..ea680db3b1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php @@ -177,6 +177,7 @@ class Increment extends Action value: $value, max: $max ); + $document->setAttribute('$databaseId', $database->getId()); $document->setAttribute('$' . $this->getCollectionsEventsContext() . 'Id', $collectionId); } catch (ConflictException) { throw new Exception($this->getConflictException()); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php index 2717e99ee0..55711495e9 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\Queries\Deployments; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Filters\ListSelection; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Order as OrderException; @@ -119,7 +120,9 @@ class XList extends Base $cursor->setValue($cursorDocument); } - $filterQueries = Query::groupByType($queries)['filters']; + $grouped = Query::groupByType($queries); + $filterQueries = $grouped['filters']; + $selectQueries = $grouped['selections'] ?? []; try { $results = $dbForProject->find('deployments', $queries); @@ -128,7 +131,8 @@ class XList extends Base throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); } - $this->applySelectQueries($request, $response, Response::MODEL_DEPLOYMENT); + $response->addFilter(new ListSelection($selectQueries, 'deployments')); + $response->dynamic(new Document([ 'deployments' => $results, 'total' => $total, diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 1d202b4948..e38a56bd2b 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -939,7 +939,7 @@ class Builds extends Action } $client = new FetchClient(); - $client->setTimeout(\intval($resource->getAttribute('timeout', '15'))); + $client->setTimeout(\intval($resource->getAttribute('timeout', '15')) * 1000); $client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON); $bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php new file mode 100644 index 0000000000..1a06c1ee84 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php @@ -0,0 +1,86 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT) + ->setHttpPath('/v1/projects/:projectId/labels') + ->desc('Update project labels') + ->groups(['api', 'projects']) + ->label('scope', 'projects.write') + ->label('sdk', new Method( + namespace: 'projects', + group: 'projects', + name: 'updateLabels', + description: <<param('projectId', '', new UID(), 'Project unique ID.') + ->param('labels', [], new ArrayList(new Text(36, allowList: [...Text::NUMBERS, ...Text::ALPHABET_UPPER, ...Text::ALPHABET_LOWER]), APP_LIMIT_ARRAY_LABELS_SIZE), 'Array of project labels. Replaces the previous labels. Maximum of ' . APP_LIMIT_ARRAY_LABELS_SIZE . ' labels are allowed, each up to 36 alphanumeric characters long.') + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + /** + * @param array $labels + */ + public function action( + string $projectId, + array $labels, + Response $response, + Database $dbForPlatform + ): void { + $project = $dbForPlatform->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $project->setAttribute('labels', (array) \array_values(\array_unique($labels))); + + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project); + + $response->dynamic($project, Response::MODEL_PROJECT); + } +} diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php index 32318dd189..7269582a15 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\Queries\Projects; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Filters\ListSelection; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; @@ -120,7 +121,8 @@ class XList extends Action throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); } - $this->applySelectQueries($request, $response, Response::MODEL_PROJECT); + $response->addFilter(new ListSelection($selectQueries, 'projects')); + $response->dynamic(new Document([ 'projects' => $projects, 'total' => $total, diff --git a/src/Appwrite/Platform/Modules/Projects/Services/Http.php b/src/Appwrite/Platform/Modules/Projects/Services/Http.php index 2a0dd0aa60..cce05a9570 100644 --- a/src/Appwrite/Platform/Modules/Projects/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Projects/Services/Http.php @@ -7,6 +7,7 @@ use Appwrite\Platform\Modules\Projects\Http\DevKeys\Delete as DeleteDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\Get as GetDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\Update as UpdateDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\XList as ListDevKeys; +use Appwrite\Platform\Modules\Projects\Http\Projects\Labels\Update as UpdateProjectLabels; use Appwrite\Platform\Modules\Projects\Http\Projects\XList as ListProjects; use Utopia\Platform\Service; @@ -22,5 +23,6 @@ class Http extends Service $this->addAction(DeleteDevKey::getName(), new DeleteDevKey()); $this->addAction(ListProjects::getName(), new ListProjects()); + $this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels()); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php index b7f9386f06..73e5ea4d77 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\Queries\Deployments; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Filters\ListSelection; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Order as OrderException; @@ -119,7 +120,9 @@ class XList extends Base $cursor->setValue($cursorDocument); } - $filterQueries = Query::groupByType($queries)['filters']; + $grouped = Query::groupByType($queries); + $filterQueries = $grouped['filters']; + $selectQueries = $grouped['selections'] ?? []; try { $results = $dbForProject->find('deployments', $queries); @@ -128,7 +131,8 @@ class XList extends Base throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); } - $this->applySelectQueries($request, $response, Response::MODEL_DEPLOYMENT); + $response->addFilter(new ListSelection($selectQueries, 'deployments')); + $response->dynamic(new Document([ 'deployments' => $results, 'total' => $total, diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 5c36fa7f7a..0b2f7c75ae 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -148,7 +148,7 @@ class Deletes extends Action break; case DELETE_TYPE_AUDIT: if (!$project->isEmpty()) { - $this->deleteAuditLogs($project, $auditRetention, $getAudit); + $this->deleteAuditLogs($project, $getAudit, $auditRetention); } break; case DELETE_TYPE_REALTIME: @@ -187,7 +187,7 @@ class Deletes extends Action case DELETE_TYPE_MAINTENANCE: $this->deleteExpiredTargets($project, $getProjectDB); $this->deleteExecutionLogs($project, $getProjectDB, $executionRetention); - $this->deleteAuditLogs($project, $getProjectDB, $auditRetention); + $this->deleteAuditLogs($project, $getAudit, $auditRetention); $this->deleteUsageStats($project, $getProjectDB, $getLogsDB, $hourlyUsageRetentionDatetime); $this->deleteExpiredSessions($project, $getProjectDB); $this->deleteExpiredTransactions($project, $getProjectDB); @@ -516,130 +516,136 @@ class Deletes extends Action $dsn = new DSN('mysql://' . $document->getAttribute('database', 'console')); } - $dbForProject = $getProjectDB($document); - - $projectCollectionIds = [ - ...\array_keys(Config::getParam('collections', [])['projects']), - SQL::COLLECTION, - AbuseDatabase::COLLECTION, - ]; - - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - $sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', '')); - - $projectTables = !\in_array($dsn->getHost(), $sharedTables); - $sharedTablesV1 = \in_array($dsn->getHost(), $sharedTablesV1); - $sharedTablesV2 = !$projectTables && !$sharedTablesV1; - /** * @var $dbForProject Database */ - $dbForProject->foreach(Database::METADATA, function (Document $collection) use ($dbForProject, $projectTables, $projectCollectionIds) { - try { - if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) { - $dbForProject->deleteCollection($collection->getId()); - } else { - $this->deleteByGroup( - $collection->getId(), - [ - Query::orderAsc() - ], - database: $dbForProject - ); + $dbForProject = $getProjectDB($document); + + try { + /** + * Disable validation because of Cursor validation on $id underscores + */ + $dbForProject->disableValidation(); + + + $projectCollectionIds = [ + ...\array_keys(Config::getParam('collections', [])['projects']), + SQL::COLLECTION, + AbuseDatabase::COLLECTION, + ]; + + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + $sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', '')); + + $projectTables = !\in_array($dsn->getHost(), $sharedTables); + $sharedTablesV1 = \in_array($dsn->getHost(), $sharedTablesV1); + $sharedTablesV2 = !$projectTables && !$sharedTablesV1; + + $dbForProject->foreach(Database::METADATA, function (Document $collection) use ($dbForProject, $projectTables, $projectCollectionIds) { + try { + if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) { + $dbForProject->deleteCollection($collection->getId()); + } else { + $this->deleteByGroup( + $collection->getId(), + [ + Query::orderAsc() + ], + database: $dbForProject + ); + } + } catch (Throwable $e) { + Console::error('Error deleting ' . $collection->getId() . ' ' . $e->getMessage()); } - } catch (Throwable $e) { - Console::error('Error deleting ' . $collection->getId() . ' ' . $e->getMessage()); - } - }); + }); - // Delete Platforms - $this->deleteByGroup('platforms', [ - Query::equal('projectInternalId', [$projectInternalId]), - Query::orderAsc() - ], $dbForPlatform); - - // Delete project and function rules - $this->deleteByGroup('rules', [ - Query::equal('projectInternalId', [$projectInternalId]), - Query::orderAsc() - ], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) { - $this->deleteRule($dbForPlatform, $document, $certificates); - }); - - // Delete Keys - $this->deleteByGroup('keys', [ - Query::or([ + // Delete Platforms + $this->deleteByGroup('platforms', [ Query::equal('projectInternalId', [$projectInternalId]), - Query::and([ - Query::equal('resourceType', ['projects']), - Query::equal('resourceInternalId', [$projectInternalId]), - ]) - ]), - Query::orderAsc() - ], $dbForPlatform); + Query::orderAsc() + ], $dbForPlatform); - // Delete Webhooks - $this->deleteByGroup('webhooks', [ - Query::equal('projectInternalId', [$projectInternalId]), - Query::orderAsc() - ], $dbForPlatform); + // Delete project and function rules + $this->deleteByGroup('rules', [ + Query::equal('projectInternalId', [$projectInternalId]), + Query::orderAsc() + ], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) { + $this->deleteRule($dbForPlatform, $document, $certificates); + }); - // Delete VCS Installations - $this->deleteByGroup('installations', [ - Query::equal('projectInternalId', [$projectInternalId]), - Query::orderAsc() - ], $dbForPlatform); + // Delete Keys + $this->deleteByGroup('keys', [ + Query::equal('resourceType', ['projects']), + Query::equal('resourceInternalId', [$projectInternalId]), + Query::orderAsc() + ], $dbForPlatform); - // Delete VCS Repositories - $this->deleteByGroup('repositories', [ - Query::equal('projectInternalId', [$projectInternalId]), - Query::orderAsc() - ], $dbForPlatform); + // Delete Webhooks + $this->deleteByGroup('webhooks', [ + Query::equal('projectInternalId', [$projectInternalId]), + Query::orderAsc() + ], $dbForPlatform); - // Delete VCS comments - $this->deleteByGroup('vcsComments', [ - Query::equal('projectInternalId', [$projectInternalId]), - Query::orderAsc() - ], $dbForPlatform); + // Delete VCS Installations + $this->deleteByGroup('installations', [ + Query::equal('projectInternalId', [$projectInternalId]), + Query::orderAsc() + ], $dbForPlatform); - // Delete Schedules - $this->deleteByGroup('schedules', [ - Query::equal('projectId', [$projectId]), - Query::orderAsc() - ], $dbForPlatform); + // Delete VCS Repositories + $this->deleteByGroup('repositories', [ + Query::equal('projectInternalId', [$projectInternalId]), + Query::orderAsc() + ], $dbForPlatform); - // Delete metadata table - if ($projectTables) { - $dbForProject->deleteCollection(Database::METADATA); - } elseif ($sharedTablesV1) { - $this->deleteByGroup( - Database::METADATA, - [ - Query::orderAsc() - ], - $dbForProject - ); - } elseif ($sharedTablesV2) { - $queries = \array_map( - fn ($id) => Query::notEqual('$id', $id), - $projectCollectionIds - ); + // Delete VCS comments + $this->deleteByGroup('vcsComments', [ + Query::equal('projectInternalId', [$projectInternalId]), + Query::orderAsc() + ], $dbForPlatform); - $queries[] = Query::orderAsc(); + // Delete Schedules + $this->deleteByGroup('schedules', [ + Query::equal('projectId', [$projectId]), + Query::orderAsc() + ], $dbForPlatform); - $this->deleteByGroup( - Database::METADATA, - $queries, - $dbForProject - ); + // Delete metadata table + if ($projectTables) { + $dbForProject->deleteCollection(Database::METADATA); + } elseif ($sharedTablesV1) { + $this->deleteByGroup( + Database::METADATA, + [ + Query::orderAsc() + ], + $dbForProject + ); + } elseif ($sharedTablesV2) { + $queries = \array_map( + fn ($id) => Query::notEqual('$id', $id), + $projectCollectionIds + ); + + $queries[] = Query::orderAsc(); + + $this->deleteByGroup( + Database::METADATA, + $queries, + $dbForProject + ); + } + + // Delete all storage directories + $deviceForFiles->delete($deviceForFiles->getRoot(), true); + $deviceForSites->delete($deviceForSites->getRoot(), true); + $deviceForFunctions->delete($deviceForFunctions->getRoot(), true); + $deviceForBuilds->delete($deviceForBuilds->getRoot(), true); + $deviceForCache->delete($deviceForCache->getRoot(), true); + + } finally { + $dbForProject->enableValidation(); } - - // Delete all storage directories - $deviceForFiles->delete($deviceForFiles->getRoot(), true); - $deviceForSites->delete($deviceForSites->getRoot(), true); - $deviceForFunctions->delete($deviceForFunctions->getRoot(), true); - $deviceForBuilds->delete($deviceForBuilds->getRoot(), true); - $deviceForCache->delete($deviceForCache->getRoot(), true); } /** @@ -783,14 +789,13 @@ class Deletes extends Action } /** - * @param Database $dbForPlatform - * @param callable $getProjectDB - * @param string $auditRetention + * @param Document $project * @param callable $getAudit + * @param string $auditRetention * @return void * @throws Exception */ - private function deleteAuditLogs(Document $project, string $auditRetention, callable $getAudit): void + private function deleteAuditLogs(Document $project, callable $getAudit, string $auditRetention): void { $projectId = $project->getId(); /** @var Audit $audit */ diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php index 01448620f3..b1f17fc648 100644 --- a/src/Appwrite/Platform/Workers/Mails.php +++ b/src/Appwrite/Platform/Workers/Mails.php @@ -68,7 +68,8 @@ class Mails extends Action throw new Exception('Skipped mail processing. No SMTP configuration has been set.'); } - $log->addTag('type', empty($smtp) ? 'cloud' : 'smtp'); + $type = empty($smtp) ? 'cloud' : 'smtp'; + $log->addTag('type', $type); $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; $hostname = System::getEnv('_APP_CONSOLE_DOMAIN'); @@ -182,6 +183,9 @@ class Mails extends Action try { $mail->send(); } catch (\Throwable $error) { + if ($type === 'smtp') { + throw new Exception('Error sending mail: ' . $error->getMessage(), 401); + } throw new Exception('Error sending mail: ' . $error->getMessage(), 500); } } @@ -209,6 +213,8 @@ class Mails extends Action $mail->SMTPSecure = $smtp['secure']; $mail->SMTPAutoTLS = false; $mail->CharSet = 'UTF-8'; + $mail->Timeout = 10; /* Connection timeout */ + $mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */ $mail->setFrom($smtp['senderEmail'], $smtp['senderName']); diff --git a/src/Appwrite/Platform/Workers/StatsResources.php b/src/Appwrite/Platform/Workers/StatsResources.php index 967dbc59a4..1c3db8d9c9 100644 --- a/src/Appwrite/Platform/Workers/StatsResources.php +++ b/src/Appwrite/Platform/Workers/StatsResources.php @@ -111,13 +111,8 @@ class StatsResources extends Action Query::equal('projectInternalId', [$project->getSequence()]) ]); $keys = $dbForPlatform->count('keys', [ - Query::or([ - Query::equal('projectInternalId', [$project->getSequence()]), - Query::and([ - Query::equal('resourceType', ['projects']), - Query::equal('resourceInternalId', [$project->getSequence()]), - ]) - ]), + Query::equal('resourceType', ['projects']), + Query::equal('resourceInternalId', [$project->getSequence()]), ]); $domains = $dbForPlatform->count('rules', [ diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php b/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php index d179703274..d96e373949 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php @@ -6,7 +6,8 @@ class Projects extends Base { public const ALLOWED_ATTRIBUTES = [ 'name', - 'teamId' + 'teamId', + 'labels', ]; /** diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 33351bea14..1dfaa1a41f 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -6,147 +6,8 @@ use Appwrite\Utopia\Database\Documents\User as DBUser; use Appwrite\Utopia\Fetch\BodyMultipart; use Appwrite\Utopia\Response\Filter; use Appwrite\Utopia\Response\Model; -use Appwrite\Utopia\Response\Model\Account; -use Appwrite\Utopia\Response\Model\AlgoArgon2; -use Appwrite\Utopia\Response\Model\AlgoBcrypt; -use Appwrite\Utopia\Response\Model\AlgoMd5; -use Appwrite\Utopia\Response\Model\AlgoPhpass; -use Appwrite\Utopia\Response\Model\AlgoScrypt; -use Appwrite\Utopia\Response\Model\AlgoScryptModified; -use Appwrite\Utopia\Response\Model\AlgoSha; -use Appwrite\Utopia\Response\Model\Any; -use Appwrite\Utopia\Response\Model\Attribute; -use Appwrite\Utopia\Response\Model\AttributeBoolean; -use Appwrite\Utopia\Response\Model\AttributeDatetime; -use Appwrite\Utopia\Response\Model\AttributeEmail; -use Appwrite\Utopia\Response\Model\AttributeEnum; -use Appwrite\Utopia\Response\Model\AttributeFloat; -use Appwrite\Utopia\Response\Model\AttributeInteger; -use Appwrite\Utopia\Response\Model\AttributeIP; -use Appwrite\Utopia\Response\Model\AttributeLine; -use Appwrite\Utopia\Response\Model\AttributeList; -use Appwrite\Utopia\Response\Model\AttributePoint; -use Appwrite\Utopia\Response\Model\AttributePolygon; -use Appwrite\Utopia\Response\Model\AttributeRelationship; -use Appwrite\Utopia\Response\Model\AttributeString; -use Appwrite\Utopia\Response\Model\AttributeURL; -use Appwrite\Utopia\Response\Model\AuthProvider; -use Appwrite\Utopia\Response\Model\BaseList; -use Appwrite\Utopia\Response\Model\Branch; -use Appwrite\Utopia\Response\Model\Bucket; -use Appwrite\Utopia\Response\Model\Collection; -use Appwrite\Utopia\Response\Model\Column; -use Appwrite\Utopia\Response\Model\ColumnBoolean; -use Appwrite\Utopia\Response\Model\ColumnDatetime; -use Appwrite\Utopia\Response\Model\ColumnEmail; -use Appwrite\Utopia\Response\Model\ColumnEnum; -use Appwrite\Utopia\Response\Model\ColumnFloat; -use Appwrite\Utopia\Response\Model\ColumnIndex; -use Appwrite\Utopia\Response\Model\ColumnInteger; -use Appwrite\Utopia\Response\Model\ColumnIP; -use Appwrite\Utopia\Response\Model\ColumnLine; -use Appwrite\Utopia\Response\Model\ColumnList; -use Appwrite\Utopia\Response\Model\ColumnPoint; -use Appwrite\Utopia\Response\Model\ColumnPolygon; -use Appwrite\Utopia\Response\Model\ColumnRelationship; -use Appwrite\Utopia\Response\Model\ColumnString; -use Appwrite\Utopia\Response\Model\ColumnURL; -use Appwrite\Utopia\Response\Model\ConsoleVariables; -use Appwrite\Utopia\Response\Model\Continent; -use Appwrite\Utopia\Response\Model\Country; -use Appwrite\Utopia\Response\Model\Currency; -use Appwrite\Utopia\Response\Model\Database; -use Appwrite\Utopia\Response\Model\Deployment; -use Appwrite\Utopia\Response\Model\DetectionFramework; -use Appwrite\Utopia\Response\Model\DetectionRuntime; -use Appwrite\Utopia\Response\Model\DetectionVariable; -use Appwrite\Utopia\Response\Model\DevKey; -use Appwrite\Utopia\Response\Model\Document as ModelDocument; -use Appwrite\Utopia\Response\Model\Error; -use Appwrite\Utopia\Response\Model\ErrorDev; -use Appwrite\Utopia\Response\Model\Execution; -use Appwrite\Utopia\Response\Model\File; -use Appwrite\Utopia\Response\Model\Framework; -use Appwrite\Utopia\Response\Model\FrameworkAdapter; -use Appwrite\Utopia\Response\Model\Func; -use Appwrite\Utopia\Response\Model\Headers; -use Appwrite\Utopia\Response\Model\HealthAntivirus; -use Appwrite\Utopia\Response\Model\HealthCertificate; -use Appwrite\Utopia\Response\Model\HealthQueue; -use Appwrite\Utopia\Response\Model\HealthStatus; -use Appwrite\Utopia\Response\Model\HealthTime; -use Appwrite\Utopia\Response\Model\HealthVersion; -use Appwrite\Utopia\Response\Model\Identity; -use Appwrite\Utopia\Response\Model\Index; -use Appwrite\Utopia\Response\Model\Installation; -use Appwrite\Utopia\Response\Model\JWT; -use Appwrite\Utopia\Response\Model\Key; -use Appwrite\Utopia\Response\Model\Language; -use Appwrite\Utopia\Response\Model\Locale; -use Appwrite\Utopia\Response\Model\LocaleCode; -use Appwrite\Utopia\Response\Model\Log; -use Appwrite\Utopia\Response\Model\Membership; -use Appwrite\Utopia\Response\Model\Message; -use Appwrite\Utopia\Response\Model\Metric; -use Appwrite\Utopia\Response\Model\MetricBreakdown; -use Appwrite\Utopia\Response\Model\MFAChallenge; -use Appwrite\Utopia\Response\Model\MFAFactors; -use Appwrite\Utopia\Response\Model\MFARecoveryCodes; -use Appwrite\Utopia\Response\Model\MFAType; -use Appwrite\Utopia\Response\Model\Migration; -use Appwrite\Utopia\Response\Model\MigrationFirebaseProject; -use Appwrite\Utopia\Response\Model\MigrationReport; -use Appwrite\Utopia\Response\Model\Mock; -use Appwrite\Utopia\Response\Model\MockNumber; -use Appwrite\Utopia\Response\Model\None; -use Appwrite\Utopia\Response\Model\Phone; -use Appwrite\Utopia\Response\Model\Platform; -use Appwrite\Utopia\Response\Model\Preferences; -use Appwrite\Utopia\Response\Model\Project; -use Appwrite\Utopia\Response\Model\Provider; -use Appwrite\Utopia\Response\Model\ProviderRepository; -use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework; -use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime; -use Appwrite\Utopia\Response\Model\ResourceToken; -use Appwrite\Utopia\Response\Model\Row; -use Appwrite\Utopia\Response\Model\Rule; -use Appwrite\Utopia\Response\Model\Runtime; -use Appwrite\Utopia\Response\Model\Session; -use Appwrite\Utopia\Response\Model\Site; -use Appwrite\Utopia\Response\Model\Specification; -use Appwrite\Utopia\Response\Model\Subscriber; -use Appwrite\Utopia\Response\Model\Table; -use Appwrite\Utopia\Response\Model\Target; -use Appwrite\Utopia\Response\Model\Team; -use Appwrite\Utopia\Response\Model\TemplateEmail; -use Appwrite\Utopia\Response\Model\TemplateFramework; -use Appwrite\Utopia\Response\Model\TemplateFunction; -use Appwrite\Utopia\Response\Model\TemplateRuntime; -use Appwrite\Utopia\Response\Model\TemplateSite; -use Appwrite\Utopia\Response\Model\TemplateSMS; -use Appwrite\Utopia\Response\Model\TemplateVariable; -use Appwrite\Utopia\Response\Model\Token; -use Appwrite\Utopia\Response\Model\Topic; -use Appwrite\Utopia\Response\Model\Transaction; -use Appwrite\Utopia\Response\Model\UsageBuckets; -use Appwrite\Utopia\Response\Model\UsageCollection; -use Appwrite\Utopia\Response\Model\UsageDatabase; -use Appwrite\Utopia\Response\Model\UsageDatabases; -use Appwrite\Utopia\Response\Model\UsageFunction; -use Appwrite\Utopia\Response\Model\UsageFunctions; -use Appwrite\Utopia\Response\Model\UsageProject; -use Appwrite\Utopia\Response\Model\UsageSite; -use Appwrite\Utopia\Response\Model\UsageSites; -use Appwrite\Utopia\Response\Model\UsageStorage; -use Appwrite\Utopia\Response\Model\UsageTable; -use Appwrite\Utopia\Response\Model\UsageUsers; -use Appwrite\Utopia\Response\Model\User; -use Appwrite\Utopia\Response\Model\Variable; -use Appwrite\Utopia\Response\Model\VcsContent; -use Appwrite\Utopia\Response\Model\Webhook; use Exception; use JsonException; -// Keep last use Swoole\Http\Response as SwooleHTTPResponse; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; @@ -418,6 +279,11 @@ class Response extends SwooleResponse */ protected static bool $showSensitive = false; + /** + * @var array + */ + protected static array $models = []; + protected SwooleHTTPResponse $swoole; /** @@ -428,206 +294,6 @@ class Response extends SwooleResponse public function __construct(SwooleHTTPResponse $response) { $this->swoole = $response; - - $this - // General - ->setModel(new None()) - ->setModel(new Any()) - ->setModel(new Error()) - ->setModel(new ErrorDev()) - // Lists - ->setModel(new BaseList('Rows List', self::MODEL_ROW_LIST, 'rows', self::MODEL_ROW)) - ->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_DOCUMENT)) - ->setModel(new BaseList('Tables List', self::MODEL_TABLE_LIST, 'tables', self::MODEL_TABLE)) - ->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'collections', self::MODEL_COLLECTION)) - ->setModel(new BaseList('Databases List', self::MODEL_DATABASE_LIST, 'databases', self::MODEL_DATABASE)) - ->setModel(new BaseList('Indexes List', self::MODEL_INDEX_LIST, 'indexes', self::MODEL_INDEX)) - ->setModel(new BaseList('Column Indexes List', self::MODEL_COLUMN_INDEX_LIST, 'indexes', self::MODEL_COLUMN_INDEX)) - ->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER)) - ->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION)) - ->setModel(new BaseList('Identities List', self::MODEL_IDENTITY_LIST, 'identities', self::MODEL_IDENTITY)) - ->setModel(new BaseList('Logs List', self::MODEL_LOG_LIST, 'logs', self::MODEL_LOG)) - ->setModel(new BaseList('Files List', self::MODEL_FILE_LIST, 'files', self::MODEL_FILE)) - ->setModel(new BaseList('Buckets List', self::MODEL_BUCKET_LIST, 'buckets', self::MODEL_BUCKET)) - ->setModel(new BaseList('Resource Tokens List', self::MODEL_RESOURCE_TOKEN_LIST, 'tokens', self::MODEL_RESOURCE_TOKEN)) - ->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM)) - ->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP)) - ->setModel(new BaseList('Sites List', self::MODEL_SITE_LIST, 'sites', self::MODEL_SITE)) - ->setModel(new BaseList('Site Templates List', self::MODEL_TEMPLATE_SITE_LIST, 'templates', self::MODEL_TEMPLATE_SITE)) - ->setModel(new BaseList('Functions List', self::MODEL_FUNCTION_LIST, 'functions', self::MODEL_FUNCTION)) - ->setModel(new BaseList('Function Templates List', self::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', self::MODEL_TEMPLATE_FUNCTION)) - ->setModel(new BaseList('Installations List', self::MODEL_INSTALLATION_LIST, 'installations', self::MODEL_INSTALLATION)) - ->setModel(new BaseList('Framework Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', self::MODEL_PROVIDER_REPOSITORY_FRAMEWORK)) - ->setModel(new BaseList('Runtime Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', self::MODEL_PROVIDER_REPOSITORY_RUNTIME)) - ->setModel(new BaseList('Branches List', self::MODEL_BRANCH_LIST, 'branches', self::MODEL_BRANCH)) - ->setModel(new BaseList('Frameworks List', self::MODEL_FRAMEWORK_LIST, 'frameworks', self::MODEL_FRAMEWORK)) - ->setModel(new BaseList('Runtimes List', self::MODEL_RUNTIME_LIST, 'runtimes', self::MODEL_RUNTIME)) - ->setModel(new BaseList('Deployments List', self::MODEL_DEPLOYMENT_LIST, 'deployments', self::MODEL_DEPLOYMENT)) - ->setModel(new BaseList('Executions List', self::MODEL_EXECUTION_LIST, 'executions', self::MODEL_EXECUTION)) - ->setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false)) - ->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false)) - ->setModel(new BaseList('API Keys List', self::MODEL_KEY_LIST, 'keys', self::MODEL_KEY, true, false)) - ->setModel(new BaseList('Dev Keys List', self::MODEL_DEV_KEY_LIST, 'devKeys', self::MODEL_DEV_KEY, true, false)) - ->setModel(new BaseList('Auth Providers List', self::MODEL_AUTH_PROVIDER_LIST, 'platforms', self::MODEL_AUTH_PROVIDER, true, false)) - ->setModel(new BaseList('Platforms List', self::MODEL_PLATFORM_LIST, 'platforms', self::MODEL_PLATFORM, true, false)) - ->setModel(new BaseList('Countries List', self::MODEL_COUNTRY_LIST, 'countries', self::MODEL_COUNTRY)) - ->setModel(new BaseList('Continents List', self::MODEL_CONTINENT_LIST, 'continents', self::MODEL_CONTINENT)) - ->setModel(new BaseList('Languages List', self::MODEL_LANGUAGE_LIST, 'languages', self::MODEL_LANGUAGE)) - ->setModel(new BaseList('Currencies List', self::MODEL_CURRENCY_LIST, 'currencies', self::MODEL_CURRENCY)) - ->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE)) - ->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false)) - ->setModel(new BaseList('Variables List', self::MODEL_VARIABLE_LIST, 'variables', self::MODEL_VARIABLE)) - ->setModel(new BaseList('Status List', self::MODEL_HEALTH_STATUS_LIST, 'statuses', self::MODEL_HEALTH_STATUS)) - ->setModel(new BaseList('Rule List', self::MODEL_PROXY_RULE_LIST, 'rules', self::MODEL_PROXY_RULE)) - ->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE)) - ->setModel(new BaseList('Provider list', self::MODEL_PROVIDER_LIST, 'providers', self::MODEL_PROVIDER)) - ->setModel(new BaseList('Message list', self::MODEL_MESSAGE_LIST, 'messages', self::MODEL_MESSAGE)) - ->setModel(new BaseList('Topic list', self::MODEL_TOPIC_LIST, 'topics', self::MODEL_TOPIC)) - ->setModel(new BaseList('Subscriber list', self::MODEL_SUBSCRIBER_LIST, 'subscribers', self::MODEL_SUBSCRIBER)) - ->setModel(new BaseList('Target list', self::MODEL_TARGET_LIST, 'targets', self::MODEL_TARGET)) - ->setModel(new BaseList('Transaction List', self::MODEL_TRANSACTION_LIST, 'transactions', self::MODEL_TRANSACTION)) - ->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION)) - ->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT)) - ->setModel(new BaseList('Specifications List', self::MODEL_SPECIFICATION_LIST, 'specifications', self::MODEL_SPECIFICATION)) - ->setModel(new BaseList('VCS Content List', self::MODEL_VCS_CONTENT_LIST, 'contents', self::MODEL_VCS_CONTENT)) - // Entities - ->setModel(new Database()) - // Collection API Models - ->setModel(new Collection()) - ->setModel(new Attribute()) - ->setModel(new AttributeList()) - ->setModel(new AttributeString()) - ->setModel(new AttributeInteger()) - ->setModel(new AttributeFloat()) - ->setModel(new AttributeBoolean()) - ->setModel(new AttributeEmail()) - ->setModel(new AttributeEnum()) - ->setModel(new AttributeIP()) - ->setModel(new AttributeURL()) - ->setModel(new AttributeDatetime()) - ->setModel(new AttributeRelationship()) - ->setModel(new AttributePoint()) - ->setModel(new AttributeLine()) - ->setModel(new AttributePolygon()) - // Table API Models - ->setModel(new Table()) - ->setModel(new Column()) - ->setModel(new ColumnList()) - ->setModel(new ColumnString()) - ->setModel(new ColumnInteger()) - ->setModel(new ColumnFloat()) - ->setModel(new ColumnBoolean()) - ->setModel(new ColumnEmail()) - ->setModel(new ColumnEnum()) - ->setModel(new ColumnIP()) - ->setModel(new ColumnURL()) - ->setModel(new ColumnDatetime()) - ->setModel(new ColumnRelationship()) - ->setModel(new ColumnPoint()) - ->setModel(new ColumnLine()) - ->setModel(new ColumnPolygon()) - ->setModel(new Index()) - ->setModel(new ColumnIndex()) - ->setModel(new Row()) - ->setModel(new ModelDocument()) - ->setModel(new Log()) - ->setModel(new User()) - ->setModel(new AlgoMd5()) - ->setModel(new AlgoSha()) - ->setModel(new AlgoPhpass()) - ->setModel(new AlgoBcrypt()) - ->setModel(new AlgoScrypt()) - ->setModel(new AlgoScryptModified()) - ->setModel(new AlgoArgon2()) - ->setModel(new Account()) - ->setModel(new Preferences()) - ->setModel(new Session()) - ->setModel(new Identity()) - ->setModel(new Token()) - ->setModel(new JWT()) - ->setModel(new Locale()) - ->setModel(new LocaleCode()) - ->setModel(new File()) - ->setModel(new Bucket()) - ->setModel(new ResourceToken()) - ->setModel(new Team()) - ->setModel(new Membership()) - ->setModel(new Site()) - ->setModel(new TemplateSite()) - ->setModel(new TemplateFramework()) - ->setModel(new Func()) - ->setModel(new TemplateFunction()) - ->setModel(new TemplateRuntime()) - ->setModel(new TemplateVariable()) - ->setModel(new Installation()) - ->setModel(new ProviderRepository()) - ->setModel(new ProviderRepositoryFramework()) - ->setModel(new ProviderRepositoryRuntime()) - ->setModel(new DetectionFramework()) - ->setModel(new DetectionRuntime()) - ->setModel(new DetectionVariable()) - ->setModel(new VcsContent()) - ->setModel(new Branch()) - ->setModel(new Runtime()) - ->setModel(new Framework()) - ->setModel(new FrameworkAdapter()) - ->setModel(new Deployment()) - ->setModel(new Execution()) - ->setModel(new Project()) - ->setModel(new Webhook()) - ->setModel(new Key()) - ->setModel(new DevKey()) - ->setModel(new MockNumber()) - ->setModel(new AuthProvider()) - ->setModel(new Platform()) - ->setModel(new Variable()) - ->setModel(new Country()) - ->setModel(new Continent()) - ->setModel(new Language()) - ->setModel(new Currency()) - ->setModel(new Phone()) - ->setModel(new HealthAntivirus()) - ->setModel(new HealthQueue()) - ->setModel(new HealthStatus()) - ->setModel(new HealthCertificate()) - ->setModel(new HealthTime()) - ->setModel(new HealthVersion()) - ->setModel(new Metric()) - ->setModel(new MetricBreakdown()) - ->setModel(new UsageDatabases()) - ->setModel(new UsageDatabase()) - ->setModel(new UsageTable()) - ->setModel(new UsageCollection()) - ->setModel(new UsageUsers()) - ->setModel(new UsageStorage()) - ->setModel(new UsageBuckets()) - ->setModel(new UsageFunctions()) - ->setModel(new UsageFunction()) - ->setModel(new UsageSites()) - ->setModel(new UsageSite()) - ->setModel(new UsageProject()) - ->setModel(new Headers()) - ->setModel(new Specification()) - ->setModel(new Rule()) - ->setModel(new TemplateSMS()) - ->setModel(new TemplateEmail()) - ->setModel(new ConsoleVariables()) - ->setModel(new MFAChallenge()) - ->setModel(new MFARecoveryCodes()) - ->setModel(new MFAType()) - ->setModel(new MFAFactors()) - ->setModel(new Provider()) - ->setModel(new Message()) - ->setModel(new Topic()) - ->setModel(new Transaction()) - ->setModel(new Subscriber()) - ->setModel(new Target()) - ->setModel(new Migration()) - ->setModel(new MigrationReport()) - ->setModel(new MigrationFirebaseProject()) - // Tests (keep last) - ->setModel(new Mock()); - parent::__construct($response); } @@ -639,20 +305,14 @@ class Response extends SwooleResponse public const CONTENT_TYPE_MULTIPART = 'multipart/form-data'; /** - * List of defined output objects - */ - protected $models = []; - - /** - * Set Model Object + * Register a model * - * @return self + * @param Model $model + * @return void */ - public function setModel(Model $instance): Response + public static function setModel(Model $model): void { - $this->models[$instance->getType()] = $instance; - - return $this; + self::$models[$model->getType()] = $model; } /** @@ -664,11 +324,11 @@ class Response extends SwooleResponse */ public function getModel(string $key): Model { - if (!isset($this->models[$key])) { + if (!isset(self::$models[$key])) { throw new Exception('Undefined model: ' . $key); } - return $this->models[$key]; + return self::$models[$key]; } /** @@ -678,7 +338,18 @@ class Response extends SwooleResponse */ public function getModels(): array { - return $this->models; + return self::$models; + } + + /** + * Check if a model exists + * + * @param string $key + * @return bool + */ + public static function hasModel(string $key): bool + { + return isset(self::$models[$key]); } public function applyFilters(array $data, string $model): array @@ -774,7 +445,7 @@ class Response extends SwooleResponse } if ($rule['array']) { - if (!is_array($data[$key])) { + if (!\is_array($data[$key])) { throw new Exception($key . ' must be an array of type ' . $rule['type']); } @@ -798,7 +469,7 @@ class Response extends SwooleResponse $ruleType = $rule['type']; } - if (!array_key_exists($ruleType, $this->models)) { + if (!self::hasModel($ruleType)) { throw new Exception('Missing model for rule: ' . $ruleType); } diff --git a/src/Appwrite/Utopia/Response/Filters/ListSelection.php b/src/Appwrite/Utopia/Response/Filters/ListSelection.php new file mode 100644 index 0000000000..53c5ae75cf --- /dev/null +++ b/src/Appwrite/Utopia/Response/Filters/ListSelection.php @@ -0,0 +1,41 @@ +selectQueries)) { + return $content; + } + + $selections = []; + foreach ($this->selectQueries as $query) { + foreach ($query->getValues() as $value) { + if ($value === '*') { + return $content; + } + $selections[$value] = true; + } + } + + return $this->handleList($content, $this->itemsKey, function (array $item) use ($selections) { + $filtered = []; + foreach ($item as $key => $value) { + if (isset($selections[$key]) || \str_starts_with($key, '$')) { + $filtered[$key] = $value; + } + } + return $filtered; + }); + } +} diff --git a/src/Appwrite/Utopia/Response/Model.php b/src/Appwrite/Utopia/Response/Model.php index 59c786ee1f..687b8b3eba 100644 --- a/src/Appwrite/Utopia/Response/Model.php +++ b/src/Appwrite/Utopia/Response/Model.php @@ -101,22 +101,6 @@ abstract class Model return $this; } - /** - * Delete an existing Rule - * If rule exists, it will be removed - * - * @param string $key - * @return Model - */ - public function removeRule(string $key): self - { - if (isset($this->rules[$key])) { - unset($this->rules[$key]); - } - - return $this; - } - /** * @return array */ diff --git a/src/Appwrite/Utopia/Response/Model/Account.php b/src/Appwrite/Utopia/Response/Model/Account.php index 07fd4e92ab..2ccbd2e480 100644 --- a/src/Appwrite/Utopia/Response/Model/Account.php +++ b/src/Appwrite/Utopia/Response/Model/Account.php @@ -3,18 +3,131 @@ namespace Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Model; +use Utopia\Database\Document; -class Account extends User +class Account extends Model { public function __construct() { - parent::__construct(); - $this - ->removeRule('password') - ->removeRule('hash') - ->removeRule('mfaRecoveryCodes') - ->removeRule('hashOptions'); + ->addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'User ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('$createdAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'User creation date in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('$updatedAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'User update date in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('name', [ + 'type' => self::TYPE_STRING, + 'description' => 'User name.', + 'default' => '', + 'example' => 'John Doe', + ]) + ->addRule('registration', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'User registration date in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('status', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'User status. Pass `true` for enabled and `false` for disabled.', + 'default' => true, + 'example' => true, + ]) + ->addRule('labels', [ + 'type' => self::TYPE_STRING, + 'description' => 'Labels for the user.', + 'default' => [], + 'example' => ['vip'], + 'array' => true, + ]) + ->addRule('passwordUpdate', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Password update time in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('email', [ + 'type' => self::TYPE_STRING, + 'description' => 'User email address.', + 'default' => '', + 'example' => 'john@appwrite.io', + ]) + ->addRule('phone', [ + 'type' => self::TYPE_STRING, + 'description' => 'User phone number in E.164 format.', + 'default' => '', + 'example' => '+4930901820', + ]) + ->addRule('emailVerification', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Email verification status.', + 'default' => false, + 'example' => true, + ]) + ->addRule('phoneVerification', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Phone verification status.', + 'default' => false, + 'example' => true, + ]) + ->addRule('mfa', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Multi factor authentication status.', + 'default' => false, + 'example' => true, + ]) + ->addRule('prefs', [ + 'type' => Response::MODEL_PREFERENCES, + 'description' => 'User preferences as a key-value object', + 'default' => new \stdClass(), + 'example' => ['theme' => 'pink', 'timezone' => 'UTC'], + ]) + ->addRule('targets', [ + 'type' => Response::MODEL_TARGET, + 'description' => 'A user-owned message receiver. A single user may have multiple e.g. emails, phones, and a browser. Each target is registered with a single provider.', + 'default' => [], + 'array' => true, + 'example' => [], + ]) + ->addRule('accessedAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Most recent access date in ISO 8601 format. This attribute is only updated again after ' . APP_USER_ACCESS / 60 / 60 . ' hours.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ; + } + + /** + * Get Collection + * + * @return Document + */ + public function filter(Document $document): Document + { + $prefs = $document->getAttribute('prefs'); + if ($prefs instanceof Document) { + $prefs = $prefs->getArrayCopy(); + } + + if (is_array($prefs) && empty($prefs)) { + $document->setAttribute('prefs', new \stdClass()); + } + return $document; } /** diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index 7641e96090..c516aab73f 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -276,6 +276,13 @@ class Project extends Model 'default' => '', 'example' => self::TYPE_DATETIME_EXAMPLE, ]) + ->addRule('labels', [ + 'type' => self::TYPE_STRING, + 'description' => 'Labels for the project.', + 'default' => [], + 'example' => ['vip'], + 'array' => true, + ]) ; $services = Config::getParam('services', []); diff --git a/src/Appwrite/Utopia/Response/Model/UsageSites.php b/src/Appwrite/Utopia/Response/Model/UsageSites.php index 74435b332c..fea87c7718 100644 --- a/src/Appwrite/Utopia/Response/Model/UsageSites.php +++ b/src/Appwrite/Utopia/Response/Model/UsageSites.php @@ -3,15 +3,19 @@ namespace Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Model; -class UsageSites extends UsageFunctions +class UsageSites extends Model { public function __construct() { - parent::__construct(); $this - ->removeRule('functionsTotal') - ->removeRule('functions') + ->addRule('range', [ + 'type' => self::TYPE_STRING, + 'description' => 'Time range of the usage stats.', + 'default' => '', + 'example' => '30d', + ]) ->addRule('sitesTotal', [ 'type' => self::TYPE_INTEGER, 'description' => 'Total aggregated number of sites.', @@ -25,6 +29,60 @@ class UsageSites extends UsageFunctions 'example' => [], 'array' => true ]) + ->addRule('deploymentsTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total aggregated number of sites deployments.', + 'default' => 0, + 'example' => 0, + ]) + ->addRule('deploymentsStorageTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total aggregated sum of sites deployment storage.', + 'default' => 0, + 'example' => 0, + ]) + ->addRule('buildsTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total aggregated number of sites build.', + 'default' => 0, + 'example' => 0, + ]) + ->addRule('buildsStorageTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'total aggregated sum of sites build storage.', + 'default' => 0, + 'example' => 0, + ]) + ->addRule('buildsTimeTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total aggregated sum of sites build compute time.', + 'default' => 0, + 'example' => 0, + ]) + ->addRule('buildsMbSecondsTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total aggregated sum of sites build mbSeconds.', + 'default' => 0, + 'example' => 0, + ]) + ->addRule('executionsTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total aggregated number of sites execution.', + 'default' => 0, + 'example' => 0, + ]) + ->addRule('executionsTimeTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total aggregated sum of sites execution compute time.', + 'default' => 0, + 'example' => 0, + ]) + ->addRule('executionsMbSecondsTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total aggregated sum of sites execution mbSeconds.', + 'default' => 0, + 'example' => 0, + ]) ->addRule('requestsTotal', [ 'type' => self::TYPE_INTEGER, 'description' => 'Total aggregated number of requests.', @@ -64,6 +122,95 @@ class UsageSites extends UsageFunctions 'example' => [], 'array' => true ]) + ->addRule('deployments', [ + 'type' => Response::MODEL_METRIC, + 'description' => 'Aggregated number of sites deployment per period.', + 'default' => [], + 'example' => [], + 'array' => true + ]) + ->addRule('deploymentsStorage', [ + 'type' => Response::MODEL_METRIC, + 'description' => 'Aggregated number of sites deployment storage per period.', + 'default' => [], + 'example' => [], + 'array' => true + ]) + ->addRule('buildsSuccessTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total aggregated number of successful site builds.', + 'default' => 0, + 'example' => 0, + ]) + ->addRule('buildsFailedTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total aggregated number of failed site builds.', + 'default' => 0, + 'example' => 0, + ]) + ->addRule('builds', [ + 'type' => Response::MODEL_METRIC, + 'description' => 'Aggregated number of sites build per period.', + 'default' => [], + 'example' => [], + 'array' => true + ]) + ->addRule('buildsStorage', [ + 'type' => Response::MODEL_METRIC, + 'description' => 'Aggregated sum of sites build storage per period.', + 'default' => [], + 'example' => [], + 'array' => true + ]) + ->addRule('buildsTime', [ + 'type' => Response::MODEL_METRIC, + 'description' => 'Aggregated sum of sites build compute time per period.', + 'default' => [], + 'example' => [], + 'array' => true + ]) + ->addRule('buildsMbSeconds', [ + 'type' => Response::MODEL_METRIC, + 'description' => 'Aggregated sum of sites build mbSeconds per period.', + 'default' => [], + 'example' => [], + 'array' => true + ]) + ->addRule('executions', [ + 'type' => Response::MODEL_METRIC, + 'description' => 'Aggregated number of sites execution per period.', + 'default' => [], + 'example' => [], + 'array' => true + ]) + ->addRule('executionsTime', [ + 'type' => Response::MODEL_METRIC, + 'description' => 'Aggregated number of sites execution compute time per period.', + 'default' => [], + 'example' => [], + 'array' => true + ]) + ->addRule('executionsMbSeconds', [ + 'type' => Response::MODEL_METRIC, + 'description' => 'Aggregated number of sites mbSeconds per period.', + 'default' => [], + 'example' => [], + 'array' => true + ]) + ->addRule('buildsSuccess', [ + 'type' => Response::MODEL_METRIC, + 'description' => 'Aggregated number of successful site builds per period.', + 'default' => [], + 'example' => [], + 'array' => true + ]) + ->addRule('buildsFailed', [ + 'type' => Response::MODEL_METRIC, + 'description' => 'Aggregated number of failed site builds per period.', + 'default' => [], + 'example' => [], + 'array' => true + ]) ; } diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index 0c9d481371..fa0d5f0fab 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -363,4 +363,77 @@ trait AccountBase $this->assertEquals($response['headers']['status-code'], 201); $this->assertEquals('191.0.113.195', $response['body']['clientIp'] ?? $response['body']['ip'] ?? ''); } + + /** + * @group abuseEnabled + */ + public function testAccountAbuseReset(): void + { + $email = \uniqid() . '.abuse.reset.test@example.com'; + $password = 'password'; + $account = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => $password, + 'name' => 'Abuse Reset Test', + ]); + + $this->assertEquals($account['headers']['status-code'], 201); + + // 20 successful requests won't get blocked + for ($i = 0; $i < 20; $i++) { + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => $password, + ]); + + $this->assertEquals($session['headers']['status-code'], 201); + } + + // 10 failures are OK + for ($i = 0; $i < 10; $i++) { + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => 'wrongPassword', + ]); + + $this->assertEquals($session['headers']['status-code'], 401); + } + + // 11th request gets limited + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => 'wrongPassword', + ]); + + $this->assertEquals($session['headers']['status-code'], 429); + + // Even correct password is now blocked, correctness doesn't matter + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => $password, + ]); + + $this->assertEquals($session['headers']['status-code'], 429); + } } diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php index d1d2c9687d..6cde01e240 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php @@ -6119,6 +6119,7 @@ trait DatabasesBase $this->assertEquals(200, $inc['headers']['status-code']); $this->assertEquals(6, $inc['body']['count']); $this->assertEquals($collectionId, $inc['body']['$collectionId']); + $this->assertEquals($databaseId, $inc['body']['$databaseId']); // Verify count = 6 $get = $this->client->call(Client::METHOD_GET, "/databases/$databaseId/collections/$collectionId/documents/$docId", array_merge([ @@ -6231,6 +6232,7 @@ trait DatabasesBase $this->assertEquals(200, $dec['headers']['status-code']); $this->assertEquals(9, $dec['body']['count']); $this->assertEquals($collectionId, $dec['body']['$collectionId']); + $this->assertEquals($databaseId, $dec['body']['$databaseId']); $get = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ 'content-type' => 'application/json', diff --git a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php index 0f85de0ff5..d4acfc338c 100644 --- a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php +++ b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php @@ -3827,6 +3827,7 @@ trait TransactionsBase $this->assertArrayHasKey('$collectionId', $decrementResponse['body'], 'Response should contain $collectionId for Collections API'); $this->assertArrayNotHasKey('$tableId', $decrementResponse['body'], 'Response should not contain $tableId for Collections API'); $this->assertEquals($collectionId, $decrementResponse['body']['$collectionId']); + $this->assertEquals($databaseId, $decrementResponse['body']['$databaseId']); // Test increment endpoint $incrementResponse = $this->client->call( @@ -3846,6 +3847,7 @@ trait TransactionsBase $this->assertArrayHasKey('$collectionId', $incrementResponse['body'], 'Response should contain $collectionId for Collections API'); $this->assertArrayNotHasKey('$tableId', $incrementResponse['body'], 'Response should not contain $tableId for Collections API'); $this->assertEquals($collectionId, $incrementResponse['body']['$collectionId']); + $this->assertEquals($databaseId, $incrementResponse['body']['$databaseId']); // Commit transaction - this will fail if transaction log has 'column' instead of 'attribute' $commitResponse = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php index ba111e5923..bcb87e92d5 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php @@ -7761,6 +7761,7 @@ trait DatabasesBase ])); $this->assertEquals(200, $inc['headers']['status-code']); $this->assertEquals($tableId, $inc['body']['$tableId']); + $this->assertEquals($databaseId, $inc['body']['$databaseId']); $this->assertEquals(6, $inc['body']['count']); // Verify count = 6 @@ -7874,6 +7875,7 @@ trait DatabasesBase $this->assertEquals(200, $dec['headers']['status-code']); $this->assertEquals(9, $dec['body']['count']); $this->assertEquals($tableId, $dec['body']['$tableId']); + $this->assertEquals($databaseId, $dec['body']['$databaseId']); $get = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ 'content-type' => 'application/json', diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php index 488dc60239..a5af7053f0 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php @@ -3963,6 +3963,7 @@ trait TransactionsBase $this->assertArrayHasKey('$tableId', $decrementResponse['body'], 'Response should contain $tableId for TablesDB API'); $this->assertArrayNotHasKey('$collectionId', $decrementResponse['body'], 'Response should not contain $collectionId for TablesDB API'); $this->assertEquals($tableId, $decrementResponse['body']['$tableId']); + $this->assertEquals($databaseId, $decrementResponse['body']['$databaseId']); // Test increment endpoint $incrementResponse = $this->client->call( @@ -3982,6 +3983,7 @@ trait TransactionsBase $this->assertArrayHasKey('$tableId', $incrementResponse['body'], 'Response should contain $tableId for TablesDB API'); $this->assertArrayNotHasKey('$collectionId', $incrementResponse['body'], 'Response should not contain $collectionId for TablesDB API'); $this->assertEquals($tableId, $incrementResponse['body']['$tableId']); + $this->assertEquals($databaseId, $incrementResponse['body']['$databaseId']); // Commit transaction - this will fail if transaction log has 'attribute' instead of 'column' $commitResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index fae6031672..e31331574f 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -4783,7 +4783,7 @@ class ProjectsConsoleClientTest extends Scope */ /** - * @group devKeys + * @group abuseEnabled */ public function testCreateProjectDevKey(): void { @@ -4844,7 +4844,7 @@ class ProjectsConsoleClientTest extends Scope /** - * @group devKeys + * @group abuseEnabled */ public function testListProjectDevKey(): void { @@ -4935,7 +4935,7 @@ class ProjectsConsoleClientTest extends Scope /** - * @group devKeys + * @group abuseEnabled */ public function testGetProjectDevKey(): void { @@ -4979,7 +4979,7 @@ class ProjectsConsoleClientTest extends Scope } /** - * @group devKeys + * @group abuseEnabled */ public function testGetDevKeyWithSdks(): void { @@ -5036,7 +5036,7 @@ class ProjectsConsoleClientTest extends Scope } /** - * @group devKeys + * @group abuseEnabled */ public function testNoHostValidationWithDevKey(): void { @@ -5117,7 +5117,7 @@ class ProjectsConsoleClientTest extends Scope } /** - * @group devKeys + * @group abuseEnabled */ public function testCorsWithDevKey(): void { @@ -5174,7 +5174,7 @@ class ProjectsConsoleClientTest extends Scope } /** - * @group devKeys + * @group abuseEnabled */ public function testNoRateLimitWithDevKey(): void { @@ -5279,7 +5279,7 @@ class ProjectsConsoleClientTest extends Scope } /** - * @group devKeys + * @group abuseEnabled */ public function testUpdateProjectDevKey(): void { @@ -5324,7 +5324,7 @@ class ProjectsConsoleClientTest extends Scope } /** - * @group devKeys + * @group abuseEnabled */ public function testDeleteProjectDevKey(): void { @@ -5382,4 +5382,216 @@ class ProjectsConsoleClientTest extends Scope /** * Devkeys Tests ends here ------------------------------------------------ */ + + public function testProjectLabels(): void + { + // Setup: Prepare team + $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => ID::unique(), + 'name' => 'Query Select Test Team', + ]); + + $this->assertEquals(201, $team['headers']['status-code']); + $teamId = $team['body']['$id']; + + // Setup: Prepare project + $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'projectId' => ID::unique(), + 'name' => 'Test project - Labels 1', + 'teamId' => $teamId, + 'region' => System::getEnv('_APP_REGION', 'default') + ]); + + $this->assertEquals(201, $project['headers']['status-code']); + $this->assertIsArray($project['body']['labels']); + $this->assertCount(0, $project['body']['labels']); + $projectId = $project['body']['$id']; + + // Apply labels + $project = $this->client->call(Client::METHOD_PUT, '/projects/' . $projectId . '/labels', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'labels' => ['vip', 'imagine', 'blocked'] + ]); + + $this->assertEquals(200, $project['headers']['status-code']); + $this->assertIsArray($project['body']['labels']); + $this->assertCount(3, $project['body']['labels']); + $this->assertEquals('vip', $project['body']['labels'][0]); + $this->assertEquals('imagine', $project['body']['labels'][1]); + $this->assertEquals('blocked', $project['body']['labels'][2]); + + // Update labels + $project = $this->client->call(Client::METHOD_PUT, '/projects/' . $projectId . '/labels', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'labels' => ['nonvip', 'imagine'] + ]); + $this->assertEquals(200, $project['headers']['status-code']); + $this->assertIsArray($project['body']['labels']); + $this->assertCount(2, $project['body']['labels']); + $this->assertEquals('nonvip', $project['body']['labels'][0]); + $this->assertEquals('imagine', $project['body']['labels'][1]); + + // Filter by labels + $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::contains('labels', ['nonvip'])->toString(), + ] + ]); + $this->assertEquals(200, $projects['headers']['status-code']); + $this->assertEquals(1, $projects['body']['total']); + $this->assertEquals($projectId, $projects['body']['projects'][0]['$id']); + + $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::contains('labels', ['vip'])->toString(), + ] + ]); + $this->assertEquals(200, $projects['headers']['status-code']); + $this->assertEquals(0, $projects['body']['total']); + + $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::contains('labels', ['imagine'])->toString(), + ] + ]); + $this->assertEquals(200, $projects['headers']['status-code']); + $this->assertEquals(1, $projects['body']['total']); + $this->assertEquals($projectId, $projects['body']['projects'][0]['$id']); + + $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::contains('labels', ['nonvip', 'imagine'])->toString(), + ] + ]); + $this->assertEquals(200, $projects['headers']['status-code']); + $this->assertEquals(1, $projects['body']['total']); + $this->assertEquals($projectId, $projects['body']['projects'][0]['$id']); + + // Setup: Second project with only imagine label + $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'projectId' => ID::unique(), + 'name' => 'Test project - Labels 2', + 'teamId' => $teamId, + 'region' => System::getEnv('_APP_REGION', 'default') + ]); + + $this->assertEquals(201, $project['headers']['status-code']); + $this->assertIsArray($project['body']['labels']); + $this->assertCount(0, $project['body']['labels']); + $projectId2 = $project['body']['$id']; + + $project = $this->client->call(Client::METHOD_PUT, '/projects/' . $projectId2 . '/labels', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'labels' => ['vip', 'imagine'] + ]); + $this->assertEquals(200, $project['headers']['status-code']); + $this->assertIsArray($project['body']['labels']); + $this->assertCount(2, $project['body']['labels']); + $this->assertEquals('vip', $project['body']['labels'][0]); + $this->assertEquals('imagine', $project['body']['labels'][1]); + + // List of imagine has both + $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::contains('labels', ['imagine'])->toString(), + ] + ]); + $this->assertEquals(200, $projects['headers']['status-code']); + $this->assertEquals(2, $projects['body']['total']); + $this->assertEquals($projectId, $projects['body']['projects'][0]['$id']); + $this->assertEquals($projectId2, $projects['body']['projects'][1]['$id']); + + // List of vip only has second + $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::contains('labels', ['vip'])->toString(), + ] + ]); + $this->assertEquals(200, $projects['headers']['status-code']); + $this->assertEquals(1, $projects['body']['total']); + $this->assertEquals($projectId2, $projects['body']['projects'][0]['$id']); + + // List of vip and imagine has second + $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::contains('labels', ['vip'])->toString(), + Query::contains('labels', ['imagine'])->toString(), + ] + ]); + $this->assertEquals(200, $projects['headers']['status-code']); + $this->assertEquals(1, $projects['body']['total']); + $this->assertEquals($projectId2, $projects['body']['projects'][0]['$id']); + + // List of vip or imagine has second + $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::contains('labels', ['vip', 'imagine'])->toString(), + ] + ]); + $this->assertEquals(200, $projects['headers']['status-code']); + $this->assertEquals(2, $projects['body']['total']); + $this->assertEquals($projectId, $projects['body']['projects'][0]['$id']); + $this->assertEquals($projectId2, $projects['body']['projects'][1]['$id']); + + // Cleanup + $response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $projectId2, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_DELETE, '/teams/' . $teamId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $response['headers']['status-code']); + } }