Compare commits

..
Author SHA1 Message Date
ArnabChatterjee20k a229cc1367 added logging 2026-04-06 13:05:27 +05:30
107 changed files with 3217 additions and 8940 deletions
-33
View File
@@ -161,27 +161,6 @@ jobs:
- name: Run PHPStan
run: composer analyze -- --no-progress
specs:
name: Checks / Specs
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: swoole
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
- name: Generate specs
run: _APP_STORAGE_LIMIT=5368709120 php app/cli.php specs --version=latest --git=no
locale:
name: Checks / Locale
runs-on: ubuntu-latest
@@ -480,10 +459,6 @@ jobs:
_APP_BROWSER_HOST: http://invalid-browser/v1
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
@@ -558,10 +533,6 @@ jobs:
_APP_OPTIONS_ABUSE: enabled
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
@@ -619,10 +590,6 @@ jobs:
env:
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
+1 -1
View File
@@ -44,7 +44,7 @@ Table of Contents:
## Products
- **[Appwrite Auth](https://appwrite.io/docs/products/auth)** - Secure user authentication with multiple login methods including email/password, SMS, OAuth, anonymous sessions, and magic links. Includes session management, multi-factor authentication, and user verification flows.
- **[Appwrite Auth](https://appwrite.io/docs/products/authentication)** - Secure user authentication with multiple login methods including email/password, SMS, OAuth, anonymous sessions, and magic links. Includes session management, multi-factor authentication, and user verification flows.
- **[Appwrite Databases](https://appwrite.io/docs/products/databases)** - Scalable structured data storage with support for databases, tables, and rows. Includes querying, pagination, indexing, and relationships to model complex application data.
+38 -34
View File
@@ -18,15 +18,13 @@ use Swoole\Timer;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\CLI\Adapters\Generic;
use Utopia\CLI\CLI;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DI\Dependency;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Platform\Service;
@@ -49,7 +47,7 @@ require_once __DIR__ . '/controllers/general.php';
global $register;
$platform = new Appwrite();
$args = $_SERVER['argv'] ?? [];
$args = $platform->getEnv('argv');
\array_shift($args);
if (! isset($args[0])) {
@@ -58,15 +56,21 @@ if (! isset($args[0])) {
}
$taskName = $args[0];
$container = new Container();
$cli = new CLI(new Generic(), $_SERVER['argv'] ?? [], $container);
$platform->setCli($cli);
$platform->init(Service::TYPE_TASK);
$cli = $platform->getCli();
$container->set('register', fn () => $register, []);
$setResource = function (string $name, callable $callback, array $injections = []) use ($cli) {
$dependency = new Dependency();
$dependency->setName($name)->setCallback($callback);
foreach ($injections as $injection) {
$dependency->inject($injection);
}
$cli->setResource($dependency);
};
$container->set('cache', function ($pools) {
$setResource('register', fn () => $register, []);
$setResource('cache', function ($pools) {
$list = Config::getParam('pools-cache', []);
$adapters = [];
@@ -77,18 +81,18 @@ $container->set('cache', function ($pools) {
return new Cache(new Sharding($adapters));
}, ['pools']);
$container->set('pools', function (Registry $register) {
$setResource('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
$container->set('authorization', function () {
$setResource('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
$container->set('dbForPlatform', function ($pools, $cache, $authorization) {
$setResource('dbForPlatform', function ($pools, $cache, $authorization) {
$sleep = 3;
$maxAttempts = 5;
$attempts = 0;
@@ -131,17 +135,17 @@ $container->set('dbForPlatform', function ($pools, $cache, $authorization) {
return $dbForPlatform;
}, ['pools', 'cache', 'authorization']);
$container->set('console', function () {
$setResource('console', function () {
return new Document(Config::getParam('console'));
}, []);
$container->set(
$setResource(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false,
[]
);
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
$setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) {
@@ -203,10 +207,10 @@ $container->set('getProjectDB', function (Group $pools, Database $dbForPlatform,
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, &$database, $authorization) {
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
return $database;
@@ -231,41 +235,41 @@ $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization
return $database;
};
}, ['pools', 'cache', 'authorization']);
$container->set('publisher', function (Group $pools) {
$setResource('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
$container->set('publisherDatabases', function (BrokerPool $publisher) {
$setResource('publisherDatabases', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherFunctions', function (BrokerPool $publisher) {
$setResource('publisherFunctions', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherMigrations', function (BrokerPool $publisher) {
$setResource('publisherMigrations', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherMessaging', function (BrokerPool $publisher) {
$setResource('publisherMessaging', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$container->set('usage', function () {
$setResource('usage', function () {
return new UsageContext();
}, []);
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
$container->set('queueForStatsResources', function (Publisher $publisher) {
$setResource('queueForStatsResources', function (Publisher $publisher) {
return new StatsResources($publisher);
}, ['publisher']);
$container->set('queueForFunctions', function (Publisher $publisher) {
$setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
$setResource('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('queueForCertificates', function (Publisher $publisher) {
$setResource('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$container->set('logError', function (Registry $register) {
$setResource('logError', function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
Console::error('[Error] Timestamp: ' . date('c', time()));
Console::error('[Error] Type: ' . get_class($error));
@@ -317,13 +321,13 @@ $container->set('logError', function (Registry $register) {
};
}, ['register']);
$container->set('executor', fn () => new Executor(), []);
$setResource('executor', fn () => new Executor(), []);
$container->set('bus', function (Registry $register) use ($container) {
return $register->get('bus')->setResolver(fn (string $name) => $container->get($name));
$setResource('bus', function (Registry $register) use ($cli) {
return $register->get('bus')->setResolver(fn (string $name) => $cli->getResource($name));
}, ['register']);
$container->set('telemetry', fn () => new NoTelemetry(), []);
$setResource('telemetry', fn () => new NoTelemetry(), []);
$exitCode = 0;
+3 -3
View File
@@ -594,7 +594,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => ID::custom('key'), // For app platforms
'$id' => ID::custom('key'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@@ -605,7 +605,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => ID::custom('store'), // Unused at the moment
'$id' => ID::custom('store'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
@@ -616,7 +616,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => ID::custom('hostname'), // For web platforms
'$id' => ID::custom('hostname'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
-10
View File
@@ -1179,16 +1179,6 @@ return [
'description' => 'Platform with the requested ID could not be found.',
'code' => 404,
],
Exception::PLATFORM_METHOD_UNSUPPORTED => [
'name' => Exception::PLATFORM_METHOD_UNSUPPORTED,
'description' => 'The requested platform has invalid type. Please use corresponding update method for the platform type.',
'code' => 400,
],
Exception::PLATFORM_ALREADY_EXISTS => [
'name' => Exception::PLATFORM_ALREADY_EXISTS,
'description' => 'Platform with the same ID already exists in this project. Try again with a different ID.',
'code' => 409,
],
Exception::VARIABLE_NOT_FOUND => [
'name' => Exception::VARIABLE_NOT_FOUND,
'description' => 'Variable with the requested ID could not be found.',
-11
View File
@@ -376,17 +376,6 @@ return [
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\Wordpress',
],
'x' => [
'name' => 'X',
'developers' => 'https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code',
'icon' => 'icon-twitter',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\X',
],
'yahoo' => [
'name' => 'Yahoo',
'developers' => 'https://developer.yahoo.com/oauth2/guide/flows_authcode/',
+14
View File
@@ -3,6 +3,13 @@
// List of scopes for organization (teams) API keys
return [
"platforms.read" => [
"description" => 'Access to read project\'s platforms',
],
"platforms.write" => [
"description" =>
'Access to create, update, and delete project\'s platforms',
],
"projects.read" => [
"description" => 'Access to read organization\'s projects',
],
@@ -10,6 +17,13 @@ return [
"description" =>
"Access to create, update, and delete projects in organization",
],
"keys.read" => [
"description" => 'Access to read project\'s API keys',
],
"keys.write" => [
"description" =>
"Access to create, update, and delete project\'s API keys",
],
"devKeys.read" => [
"description" => 'Access to read project\'s development keys',
],
-16
View File
@@ -188,20 +188,4 @@ return [ // List of publicly visible scopes
"description" =>
"Access to update project\'s information",
],
"keys.read" => [
"description" =>
"Access to read project\'s keys",
],
"keys.write" => [
"description" =>
"Access to create, update, and delete project\'s keys",
],
"platforms.read" => [
"description" =>
"Access to read project\'s platforms",
],
"platforms.write" => [
"description" =>
"Access to create, update, and delete project\'s platforms",
],
];
+36 -56
View File
@@ -207,7 +207,7 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
}
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, Authorization $authorization) {
// Attempt to decode secret as a JWT (used by OAuth2 token flow to carry provider info)
$oauthProvider = null;
@@ -345,7 +345,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
->setProperty('secret', $sessionSecret)
->encode();
if (!$domainVerification) {
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
@@ -353,8 +353,8 @@ $createSession = function (string $userId, string $secret, Request $request, Res
$protocol = $request->getProtocol();
$response
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED);
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
@@ -719,9 +719,7 @@ Http::delete('/v1/account/sessions')
->inject('queueForDeletes')
->inject('store')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) {
$protocol = $request->getProtocol();
$sessions = $user->getAttribute('sessions', []);
@@ -730,7 +728,7 @@ Http::delete('/v1/account/sessions')
foreach ($sessions as $session) {/** @var Document $session */
$dbForProject->deleteDocument('sessions', $session->getId());
if (!$domainVerification) {
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
}
@@ -743,8 +741,8 @@ Http::delete('/v1/account/sessions')
// If current session delete the cookies too
$response
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
// Use current session for events.
$currentSession = $session;
@@ -851,9 +849,7 @@ Http::delete('/v1/account/sessions/:sessionId')
->inject('queueForDeletes')
->inject('store')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) {
$protocol = $request->getProtocol();
$sessionId = ($sessionId === 'current')
@@ -879,13 +875,13 @@ Http::delete('/v1/account/sessions/:sessionId')
->setAttribute('current', true)
->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')));
if (!$domainVerification) {
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
}
$response
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
}
$dbForProject->purgeCachedDocument('users', $user->getId());
@@ -1039,10 +1035,8 @@ Http::post('/v1/account/sessions/email')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
$email = \strtolower($email);
$protocol = $request->getProtocol();
@@ -1103,28 +1097,28 @@ Http::post('/v1/account/sessions/email')
]));
}
$dbForProject->purgeCachedDocument('users', $user->getId());
$session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
$dbForProject->purgeCachedDocument('users', $user->getId());
$encoded = $store
->setProperty('id', $user->getId())
->setProperty('secret', $secret)
->encode();
if (!$domainVerification) {
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$response
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED)
;
@@ -1190,10 +1184,8 @@ Http::post('/v1/account/sessions/anonymous')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
$protocol = $request->getProtocol();
if ('console' === $project->getId()) {
@@ -1284,15 +1276,15 @@ Http::post('/v1/account/sessions/anonymous')
->setProperty('secret', $secret)
->encode();
if (!$domainVerification) {
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$response
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED)
;
@@ -1347,9 +1339,7 @@ Http::post('/v1/account/sessions/token')
->inject('store')
->inject('proofForToken')
->inject('proofForCode')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->inject('authorization')
->action($createSession);
Http::get('/v1/account/sessions/oauth2/:provider')
@@ -1548,10 +1538,8 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->inject('proofForPassword')
->inject('proofForToken')
->inject('plan')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) use ($oauthDefaultSuccess) {
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, Authorization $authorization) use ($oauthDefaultSuccess) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
@@ -2067,7 +2055,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->setProperty('secret', $secret)
->encode();
if (!$domainVerification) {
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
@@ -2080,14 +2068,14 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
// TODO: Remove this deprecated workaround - support only token
if ($state['success']['path'] == $oauthDefaultSuccess) {
$query['project'] = $project->getId();
$query['domain'] = $cookieDomain;
$query['domain'] = Config::getParam('cookieDomain');
$query['key'] = $store->getKey();
$query['secret'] = $encoded;
}
$response
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
}
if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) {
@@ -2185,7 +2173,7 @@ Http::get('/v1/account/tokens/oauth2/:provider')
}
$host = $platform['consoleHostname'] ?? '';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$redirectBase = $protocol . '://' . $host;
if ($protocol === 'https' && $port !== '443') {
@@ -2208,12 +2196,10 @@ Http::get('/v1/account/tokens/oauth2/:provider')
'token' => true,
], $scopes);
$loginURL = $oauth2->getLoginURL();
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($loginURL);
->redirect($oauth2->getLoginURL());
});
Http::post('/v1/account/tokens/magic-url')
@@ -2900,13 +2886,11 @@ Http::put('/v1/account/sessions/magic-url')
->inject('queueForMails')
->inject('store')
->inject('proofForCode')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $domainVerification, $cookieDomain, $authorization) use ($createSession) {
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $authorization) use ($createSession) {
$proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
$proofForToken->setHash(new Sha());
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $domainVerification, $cookieDomain, $authorization);
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $authorization);
});
Http::put('/v1/account/sessions/phone')
@@ -2952,8 +2936,6 @@ Http::put('/v1/account/sessions/phone')
->inject('store')
->inject('proofForToken')
->inject('proofForCode')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action($createSession);
@@ -3745,9 +3727,7 @@ Http::patch('/v1/account/status')
->inject('dbForProject')
->inject('queueForEvents')
->inject('store')
->inject('domainVerification')
->inject('cookieDomain')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store, bool $domainVerification, ?string $cookieDomain) {
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store) {
$user->setAttribute('status', false);
@@ -3757,14 +3737,14 @@ Http::patch('/v1/account/status')
->setParam('userId', $user->getId())
->setPayload($response->output($user, Response::MODEL_ACCOUNT));
if (!$domainVerification) {
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
}
$protocol = $request->getProtocol();
$response
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
;
$response->dynamic($user, Response::MODEL_ACCOUNT);
+558
View File
@@ -5,18 +5,28 @@ use Appwrite\Auth\Validator\MockNumber;
use Appwrite\Event\Delete;
use Appwrite\Event\Mail;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Keys;
use Appwrite\Utopia\Response;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Validator\Email;
use Utopia\Http\Http;
@@ -759,6 +769,288 @@ Http::delete('/v1/projects/:projectId')
$response->noContent();
});
// Keys
Http::post('/v1/projects/:projectId/keys')
->desc('Create key')
->groups(['api', 'projects'])
->label('scope', 'keys.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'keys',
name: 'createKey',
description: '/docs/references/projects/create-key.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_KEY,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
// TODO: When migrating to Platform API, mark keyId required for consistency
->param('keyId', 'unique()', fn (Database $dbForPlatform) => new CustomId($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key 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.', true, ['dbForPlatform'])->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
$keyId = $keyId == 'unique()' ? ID::unique() : $keyId;
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$key = new Document([
'$id' => $keyId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $project->getSequence(),
'resourceId' => $project->getId(),
'resourceType' => 'projects',
'name' => $name,
'scopes' => $scopes,
'expire' => $expire,
'sdks' => [],
'accessedAt' => null,
'secret' => API_KEY_STANDARD . '_' . \bin2hex(\random_bytes(128)),
]);
try {
$key = $dbForPlatform->createDocument('keys', $key);
} catch (Duplicate) {
throw new Exception(Exception::KEY_ALREADY_EXISTS);
}
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($key, Response::MODEL_KEY);
});
Http::get('/v1/projects/:projectId/keys')
->desc('List keys')
->groups(['api', 'projects'])
->label('scope', 'keys.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'keys',
name: 'listKeys',
description: '/docs/references/projects/list-keys.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_KEY_LIST,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('queries', [], new Keys(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Keys::ALLOWED_ATTRIBUTES), true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, array $queries, bool $includeTotal, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Backwards compatibility
if (\count(Query::getByType($queries, [Query::TYPE_LIMIT])) === 0) {
$queries[] = Query::limit(5000);
}
$queries[] = Query::equal('resourceType', ['projects']);
$queries[] = Query::equal('resourceInternalId', [$project->getSequence()]);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$keyId = $cursor->getValue();
$cursorDocument = $dbForPlatform->getDocument('keys', $keyId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Key '{$keyId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
$keys = $dbForPlatform->find('keys', $queries);
$response->dynamic(new Document([
'keys' => $keys,
'total' => $includeTotal ? $dbForPlatform->count('keys', $filterQueries, APP_LIMIT_COUNT) : 0,
]), Response::MODEL_KEY_LIST);
});
Http::get('/v1/projects/:projectId/keys/:keyId')
->desc('Get key')
->groups(['api', 'projects'])
->label('scope', 'keys.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'keys',
name: 'getKey',
description: '/docs/references/projects/get-key.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_KEY,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key unique ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $keyId, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$key = $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
]);
if ($key->isEmpty()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
$response->dynamic($key, Response::MODEL_KEY);
});
Http::put('/v1/projects/:projectId/keys/:keyId')
->desc('Update key')
->groups(['api', 'projects'])
->label('scope', 'keys.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'keys',
name: 'updateKey',
description: '/docs/references/projects/update-key.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_KEY,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key unique ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$key = $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
]);
if ($key->isEmpty()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
$key
->setAttribute('name', $name)
->setAttribute('scopes', $scopes)
->setAttribute('expire', $expire);
$dbForPlatform->updateDocument('keys', $key->getId(), $key);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response->dynamic($key, Response::MODEL_KEY);
});
Http::delete('/v1/projects/:projectId/keys/:keyId')
->desc('Delete key')
->groups(['api', 'projects'])
->label('scope', 'keys.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'keys',
name: 'deleteKey',
description: '/docs/references/projects/delete-key.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key unique ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $keyId, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$key = $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
]);
if ($key->isEmpty()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
$dbForPlatform->deleteDocument('keys', $key->getId());
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response->noContent();
});
// JWT Keys
Http::post('/v1/projects/:projectId/jwts')
@@ -801,6 +1093,272 @@ Http::post('/v1/projects/:projectId/jwts')
])]), Response::MODEL_JWT);
});
// Platforms
Http::post('/v1/projects/:projectId/platforms')
->desc('Create platform')
->groups(['api', 'projects'])
->label('audits.event', 'platforms.create')
->label('audits.resource', 'project/{request.projectId}')
->label('scope', 'platforms.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'platforms',
name: 'createPlatform',
description: '/docs/references/projects/create-platform.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PLATFORM,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param(
'type',
null,
new WhiteList([
Platform::TYPE_WEB,
Platform::TYPE_FLUTTER_WEB,
Platform::TYPE_FLUTTER_IOS,
Platform::TYPE_FLUTTER_ANDROID,
Platform::TYPE_FLUTTER_LINUX,
Platform::TYPE_FLUTTER_MACOS,
Platform::TYPE_FLUTTER_WINDOWS,
Platform::TYPE_APPLE_IOS,
Platform::TYPE_APPLE_MACOS,
Platform::TYPE_APPLE_WATCHOS,
Platform::TYPE_APPLE_TVOS,
Platform::TYPE_ANDROID,
Platform::TYPE_UNITY,
Platform::TYPE_REACT_NATIVE_IOS,
Platform::TYPE_REACT_NATIVE_ANDROID,
], true),
'Platform type. Possible values are: web, flutter-web, flutter-ios, flutter-android, flutter-linux, flutter-macos, flutter-windows, apple-ios, apple-macos, apple-watchos, apple-tvos, android, unity, react-native-ios, react-native-android.'
)
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('key', '', new Text(256), 'Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', true)
->param('store', '', new Text(256), 'App store or Google Play store ID. Max length: 256 chars.', true)
->param('hostname', '', new Hostname(), 'Platform client hostname. Max length: 256 chars.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $type, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'type' => $type,
'name' => $name,
'key' => $key,
'store' => $store,
'hostname' => $hostname
]);
$platform = $dbForPlatform->createDocument('platforms', $platform);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($platform, Response::MODEL_PLATFORM);
});
Http::get('/v1/projects/:projectId/platforms')
->desc('List platforms')
->groups(['api', 'projects'])
->label('scope', 'platforms.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'platforms',
name: 'listPlatforms',
description: '/docs/references/projects/list-platforms.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM_LIST,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platforms = $dbForPlatform->find('platforms', [
Query::equal('projectInternalId', [$project->getSequence()]),
Query::limit(5000),
]);
$response->dynamic(new Document([
'platforms' => $platforms,
'total' => $includeTotal ? count($platforms) : 0,
]), Response::MODEL_PLATFORM_LIST);
});
Http::get('/v1/projects/:projectId/platforms/:platformId')
->desc('Get platform')
->groups(['api', 'projects'])
->label('scope', 'platforms.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'platforms',
name: 'getPlatform',
description: '/docs/references/projects/get-platform.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform unique ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $platformId, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = $dbForPlatform->findOne('platforms', [
Query::equal('$id', [$platformId]),
Query::equal('projectInternalId', [$project->getSequence()]),
]);
if ($platform->isEmpty()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
$response->dynamic($platform, Response::MODEL_PLATFORM);
});
Http::put('/v1/projects/:projectId/platforms/:platformId')
->desc('Update platform')
->groups(['api', 'projects'])
->label('scope', 'platforms.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'platforms',
name: 'updatePlatform',
description: '/docs/references/projects/update-platform.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform unique ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('key', '', new Text(256), 'Package name for android or bundle ID for iOS. Max length: 256 chars.', true)
->param('store', '', new Text(256), 'App store or Google Play store ID. Max length: 256 chars.', true)
->param('hostname', '', new Hostname(), 'Platform client URL. Max length: 256 chars.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $platformId, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = $dbForPlatform->findOne('platforms', [
Query::equal('$id', [$platformId]),
Query::equal('projectInternalId', [$project->getSequence()]),
]);
if ($platform->isEmpty()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
$platform
->setAttribute('name', $name)
->setAttribute('key', $key)
->setAttribute('store', $store)
->setAttribute('hostname', $hostname);
$dbForPlatform->updateDocument('platforms', $platform->getId(), $platform);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response->dynamic($platform, Response::MODEL_PLATFORM);
});
Http::delete('/v1/projects/:projectId/platforms/:platformId')
->desc('Delete platform')
->groups(['api', 'projects'])
->label('audits.event', 'platforms.delete')
->label('audits.resource', 'project/{request.projectId}/platform/${request.platformId}')
->label('scope', 'platforms.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'platforms',
name: 'deletePlatform',
description: '/docs/references/projects/delete-platform.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform unique ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $platformId, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = $dbForPlatform->findOne('platforms', [
Query::equal('$id', [$platformId]),
Query::equal('projectInternalId', [$project->getSequence()]),
]);
if ($platform->isEmpty()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
$dbForPlatform->deleteDocument('platforms', $platformId);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response->noContent();
});
// CUSTOM SMTP and Templates
Http::patch('/v1/projects/:projectId/smtp')
->desc('Update SMTP')
+33 -6
View File
@@ -61,6 +61,8 @@ use Utopia\System\System;
use Utopia\Validator;
use Utopia\Validator\Text;
Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
@@ -752,8 +754,11 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
if (\is_array($values)) {
$count = 0;
foreach ($values as $value) {
$response->addHeader($name, $value);
$override = $count === 0;
$response->addHeader($name, $value, override: $override);
$count++;
}
} else {
$response->addHeader($name, $values);
@@ -899,16 +904,40 @@ Http::init()
$locale->setDefault($localeParam);
}
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
$selfDomain = new Domain($request->getHostname());
$endDomain = new Domain((string)$origin);
Config::setParam(
'domainVerification',
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
$endDomain->getRegisterable() !== ''
);
$localHosts = ['localhost','localhost:'.$request->getPort()];
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
if (!empty($migrationHost)) {
// Treat the migration host like localhost because internal migration and
// CI traffic may use it before a public domain is configured.
$localHosts[] = $migrationHost;
$localHosts[] = $migrationHost.':'.$request->getPort();
}
$isLocalHost = in_array($request->getHostname(), $localHosts);
$isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false;
$isConsoleProject = $project->getAttribute('$id', '') === 'console';
$isConsoleRootSession = System::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled';
Config::setParam(
'cookieDomain',
$isLocalHost || $isIpAddress
? null
: (
$isConsoleProject && $isConsoleRootSession
? '.' . $selfDomain->getRegisterable()
: '.' . $request->getHostname()
)
);
$warnings = [];
/*
@@ -1471,9 +1500,7 @@ Http::error()
try {
$cors = $utopia->getResource('cors');
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
$response
->removeHeader($name)
->addHeader($name, $value);
$response->addHeader($name, $value, override: true);
}
} catch (Throwable) {
// Degrade gracefully - error response without CORS is no worse than before.
-7
View File
@@ -98,9 +98,6 @@ Http::init()
->inject('authorization')
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
/**
* Handle user authentication and session validation.
@@ -492,10 +489,6 @@ Http::init()
$request->setUser($user);
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
$path = $route->getMatchedPath();
$databaseType = match (true) {
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
+54 -57
View File
@@ -1,13 +1,16 @@
<?php
require_once __DIR__ . '/init.php';
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/init/span.php';
$registerRequestResources = require __DIR__ . '/init/resources/request.php';
global $register;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Swoole\Constant;
use Swoole\Http\Request as SwooleRequest;
use Swoole\Http\Response as SwooleResponse;
use Swoole\Http\Server;
use Swoole\Process;
use Swoole\Table;
use Swoole\Timer;
@@ -26,7 +29,6 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Http\Adapter\Swoole\Server;
use Utopia\Http\Files;
use Utopia\Http\Http;
use Utopia\Logger\Log;
@@ -47,35 +49,18 @@ $certifiedDomains = new Table(100_000);
$certifiedDomains->column('value', Table::TYPE_INT, 1);
$certifiedDomains->create();
global $container;
$container->set('riskyDomains', fn () => $riskyDomains);
$container->set('certifiedDomains', fn () => $certifiedDomains);
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['register']);
Http::setResource('riskyDomains', fn () => $riskyDomains);
Http::setResource('certifiedDomains', fn () => $certifiedDomains);
$http = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
mode: SWOOLE_PROCESS,
);
$payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$swooleAdapter = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
settings: [
Constant::OPTION_WORKER_NUM => $totalWorkers,
Constant::OPTION_DISPATCH_FUNC => dispatch(...),
Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD,
Constant::OPTION_HTTP_COMPRESSION => false,
Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize,
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
],
container: $container,
);
$container->set('container', fn () => fn () => $swooleAdapter->getContainer());
$http = $swooleAdapter->getServer();
/**
* Assigns HTTP requests to worker threads by analyzing its payload/content.
*
@@ -84,16 +69,16 @@ $http = $swooleAdapter->getServer();
* riskier tasks to a dedicated worker subset. Prefers idle workers, with fallback to random selection if necessary.
* doc: https://openswoole.com/docs/modules/swoole-server/configuration#dispatch_func
*
* @param \Swoole\Http\Server $server Swoole server instance.
* @param Server $server Swoole server instance.
* @param int $fd client ID
* @param int $type the type of data and its current state
* @param string|null $data Request content for categorization.
* @global int $totalThreads Total number of workers.
* @return int Chosen worker ID for the request.
*/
function dispatch(\Swoole\Http\Server $server, int $fd, int $type, $data = null): int
function dispatch(Server $server, int $fd, int $type, $data = null): int
{
$resolveWorkerId = function (\Swoole\Http\Server $server, $data = null) {
$resolveWorkerId = function (Server $server, $data = null) {
global $totalWorkers, $riskyDomains;
// If data is not set we can send request to any worker
@@ -176,6 +161,18 @@ function dispatch(\Swoole\Http\Server $server, int $fd, int $type, $data = null)
return $workerId;
}
$http
->set([
Constant::OPTION_WORKER_NUM => $totalWorkers,
Constant::OPTION_DISPATCH_FUNC => dispatch(...),
Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD,
Constant::OPTION_HTTP_COMPRESSION => false,
Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize,
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
]);
$http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) {
});
@@ -192,14 +189,16 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
Console::success('Reload completed...');
});
$container->set('bus', function ($register) use ($swooleAdapter) {
return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name));
}, ['register']);
Http::setResource('bus', function ($register, $utopia) {
return $register->get('bus')->setResolver(fn (string $name) => $utopia->getResource($name));
}, ['register', 'utopia']);
include __DIR__ . '/controllers/general.php';
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
{
$max = 15;
$sleep = 2;
$max = 15;
$sleep = 2;
$attempts = 0;
@@ -290,13 +289,13 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
Span::current()?->finish();
}
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) {
$app = new Http($swooleAdapter, 'UTC');
$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $totalWorkers, $register) {
$app = new Http('UTC');
/** @var \Utopia\Pools\Group $pools */
$pools = $app->getResource('pools');
go(function () use ($app, $pools) {
go(function () use ($register, $app) {
$pools = $register->get('pools');
/** @var \Utopia\Pools\Group $pools */
Http::setResource('pools', fn () => $pools);
/** @var array $collections */
$collections = Config::getParam('collections', []);
@@ -512,11 +511,14 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
});
});
$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) {
$http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register, $files) {
Span::init('http.request');
$request = new Request($utopiaRequest->getSwooleRequest());
$response = new Response($utopiaResponse->getSwooleResponse());
Http::setResource('swooleRequest', fn () => $swooleRequest);
Http::setResource('swooleResponse', fn () => $swooleResponse);
$request = new Request($swooleRequest);
$response = new Response($swooleResponse);
Span::add('http.method', $request->getMethod());
@@ -532,18 +534,13 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
return;
}
$requestContainer = $swooleAdapter->getContainer();
$requestContainer->set('request', fn () => $request);
$requestContainer->set('response', fn () => $response);
$app = new Http($swooleAdapter, 'UTC');
$requestContainer->set('utopia', fn () => $app);
$registerRequestResources($requestContainer);
$app = new Http('UTC');
$app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled');
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
$pools = $register->get('pools');
Http::setResource('pools', fn () => $pools);
try {
$authorization = $app->getResource('authorization');
@@ -627,7 +624,6 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
}
}
$swooleResponse = $utopiaResponse->getSwooleResponse();
$swooleResponse->setStatusCode(500);
$output = ((Http::isDevelopment())) ? [
@@ -651,10 +647,11 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
});
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
$http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
$http->on(Constant::EVENT_TASK, function () use ($register) {
$lastSyncUpdate = null;
$app = new Http($swooleAdapter, 'UTC');
$pools = $register->get('pools');
Http::setResource('pools', fn () => $pools);
$app = new Http('UTC');
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
@@ -729,4 +726,4 @@ $http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
});
});
$swooleAdapter->start();
$http->start();
+3 -12
View File
@@ -106,12 +106,7 @@ 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\PlatformAndroid;
use Appwrite\Utopia\Response\Model\PlatformApple;
use Appwrite\Utopia\Response\Model\PlatformLinux;
use Appwrite\Utopia\Response\Model\PlatformList;
use Appwrite\Utopia\Response\Model\PlatformWeb;
use Appwrite\Utopia\Response\Model\PlatformWindows;
use Appwrite\Utopia\Response\Model\Platform;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\Provider;
@@ -202,6 +197,7 @@ Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, '
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true));
Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false));
Response::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false));
Response::setModel(new BaseList('Platforms List', Response::MODEL_PLATFORM_LIST, 'platforms', Response::MODEL_PLATFORM, true, false));
Response::setModel(new BaseList('Countries List', Response::MODEL_COUNTRY_LIST, 'countries', Response::MODEL_COUNTRY));
Response::setModel(new BaseList('Continents List', Response::MODEL_CONTINENT_LIST, 'continents', Response::MODEL_CONTINENT));
Response::setModel(new BaseList('Languages List', Response::MODEL_LANGUAGE_LIST, 'languages', Response::MODEL_LANGUAGE));
@@ -337,12 +333,7 @@ Response::setModel(new Key());
Response::setModel(new DevKey());
Response::setModel(new MockNumber());
Response::setModel(new AuthProvider());
Response::setModel(new PlatformWeb());
Response::setModel(new PlatformApple());
Response::setModel(new PlatformAndroid());
Response::setModel(new PlatformWindows());
Response::setModel(new PlatformLinux());
Response::setModel(new PlatformList());
Response::setModel(new Platform());
Response::setModel(new Variable());
Response::setModel(new Country());
Response::setModel(new Continent());
+13 -2
View File
@@ -245,8 +245,19 @@ $register->set('pools', function () {
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
$instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14);
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$poolSize = max(1, (int)($instanceConnections / $workerCount));
$multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled';
if ($multiprocessing) {
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
} else {
$workerCount = 1;
}
if ($workerCount > $instanceConnections) {
throw new \Exception('Pool size is too small. Increase the number of allowed database connections or decrease the number of workers.', 500);
}
$poolSize = (int)($instanceConnections / $workerCount);
foreach ($connections as $key => $connection) {
$type = $connection['type'] ?? '';
+1372 -38
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-456
View File
@@ -1,456 +0,0 @@
<?php
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit as UtopiaAudit;
use Utopia\Cache\Cache;
use Utopia\Console;
use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
use Utopia\Registry\Registry;
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
/**
* Register per-job resources on the given container.
* These resources depend on the queue message or keep mutable state and
* must be fresh for each worker job.
*/
return function (Container $container): void {
$container->set('log', fn () => new Log(), []);
$container->set('usage', fn () => new Context(), []);
$container->set('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
$container->set('dbForPlatform', function (Cache $cache, Group $pools, Authorization $authorization) {
$adapter = new DatabasePool($pools->get('console'));
$dbForPlatform = new Database($adapter, $cache);
$dbForPlatform
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setDocumentType('users', User::class);
return $dbForPlatform;
}, ['cache', 'pools', 'authorization']);
$container->set('project', function ($message, Database $dbForPlatform) {
$payload = $message->getPayload() ?? [];
$project = new Document($payload['project'] ?? []);
if ($project->isEmpty() || $project->getId() === 'console') {
return $project;
}
return $dbForPlatform->getDocument('projects', $project->getId());
}, ['message', 'dbForPlatform']);
$container->set('dbForProject', function (Cache $cache, Group $pools, Document $project, Database $dbForPlatform, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
}, ['cache', 'pools', 'project', 'dbForPlatform', 'authorization']);
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
$database->setAuthorization($authorization);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
return $database;
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$databases[$dsn->getHost()] = $database;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
$container->set('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
$projectDocument ??= $project;
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
$databaseType = $database->getAttribute('type', '');
// Backwards-compatibility: older or seeded legacy databases may not have a DSN stored
// in the "database" attribute. In that case, fall back to the project's database DSN.
if ($databaseDSN === '') {
$databaseDSN = $projectDocument->getAttribute('database', '');
}
try {
$databaseDSN = new DSN($databaseDSN);
} catch (\InvalidArgumentException) {
$databaseDSN = new DSN('mysql://' . $databaseDSN);
}
try {
$dsn = new DSN($projectDocument->getAttribute('database'));
} catch (\InvalidArgumentException) {
// Temporary fallback until all projects use shared tables
$dsn = new DSN('mysql://' . $projectDocument->getAttribute('database'));
}
$pools = $register->get('pools');
$databaseHost = $databaseDSN->getHost();
$pool = $pools->get($databaseHost);
$adapter = new DatabasePool($pool);
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization);
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
// For separate pools (documentsdb/vectorsdb), check their own shared tables config.
// If not configured, use dedicated mode to avoid cross-engine tenant type mismatches.
if ($databaseHost !== $dsn->getHost()) {
$dbTypeSharedTables = match ($databaseType) {
DOCUMENTSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))),
VECTORSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))),
default => [],
};
if (\in_array($databaseHost, $dbTypeSharedTables)) {
$database
->setSharedTables(true)
->setTenant($projectDocument->getSequence())
->setNamespace($databaseDSN->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $projectDocument->getSequence());
}
} elseif (\in_array($dsn->getHost(), $sharedTables, true)) {
$database
->setSharedTables(true)
->setTenant($projectDocument->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $projectDocument->getSequence());
}
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['cache', 'register', 'project', 'authorization']);
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
return $database;
}
$adapter = new DatabasePool($pools->get('logs'));
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
}
return $database;
};
}, ['pools', 'cache', 'authorization']);
$container->set('abuseRetention', function () {
return \time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
}, []);
$container->set('auditRetention', function (Document $project) {
if ($project->getId() === 'console') {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
}
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
}, ['project']);
$container->set('executionRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
}, []);
$container->set('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
$container->set('queueForMessaging', function (Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
$container->set('queueForMails', function (Publisher $publisher) {
return new Mail($publisher);
}, ['publisher']);
$container->set('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
$container->set('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
$container->set('queueForAudits', function (Publisher $publisher) {
return new Audit($publisher);
}, ['publisher']);
$container->set('queueForWebhooks', function (Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('queueForRealtime', function () {
return new Realtime();
}, []);
$container->set('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$container->set('queueForMigrations', function (Publisher $publisher) {
return new Migration($publisher);
}, ['publisher']);
$container->set('deviceForSites', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForFunctions', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForFiles', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForBuilds', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForCache', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('logError', function (Registry $register, Document $project) {
return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $project) {
$logger = $register->get('logger');
if ($logger) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
$log->setNamespace($namespace);
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', \get_class($error));
$log->addTag('projectId', $project->getId() ?? '');
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
}
$log->addExtra('previousFile', $error->getPrevious()->getFile());
$log->addExtra('previousLine', $error->getPrevious()->getLine());
}
foreach (($extras ?? []) as $key => $value) {
$log->addExtra($key, $value);
}
$log->setAction($action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
try {
$responseCode = $logger->addLog($log);
Console::info('Error log pushed with status code: ' . $responseCode);
} catch (Throwable $th) {
Console::error('Error pushing log: ' . $th->getMessage());
}
}
Console::warning("Failed: {$error->getMessage()}");
Console::warning($error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}");
}
Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}");
}
};
}, ['register', 'project']);
$container->set('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
if ($project->isEmpty() || $project->getId() === 'console') {
$adapter = new AdapterDatabase($dbForPlatform);
return new UtopiaAudit($adapter);
}
$dbForProject = $getProjectDB($project);
$adapter = new AdapterDatabase($dbForProject);
return new UtopiaAudit($adapter);
};
}, ['dbForPlatform', 'getProjectDB']);
$container->set('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
};
+24 -20
View File
@@ -33,9 +33,7 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Http\Adapter\FPM\Server as HttpServer;
use Utopia\Http\Http;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
@@ -50,8 +48,6 @@ use Utopia\WebSocket\Server;
*/
require_once __DIR__ . '/init.php';
$registerRequestResources ??= require __DIR__ . '/init/resources/request.php';
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
// Log uncaught exceptions in one line instead of relying on Swoole's full backtrace dump
@@ -244,11 +240,6 @@ if (!function_exists('triggerStats')) {
}
}
global $container;
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['register']);
$realtime = getRealtime();
/**
@@ -392,6 +383,22 @@ $server->onStart(function () use ($stats, $containerId, &$statsDocument) {
}
});
function cloudRealtimeLogConnectionHostnames(Http $app, Document $project, Request $request): void
{
try {
/** @var array<int, string> $allowed */
$allowed = $app->getResource('allowedHostnames');
Console::info(sprintf(
'[Realtime] project=%s origin=%s allowedHostnames=%s',
$project->getId(),
$request->getOrigin(),
json_encode(array_values($allowed))
));
} catch (Throwable $e) {
Console::error('[Realtime] allowedHostnames log failed: ' . $e->getMessage());
}
}
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime) {
Console::success('Worker ' . $workerId . ' started successfully');
@@ -623,22 +630,16 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
Console::error('Failed to restart pub/sub...');
});
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $registerRequestResources) {
global $container;
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) {
$app = new Http('UTC');
$request = new Request($request);
$response = new Response(new SwooleResponse());
Console::info("Connection open (user: {$connection})");
$connectionContainer = new Container($container);
$adapter = new HttpServer($connectionContainer);
$app = new Http($adapter, 'UTC');
$connectionContainer->set('utopia', fn () => $app);
$connectionContainer->set('request', fn () => $request);
$connectionContainer->set('response', fn () => $response);
$registerRequestResources($connectionContainer);
Http::setResource('pools', fn () => $register->get('pools'));
Http::setResource('request', fn () => $request);
Http::setResource('response', fn () => $response);
$project = null;
$logUser = null;
@@ -703,6 +704,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
*/
$origin = $request->getOrigin();
$originValidator = $app->getResource('originValidator');
cloudRealtimeLogConnectionHostnames($app, $project, $request);
if (!empty($origin) && !$originValidator->isValid($origin) && $project->getId() !== 'console') {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
+530 -42
View File
@@ -1,69 +1,564 @@
<?php
require_once __DIR__ . '/init.php';
$registerWorkerMessageResources = require __DIR__ . '/init/worker/message.php';
use Appwrite\Certificates\LetsEncrypt;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Appwrite;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit as UtopiaAudit;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Service;
use Utopia\Pools\Group;
use Utopia\Queue\Adapter\Swoole;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Message;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Queue\Server;
use Utopia\Registry\Registry;
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
Runtime::enableCoroutine();
require_once __DIR__ . '/init/span.php';
global $container;
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['register']);
global $register;
Server::setResource('register', fn () => $register);
$container->set('authorization', function () {
Server::setResource('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
$container->set('project', fn () => new Document([]), []);
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register, Authorization $authorization) {
$pools = $register->get('pools');
$adapter = new DatabasePool($pools->get('console'));
$dbForPlatform = new Database($adapter, $cache);
$container->set('log', fn () => new Log(), []);
$dbForPlatform
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setDocumentType('users', User::class);
$container->set('consumer', function (Group $pools) {
return $dbForPlatform;
}, ['cache', 'register', 'authorization']);
Server::setResource('project', function (Message $message, Database $dbForPlatform) {
$payload = $message->getPayload() ?? [];
$project = new Document($payload['project'] ?? []);
if ($project->getId() === 'console') {
return $project;
}
return $dbForPlatform->getDocument('projects', $project->getId());
}, ['message', 'dbForPlatform']);
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
$pools = $register->get('pools');
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
}, ['cache', 'register', 'message', 'project', 'dbForPlatform', 'authorization']);
Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
$database->setAuthorization($authorization);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
return $database;
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$databases[$dsn->getHost()] = $database;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
return $database;
}
$adapter = new DatabasePool($pools->get('logs'));
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
}
return $database;
};
}, ['pools', 'cache', 'authorization']);
Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
$projectDocument ??= $project;
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
$databaseType = $database->getAttribute('type', '');
// Backwardscompatibility: older or seeded legacy databases may not have a DSN stored
// in the "database" attribute. In that case, fall back to the project's database DSN.
if ($databaseDSN === '') {
$databaseDSN = $projectDocument->getAttribute('database', '');
}
try {
$databaseDSN = new DSN($databaseDSN);
} catch (\InvalidArgumentException) {
$databaseDSN = new DSN('mysql://'.$databaseDSN);
}
try {
$dsn = new DSN($projectDocument->getAttribute('database'));
} catch (\InvalidArgumentException) {
// Temporary fallback until all projects use shared tables
$dsn = new DSN('mysql://' . $projectDocument->getAttribute('database'));
}
$pools = $register->get('pools');
$pool = $pools->get($databaseDSN->getHost());
$adapter = new DatabasePool($pool);
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization);
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables, true)) {
$database
->setSharedTables(true)
->setTenant((int) $projectDocument->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $projectDocument->getSequence());
}
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['cache', 'register', 'project', 'authorization']);
Server::setResource('abuseRetention', function () {
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
});
Server::setResource('auditRetention', function (Document $project) {
if ($project->getId() === 'console') {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
}
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
}, ['project']);
Server::setResource('executionRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
});
Server::setResource('cache', function (Registry $register) {
$pools = $register->get('pools');
$list = Config::getParam('pools-cache', []);
$adapters = [];
foreach ($list as $value) {
$adapters[] = new CachePool($pools->get($value));
}
return new Cache(new Sharding($adapters));
}, ['register']);
Server::setResource('redis', function () {
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
$port = System::getEnv('_APP_REDIS_PORT', 6379);
$pass = System::getEnv('_APP_REDIS_PASS', '');
$redis = new \Redis();
@$redis->pconnect($host, (int) $port);
if ($pass) {
$redis->auth($pass);
}
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
return $redis;
});
Server::setResource('timelimit', function (\Redis $redis) {
return function (string $key, int $limit, int $time) use ($redis) {
return new TimeLimitRedis($key, $limit, $time, $redis);
};
}, ['redis']);
Server::setResource('log', fn () => new Log());
Server::setResource('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
Server::setResource('publisherDatabases', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('publisherFunctions', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('publisherMigrations', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('publisherMessaging', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('consumer', function (Group $pools) {
return new BrokerPool(consumer: $pools->get('consumer'));
}, ['pools']);
$container->set('consumerDatabases', function (BrokerPool $consumer) {
Server::setResource('consumerDatabases', function (BrokerPool $consumer) {
return $consumer;
}, ['consumer']);
$container->set('consumerMigrations', function (BrokerPool $consumer) {
Server::setResource('consumerMigrations', function (BrokerPool $consumer) {
return $consumer;
}, ['consumer']);
$container->set('consumerStatsUsage', function (BrokerPool $consumer) {
Server::setResource('consumerStatsUsage', function (BrokerPool $consumer) {
return $consumer;
}, ['consumer']);
$container->set('certificates', function () {
Server::setResource('usage', function () {
return new Context();
}, []);
Server::setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
Server::setResource('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
Server::setResource('queueForMessaging', function (Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
Server::setResource('queueForMails', function (Publisher $publisher) {
return new Mail($publisher);
}, ['publisher']);
Server::setResource('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
Server::setResource('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
Server::setResource('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
Server::setResource('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
Server::setResource('queueForAudits', function (Publisher $publisher) {
return new Audit($publisher);
}, ['publisher']);
Server::setResource('queueForWebhooks', function (Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
Server::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
Server::setResource('queueForRealtime', function () {
return new Realtime();
}, []);
Server::setResource('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
Server::setResource('queueForMigrations', function (Publisher $publisher) {
return new Migration($publisher);
}, ['publisher']);
Server::setResource('logger', function (Registry $register) {
return $register->get('logger');
}, ['register']);
Server::setResource('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
Server::setResource('telemetry', fn () => new NoTelemetry());
Server::setResource('deviceForSites', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForFunctions', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForFiles', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForBuilds', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForCache', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
Server::setResource('plan', function (array $plan = []) {
return [];
});
Server::setResource('certificates', function () {
$email = System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'));
if (empty($email)) {
throw new Exception('You must set a valid security email address (_APP_EMAIL_CERTIFICATES) to issue a LetsEncrypt SSL certificate.');
}
return new LetsEncrypt($email);
}, []);
});
Server::setResource('logError', function (Registry $register, Document $project) {
return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $project) {
$logger = $register->get('logger');
if ($logger) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
$log->setNamespace($namespace);
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', get_class($error));
$log->addTag('projectId', $project->getId() ?? '');
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
}
$log->addExtra('previousFile', $error->getPrevious()->getFile());
$log->addExtra('previousLine', $error->getPrevious()->getLine());
}
foreach (($extras ?? []) as $key => $value) {
$log->addExtra($key, $value);
}
$log->setAction($action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
try {
$responseCode = $logger->addLog($log);
Console::info('Error log pushed with status code: ' . $responseCode);
} catch (Throwable $th) {
Console::error('Error pushing log: ' . $th->getMessage());
}
}
Console::warning("Failed: {$error->getMessage()}");
Console::warning($error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}");
}
Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}");
}
};
}, ['register', 'project']);
Server::setResource('executor', fn () => new Executor());
Server::setResource('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
if ($project->isEmpty() || $project->getId() === 'console') {
$adapter = new AdapterDatabase($dbForPlatform);
return new UtopiaAudit($adapter);
}
$dbForProject = $getProjectDB($project);
$adapter = new AdapterDatabase($dbForProject);
return new UtopiaAudit($adapter);
};
}, ['dbForPlatform', 'getProjectDB']);
Server::setResource('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
$pools = $register->get('pools');
$platform = new Appwrite();
$args = $_SERVER['argv'] ?? [];
$args = $platform->getEnv('argv');
if (! isset($args[1])) {
Console::error('Missing worker name');
@@ -79,45 +574,38 @@ if (\str_starts_with($workerName, 'databases')) {
$queueName = System::getEnv('_APP_QUEUE_NAME', 'v1-' . strtolower($workerName));
}
/** @var \Utopia\Pools\Group $pools */
$pools = $container->get('pools');
$adapter = new Swoole(
$pools->get('consumer')->pop()->getResource(),
System::getEnv('_APP_WORKERS_NUM', 1),
$queueName
);
$worker = new Server($adapter, $container);
try {
$worker->init()->action(function () use ($worker, $registerWorkerMessageResources) {
$registerWorkerMessageResources($worker->getContainer());
});
$container->set('bus', function ($register) use ($worker) {
return $register->get('bus')->setResolver(
fn (string $name) => $worker->getContainer()->get($name)
);
}, ['register']);
$platform->setWorker($worker);
/**
* Any worker can be configured with the following env vars:
* - _APP_WORKERS_NUM The total number of worker processes
* - _APP_WORKER_PER_CORE The number of worker processes per core (ignored if _APP_WORKERS_NUM is set)
* - _APP_QUEUE_NAME The name of the queue to read for database events
*/
$platform->init(Service::TYPE_WORKER, [
'workerName' => strtolower($workerName),
'workersNum' => System::getEnv('_APP_WORKERS_NUM', 1),
'connection' => $pools->get('consumer')->pop()->getResource(),
'workerName' => strtolower($workerName) ?? null,
'queueName' => $queueName,
]);
} catch (\Throwable $e) {
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
Console::exit(1);
}
$worker = $platform->getWorker();
Server::setResource('bus', function ($register) use ($worker) {
return $register->get('bus')->setResolver(fn (string $name) => $worker->getResource($name));
}, ['register']);
$worker
->error()
->inject('error')
->inject('logger')
->inject('log')
->inject('pools')
->inject('project')
->inject('authorization')
->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project, Authorization $authorization) use ($queueName) {
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($queueName) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
if ($logger) {
+8 -7
View File
@@ -52,34 +52,34 @@
"appwrite/php-runtimes": "0.19.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "1.2.*",
"utopia-php/agents": "1.2.*",
"utopia-php/analytics": "0.15.*",
"utopia-php/audit": "2.2.*",
"utopia-php/auth": "0.5.*",
"utopia-php/cache": "1.0.*",
"utopia-php/cli": "0.23.*",
"utopia-php/cli": "0.22.*",
"utopia-php/compression": "0.1.*",
"utopia-php/config": "1.*",
"utopia-php/console": "0.1.*",
"utopia-php/database": "5.*",
"utopia-php/agents": "1.*",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "1.*",
"utopia-php/emails": "0.6.*",
"utopia-php/dns": "1.6.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/http": "0.34.*",
"utopia-php/framework": "0.33.*",
"utopia-php/fetch": "0.5.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.9.*",
"utopia-php/platform": "0.12.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/queue": "0.17.*",
"utopia-php/servers": "0.3.*",
"utopia-php/queue": "0.15.*",
"utopia-php/servers": "0.2.5",
"utopia-php/registry": "0.5.*",
"utopia-php/storage": "1.0.*",
"utopia-php/system": "0.10.*",
@@ -94,7 +94,8 @@
"spomky-labs/otphp": "11.*",
"webonyx/graphql-php": "14.11.*",
"league/csv": "9.14.*",
"enshrined/svg-sanitize": "0.22.*"
"enshrined/svg-sanitize": "0.22.*",
"utopia-php/di": "0.1.0"
},
"require-dev": {
"ext-fileinfo": "*",
Generated
+146 -139
View File
@@ -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": "4fb974e9843f6104e40396e7cad4a833",
"content-hash": "4fe91e67f343fbe6deac1fdc7eda949f",
"packages": [
{
"name": "adhocore/jwt",
@@ -2708,16 +2708,16 @@
},
{
"name": "symfony/http-client",
"version": "v7.4.8",
"version": "v7.4.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "01933e626c3de76bea1e22641e205e78f6a34342"
"reference": "1010624285470eb60e88ed10035102c75b4ea6af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/01933e626c3de76bea1e22641e205e78f6a34342",
"reference": "01933e626c3de76bea1e22641e205e78f6a34342",
"url": "https://api.github.com/repos/symfony/http-client/zipball/1010624285470eb60e88ed10035102c75b4ea6af",
"reference": "1010624285470eb60e88ed10035102c75b4ea6af",
"shasum": ""
},
"require": {
@@ -2785,7 +2785,7 @@
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.4.8"
"source": "https://github.com/symfony/http-client/tree/v7.4.7"
},
"funding": [
{
@@ -2805,7 +2805,7 @@
"type": "tidelift"
}
],
"time": "2026-03-30T12:55:43+00:00"
"time": "2026-03-05T11:16:58+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -3403,16 +3403,16 @@
},
{
"name": "utopia-php/agents",
"version": "1.2.1",
"version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/agents.git",
"reference": "052227953678a30ecc4b5467401fcb0b2386471e"
"reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/agents/zipball/052227953678a30ecc4b5467401fcb0b2386471e",
"reference": "052227953678a30ecc4b5467401fcb0b2386471e",
"url": "https://api.github.com/repos/utopia-php/agents/zipball/06064fd9fb19b77ae45a12ec7bcbc17670912c30",
"reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30",
"shasum": ""
},
"require": {
@@ -3450,9 +3450,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/agents/issues",
"source": "https://github.com/utopia-php/agents/tree/1.2.1"
"source": "https://github.com/utopia-php/agents/tree/1.3.0"
},
"time": "2026-02-24T06:03:55+00:00"
"time": "2026-03-26T03:51:11+00:00"
},
{
"name": "utopia-php/analytics",
@@ -3658,21 +3658,21 @@
},
{
"name": "utopia-php/cli",
"version": "0.23.1",
"version": "0.22.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/cli.git",
"reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621"
"reference": "a7ac387ee626fd27075a87e836fb72c5be38add4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/cli/zipball/8d1955b8bc4dc631f45d7c7df689ed7b63f70621",
"reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621",
"url": "https://api.github.com/repos/utopia-php/cli/zipball/a7ac387ee626fd27075a87e836fb72c5be38add4",
"reference": "a7ac387ee626fd27075a87e836fb72c5be38add4",
"shasum": ""
},
"require": {
"php": ">=7.4",
"utopia-php/servers": "0.3.*"
"utopia-php/servers": "0.2.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
@@ -3703,9 +3703,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/cli/issues",
"source": "https://github.com/utopia-php/cli/tree/0.23.1"
"source": "https://github.com/utopia-php/cli/tree/0.22.0"
},
"time": "2026-04-05T15:27:35+00:00"
"time": "2025-10-21T10:42:45+00:00"
},
{
"name": "utopia-php/compression",
@@ -3850,16 +3850,16 @@
},
{
"name": "utopia-php/database",
"version": "5.3.19",
"version": "5.3.17",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691"
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
"url": "https://api.github.com/repos/utopia-php/database/zipball/cff2b6ed63d3291b74110d086e16ff089fe05993",
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993",
"shasum": ""
},
"require": {
@@ -3903,9 +3903,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/5.3.19"
"source": "https://github.com/utopia-php/database/tree/5.3.17"
},
"time": "2026-03-31T15:52:08+00:00"
"time": "2026-03-20T01:18:52+00:00"
},
{
"name": "utopia-php/detector",
@@ -3954,26 +3954,25 @@
},
{
"name": "utopia-php/di",
"version": "0.3.2",
"version": "0.1.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/di.git",
"reference": "07025d721ed5d9be27932e8e640acf1467fc4b9d"
"reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/di/zipball/07025d721ed5d9be27932e8e640acf1467fc4b9d",
"reference": "07025d721ed5d9be27932e8e640acf1467fc4b9d",
"url": "https://api.github.com/repos/utopia-php/di/zipball/22490c95f7ac3898ed1c33f1b1b5dd577305ee31",
"reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/container": "^2.0"
"php": ">=8.2"
},
"require-dev": {
"laravel/pint": "^1.27",
"laravel/pint": "^1.2",
"phpbench/phpbench": "^1.2",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.5.25",
"swoole/ide-helper": "4.8.3"
},
@@ -3990,18 +3989,16 @@
],
"description": "A simple and lite library for managing dependency injections",
"keywords": [
"PSR-11",
"container",
"dependency-injection",
"di",
"framework",
"http",
"php",
"utopia"
"upf"
],
"support": {
"issues": "https://github.com/utopia-php/di/issues",
"source": "https://github.com/utopia-php/di/tree/0.3.2"
"source": "https://github.com/utopia-php/di/tree/0.1.0"
},
"time": "2026-03-21T07:42:10+00:00"
"time": "2024-08-08T14:35:19+00:00"
},
{
"name": "utopia-php/dns",
@@ -4270,35 +4267,31 @@
"time": "2025-12-18T16:25:10+00:00"
},
{
"name": "utopia-php/http",
"version": "0.34.19",
"name": "utopia-php/framework",
"version": "0.33.41",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
"reference": "995c119f31866cacd42d63b1f922bf86eabb396c"
"reference": "0f3bf2377c867e547c929c3733b8224afee6ef06"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/995c119f31866cacd42d63b1f922bf86eabb396c",
"reference": "995c119f31866cacd42d63b1f922bf86eabb396c",
"url": "https://api.github.com/repos/utopia-php/http/zipball/0f3bf2377c867e547c929c3733b8224afee6ef06",
"reference": "0f3bf2377c867e547c929c3733b8224afee6ef06",
"shasum": ""
},
"require": {
"ext-swoole": "*",
"php": ">=8.2",
"php": ">=8.3",
"utopia-php/compression": "0.1.*",
"utopia-php/di": "0.3.*",
"utopia-php/servers": "0.3.*",
"utopia-php/telemetry": "0.2.*",
"utopia-php/validators": "0.2.*"
},
"require-dev": {
"doctrine/instantiator": "^1.5",
"laravel/pint": "1.*",
"phpbench/phpbench": "^1.2",
"phpbench/phpbench": "1.*",
"phpstan/phpstan": "1.*",
"phpunit/phpunit": "^9.5.25",
"swoole/ide-helper": "4.8.3"
"phpunit/phpunit": "9.*",
"swoole/ide-helper": "^6.0"
},
"type": "library",
"autoload": {
@@ -4310,18 +4303,17 @@
"license": [
"MIT"
],
"description": "A simple, light and advanced PHP HTTP framework",
"description": "A simple, light and advanced PHP framework",
"keywords": [
"framework",
"http",
"php",
"upf"
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
"source": "https://github.com/utopia-php/http/tree/0.34.19"
"source": "https://github.com/utopia-php/http/tree/0.33.41"
},
"time": "2026-04-08T10:23:17+00:00"
"time": "2026-02-24T12:01:28+00:00"
},
{
"name": "utopia-php/image",
@@ -4642,30 +4634,30 @@
},
{
"name": "utopia-php/platform",
"version": "0.12.1",
"version": "0.7.16",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/platform.git",
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc"
"reference": "34e67e4b80b5741c380071fe765fbc12a132de4f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/34e67e4b80b5741c380071fe765fbc12a132de4f",
"reference": "34e67e4b80b5741c380071fe765fbc12a132de4f",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-redis": "*",
"php": ">=8.1",
"utopia-php/cli": "0.23.*",
"utopia-php/http": "0.34.*",
"utopia-php/queue": "0.17.*",
"utopia-php/servers": "0.3.*"
"php": ">=8.0",
"utopia-php/cli": "0.22.*",
"utopia-php/framework": "0.33.*",
"utopia-php/queue": "0.15.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
"phpunit/phpunit": "^9.3"
"laravel/pint": "1.*",
"phpstan/phpstan": "2.*",
"phpunit/phpunit": "9.*"
},
"type": "library",
"autoload": {
@@ -4687,9 +4679,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/platform/issues",
"source": "https://github.com/utopia-php/platform/tree/0.12.1"
"source": "https://github.com/utopia-php/platform/tree/0.7.16"
},
"time": "2026-04-08T04:11:31+00:00"
"time": "2026-02-11T06:36:48+00:00"
},
{
"name": "utopia-php/pools",
@@ -4799,33 +4791,32 @@
},
{
"name": "utopia-php/queue",
"version": "0.17.0",
"version": "0.15.6",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/queue.git",
"reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f"
"reference": "08e361d69610f371382b344c369eef355ca414b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/0fbc7d7312f5cf76ec112513fb93317000901f5f",
"reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/08e361d69610f371382b344c369eef355ca414b4",
"reference": "08e361d69610f371382b344c369eef355ca414b4",
"shasum": ""
},
"require": {
"php": ">=8.3",
"php-amqplib/php-amqplib": "^3.7",
"utopia-php/di": "0.3.*",
"utopia-php/fetch": "0.5.*",
"utopia-php/pools": "1.*",
"utopia-php/servers": "0.3.*",
"utopia-php/servers": "0.2.*",
"utopia-php/telemetry": "0.2.*",
"utopia-php/validators": "0.2.*"
},
"require-dev": {
"ext-redis": "*",
"laravel/pint": "^1.0",
"laravel/pint": "^0.2.3",
"phpstan/phpstan": "^1.8",
"phpunit/phpunit": "^11.0",
"phpunit/phpunit": "^9.5.5",
"swoole/ide-helper": "4.8.8",
"workerman/workerman": "^4.0"
},
@@ -4860,9 +4851,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/queue/issues",
"source": "https://github.com/utopia-php/queue/tree/0.17.0"
"source": "https://github.com/utopia-php/queue/tree/0.15.6"
},
"time": "2026-03-23T16:21:31+00:00"
"time": "2026-02-23T13:03:51+00:00"
},
{
"name": "utopia-php/registry",
@@ -4918,21 +4909,21 @@
},
{
"name": "utopia-php/servers",
"version": "0.3.0",
"version": "0.2.5",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/servers.git",
"reference": "235be31200df9437fc96a1c270ffef4c64fafe52"
"reference": "4770e879a90685af4ba14e7e5d95d0a17c7fdf03"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/servers/zipball/235be31200df9437fc96a1c270ffef4c64fafe52",
"reference": "235be31200df9437fc96a1c270ffef4c64fafe52",
"url": "https://api.github.com/repos/utopia-php/servers/zipball/4770e879a90685af4ba14e7e5d95d0a17c7fdf03",
"reference": "4770e879a90685af4ba14e7e5d95d0a17c7fdf03",
"shasum": ""
},
"require": {
"php": ">=8.2",
"utopia-php/di": "0.3.*",
"php": ">=8.0",
"utopia-php/di": "0.1.*",
"utopia-php/validators": "0.*"
},
"require-dev": {
@@ -4966,9 +4957,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/servers/issues",
"source": "https://github.com/utopia-php/servers/tree/0.3.0"
"source": "https://github.com/utopia-php/servers/tree/0.2.5"
},
"time": "2026-03-13T11:31:42+00:00"
"time": "2026-02-10T04:21:53+00:00"
},
{
"name": "utopia-php/span",
@@ -5448,16 +5439,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.17.7",
"version": "1.14.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "291471d04c3f0e7b9fcc46668a6255a4c0f2947e"
"reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/291471d04c3f0e7b9fcc46668a6255a4c0f2947e",
"reference": "291471d04c3f0e7b9fcc46668a6255a4c0f2947e",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/7e7e257b10a8c1384a237e7d8d73452e2108901e",
"reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e",
"shasum": ""
},
"require": {
@@ -5493,22 +5484,22 @@
"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.17.7"
"source": "https://github.com/appwrite/sdk-generator/tree/1.14.0"
},
"time": "2026-04-08T08:51:05+00:00"
"time": "2026-03-26T12:50:11+00:00"
},
{
"name": "brianium/paratest",
"version": "v7.20.0",
"version": "v7.19.2",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
"reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d"
"reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/81c80677c9ec0ed4ef16b246167f11dec81a6e3d",
"reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9",
"reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9",
"shasum": ""
},
"require": {
@@ -5532,7 +5523,7 @@
"ext-pcntl": "*",
"ext-pcov": "*",
"ext-posix": "*",
"phpstan/phpstan": "^2.1.44",
"phpstan/phpstan": "^2.1.40",
"phpstan/phpstan-deprecation-rules": "^2.0.4",
"phpstan/phpstan-phpunit": "^2.0.16",
"phpstan/phpstan-strict-rules": "^2.0.10",
@@ -5576,7 +5567,7 @@
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
"source": "https://github.com/paratestphp/paratest/tree/v7.20.0"
"source": "https://github.com/paratestphp/paratest/tree/v7.19.2"
},
"funding": [
{
@@ -5588,7 +5579,7 @@
"type": "paypal"
}
],
"time": "2026-03-29T15:46:14+00:00"
"time": "2026-03-09T14:33:17+00:00"
},
{
"name": "czproject/git-php",
@@ -6204,11 +6195,11 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.46",
"version": "2.1.44",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
"reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/4a88c083c668b2c364a425c9b3171b2d9ea5d218",
"reference": "4a88c083c668b2c364a425c9b3171b2d9ea5d218",
"shasum": ""
},
"require": {
@@ -6253,7 +6244,7 @@
"type": "github"
}
],
"time": "2026-04-01T09:25:14+00:00"
"time": "2026-03-25T17:34:21+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -6603,16 +6594,16 @@
},
{
"name": "phpunit/phpunit",
"version": "12.5.17",
"version": "12.5.14",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "85b62adab1a340982df64e66daa4a4435eb5723b"
"reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/85b62adab1a340982df64e66daa4a4435eb5723b",
"reference": "85b62adab1a340982df64e66daa4a4435eb5723b",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/47283cfd98d553edcb1353591f4e255dc1bb61f0",
"reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0",
"shasum": ""
},
"require": {
@@ -6634,7 +6625,7 @@
"sebastian/cli-parser": "^4.2.0",
"sebastian/comparator": "^7.1.4",
"sebastian/diff": "^7.0.0",
"sebastian/environment": "^8.0.4",
"sebastian/environment": "^8.0.3",
"sebastian/exporter": "^7.0.2",
"sebastian/global-state": "^8.0.2",
"sebastian/object-enumerator": "^7.0.0",
@@ -6681,15 +6672,31 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.17"
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.14"
},
"funding": [
{
"url": "https://phpunit.de/sponsoring.html",
"type": "other"
"url": "https://phpunit.de/sponsors.html",
"type": "custom"
},
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
"type": "tidelift"
}
],
"time": "2026-04-08T03:04:19+00:00"
"time": "2026-02-18T12:38:40+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -6762,16 +6769,16 @@
},
{
"name": "sebastian/comparator",
"version": "7.1.5",
"version": "7.1.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "c284f55811f43d555e51e8e5c166ac40d3e33c63"
"reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c284f55811f43d555e51e8e5c166ac40d3e33c63",
"reference": "c284f55811f43d555e51e8e5c166ac40d3e33c63",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6",
"reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6",
"shasum": ""
},
"require": {
@@ -6830,7 +6837,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
"source": "https://github.com/sebastianbergmann/comparator/tree/7.1.5"
"source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4"
},
"funding": [
{
@@ -6850,7 +6857,7 @@
"type": "tidelift"
}
],
"time": "2026-04-08T04:43:00+00:00"
"time": "2026-01-24T09:28:48+00:00"
},
{
"name": "sebastian/complexity",
@@ -7674,16 +7681,16 @@
},
{
"name": "symfony/console",
"version": "v8.0.8",
"version": "v8.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7"
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/5b66d385dc58f69652e56f78a4184615e3f2b7f7",
"reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7",
"url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
"shasum": ""
},
"require": {
@@ -7740,7 +7747,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v8.0.8"
"source": "https://github.com/symfony/console/tree/v8.0.7"
},
"funding": [
{
@@ -7760,7 +7767,7 @@
"type": "tidelift"
}
],
"time": "2026-03-30T15:14:47+00:00"
"time": "2026-03-06T14:06:22+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -8094,16 +8101,16 @@
},
{
"name": "symfony/process",
"version": "v8.0.8",
"version": "v8.0.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc"
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
"url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
"shasum": ""
},
"require": {
@@ -8135,7 +8142,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v8.0.8"
"source": "https://github.com/symfony/process/tree/v8.0.5"
},
"funding": [
{
@@ -8155,20 +8162,20 @@
"type": "tidelift"
}
],
"time": "2026-03-30T15:14:47+00:00"
"time": "2026-01-26T15:08:38+00:00"
},
{
"name": "symfony/string",
"version": "v8.0.8",
"version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "ae9488f874d7603f9d2dfbf120203882b645d963"
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963",
"reference": "ae9488f874d7603f9d2dfbf120203882b645d963",
"url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
"shasum": ""
},
"require": {
@@ -8225,7 +8232,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v8.0.8"
"source": "https://github.com/symfony/string/tree/v8.0.6"
},
"funding": [
{
@@ -8245,7 +8252,7 @@
"type": "tidelift"
}
],
"time": "2026-03-30T15:14:47+00:00"
"time": "2026-02-09T10:14:57+00:00"
},
{
"name": "textalk/websocket",
-320
View File
@@ -1,320 +0,0 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Appwrite\OpenSSL\OpenSSL;
use Utopia\System\System;
// Reference Material
// https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code
// https://docs.x.com/x-api/users/get-me
class X extends OAuth2
{
private const PKCE_STATE_KEY = '_pkce';
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @var array
*/
protected array $scopes = [
'tweet.read',
'users.read',
'users.email',
'offline.access',
];
/**
* @var string
*/
private string $pkceVerifier = '';
/**
* @return string
*/
public function getName(): string
{
return 'x';
}
public function getLoginURL(): string
{
$state = $this->state;
$state[self::PKCE_STATE_KEY] = $this->encryptPKCEVerifier($this->getPKCEVerifier());
return 'https://x.com/i/oauth2/authorize?' . \http_build_query([
'response_type' => 'code',
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'state' => $this->base64UrlEncode(\json_encode($state, JSON_THROW_ON_ERROR)),
'code_challenge' => $this->getPKCEChallenge(),
'code_challenge_method' => 'S256',
]);
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if (empty($this->tokens)) {
$this->tokens = $this->decodeJsonObject($this->request(
'POST',
'https://api.x.com/2/oauth2/token',
$this->tokenEndpointHeaders(),
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'grant_type' => 'authorization_code',
'redirect_uri' => $this->callback,
'code_verifier' => $this->getPKCEVerifier(),
])
));
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken): array
{
$this->tokens = $this->decodeJsonObject($this->request(
'POST',
'https://api.x.com/2/oauth2/token',
$this->tokenEndpointHeaders(),
\http_build_query([
'client_id' => $this->appID,
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token',
])
));
if (empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserID(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['data']['id'] ?? '';
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['data']['confirmed_email'] ?? '';
}
/**
* Check if the OAuth email is verified.
*
* X returns a confirmed email only when the app has email access enabled
* and the authenticated user has a confirmed email address.
*
* @param string $accessToken
*
* @return bool
*/
public function isEmailVerified(string $accessToken): bool
{
return !empty($this->getUserEmail($accessToken));
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserName(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['data']['name'] ?? '';
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken): array
{
if (empty($this->user)) {
$this->user = $this->decodeJsonObject($this->request(
'GET',
'https://api.x.com/2/users/me?user.fields=confirmed_email',
['Authorization: Bearer ' . $accessToken]
));
}
return $this->user;
}
/**
* @return array<string, mixed>|null
*/
public function parseState(string $state): ?array
{
$decoded = $this->base64UrlDecode($state);
if ($decoded === false) {
return null;
}
$parsed = \json_decode($decoded, true);
if (!\is_array($parsed)) {
return null;
}
$pkce = $parsed[self::PKCE_STATE_KEY] ?? null;
if (\is_array($pkce)) {
$this->pkceVerifier = $this->decryptPKCEVerifier($pkce);
}
unset($parsed[self::PKCE_STATE_KEY]);
return $parsed;
}
/**
* @return list<string>
*/
private function tokenEndpointHeaders(): array
{
return [
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
'Content-Type: application/x-www-form-urlencoded',
];
}
/**
* @return array<string, mixed>
*/
private function decodeJsonObject(string $json): array
{
$decoded = \json_decode($json, true);
return \is_array($decoded) ? $decoded : [];
}
private function getPKCEVerifier(): string
{
if ($this->pkceVerifier === '') {
$this->pkceVerifier = $this->base64UrlEncode(\random_bytes(64));
}
return $this->pkceVerifier;
}
private function getPKCEChallenge(): string
{
return $this->base64UrlEncode(\hash('sha256', $this->getPKCEVerifier(), true));
}
private function encryptPKCEVerifier(string $verifier): array
{
$iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
$key = $this->getPKCEStateKey();
$tag = null;
$data = OpenSSL::encrypt($verifier, OpenSSL::CIPHER_AES_128_GCM, $key, OPENSSL_RAW_DATA, $iv, $tag);
if ($data === false || $tag === null) {
throw new \Exception('Failed to encrypt PKCE verifier.');
}
return [
'data' => $this->base64UrlEncode($data),
'iv' => \bin2hex($iv),
'tag' => \bin2hex($tag),
];
}
private function decryptPKCEVerifier(array $payload): string
{
$data = $payload['data'] ?? '';
$iv = $payload['iv'] ?? '';
$tag = $payload['tag'] ?? '';
if ($data === '' || $iv === '' || $tag === '') {
return '';
}
$decodedData = $this->base64UrlDecode($data);
$decodedIv = \hex2bin($iv);
$decodedTag = \hex2bin($tag);
if ($decodedData === false || $decodedIv === false || $decodedTag === false) {
return '';
}
return OpenSSL::decrypt(
$decodedData,
OpenSSL::CIPHER_AES_128_GCM,
$this->getPKCEStateKey(),
OPENSSL_RAW_DATA,
$decodedIv,
$decodedTag
) ?: '';
}
private function getPKCEStateKey(): string
{
$key = System::getEnv('_APP_OPENSSL_KEY_V1', '');
if ($key === '') {
throw new \Exception('X OAuth2 requires _APP_OPENSSL_KEY_V1 to encrypt PKCE state.');
}
return $key;
}
private function base64UrlEncode(string $value): string
{
return \rtrim(\strtr(\base64_encode($value), '+/', '-_'), '=');
}
private function base64UrlDecode(string $value): string|false
{
$padding = \strlen($value) % 4;
if ($padding > 0) {
$value .= \str_repeat('=', 4 - $padding);
}
return \base64_decode(\strtr($value, '-_', '+/'), true);
}
}
-2
View File
@@ -333,8 +333,6 @@ class Exception extends \Exception
/** Platform */
public const string PLATFORM_NOT_FOUND = 'platform_not_found';
public const string PLATFORM_METHOD_UNSUPPORTED = 'platform_method_unsupported';
public const string PLATFORM_ALREADY_EXISTS = 'platform_already_exists';
/** GraphqQL */
public const string GRAPHQL_NO_QUERY = 'graphql_no_query';
+19 -19
View File
@@ -26,9 +26,9 @@ class Resolvers
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$path = $route->getPath();
foreach ($args as $key => $value) {
@@ -93,9 +93,9 @@ class Resolvers
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
@@ -124,9 +124,9 @@ class Resolvers
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
@@ -160,9 +160,9 @@ class Resolvers
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$request->setMethod('POST');
$request->setURI($url($databaseId, $collectionId, $args));
@@ -192,9 +192,9 @@ class Resolvers
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$request->setMethod('PATCH');
$request->setURI($url($databaseId, $collectionId, $args));
@@ -222,9 +222,9 @@ class Resolvers
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$request->setMethod('DELETE');
$request->setURI($url($databaseId, $collectionId, $args));
@@ -266,7 +266,7 @@ class Resolvers
try {
$route = $utopia->match($request, fresh: true);
$utopia->execute($route, $request);
$utopia->execute($route, $request, $response);
} catch (\Throwable $e) {
if ($beforeReject) {
$e = $beforeReject($e);
+4
View File
@@ -32,6 +32,10 @@ class Schema
array $urls,
array $params,
): GQLSchema {
Http::setResource('utopia:graphql', static function () use ($utopia) {
return $utopia;
});
if (!empty(self::$schema)) {
return self::$schema;
}
+48 -15
View File
@@ -6,10 +6,20 @@ class Platform
{
public const TYPE_UNKNOWN = 'unknown';
public const TYPE_WEB = 'web';
public const TYPE_APPLE = 'apple';
public const TYPE_FLUTTER_IOS = 'flutter-ios';
public const TYPE_FLUTTER_ANDROID = 'flutter-android';
public const TYPE_FLUTTER_MACOS = 'flutter-macos';
public const TYPE_FLUTTER_WINDOWS = 'flutter-windows';
public const TYPE_FLUTTER_LINUX = 'flutter-linux';
public const TYPE_FLUTTER_WEB = 'flutter-web';
public const TYPE_APPLE_IOS = 'apple-ios';
public const TYPE_APPLE_MACOS = 'apple-macos';
public const TYPE_APPLE_WATCHOS = 'apple-watchos';
public const TYPE_APPLE_TVOS = 'apple-tvos';
public const TYPE_ANDROID = 'android';
public const TYPE_WINDOWS = 'windows';
public const TYPE_LINUX = 'linux';
public const TYPE_UNITY = 'unity';
public const TYPE_REACT_NATIVE_IOS = 'react-native-ios';
public const TYPE_REACT_NATIVE_ANDROID = 'react-native-android';
public const TYPE_SCHEME = 'scheme';
public const SCHEME_HTTP = 'http';
@@ -68,14 +78,24 @@ class Platform
switch ($type) {
case self::TYPE_WEB:
case self::TYPE_FLUTTER_WEB:
if (!empty($hostname)) {
$hostnames[] = $hostname;
}
break;
case self::TYPE_FLUTTER_IOS:
case self::TYPE_FLUTTER_ANDROID:
case self::TYPE_FLUTTER_MACOS:
case self::TYPE_FLUTTER_WINDOWS:
case self::TYPE_FLUTTER_LINUX:
case self::TYPE_ANDROID:
case self::TYPE_WINDOWS:
case self::TYPE_LINUX:
case self::TYPE_APPLE:
case self::TYPE_APPLE_IOS:
case self::TYPE_APPLE_MACOS:
case self::TYPE_APPLE_WATCHOS:
case self::TYPE_APPLE_TVOS:
case self::TYPE_REACT_NATIVE_IOS:
case self::TYPE_REACT_NATIVE_ANDROID:
case self::TYPE_UNITY:
if (!empty($key)) {
$hostnames[] = $key;
}
@@ -101,24 +121,37 @@ class Platform
}
break;
case self::TYPE_WEB:
case self::TYPE_FLUTTER_WEB:
$schemes[] = self::SCHEME_HTTP;
$schemes[] = self::SCHEME_HTTPS;
break;
case self::TYPE_ANDROID:
$schemes[] = self::SCHEME_ANDROID;
break;
case self::TYPE_APPLE:
$schemes[] = self::SCHEME_WATCHOS;
$schemes[] = self::SCHEME_MACOS;
$schemes[] = self::SCHEME_TVOS;
case self::TYPE_FLUTTER_IOS:
case self::TYPE_APPLE_IOS:
case self::TYPE_REACT_NATIVE_IOS:
$schemes[] = self::SCHEME_IOS;
break;
case self::TYPE_WINDOWS:
case self::TYPE_FLUTTER_ANDROID:
case self::TYPE_ANDROID:
case self::TYPE_REACT_NATIVE_ANDROID:
$schemes[] = self::SCHEME_ANDROID;
break;
case self::TYPE_FLUTTER_MACOS:
case self::TYPE_APPLE_MACOS:
$schemes[] = self::SCHEME_MACOS;
break;
case self::TYPE_FLUTTER_WINDOWS:
case self::TYPE_UNITY:
$schemes[] = self::SCHEME_WINDOWS;
break;
case self::TYPE_LINUX:
case self::TYPE_FLUTTER_LINUX:
$schemes[] = self::SCHEME_LINUX;
break;
case self::TYPE_APPLE_WATCHOS:
$schemes[] = self::SCHEME_WATCHOS;
break;
case self::TYPE_APPLE_TVOS:
$schemes[] = self::SCHEME_TVOS;
break;
default:
break;
}
+16 -16
View File
@@ -145,20 +145,9 @@ class Server
$paths = $this->paths;
$state = $this->state;
$adapter = new class ($host, $port, ['worker_num' => 1]) extends SwooleAdapter {
public function getNativeServer(): SwooleServer
{
return $this->server;
}
};
$nativeServer = $adapter->getNativeServer();
$container = $adapter->getContainer();
$container->set('installerState', fn () => $state);
$container->set('installerConfig', fn () => $config);
$container->set('installerPaths', fn () => $paths);
$container->set('swooleServer', fn () => $nativeServer);
Http::setResource('installerState', fn () => $state);
Http::setResource('installerConfig', fn () => $config);
Http::setResource('installerPaths', fn () => $paths);
// Register routes via Utopia Platform
$platform = new Installer();
@@ -171,6 +160,17 @@ class Server
->inject('response')
->action($errorHandler->action(...));
$adapter = new class ($host, $port, ['worker_num' => 1]) extends SwooleAdapter {
public function getNativeServer(): SwooleServer
{
return $this->server;
}
};
$nativeServer = $adapter->getNativeServer();
Http::setResource('swooleServer', fn () => $nativeServer);
$nativeServer->on('start', function () use ($nativeServer, $port, $readyFile) {
\Swoole\Process::signal(SIGTERM, fn () => $nativeServer->shutdown());
\Swoole\Process::signal(SIGINT, fn () => $nativeServer->shutdown());
@@ -180,7 +180,7 @@ class Server
}
});
$adapter->onRequest(function (Request $request, Response $response) use ($adapter, $files) {
$adapter->onRequest(function (Request $request, Response $response) use ($files) {
// Serve static files from memory
$uri = $request->getURI();
if ($files->isFileLoaded($uri)) {
@@ -190,7 +190,7 @@ class Server
return;
}
$app = new Http($adapter, 'UTC');
$app = new Http('UTC');
$app->run($request, $response);
});
@@ -207,8 +207,6 @@ class Decrement extends Action
->addMetric($this->getDatabasesOperationWriteMetric(), 1)
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), 1);
$response->dynamic($document, $this->getResponseModel());
$queueForEvents
->setParam('databaseId', $databaseId)
->setParam('collectionId', $collectionId)
@@ -218,5 +216,7 @@ class Decrement extends Action
->setContext('database', $database)
->setContext($this->getCollectionsEventsContext(), $collection)
->setPayload($response->getPayload(), sensitive: $relationships);
$response->dynamic($document, $this->getResponseModel());
}
}
@@ -207,8 +207,6 @@ class Increment extends Action
->addMetric($this->getDatabasesOperationWriteMetric(), 1)
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), 1);
$response->dynamic($document, $this->getResponseModel());
$queueForEvents
->setParam('databaseId', $databaseId)
->setParam('collectionId', $collectionId)
@@ -218,5 +216,7 @@ class Increment extends Action
->setContext('database', $database)
->setContext($this->getCollectionsEventsContext(), $collection)
->setPayload($response->getPayload(), sensitive: $relationships);
$response->dynamic($document, $this->getResponseModel());
}
}
@@ -61,16 +61,16 @@ class Create extends Action
$databaseKeys = System::getEnv('_APP_DATABASE_DOCUMENTSDB_KEYS', '');
$databaseOverride = System::getEnv('_APP_DATABASE_DOCUMENTSDB_OVERRIDE');
$dbScheme = System::getEnv('_APP_DB_HOST_DOCUMENTSDB', 'mongodb');
$databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', '')));
$databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', '')));
$databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
$databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''));
break;
case VECTORSDB:
$databases = Config::getParam('pools-vectorsdb', []);
$databaseKeys = System::getEnv('_APP_DATABASE_VECTORSDB_KEYS', '');
$databaseOverride = System::getEnv('_APP_DATABASE_VECTORSDB_OVERRIDE');
$dbScheme = System::getEnv('_APP_DB_HOST_VECTORSDB', 'postgresql');
$databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', '')));
$databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', '')));
$databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
$databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''));
break;
default:
// legacy/tablesdb
@@ -108,7 +108,7 @@ class Create extends Action
if ($index !== false) {
$selectedDsn = $databases[$index];
} else {
if (!empty($dsn) && !empty($databaseSharedTables)) {
if (!empty($dsn)) {
$beforeFilter = \array_values($databases);
if ($isSharedTablesV1) {
$databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTablesV1));
@@ -118,10 +118,7 @@ class Create extends Action
$databases = array_filter($databases, fn ($value) => !\in_array($value, $databaseSharedTables));
}
}
if (empty($databases)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "No {$databasetype} database pool available for the current shared-tables mode");
}
$selectedDsn = $databases[array_rand($databases)];
$selectedDsn = !empty($databases) ? $databases[array_rand($databases)] : '';
}
if (\in_array($selectedDsn, $databaseSharedTables)) {
@@ -182,33 +182,19 @@ class Update extends Action
$dbForDatabases = $getDatabasesDB($databaseDoc);
try {
$transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
'transactions',
$transactionId,
new Document(['status' => 'committing'])
));
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $authorization) {
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
'status' => 'committing',
])));
$operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [
Query::equal('transactionInternalId', [$transaction->getSequence()]),
Query::orderAsc(),
Query::limit(PHP_INT_MAX),
]));
$operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [
Query::equal('transactionInternalId', [$transaction->getSequence()]),
Query::orderAsc(),
Query::limit(PHP_INT_MAX),
]));
$collections = [];
foreach ($operations as $operation) {
$databaseInternalId = $operation['databaseInternalId'];
$collectionInternalId = $operation['collectionInternalId'];
$collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}";
if (!isset($collections[$collectionId])) {
$collections[$collectionId] = $authorization->skip(
fn () => $dbForProject->getCollection($collectionId)
);
}
}
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $transactionState, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $collections) {
$state = [];
$collections = [];
foreach ($operations as $operation) {
$databaseInternalId = $operation['databaseInternalId'];
@@ -224,6 +210,11 @@ class Update extends Action
$data = $data->getArrayCopy();
}
if (!isset($collections[$collectionId])) {
$collections[$collectionId] = $authorization->skip(
fn () => $dbForProject->getCollection($collectionId)
);
}
$collection = $collections[$collectionId];
if (\is_array($data) && !empty($data)) {
@@ -285,17 +276,16 @@ class Update extends Action
}
}
$transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
'transactions',
$transactionId,
new Document(['status' => 'committed'])
));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($transaction);
});
$transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
'transactions',
$transactionId,
new Document(['status' => 'committed'])
));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($transaction);
} catch (NotFoundException $e) {
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
'status' => 'failed',
@@ -1,119 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Keys;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class Create extends Base
{
use HTTP;
public static function getName()
{
return 'createProjectKey';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/project/keys')
->httpAlias('/v1/projects/:projectId/keys')
->desc('Create project key')
->groups(['api', 'project'])
->label('scope', 'keys.write')
->label('event', 'keys.[keyId].create')
->label('audits.event', 'project.key.create')
->label('audits.resource', 'project.key/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'keys',
name: 'createKey',
description: <<<EOT
Create a new API key. It's recommended to have multiple API keys with strict scopes for separate functions within your project.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_KEY,
)
],
))
->param('keyId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key 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.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', null, new Nullable(new Datetime()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->callback($this->action(...));
}
/**
* @param array<string>|null $scopes
*/
public function action(
string $keyId,
string $name,
?array $scopes,
?string $expire,
Response $response,
QueueEvent $queueForEvents,
Database $dbForPlatform,
Document $project,
Authorization $authorization,
) {
$keyId = ($keyId == 'unique()') ? ID::unique() : $keyId;
$key = new Document([
'$id' => $keyId,
'$permissions' => [],
'resourceInternalId' => $project->getSequence(),
'resourceId' => $project->getId(),
'resourceType' => 'projects',
'name' => $name,
'scopes' => $scopes ?? [],
'expire' => $expire,
'sdks' => [],
'accessedAt' => null,
'secret' => API_KEY_STANDARD . '_' . \bin2hex(\random_bytes(128)),
]);
try {
$key = $authorization->skip(fn () => $dbForPlatform->createDocument('keys', $key));
} catch (DuplicateException) {
throw new Exception(Exception::KEY_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('keyId', $key->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($key, Response::MODEL_KEY);
}
}
@@ -1,90 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Keys;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Base
{
use HTTP;
public static function getName()
{
return 'deleteProjectKey';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/project/keys/:keyId')
->httpAlias('/v1/projects/:projectId/keys/:keyId')
->desc('Delete project key')
->groups(['api', 'project'])
->label('scope', 'keys.write')
->label('event', 'keys.[keyId].delete')
->label('audits.event', 'project.key.delete')
->label('audits.resource', 'project.key/{request.keyId}')
->label('sdk', new Method(
namespace: 'project',
group: 'keys',
name: 'deleteKey',
description: <<<EOT
Delete a key by its unique ID. Once deleted, the key can no longer be used to authenticate API calls.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('project')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $keyId,
Response $response,
Database $dbForPlatform,
Event $queueForEvents,
Document $project,
Authorization $authorization,
) {
$key = $authorization->skip(fn () => $dbForPlatform->getDocument('keys', $keyId));
if ($key->isEmpty() || $key->getAttribute('resourceType', '') !== 'projects' || $key->getAttribute('resourceInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
if (!$authorization->skip(fn () => $dbForPlatform->deleteDocument('keys', $key->getId()))) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove document from DB');
};
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('keyId', $key->getId());
$response->noContent();
}
}
@@ -1,74 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Keys;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getProjectKey';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/project/keys/:keyId')
->httpAlias('/v1/projects/:projectId/keys/:keyId')
->desc('Get project key')
->groups(['api', 'project'])
->label('scope', 'keys.read')
->label('sdk', new Method(
namespace: 'project',
group: 'keys',
name: 'getKey',
description: <<<EOT
Get a key by its unique ID.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_KEY,
)
]
))
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $keyId,
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization,
) {
$key = $authorization->skip(fn () => $dbForPlatform->getDocument('keys', $keyId));
if ($key->isEmpty() || $key->getAttribute('resourceType', '') !== 'projects' || $key->getAttribute('resourceInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
$response->dynamic($key, Response::MODEL_KEY);
}
}
@@ -1,111 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Keys;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class Update extends Base
{
use HTTP;
public static function getName()
{
return 'updateProjectKey';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/project/keys/:keyId')
->httpAlias('/v1/projects/:projectId/keys/:keyId')
->desc('Update project key')
->groups(['api', 'project'])
->label('scope', 'keys.write')
->label('event', 'keys.[keyId].update')
->label('audits.event', 'project.key.update')
->label('audits.resource', 'project.key/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'keys',
name: 'updateKey',
description: <<<EOT
Update a key by its unique ID. Use this endpoint to update the name, scopes, or expiration time of an API key.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_KEY,
)
]
))
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', null, new Nullable(new Datetime()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->callback($this->action(...));
}
/**
* @param array<string>|null $scopes
*/
public function action(
string $keyId,
string $name,
?array $scopes,
?string $expire,
Response $response,
QueueEvent $queueForEvents,
Database $dbForPlatform,
Document $project,
Authorization $authorization,
) {
$key = $authorization->skip(fn () => $dbForPlatform->getDocument('keys', $keyId));
if ($key->isEmpty() || $key->getAttribute('resourceType', '') !== 'projects' || $key->getAttribute('resourceInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
$updates = new Document([
'name' => $name,
'scopes' => $scopes ?? [],
'expire' => $expire,
]);
try {
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('keys', $key->getId(), $updates));
} catch (Duplicate) {
throw new Exception(Exception::KEY_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('keyId', $key->getId());
$response->dynamic($key, Response::MODEL_KEY);
}
}
@@ -1,127 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Keys;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Keys;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listProjectKeys';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/project/keys')
->httpAlias('/v1/projects/:projectId/keys')
->desc('List project keys')
->groups(['api', 'project'])
->label('scope', 'keys.read')
->label('sdk', new Method(
namespace: 'project',
group: 'keys',
name: 'listKeys',
description: <<<EOT
Get a list of all API keys from the current project.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_KEY_LIST,
)
]
))
->param('queries', [], new Keys(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Keys::ALLOWED_ATTRIBUTES), true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('project')
->inject('response')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
/**
* @param array<string> $queries
*/
public function action(
array $queries,
bool $includeTotal,
Document $project,
Response $response,
Database $dbForPlatform,
Authorization $authorization,
) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Backwards compatibility
if (\count(Query::getByType($queries, [Query::TYPE_LIMIT])) === 0) {
$queries[] = Query::limit(5000);
}
$queries[] = Query::equal('resourceType', ['projects']);
$queries[] = Query::equal('resourceInternalId', [$project->getSequence()]);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$keyId = $cursor->getValue();
$cursorDocument = $authorization->skip(fn () => $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
]));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Key '{$keyId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$keys = $authorization->skip(fn () => $dbForPlatform->find('keys', $queries));
$total = $includeTotal ? $authorization->skip(fn () => $dbForPlatform->count('keys', $filterQueries, APP_LIMIT_COUNT)) : 0;
} catch (OrderException $e) {
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.");
}
$response->dynamic(new Document([
'keys' => $keys,
'total' => $total,
]), Response::MODEL_KEY_LIST);
}
}
@@ -1,105 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createProjectAndroidPlatform';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/project/platforms/android')
->desc('Create project Android platform')
->groups(['api', 'project'])
->label('scope', 'platforms.write')
->label('event', 'platforms.[platformId].create')
->label('audits.event', 'project.platform.create')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'createAndroidPlatform',
description: <<<EOT
Create a new Android platform for your project. Use this endpoint to register a new Android platform where your users will run your application which will interact with the Appwrite API.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PLATFORM_ANDROID,
)
],
))
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform 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.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('applicationId', '', new Text(256), 'Android application ID. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $applicationId,
Response $response,
QueueEvent $queueForEvents,
Document $project,
Database $dbForPlatform,
Authorization $authorization,
) {
$platformId = ($platformId == 'unique()') ? ID::unique() : $platformId;
$platform = new Document([
'$id' => $platformId,
'$permissions' => [],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'type' => Platform::TYPE_ANDROID,
'name' => $name,
'key' => $applicationId,
'hostname' => '',
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->createDocument('platforms', $platform));
} catch (DuplicateException) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($platform, Response::MODEL_PLATFORM_ANDROID);
}
}
@@ -1,103 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectAndroidPlatform';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/project/platforms/android/:platformId')
->desc('Update project Android platform')
->groups(['api', 'project'])
->label('scope', 'platforms.write')
->label('event', 'platforms.[platformId].update')
->label('audits.event', 'project.platform.update')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'updateAndroidPlatform',
description: <<<EOT
Update an Android platform by its unique ID. Use this endpoint to update the platform's name or application ID.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM_ANDROID,
)
]
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('applicationId', '', new Text(256), 'Android application ID. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $applicationId,
Response $response,
QueueEvent $queueForEvents,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
) {
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
if ($platform->getAttribute('type', '') !== Platform::TYPE_ANDROID) {
throw new Exception(Exception::PLATFORM_METHOD_UNSUPPORTED);
}
$updates = new Document([
'name' => $name,
'key' => $applicationId,
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->updateDocument('platforms', $platform->getId(), $updates));
} catch (Duplicate) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response->dynamic($platform, Response::MODEL_PLATFORM_ANDROID);
}
}
@@ -1,105 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createProjectApplePlatform';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/project/platforms/apple')
->desc('Create project Apple platform')
->groups(['api', 'project'])
->label('scope', 'platforms.write')
->label('event', 'platforms.[platformId].create')
->label('audits.event', 'project.platform.create')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'createApplePlatform',
description: <<<EOT
Create a new Apple platform for your project. Use this endpoint to register a new Apple platform where your users will run your application which will interact with the Appwrite API.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PLATFORM_APPLE,
)
],
))
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform 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.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('bundleIdentifier', '', new Text(256), 'Apple bundle identifier. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $bundleIdentifier,
Response $response,
QueueEvent $queueForEvents,
Document $project,
Database $dbForPlatform,
Authorization $authorization,
) {
$platformId = ($platformId == 'unique()') ? ID::unique() : $platformId;
$platform = new Document([
'$id' => $platformId,
'$permissions' => [],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'type' => Platform::TYPE_APPLE,
'name' => $name,
'key' => $bundleIdentifier,
'hostname' => '',
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->createDocument('platforms', $platform));
} catch (DuplicateException) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($platform, Response::MODEL_PLATFORM_APPLE);
}
}
@@ -1,103 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectApplePlatform';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/project/platforms/apple/:platformId')
->desc('Update project Apple platform')
->groups(['api', 'project'])
->label('scope', 'platforms.write')
->label('event', 'platforms.[platformId].update')
->label('audits.event', 'project.platform.update')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'updateApplePlatform',
description: <<<EOT
Update an Apple platform by its unique ID. Use this endpoint to update the platform's name or bundle identifier.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM_APPLE,
)
]
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('bundleIdentifier', '', new Text(256), 'Apple bundle identifier. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $bundleIdentifier,
Response $response,
QueueEvent $queueForEvents,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
) {
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
if ($platform->getAttribute('type', '') !== Platform::TYPE_APPLE) {
throw new Exception(Exception::PLATFORM_METHOD_UNSUPPORTED);
}
$updates = new Document([
'name' => $name,
'key' => $bundleIdentifier,
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->updateDocument('platforms', $platform->getId(), $updates));
} catch (Duplicate) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response->dynamic($platform, Response::MODEL_PLATFORM_APPLE);
}
}
@@ -1,89 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Action
{
use HTTP;
public static function getName()
{
return 'deleteProjectPlatform';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/project/platforms/:platformId')
->httpAlias('/v1/projects/:projectId/platforms/:platformId')
->desc('Delete project platform')
->groups(['api', 'project'])
->label('scope', 'platforms.write')
->label('event', 'platforms.[platformId].delete')
->label('audits.event', 'project.platform.delete')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'deletePlatform',
description: <<<EOT
Delete a platform by its unique ID. This endpoint removes the platform and all its configurations from the project.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $platformId,
Response $response,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
Event $queueForEvents,
) {
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
if (!$authorization->skip(fn () => $dbForPlatform->deleteDocument('platforms', $platform->getId()))) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove document from DB');
};
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response->noContent();
}
}
@@ -1,91 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getProjectPlatform';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/project/platforms/:platformId')
->httpAlias('/v1/projects/:projectId/platforms/:platformId')
->desc('Get project platform')
->groups(['api', 'project'])
->label('scope', 'platforms.read')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'getPlatform',
description: <<<EOT
Get a platform by its unique ID. This endpoint returns the platform's details, including its name, type, and key configurations.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: [
Response::MODEL_PLATFORM_WEB,
Response::MODEL_PLATFORM_APPLE,
Response::MODEL_PLATFORM_ANDROID,
Response::MODEL_PLATFORM_WINDOWS,
Response::MODEL_PLATFORM_LINUX,
],
)
]
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->callback($this->action(...));
}
public function action(
string $platformId,
Response $response,
Database $dbForPlatform,
Authorization $authorization,
Document $project
) {
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
$type = $platform->getAttribute('type');
$model = match($type) {
Platform::TYPE_WEB => Response::MODEL_PLATFORM_WEB,
Platform::TYPE_APPLE => Response::MODEL_PLATFORM_APPLE,
Platform::TYPE_ANDROID => Response::MODEL_PLATFORM_ANDROID,
Platform::TYPE_WINDOWS => Response::MODEL_PLATFORM_WINDOWS,
Platform::TYPE_LINUX => Response::MODEL_PLATFORM_LINUX,
default => Response::MODEL_PLATFORM_WEB // Backwards compatibility
};
$response->dynamic($platform, $model);
}
}
@@ -1,105 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Linux;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createProjectLinuxPlatform';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/project/platforms/linux')
->desc('Create project Linux platform')
->groups(['api', 'project'])
->label('scope', 'platforms.write')
->label('event', 'platforms.[platformId].create')
->label('audits.event', 'project.platform.create')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'createLinuxPlatform',
description: <<<EOT
Create a new Linux platform for your project. Use this endpoint to register a new Linux platform where your users will run your application which will interact with the Appwrite API.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PLATFORM_LINUX,
)
],
))
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform 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.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('packageName', '', new Text(256), 'Linux package name. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $packageName,
Response $response,
QueueEvent $queueForEvents,
Document $project,
Database $dbForPlatform,
Authorization $authorization,
) {
$platformId = ($platformId == 'unique()') ? ID::unique() : $platformId;
$platform = new Document([
'$id' => $platformId,
'$permissions' => [],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'type' => Platform::TYPE_LINUX,
'name' => $name,
'key' => $packageName,
'hostname' => '', // Web platform attribute
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->createDocument('platforms', $platform));
} catch (DuplicateException) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($platform, Response::MODEL_PLATFORM_LINUX);
}
}
@@ -1,103 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Linux;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectLinuxPlatform';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/project/platforms/linux/:platformId')
->desc('Update project Linux platform')
->groups(['api', 'project'])
->label('scope', 'platforms.write')
->label('event', 'platforms.[platformId].update')
->label('audits.event', 'project.platform.update')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'updateLinuxPlatform',
description: <<<EOT
Update a Linux platform by its unique ID. Use this endpoint to update the platform's name or package name.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM_LINUX,
)
]
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('packageName', '', new Text(256), 'Linux package name. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $packageName,
Response $response,
QueueEvent $queueForEvents,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
) {
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
if ($platform->getAttribute('type', '') !== Platform::TYPE_LINUX) {
throw new Exception(Exception::PLATFORM_METHOD_UNSUPPORTED);
}
$updates = new Document([
'name' => $name,
'key' => $packageName,
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->updateDocument('platforms', $platform->getId(), $updates));
} catch (Duplicate) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response->dynamic($platform, Response::MODEL_PLATFORM_LINUX);
}
}
@@ -1,173 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Hostname;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
/**
* WARNING: This kind of platform has most complex action, because it holds backwards compatibility too.
* If possible, refer to any other type of platform for APIs, for more simpler endpoint.
*/
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createProjectWebPlatform';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/project/platforms/web')
->httpAlias('/v1/projects/:projectId/platforms')
->desc('Create project web platform')
->groups(['api', 'project'])
->label('scope', 'platforms.write')
->label('event', 'platforms.[platformId].create')
->label('audits.event', 'project.platform.create')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'createWebPlatform',
description: <<<EOT
Create a new web platform for your project. Use this endpoint to register a new platform where your users will run your application which will interact with the Appwrite API.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PLATFORM_WEB,
)
],
))
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform 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.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true) // Optional for backwards compatibility
->param('key', '', new Text(256), 'Deprecated: Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
->param('type', '', new Text(256), 'Deprecated: Platform type. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
->inject('request')
->inject('response')
->inject('queueForEvents')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $hostname,
?string $key, // For backwards compatibility
?string $type, // For backwards compatibility
Request $request,
Response $response,
QueueEvent $queueForEvents,
Document $project,
Database $dbForPlatform,
Authorization $authorization,
) {
$key = $key ?? ''; // App platform attribute, backwards compatibility
$type = $type ?? ''; // App platform attribute, backwards compatibility
// Backwards compatibility
// Used to have: type, name, key, hostname
if (!empty($type)) {
// Validate deprecated type, and rename to new type
$deprecatedTypeMapping = [
// Web
'web' => Platform::TYPE_WEB,
'flutter-web' => Platform::TYPE_WEB,
'unity' => Platform::TYPE_WEB, // Was not officially supported anyway
// Apple
'flutter-macos' => Platform::TYPE_APPLE,
'flutter-ios' => Platform::TYPE_APPLE,
'react-native-ios' => Platform::TYPE_APPLE,
'apple-ios' => Platform::TYPE_APPLE,
'apple-macos' => Platform::TYPE_APPLE,
'apple-watchos' => Platform::TYPE_APPLE,
'apple-tvos' => Platform::TYPE_APPLE,
// Android
'flutter-android' => Platform::TYPE_ANDROID,
'android' => Platform::TYPE_ANDROID,
'react-native-android' => Platform::TYPE_ANDROID,
'flutter-linux' => Platform::TYPE_LINUX,
'flutter-windows' => Platform::TYPE_WINDOWS,
];
$typeValidator = new WhiteList(\array_keys($deprecatedTypeMapping));
if (!$typeValidator->isValid($request->getParam('type', ''))) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "type" is invalid: ' . $typeValidator->getDescription());
}
$type = $deprecatedTypeMapping[$request->getParam('type', '')] ?? '';
}
if (!empty($key)) {
// Validate deprecated app id (key)
$keyValidator = new Text(256);
if (!$keyValidator->isValid($key)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "key" is invalid: ' . $keyValidator->getDescription());
}
}
if (empty($key) && empty($type)) {
// Modern request, validate hostname
if (empty($hostname)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "hostname" is not optional.');
}
}
$platformId = ($platformId == 'unique()') ? ID::unique() : $platformId;
$platform = new Document([
'$id' => $platformId,
'$permissions' => [],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'type' => $type ?: Platform::TYPE_WEB, // Preserve type for backwards compatibility
'name' => $name,
'key' => $key,
'hostname' => $hostname
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->createDocument('platforms', $platform));
} catch (DuplicateException) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($platform, Response::MODEL_PLATFORM_WEB);
}
}
@@ -1,151 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Hostname;
use Utopia\Validator\Text;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectWebPlatform';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/project/platforms/web/:platformId')
->httpAlias('/v1/projects/:projectId/platforms/:platformId')
->desc('Update project web platform')
->groups(['api', 'project'])
->label('scope', 'platforms.write')
->label('event', 'platforms.[platformId].update')
->label('audits.event', 'project.platform.update')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'updateWebPlatform',
description: <<<EOT
Update a web platform by its unique ID. Use this endpoint to update the platform's name or hostname.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM_WEB,
)
]
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true) // Optional for backwards compatibility
->param('key', '', new Text(256), 'Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
->inject('response')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $hostname,
?string $key, // For backwards compatibility
Response $response,
QueueEvent $queueForEvents,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
) {
$key = $key ?? ''; // App platform attribute, backwards compatibility
// Backwards compatibility
// Used to have: type, name, key, hostname
if (!empty($key)) {
// Validate deprecated app id (key)
$keyValidator = new Text(256);
if (!$keyValidator->isValid($key)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "key" is invalid: ' . $keyValidator->getDescription());
}
}
// One day, ideally, we ensure hostname is not empty
// But for backwards compatibility backend must threat it as optional for now
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
// Wrapped in if, for backwards compatibility
if (!empty($hostname)) {
$supportedTypes = [
Platform::TYPE_WEB,
// Backwards compatibility
'flutter-web',
'unity',
'flutter-macos',
'flutter-ios',
'react-native-ios',
'apple-ios',
'apple-macos',
'apple-watchos',
'apple-tvos',
'flutter-android',
'react-native-android',
'flutter-windows',
'flutter-linux',
];
if (!in_array($platform->getAttribute('type', ''), $supportedTypes)) {
throw new Exception(Exception::PLATFORM_METHOD_UNSUPPORTED);
}
}
$updates = new Document([
'name' => $name,
]);
// Wrapped in if, for backwards compatibility
if (!empty($hostname)) {
$updates->setAttribute('hostname', $hostname);
}
// Backwards compatibility
if (!empty($key)) {
$updates->setAttribute('key', $key);
}
try {
$platform = $authorization->skip(fn () => $dbForPlatform->updateDocument('platforms', $platform->getId(), $updates));
} catch (Duplicate) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response->dynamic($platform, Response::MODEL_PLATFORM_WEB);
}
}
@@ -1,105 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createProjectWindowsPlatform';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/project/platforms/windows')
->desc('Create project Windows platform')
->groups(['api', 'project'])
->label('scope', 'platforms.write')
->label('event', 'platforms.[platformId].create')
->label('audits.event', 'project.platform.create')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'createWindowsPlatform',
description: <<<EOT
Create a new Windows platform for your project. Use this endpoint to register a new Windows platform where your users will run your application which will interact with the Appwrite API.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PLATFORM_WINDOWS,
)
],
))
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform 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.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('packageIdentifierName', '', new Text(256), 'Windows package identifier name. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $packageIdentifierName,
Response $response,
QueueEvent $queueForEvents,
Document $project,
Database $dbForPlatform,
Authorization $authorization,
) {
$platformId = ($platformId == 'unique()') ? ID::unique() : $platformId;
$platform = new Document([
'$id' => $platformId,
'$permissions' => [],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'type' => Platform::TYPE_WINDOWS,
'name' => $name,
'key' => $packageIdentifierName,
'hostname' => '',
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->createDocument('platforms', $platform));
} catch (DuplicateException) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($platform, Response::MODEL_PLATFORM_WINDOWS);
}
}
@@ -1,103 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectWindowsPlatform';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/project/platforms/windows/:platformId')
->desc('Update project Windows platform')
->groups(['api', 'project'])
->label('scope', 'platforms.write')
->label('event', 'platforms.[platformId].update')
->label('audits.event', 'project.platform.update')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'updateWindowsPlatform',
description: <<<EOT
Update a Windows platform by its unique ID. Use this endpoint to update the platform's name or package identifier name.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM_WINDOWS,
)
]
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('packageIdentifierName', '', new Text(256), 'Windows package identifier name. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $packageIdentifierName,
Response $response,
QueueEvent $queueForEvents,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
) {
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
if ($platform->getAttribute('type', '') !== Platform::TYPE_WINDOWS) {
throw new Exception(Exception::PLATFORM_METHOD_UNSUPPORTED);
}
$updates = new Document([
'name' => $name,
'key' => $packageIdentifierName,
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->updateDocument('platforms', $platform->getId(), $updates));
} catch (Duplicate) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response->dynamic($platform, Response::MODEL_PLATFORM_WINDOWS);
}
}
@@ -1,125 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Platforms;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Action
{
use HTTP;
public static function getName()
{
return 'listProjectPlatforms';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/project/platforms')
->httpAlias('/v1/projects/:projectId/platforms')
->desc('List project platforms')
->groups(['api', 'project'])
->label('scope', 'platforms.read')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'listPlatforms',
description: <<<EOT
Get a list of all platforms in the project. This endpoint returns an array of all platforms and their configurations.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM_LIST,
)
]
))
->param('queries', [], new Platforms(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Platforms::ALLOWED_ATTRIBUTES), true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('project')
->inject('response')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
/**
* @param array<string> $queries
*/
public function action(
array $queries,
bool $includeTotal,
Document $project,
Response $response,
Database $dbForPlatform,
Authorization $authorization,
) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
foreach ($queries as $query) {
if (\in_array($query->getAttribute(), ['bundleIdentifier', 'applicationId', 'packageIdentifierName', 'packageName'])) {
$query->setAttribute('key');
}
}
$queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$platformId = $cursor->getValue();
$cursorDocument = $authorization->skip(fn () => $dbForPlatform->findOne('platforms', [
Query::equal('$id', [$platformId]),
Query::equal('projectInternalId', [$project->getSequence()]),
]));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Platform '{$platformId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$platforms = $authorization->skip(fn () => $dbForPlatform->find('platforms', $queries));
$total = $includeTotal ? $authorization->skip(fn () => $dbForPlatform->count('platforms', $filterQueries, APP_LIMIT_COUNT)) : 0;
} catch (OrderException $e) {
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.");
}
$response->dynamic(new Document([
'platforms' => $platforms,
'total' => $total,
]), Response::MODEL_PLATFORM_LIST);
}
}
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -18,7 +19,7 @@ use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends Action
class Create extends Base
{
use HTTP;
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
@@ -15,7 +16,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Action
class Delete extends Base
{
use HTTP;
@@ -34,7 +35,7 @@ class Delete extends Action
->label('scope', 'project.write')
->label('event', 'variables.[variableId].delete')
->label('audits.event', 'project.variable.delete')
->label('audits.resource', 'project.variable/{request.variableId}')
->label('audits.resource', 'project.variable/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'variables',
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -12,7 +13,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
class Get extends Base
{
use HTTP;
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -18,7 +19,7 @@ use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Action
class Update extends Base
{
use HTTP;
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -18,7 +19,7 @@ use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Action
class XList extends Base
{
use HTTP;
@@ -3,25 +3,6 @@
namespace Appwrite\Platform\Modules\Project\Services;
use Appwrite\Platform\Modules\Project\Http\Init;
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Create as CreateKey;
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Delete as DeleteKey;
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Get as GetKey;
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Update as UpdateKey;
use Appwrite\Platform\Modules\Project\Http\Project\Keys\XList as ListKeys;
use Appwrite\Platform\Modules\Project\Http\Project\Labels\Update as UpdateProjectLabels;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Create as CreateAndroidPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Update as UpdateAndroidPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple\Create as CreateApplePlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple\Update as UpdateApplePlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Delete as DeletePlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Get as GetPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Linux\Create as CreateLinuxPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Linux\Update as UpdateLinuxPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web\Create as CreateWebPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web\Update as UpdateWebPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Create as CreateWindowsPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Update as UpdateWindowsPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\XList as ListPlatforms;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable;
@@ -39,35 +20,10 @@ class Http extends Service
$this->addAction(Init::getName(), new Init());
// Project
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
// Variables
$this->addAction(CreateVariable::getName(), new CreateVariable());
$this->addAction(ListVariables::getName(), new ListVariables());
$this->addAction(GetVariable::getName(), new GetVariable());
$this->addAction(DeleteVariable::getName(), new DeleteVariable());
$this->addAction(UpdateVariable::getName(), new UpdateVariable());
// Keys
$this->addAction(CreateKey::getName(), new CreateKey());
$this->addAction(ListKeys::getName(), new ListKeys());
$this->addAction(GetKey::getName(), new GetKey());
$this->addAction(DeleteKey::getName(), new DeleteKey());
$this->addAction(UpdateKey::getName(), new UpdateKey());
// Platforms
$this->addAction(DeletePlatform::getName(), new DeletePlatform());
$this->addAction(UpdateWebPlatform::getName(), new UpdateWebPlatform());
$this->addAction(UpdateApplePlatform::getName(), new UpdateApplePlatform());
$this->addAction(UpdateAndroidPlatform::getName(), new UpdateAndroidPlatform());
$this->addAction(UpdateWindowsPlatform::getName(), new UpdateWindowsPlatform());
$this->addAction(UpdateLinuxPlatform::getName(), new UpdateLinuxPlatform());
$this->addAction(CreateWebPlatform::getName(), new CreateWebPlatform());
$this->addAction(CreateApplePlatform::getName(), new CreateApplePlatform());
$this->addAction(CreateAndroidPlatform::getName(), new CreateAndroidPlatform());
$this->addAction(CreateWindowsPlatform::getName(), new CreateWindowsPlatform());
$this->addAction(CreateLinuxPlatform::getName(), new CreateLinuxPlatform());
$this->addAction(GetPlatform::getName(), new GetPlatform());
$this->addAction(ListPlatforms::getName(), new ListPlatforms());
}
}
@@ -1,15 +1,20 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Labels;
namespace Appwrite\Platform\Modules\Projects\Http\Projects\Labels;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Text;
@@ -22,37 +27,39 @@ class Update extends Action
return 'updateProjectLabels';
}
protected function getQueriesValidator(): Validator
{
return new Projects();
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/project/labels')
->httpAlias('/v1/projects/:projectId/labels')
->setHttpPath('/v1/projects/:projectId/labels')
->desc('Update project labels')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'labels.*.update')
->label('audits.event', 'project.labels.update')
->label('audits.resource', 'project.labels/{response.$id}')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'project',
group: null,
namespace: 'projects',
group: 'projects',
name: 'updateLabels',
description: <<<EOT
Update the project labels. Labels can be used to easily filter projects in an organization.
Update the project labels by its unique ID. Labels can be used to easily filter projects in an organization.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
model: Response::MODEL_PROJECT
)
],
contentType: ContentType::JSON
))
->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')
->inject('project')
->callback($this->action(...));
}
@@ -60,11 +67,17 @@ class Update extends Action
* @param array<string> $labels
*/
public function action(
string $projectId,
array $labels,
Response $response,
Database $dbForPlatform,
Document $project
Database $dbForPlatform
): void {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$labels = (array) \array_values(\array_unique($labels));
$project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document(['labels' => $labels]));
@@ -8,6 +8,7 @@ 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\Create as CreateProject;
use Appwrite\Platform\Modules\Projects\Http\Projects\Labels\Update as UpdateProjectLabels;
use Appwrite\Platform\Modules\Projects\Http\Projects\Team\Update as UpdateProjectTeam;
use Appwrite\Platform\Modules\Projects\Http\Projects\Update as UpdateProject;
use Appwrite\Platform\Modules\Projects\Http\Projects\XList as ListProjects;
@@ -30,6 +31,7 @@ class Http extends Service
$this->addAction(CreateProject::getName(), new CreateProject());
$this->addAction(UpdateProject::getName(), new UpdateProject());
$this->addAction(ListProjects::getName(), new ListProjects());
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
$this->addAction(UpdateProjectTeam::getName(), new UpdateProjectTeam());
$this->addAction(CreateSchedule::getName(), new CreateSchedule());
@@ -74,12 +74,10 @@ class Update extends Action
->inject('queueForEvents')
->inject('store')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->callback($this->action(...));
}
public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken, bool $domainVerification, ?string $cookieDomain)
public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken)
{
$protocol = $request->getProtocol();
@@ -164,7 +162,7 @@ class Update extends Action
->setProperty('secret', $secret)
->encode();
if (!$domainVerification) {
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
@@ -174,7 +172,7 @@ class Update extends Action
value: $encoded,
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: $cookieDomain,
domain: Config::getParam('cookieDomain'),
secure: ('https' === $protocol),
httponly: true
)
@@ -183,7 +181,7 @@ class Update extends Action
value: $encoded,
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: $cookieDomain,
domain: Config::getParam('cookieDomain'),
secure: ('https' === $protocol),
httponly: true,
sameSite: Config::getParam('cookieSamesite')
@@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Event\Validator\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -24,7 +25,7 @@ use Utopia\Validator\Multiple;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
class Create extends Action
class Create extends Base
{
use HTTP;
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
@@ -17,7 +18,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Action
class Delete extends Base
{
use HTTP;
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -15,7 +16,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
class Get extends Base
{
use HTTP;
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Signature;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -16,7 +17,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Update extends Action
class Update extends Base
{
use HTTP;
@@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Event\Validator\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -23,7 +24,7 @@ use Utopia\Validator\Multiple;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
class Update extends Action
class Update extends Base
{
use HTTP;
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -19,7 +20,7 @@ use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Action
class XList extends Base
{
use HTTP;
+4 -3
View File
@@ -9,6 +9,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception;
use Utopia\Database\Validator\Authorization;
use Utopia\Http\Http;
use Utopia\Platform\Action;
use Utopia\Registry\Registry;
use Utopia\Validator\Text;
@@ -31,7 +32,6 @@ class Migrate extends Action
->inject('getProjectDB')
->inject('register')
->inject('authorization')
->inject('console')
->callback($this->action(...));
}
@@ -48,8 +48,7 @@ class Migrate extends Action
Database $dbForPlatform,
callable $getProjectDB,
Registry $register,
Authorization $authorization,
Document $console
Authorization $authorization
): void {
if (!\array_key_exists($version, Migration::$versions)) {
@@ -126,6 +125,8 @@ class Migrate extends Action
Console::log('Migrated ' . ++$count . '/' . $total . ' projects...');
});
$console = (new Http('UTC'))->getResource('console');
try {
$migration
->setProject($console, $getProjectDB($console), $dbForPlatform, $authorization, $getProjectDB);
+19 -5
View File
@@ -639,15 +639,29 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
} catch (\Throwable) {
}
// Create or checkout dev branch from the base branch
// This ensures dev always starts from the latest base branch,
// avoiding history divergence caused by squash merges.
// Checkout dev branch (or create if it doesn't exist)
try {
$repo->execute('checkout', '-B', $gitBranch, $repoBranch);
$repo->execute('checkout', '-f', $gitBranch);
} catch (\Throwable) {
$repo->execute('checkout', '-b', $gitBranch);
}
// Fetch dev branch, or push to create it on remote
try {
$repo->execute('fetch', 'origin', $gitBranch, '--quiet', '--no-tags', '--depth', '1');
} catch (\Throwable) {
try {
$repo->execute('push', '-u', 'origin', $gitBranch, '--quiet');
} catch (\Throwable) {
}
}
// Sync with remote dev branch
try {
$repo->execute('reset', '--hard', "origin/{$gitBranch}");
} catch (\Throwable) {
}
// Backup .github before cleaning working tree
$githubDir = $target . '/.github';
$githubBackup = \sys_get_temp_dir() . '/.github-backup-' . \getmypid();
@@ -685,7 +699,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
return true;
}
$repo->execute('push', '--force-with-lease', '-u', 'origin', $gitBranch, '--quiet');
$repo->execute('push', '-u', 'origin', $gitBranch, '--quiet');
} catch (\Throwable $e) {
Console::warning(" Git push failed: " . $e->getMessage());
return false;
@@ -61,7 +61,7 @@ class ScheduleFunctions extends ScheduleBase
$nextDate = $cron->getNextRunDate();
$next = DateTime::format($nextDate);
$currentTick = $next < $timeFrame;
$currentTick = $next <= $timeFrame;
if (!$currentTick) {
continue;
@@ -88,7 +88,7 @@ class ScheduleFunctions extends ScheduleBase
$scheduleKey = $delayConfig['key'];
// Ensure schedule was not deleted
if (!\array_key_exists($scheduleKey, $this->schedules)) {
return;
continue;
}
$schedule = $this->schedules[$scheduleKey];
+7 -22
View File
@@ -2,7 +2,6 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\Network\Validator\Redirect;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Specification\Format\OpenAPI3;
@@ -19,9 +18,6 @@ use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Adapter\MySQL;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\DI\Container;
use Utopia\Http\Adapter\FPM\Server as FPMServer;
use Utopia\Http\Http;
use Utopia\Http\Request as UtopiaRequest;
use Utopia\Http\Response as UtopiaResponse;
@@ -340,17 +336,11 @@ class Specs extends Action
$mocks = ($mode === 'mocks');
// Mock dependencies needed by param validator injections in route definitions
$specsContainer = new Container();
$specsContainer->set('request', fn () => $this->getRequest());
$specsContainer->set('response', fn () => $response);
$specsContainer->set('dbForPlatform', fn () => new Database(new MySQL(''), new Cache(new None())));
$specsContainer->set('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None())));
$specsContainer->set('redirectValidator', fn () => new Redirect([], []));
$specsContainer->set('project', fn () => new Document([]));
$specsContainer->set('passwordsDictionary', fn () => []);
$specsContainer->set('localeCodes', fn () => \array_map(fn ($locale) => $locale['code'], Config::getParam('locale-codes', [])));
$specsContainer->set('plan', fn () => []);
// Mock dependencies
Http::setResource('request', fn () => $this->getRequest());
Http::setResource('response', fn () => $response);
Http::setResource('dbForPlatform', fn () => new Database(new MySQL(''), new Cache(new None())));
Http::setResource('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None())));
$platforms = static::getPlatforms();
$authCounts = $this->getAuthCounts();
@@ -448,7 +438,7 @@ class Specs extends Action
}
$arguments = [
new Http(new FPMServer($specsContainer), 'UTC'),
new Http('UTC'),
$services,
$routes,
$models,
@@ -482,12 +472,7 @@ class Specs extends Action
? $specsDir . '/' . $format . '-mocks-' . $platform . '.json'
: $specsDir . '/' . $format . '-' . $version . '-' . $platform . '.json';
try {
$parsedSpecs = $specs->parse();
} catch (\RuntimeException $e) {
throw new \RuntimeException("Spec generation failed for {$platform} ({$format}): " . $e->getMessage(), 0, $e);
}
$parsedSpecs = $specs->parse();
$encodedSpecs = \json_encode($parsedSpecs, JSON_PRETTY_PRINT);
unset($parsedSpecs);
+1 -5
View File
@@ -379,11 +379,7 @@ class Migrations extends Action
'webhooks.read',
'webhooks.write',
'project.read',
'project.write',
'keys.read',
'keys.write',
'platforms.read',
'platforms.write',
'project.write'
]
]);
+1 -12
View File
@@ -2,14 +2,10 @@
namespace Appwrite\Promises;
use Swoole\Coroutine;
use Swoole\Coroutine\Channel;
use Utopia\DI\Container;
class Swoole extends Promise
{
private const REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container';
public function __construct(?callable $executor = null)
{
parent::__construct($executor);
@@ -20,14 +16,7 @@ class Swoole extends Promise
callable $resolve,
callable $reject
): void {
$parentContainer = (Coroutine::getCid() !== -1)
? (Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? null)
: null;
\go(function () use ($executor, $resolve, $reject, $parentContainer) {
if ($parentContainer !== null) {
Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] = new Container($parentContainer);
}
\go(function () use ($executor, $resolve, $reject) {
try {
$executor($resolve, $reject);
} catch (\Throwable $exception) {
@@ -769,9 +769,6 @@ abstract class Format
protected function getNestedModels(Model $model, array &$usedModels): void
{
foreach ($model->getRules() as $rule) {
if (($rule['hidden'] ?? false) === true) {
continue;
}
if (!in_array($model->getType(), $usedModels)) {
continue;
}
@@ -278,18 +278,6 @@ class OpenAPI3 extends Format
}
}
if (\is_string($model)) {
throw new \RuntimeException("Unresolved response model '{$model}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered.");
}
if (\is_array($model)) {
foreach ($model as $m) {
if (\is_string($m)) {
throw new \RuntimeException("Unresolved response model '{$m}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered.");
}
}
}
if (!(\is_array($model)) && $model->isNone()) {
$temp['responses'][(string)$response->getCode() ?? '500'] = [
'description' => in_array($produces, [
@@ -833,10 +821,6 @@ class OpenAPI3 extends Format
}
foreach ($model->getRules() as $name => $rule) {
if (($rule['hidden'] ?? false) === true) {
continue;
}
$type = '';
$format = null;
$items = null;
@@ -285,18 +285,6 @@ class Swagger2 extends Format
}
}
if (\is_string($model)) {
throw new \RuntimeException("Unresolved response model '{$model}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered.");
}
if (\is_array($model)) {
foreach ($model as $m) {
if (\is_string($m)) {
throw new \RuntimeException("Unresolved response model '{$m}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered.");
}
}
}
if (!(\is_array($model)) && $model->isNone()) {
$temp['responses'][(string)$response->getCode() ?? '500'] = [
'description' => in_array($produces, [
@@ -813,10 +801,6 @@ class Swagger2 extends Format
}
foreach ($model->getRules() as $name => $rule) {
if (($rule['hidden'] ?? false) === true) {
continue;
}
$type = '';
$format = null;
$items = null;
@@ -1,25 +0,0 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Platforms extends Base
{
public const ALLOWED_ATTRIBUTES = [
'type',
'name',
'hostname',
'bundleIdentifier',
'applicationId',
'packageIdentifierName',
'packageName',
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('platforms', self::ALLOWED_ATTRIBUTES);
}
}
@@ -11,69 +11,9 @@ class V21 extends Filter
public function parse(array $content, string $model): array
{
switch ($model) {
// Web is special case compared to others, because it holds backwards compatibility logic
case 'project.createWebPlatform':
$content = $this->fillPlatformId($content);
$content = $this->removePlatformStore($content);
// Keep 'key' for backwards compatibility
break;
case 'project.updateWebPlatform':
$content = $this->removePlatformStore($content);
// Keep 'key' for backwards compatibility
break;
case 'project.createApplePlatform':
$content = $this->fillPlatformId($content);
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'bundleIdentifier');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.updateApplePlatform':
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'bundleIdentifier');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.createAndroidPlatform':
$content = $this->fillPlatformId($content);
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'applicationId');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.updateAndroidPlatform':
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'applicationId');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.createWindowsPlatform':
$content = $this->fillPlatformId($content);
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'packageIdentifierName');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.updateWindowsPlatform':
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'packageIdentifierName');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.createLinuxPlatform':
$content = $this->fillPlatformId($content);
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'packageName');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.updateLinuxPlatform':
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'packageName');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.listPlatforms':
$content = $this->preservePlatformsQueries($content);
break;
case 'webhooks.create':
$content = $this->fillWebhookid($content);
break;
case 'project.createKey':
$content = $this->fillKeyId($content);
break;
case 'project.createVariable':
$content = $this->fillVariableId($content);
break;
@@ -125,12 +65,6 @@ class V21 extends Filter
return $content;
}
protected function fillKeyId(array $content): array
{
$content['keyId'] = $content['keyId'] ?? 'unique()';
return $content;
}
protected function fillVariableId(array $content): array
{
$content['variableId'] = $content['variableId'] ?? 'unique()';
@@ -145,33 +79,4 @@ class V21 extends Filter
return $content;
}
protected function fillPlatformId(array $content): array
{
$content['platformId'] = $content['platformId'] ?? 'unique()';
return $content;
}
protected function replacePlatformKey(array $content, string $newKey): array
{
$content[$newKey] = $content[$newKey] ?? $content['key'] ?? null;
unset($content['key']);
return $content;
}
protected function removePlatformStore(array $content): array
{
unset($content['store']);
return $content;
}
protected function preservePlatformsQueries(array $content): array
{
$content['queries'] = $content['queries'] ?? [
Query::limit(5000)
];
return $content;
}
}
+2 -12
View File
@@ -256,11 +256,7 @@ class Response extends SwooleResponse
public const MODEL_MOCK_NUMBER = 'mockNumber';
public const MODEL_AUTH_PROVIDER = 'authProvider';
public const MODEL_AUTH_PROVIDER_LIST = 'authProviderList';
public const MODEL_PLATFORM_APPLE = 'platformApple';
public const MODEL_PLATFORM_ANDROID = 'platformAndroid';
public const MODEL_PLATFORM_WINDOWS = 'platformWindows';
public const MODEL_PLATFORM_LINUX = 'platformLinux';
public const MODEL_PLATFORM_WEB = 'platformWeb';
public const MODEL_PLATFORM = 'platform';
public const MODEL_PLATFORM_LIST = 'platformList';
public const MODEL_VARIABLE = 'variable';
public const MODEL_VARIABLE_LIST = 'variableList';
@@ -480,13 +476,7 @@ class Response extends SwooleResponse
foreach ($rule['type'] as $type) {
$condition = false;
foreach ($this->getModel($type)->conditions as $attribute => $val) {
if (\is_array($val)) {
$condition = \in_array($item->getAttribute($attribute), $val);
} else {
$condition = $item->getAttribute($attribute) === $val;
}
$condition = $item->getAttribute($attribute) === $val;
if (!$condition) {
break;
}
@@ -11,23 +11,6 @@ class V21 extends Filter
public function parse(array $content, string $model): array
{
return match ($model) {
// Web is special case, it has backwards compatibility
Response::MODEL_PLATFORM_WEB => $this->parsePlatform($content),
Response::MODEL_PLATFORM_APPLE => $this->parsePlatform($content),
Response::MODEL_PLATFORM_ANDROID => $this->parsePlatform($content),
Response::MODEL_PLATFORM_WINDOWS => $this->parsePlatform($content),
Response::MODEL_PLATFORM_LINUX => $this->parsePlatform($content),
Response::MODEL_PLATFORM_LIST => $this->handleList(
$content,
"platforms",
fn ($item) => $this->parsePlatform($item),
),
Response::MODEL_PROJECT => $this->parseProjectForPlatform($content),
Response::MODEL_PROJECT_LIST => $this->handleList(
$content,
"projects",
fn ($item) => $this->parseProjectForPlatform($item),
),
Response::MODEL_USER => $this->parseUser($content),
Response::MODEL_USER_LIST => $this->handleList(
$content,
@@ -124,34 +107,4 @@ class V21 extends Filter
return $content;
}
protected function parseProjectForPlatform(array $content): array
{
// Parse platforms under project, since it's a subquery
$content['platforms'] = \array_map(fn ($item) => $this->parsePlatform($item), $content['platforms']);
return $content;
}
protected function parsePlatform(array $content): array
{
// Map platform-specific identifier fields back to 'key'
$content['key'] =
($content['bundleIdentifier'] ?? '')
?: ($content['applicationId'] ?? '')
?: ($content['packageIdentifierName'] ?? '')
?: ($content['packageName'] ?? '')
?: ($content['key'] ?? '')
?: '';
unset($content['bundleIdentifier']);
unset($content['applicationId']);
unset($content['packageIdentifierName']);
unset($content['packageName']);
// Restore fields removed in v1.9
$content['store'] = $content['store'] ?? '';
$content['hostname'] = $content['hostname'] ?? '';
return $content;
}
}
@@ -7,6 +7,11 @@ use Appwrite\Utopia\Response\Model;
class AuthProvider extends Model
{
/**
* @var bool
*/
protected bool $public = false;
public function __construct()
{
$this
@@ -7,6 +7,11 @@ use Appwrite\Utopia\Response\Model;
class DevKey extends Model
{
/**
* @var bool
*/
protected bool $public = false;
public function __construct()
{
$this
@@ -0,0 +1,100 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Platform extends Model
{
/**
* @var bool
*/
protected bool $public = false;
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Platform ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Platform creation date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Platform update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Platform name.',
'default' => '',
'example' => 'My Web App',
])
->addRule('type', [
'type' => self::TYPE_ENUM,
'description' => 'Platform type. Possible values are: web, flutter-web, flutter-ios, flutter-android, flutter-linux, flutter-macos, flutter-windows, apple-ios, apple-macos, apple-watchos, apple-tvos, android, unity, react-native-ios, react-native-android.',
'default' => '',
'example' => 'web',
'enum' => ['web', 'flutter-web', 'flutter-ios', 'flutter-android', 'flutter-linux', 'flutter-macos', 'flutter-windows', 'apple-ios', 'apple-macos', 'apple-watchos', 'apple-tvos', 'android', 'unity', 'react-native-ios', 'react-native-android'],
])
->addRule('key', [
'type' => self::TYPE_STRING,
'description' => 'Platform Key. iOS bundle ID or Android package name. Empty string for other platforms.',
'default' => '',
'example' => 'com.company.appname',
])
->addRule('store', [
'type' => self::TYPE_STRING,
'description' => 'App store or Google Play store ID.',
'example' => '',
])
->addRule('hostname', [
'type' => self::TYPE_STRING,
'description' => 'Web app hostname. Empty string for other platforms.',
'default' => '',
'example' => 'app.example.com',
])
->addRule('httpUser', [
'type' => self::TYPE_STRING,
'description' => 'HTTP basic authentication username.',
'default' => '',
'example' => 'username',
])
->addRule('httpPass', [
'type' => self::TYPE_STRING,
'description' => 'HTTP basic authentication password.',
'default' => '',
'example' => 'password',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Platform';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PLATFORM;
}
}
@@ -1,58 +0,0 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Network\Platform;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
class PlatformAndroid extends PlatformBase
{
public function __construct()
{
$this->conditions = [
'type' => Platform::TYPE_ANDROID,
];
parent::__construct();
$this
->addRule('applicationId', [
'type' => self::TYPE_STRING,
'description' => 'Android application ID.',
'default' => '',
'example' => 'com.company.appname',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Platform Android';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PLATFORM_ANDROID;
}
public function filter(Document $document): Document
{
// DB level: 'key'
// API level: 'applicationId'
$document->setAttribute('applicationId', $document->getAttribute('key', null));
$document->removeAttribute('key');
return $document;
}
}
@@ -1,58 +0,0 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Network\Platform;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
class PlatformApple extends PlatformBase
{
public function __construct()
{
$this->conditions = [
'type' => Platform::TYPE_APPLE,
];
parent::__construct();
$this
->addRule('bundleIdentifier', [
'type' => self::TYPE_STRING,
'description' => 'Apple bundle identifier.',
'default' => '',
'example' => 'com.company.appname',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Platform Apple';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PLATFORM_APPLE;
}
public function filter(Document $document): Document
{
// DB level: 'key'
// API level: 'bundleIdentifier'
$document->setAttribute('bundleIdentifier', $document->getAttribute('key', null));
$document->removeAttribute('key');
return $document;
}
}
@@ -1,57 +0,0 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Network\Platform;
use Appwrite\Utopia\Response\Model;
abstract class PlatformBase extends Model
{
public function getSupportedTypes(): array
{
return [
Platform::TYPE_WINDOWS,
Platform::TYPE_APPLE,
Platform::TYPE_ANDROID,
Platform::TYPE_LINUX,
Platform::TYPE_WEB,
];
}
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Platform ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Platform creation date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Platform update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Platform name.',
'default' => '',
'example' => 'My Web App',
])
->addRule('type', [
'type' => self::TYPE_ENUM,
'description' => 'Platform type. Possible values are: ' . implode(', ', self::getSupportedTypes()) . '.',
'default' => '',
'example' => Platform::TYPE_WEB,
'enum' => self::getSupportedTypes(),
])
;
}
}
@@ -1,58 +0,0 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Network\Platform;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
class PlatformLinux extends PlatformBase
{
public function __construct()
{
$this->conditions = [
'type' => Platform::TYPE_LINUX,
];
parent::__construct();
$this
->addRule('packageName', [
'type' => self::TYPE_STRING,
'description' => 'Linux package name.',
'default' => '',
'example' => 'com.company.appname',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Platform Linux';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PLATFORM_LINUX;
}
public function filter(Document $document): Document
{
// DB level: 'key'
// API level: 'packageName'
$document->setAttribute('packageName', $document->getAttribute('key', null));
$document->removeAttribute('key');
return $document;
}
}
@@ -1,53 +0,0 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class PlatformList extends Model
{
public function __construct()
{
$this
->addRule('total', [
'type' => self::TYPE_INTEGER,
'description' => 'Total number of platforms in the given project.',
'default' => 0,
'example' => 5,
])
->addRule('platforms', [
'type' => [
Response::MODEL_PLATFORM_WEB,
Response::MODEL_PLATFORM_APPLE,
Response::MODEL_PLATFORM_ANDROID,
Response::MODEL_PLATFORM_WINDOWS,
Response::MODEL_PLATFORM_LINUX,
],
'description' => 'List of platforms.',
'default' => [],
'array' => true
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Platforms List';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PLATFORM_LIST;
}
}
@@ -1,71 +0,0 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Network\Platform;
use Appwrite\Utopia\Response;
class PlatformWeb extends PlatformBase
{
public function __construct()
{
$this->conditions = [
'type' => [
Platform::TYPE_WEB,
// Backwards compatibility
'flutter-web',
'unity',
'flutter-macos',
'flutter-ios',
'react-native-ios',
'apple-ios',
'apple-macos',
'apple-watchos',
'apple-tvos',
'flutter-android',
'react-native-android',
'flutter-windows',
'flutter-linux',
],
];
parent::__construct();
$this
->addRule('hostname', [
'type' => self::TYPE_STRING,
'description' => 'Web app hostname. Empty string for other platforms.',
'default' => '',
'example' => 'app.example.com',
])
// Backwards compatibility
->addRule('key', [
'hidden' => true,
'type' => self::TYPE_STRING,
'description' => 'Deprecated for old versions using alias endpoint to create universal platform.',
'default' => '',
'example' => 'com.company.appname',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Platform Web';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PLATFORM_WEB;
}
}
@@ -1,58 +0,0 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Network\Platform;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
class PlatformWindows extends PlatformBase
{
public function __construct()
{
$this->conditions = [
'type' => Platform::TYPE_WINDOWS,
];
parent::__construct();
$this
->addRule('packageIdentifierName', [
'type' => self::TYPE_STRING,
'description' => 'Windows package identifier name.',
'default' => '',
'example' => 'com.company.appname',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Platform Windows';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PLATFORM_WINDOWS;
}
public function filter(Document $document): Document
{
// DB level: 'key'
// API level: 'packageIdentifierName'
$document->setAttribute('packageIdentifierName', $document->getAttribute('key', null));
$document->removeAttribute('key');
return $document;
}
}
@@ -9,6 +9,11 @@ use Utopia\Database\Document;
class Project extends Model
{
/**
* @var bool
*/
protected bool $public = false;
public function __construct()
{
$this
@@ -195,13 +200,7 @@ class Project extends Model
'array' => true,
])
->addRule('platforms', [
'type' => [
Response::MODEL_PLATFORM_WEB,
Response::MODEL_PLATFORM_APPLE,
Response::MODEL_PLATFORM_ANDROID,
Response::MODEL_PLATFORM_WINDOWS,
Response::MODEL_PLATFORM_LINUX,
],
'type' => Response::MODEL_PLATFORM,
'description' => 'List of Platforms.',
'default' => [],
'example' => new \stdClass(),
@@ -7,6 +7,11 @@ use Appwrite\Utopia\Response\Model;
class Webhook extends Model
{
/**
* @var bool
*/
protected bool $public = true;
public function __construct()
{
$this
+22 -7
View File
@@ -219,8 +219,7 @@ class Client
curl_setopt($ch, CURLOPT_HTTPHEADER, $formattedHeaders);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
curl_setopt($ch, CURLOPT_COOKIEFILE, ''); // enable in-memory RFC 6265 cookie engine
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders, &$cookies) {
$len = strlen($header);
$header = explode(':', $header, 2);
@@ -228,6 +227,12 @@ class Client
return $len;
}
if (strtolower(trim($header[0])) == 'set-cookie') {
$parsed = $this->parseCookie((string)trim($header[1]));
$name = array_key_first($parsed);
$cookies[$name] = $parsed[$name];
}
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
return $len;
@@ -254,11 +259,6 @@ class Client
$responseType = $responseHeaders['content-type'] ?? '';
$responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
foreach (curl_getinfo($ch, CURLINFO_COOKIELIST) as $line) {
$parts = explode("\t", $line);
$cookies[$parts[5]] = $parts[6] ?? '';
}
if ($decode && $method !== self::METHOD_HEAD) {
$strpos = strpos($responseType, ';');
$strpos = \is_bool($strpos) ? \strlen($responseType) : $strpos;
@@ -309,6 +309,21 @@ class Client
];
}
/**
* Parse Cookie String
*
* @param string $cookie
* @return array
*/
public function parseCookie(string $cookie): array
{
$cookies = [];
parse_str(strtr($cookie, ['&' => '%26', '+' => '%2B', ';' => '&']), $cookies);
return $cookies;
}
/**
* Flatten params array to PHP multiple format
*
+2 -2
View File
@@ -1324,8 +1324,8 @@ class UsageTest extends Scope
$this->assertEquals($requestsTotal, $response['body']['requests'][array_key_last($response['body']['requests'])]['value']);
$this->validateDates($response['body']['requests']);
// vectordbTotal should reflect only VectorsDB instances, not relational databases.
$this->assertEquals($vectordbTotal, $response['body']['vectorsdbDatabasesTotal']);
$this->assertEquals($documentsTotal, $response['body']['vectorsdbDocumentsTotal']);
$this->assertEquals($vectordbTotal, $response['body']['vectordbDatabasesTotal']);
$this->assertEquals($documentsTotal, $response['body']['vectordbDocumentsTotal']);
});
$response = $this->client->call(
+1 -5
View File
@@ -164,11 +164,7 @@ trait ProjectCustom
'webhooks.read',
'webhooks.write',
'project.read',
'project.write',
'keys.read',
'keys.write',
'platforms.read',
'platforms.write',
'project.write'
],
]);
@@ -802,16 +802,6 @@ class AccountCustomClientTest extends Scope
$sessionId = $response['body']['$id'];
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
$accountResponse = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$this->assertEquals(200, $accountResponse['headers']['status-code']);
$this->assertEquals($email, $accountResponse['body']['email']);
// apiKey is only available in custom client test
$apiKey = $this->getProject()['apiKey'];
if (!empty($apiKey)) {
@@ -4160,178 +4150,4 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(401, $verification3['headers']['status-code']);
}
/**
* Test that a new email/password session is immediately usable even when
* a concurrent request re-populates the user cache between the cache purge
* and session creation.
*
* Regression test for: purging the user cache BEFORE persisting the session
* allows a concurrent request (from a different Swoole worker) to re-cache
* a stale user document that lacks the new session, causing sessionVerify
* to fail with 401 on subsequent requests using the new session.
*/
public function testEmailPasswordSessionNotCorruptedByConcurrentRequests(): void
{
$projectId = $this->getProject()['$id'];
$endpoint = $this->client->getEndpoint();
$email = uniqid('race_', true) . getmypid() . '@localhost.test';
$password = 'password123!';
// Create user
$response = $this->client->call(Client::METHOD_POST, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => 'Race Test User',
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Login to get session A
$responseA = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], [
'email' => $email,
'password' => $password,
]);
$this->assertEquals(201, $responseA['headers']['status-code']);
$sessionA = $responseA['cookies']['a_session_' . $projectId];
// Verify session A works
$verifyA = $this->client->call(Client::METHOD_GET, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => 'a_session_' . $projectId . '=' . $sessionA,
]);
$this->assertEquals(200, $verifyA['headers']['status-code']);
/**
* Race condition scenario:
* 1. Start login B via curl_multi (non-blocking)
* 2. Drive the transfer for ~150ms so login B reaches purgeCachedDocument
* (findOne ~15ms + Argon2 hash verify ~60ms + middleware overhead)
* 3. THEN add GET requests to curl_multi - these hit different workers and
* re-cache a stale user document (without session B) during the window
* between purgeCachedDocument and createDocument
* 4. After all complete, verify session B is usable
*/
for ($attempt = 0; $attempt < 5; $attempt++) {
$loginCookies = [];
$multi = curl_multi_init();
// Start login B first (alone)
$loginHandle = curl_init("{$endpoint}/account/sessions/email");
curl_setopt_array($loginHandle, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'origin: http://localhost',
'content-type: application/json',
"x-appwrite-project: {$projectId}",
],
CURLOPT_POSTFIELDS => \json_encode([
'email' => $email,
'password' => $password,
]),
CURLOPT_HEADERFUNCTION => function ($curl, $header) use (&$loginCookies) {
if (\stripos($header, 'set-cookie:') === 0) {
$cookiePart = \trim(\substr($header, 11));
$eqPos = \strpos($cookiePart, '=');
if ($eqPos !== false) {
$name = \substr($cookiePart, 0, $eqPos);
$rest = \substr($cookiePart, $eqPos + 1);
$semiPos = \strpos($rest, ';');
$loginCookies[$name] = $semiPos !== false
? \substr($rest, 0, $semiPos)
: $rest;
}
}
return \strlen($header);
},
]);
curl_multi_add_handle($multi, $loginHandle);
// Drive the login transfer forward and wait for the server to start
// processing the login (past hash verification + cache purge).
$deadline = \microtime(true) + 0.15; // 150ms
do {
curl_multi_exec($multi, $active);
curl_multi_select($multi, 0.005);
} while (\microtime(true) < $deadline && $active);
// NOW add GET requests - they arrive after the cache purge
// but before session creation (which is delayed by the usleep or I/O).
$getHandles = [];
for ($i = 0; $i < 10; $i++) {
$gh = curl_init("{$endpoint}/account");
curl_setopt_array($gh, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'origin: http://localhost',
'content-type: application/json',
"x-appwrite-project: {$projectId}",
"cookie: a_session_{$projectId}={$sessionA}",
],
]);
curl_multi_add_handle($multi, $gh);
$getHandles[] = $gh;
}
// Drive all to completion
do {
$status = curl_multi_exec($multi, $active);
if ($active) {
curl_multi_select($multi, 0.05);
}
} while ($active && $status === CURLM_OK);
$loginStatus = curl_getinfo($loginHandle, CURLINFO_HTTP_CODE);
curl_multi_remove_handle($multi, $loginHandle);
curl_close($loginHandle);
foreach ($getHandles as $gh) {
curl_multi_remove_handle($multi, $gh);
curl_close($gh);
}
curl_multi_close($multi);
$this->assertEquals(201, $loginStatus, 'Login for session B should succeed');
$sessionBCookie = $loginCookies["a_session_{$projectId}"] ?? null;
$this->assertNotNull($sessionBCookie, 'Session B cookie should be set');
// THE CRITICAL CHECK: verify session B is usable immediately
$verifyB = $this->client->call(Client::METHOD_GET, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => "a_session_{$projectId}={$sessionBCookie}",
]);
$this->assertEquals(
200,
$verifyB['headers']['status-code'],
'Session B must be immediately usable after login. '
. 'A 401 here means a stale user cache (without the new session) was served. '
. 'The fix is to create the session document BEFORE purging the user cache.'
);
// Clean up session B for next iteration
$this->client->call(Client::METHOD_DELETE, '/account/sessions/current', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => "a_session_{$projectId}={$sessionBCookie}",
]);
}
}
}
-806
View File
@@ -1,806 +0,0 @@
<?php
namespace Tests\E2E\Services\Project;
use Appwrite\Tests\Async;
use Tests\E2E\Client;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
trait KeysBase
{
use Async;
// =========================================================================
// Create key tests
// =========================================================================
public function testCreateKey(): void
{
$key = $this->createKey(
ID::unique(),
'My API Key',
['users.read', 'users.write'],
);
$this->assertSame(201, $key['headers']['status-code']);
$this->assertNotEmpty($key['body']['$id']);
$this->assertSame('My API Key', $key['body']['name']);
$this->assertSame(['users.read', 'users.write'], $key['body']['scopes']);
$this->assertNotEmpty($key['body']['secret']);
$this->assertSame('', $key['body']['expire']);
$this->assertSame('', $key['body']['accessedAt']);
$this->assertSame([], $key['body']['sdks']);
$dateValidator = new DatetimeValidator();
$this->assertSame(true, $dateValidator->isValid($key['body']['$createdAt']));
$this->assertSame(true, $dateValidator->isValid($key['body']['$updatedAt']));
// Verify via GET
$get = $this->getKey($key['body']['$id']);
$this->assertSame(200, $get['headers']['status-code']);
$this->assertSame($key['body']['$id'], $get['body']['$id']);
$this->assertSame('My API Key', $get['body']['name']);
$this->assertSame(['users.read', 'users.write'], $get['body']['scopes']);
// Verify via LIST
$list = $this->listKeys(null, true);
$this->assertSame(200, $list['headers']['status-code']);
$this->assertGreaterThanOrEqual(1, $list['body']['total']);
$this->assertGreaterThanOrEqual(1, \count($list['body']['keys']));
// Cleanup
$this->deleteKey($key['body']['$id']);
}
public function testCreateKeyWithExpire(): void
{
$expire = '2030-01-01T00:00:00.000+00:00';
$key = $this->createKey(
ID::unique(),
'Expiring Key',
['users.read'],
$expire,
);
$this->assertSame(201, $key['headers']['status-code']);
$this->assertSame($expire, $key['body']['expire']);
// Verify via GET
$get = $this->getKey($key['body']['$id']);
$this->assertSame(200, $get['headers']['status-code']);
$this->assertSame($expire, $get['body']['expire']);
// Cleanup
$this->deleteKey($key['body']['$id']);
}
public function testCreateKeyWithNullScopes(): void
{
$key = $this->createKey(
ID::unique(),
'Null Scopes Key',
null,
);
$this->assertSame(201, $key['headers']['status-code']);
$this->assertSame([], $key['body']['scopes']);
// Cleanup
$this->deleteKey($key['body']['$id']);
}
public function testCreateKeyWithoutAuthentication(): void
{
$response = $this->createKey(
ID::unique(),
'No Auth Key',
['users.read'],
null,
false
);
$this->assertSame(401, $response['headers']['status-code']);
}
public function testCreateKeyInvalidId(): void
{
$key = $this->createKey(
'!invalid-id!',
'Invalid ID Key',
['users.read'],
);
$this->assertSame(400, $key['headers']['status-code']);
}
public function testCreateKeyMissingName(): void
{
$response = $this->createKey(
ID::unique(),
null,
['users.read'],
);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testCreateKeyInvalidScope(): void
{
$response = $this->createKey(
ID::unique(),
'Invalid Scope Key',
['invalid.scope'],
);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testCreateKeyDuplicateId(): void
{
$keyId = ID::unique();
$key = $this->createKey(
$keyId,
'Key Dup 1',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
// Attempt to create with same ID
$duplicate = $this->createKey(
$keyId,
'Key Dup 2',
['users.write'],
);
$this->assertSame(409, $duplicate['headers']['status-code']);
$this->assertSame('key_already_exists', $duplicate['body']['type']);
// Cleanup
$this->deleteKey($keyId);
}
public function testCreateKeyCustomId(): void
{
$customId = 'my-custom-key-id';
$key = $this->createKey(
$customId,
'Custom ID Key',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
$this->assertSame($customId, $key['body']['$id']);
// Verify via GET
$get = $this->getKey($customId);
$this->assertSame(200, $get['headers']['status-code']);
$this->assertSame($customId, $get['body']['$id']);
// Cleanup
$this->deleteKey($customId);
}
// =========================================================================
// Update key tests
// =========================================================================
public function testUpdateKey(): void
{
$key = $this->createKey(
ID::unique(),
'Original Key',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
$keyId = $key['body']['$id'];
// Update name, scopes, and expire
$expire = '2031-06-15T12:00:00.000+00:00';
$updated = $this->updateKey($keyId, 'Updated Key', ['users.write', 'databases.read'], $expire);
$this->assertSame(200, $updated['headers']['status-code']);
$this->assertSame($keyId, $updated['body']['$id']);
$this->assertSame('Updated Key', $updated['body']['name']);
$this->assertSame(['users.write', 'databases.read'], $updated['body']['scopes']);
$this->assertSame($expire, $updated['body']['expire']);
// Verify update persisted via GET
$get = $this->getKey($keyId);
$this->assertSame(200, $get['headers']['status-code']);
$this->assertSame('Updated Key', $get['body']['name']);
$this->assertSame(['users.write', 'databases.read'], $get['body']['scopes']);
$this->assertSame($expire, $get['body']['expire']);
// Cleanup
$this->deleteKey($keyId);
}
public function testUpdateKeyName(): void
{
$key = $this->createKey(
ID::unique(),
'Name Before',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
$keyId = $key['body']['$id'];
$updated = $this->updateKey($keyId, 'Name After', ['users.read']);
$this->assertSame(200, $updated['headers']['status-code']);
$this->assertSame('Name After', $updated['body']['name']);
$this->assertSame(['users.read'], $updated['body']['scopes']);
// Cleanup
$this->deleteKey($keyId);
}
public function testUpdateKeyScopes(): void
{
$key = $this->createKey(
ID::unique(),
'Scopes Key',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
$keyId = $key['body']['$id'];
$updated = $this->updateKey($keyId, 'Scopes Key', ['databases.read', 'databases.write']);
$this->assertSame(200, $updated['headers']['status-code']);
$this->assertSame(['databases.read', 'databases.write'], $updated['body']['scopes']);
// Cleanup
$this->deleteKey($keyId);
}
public function testUpdateKeySetExpire(): void
{
$key = $this->createKey(
ID::unique(),
'No Expire Key',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
$this->assertSame('', $key['body']['expire']);
$keyId = $key['body']['$id'];
$expire = '2032-12-31T23:59:59.000+00:00';
$updated = $this->updateKey($keyId, 'No Expire Key', ['users.read'], $expire);
$this->assertSame(200, $updated['headers']['status-code']);
$this->assertSame($expire, $updated['body']['expire']);
// Cleanup
$this->deleteKey($keyId);
}
public function testUpdateKeyRemoveExpire(): void
{
$key = $this->createKey(
ID::unique(),
'Expire Key',
['users.read'],
'2030-01-01T00:00:00.000+00:00',
);
$this->assertSame(201, $key['headers']['status-code']);
$keyId = $key['body']['$id'];
// Remove expire by setting to null
$updated = $this->updateKey($keyId, 'Expire Key', ['users.read'], null);
$this->assertSame(200, $updated['headers']['status-code']);
$this->assertSame('', $updated['body']['expire']);
// Cleanup
$this->deleteKey($keyId);
}
public function testUpdateKeyWithoutAuthentication(): void
{
$key = $this->createKey(
ID::unique(),
'Auth Update Key',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
$keyId = $key['body']['$id'];
// Attempt update without authentication
$response = $this->updateKey($keyId, 'Updated Name', ['users.read'], null, false);
$this->assertSame(401, $response['headers']['status-code']);
// Cleanup
$this->deleteKey($keyId);
}
public function testUpdateKeyNotFound(): void
{
$updated = $this->updateKey('non-existent-id', 'New Name', ['users.read']);
$this->assertSame(404, $updated['headers']['status-code']);
$this->assertSame('key_not_found', $updated['body']['type']);
}
public function testUpdateKeyInvalidScope(): void
{
$key = $this->createKey(
ID::unique(),
'Invalid Scope Update',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
$keyId = $key['body']['$id'];
$updated = $this->updateKey($keyId, 'Invalid Scope Update', ['invalid.scope']);
$this->assertSame(400, $updated['headers']['status-code']);
// Cleanup
$this->deleteKey($keyId);
}
// =========================================================================
// Get key tests
// =========================================================================
public function testGetKey(): void
{
$key = $this->createKey(
ID::unique(),
'Get Test Key',
['users.read', 'databases.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
$keyId = $key['body']['$id'];
$get = $this->getKey($keyId);
$this->assertSame(200, $get['headers']['status-code']);
$this->assertSame($keyId, $get['body']['$id']);
$this->assertSame('Get Test Key', $get['body']['name']);
$this->assertSame(['users.read', 'databases.read'], $get['body']['scopes']);
$this->assertNotEmpty($get['body']['secret']);
$this->assertSame('', $get['body']['expire']);
$this->assertSame('', $get['body']['accessedAt']);
$this->assertSame([], $get['body']['sdks']);
$dateValidator = new DatetimeValidator();
$this->assertSame(true, $dateValidator->isValid($get['body']['$createdAt']));
$this->assertSame(true, $dateValidator->isValid($get['body']['$updatedAt']));
// Cleanup
$this->deleteKey($keyId);
}
public function testGetKeyNotFound(): void
{
$get = $this->getKey('non-existent-id');
$this->assertSame(404, $get['headers']['status-code']);
$this->assertSame('key_not_found', $get['body']['type']);
}
public function testGetKeyWithoutAuthentication(): void
{
$key = $this->createKey(
ID::unique(),
'Auth Get Key',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
$keyId = $key['body']['$id'];
// Attempt GET without authentication
$response = $this->getKey($keyId, false);
$this->assertSame(401, $response['headers']['status-code']);
// Cleanup
$this->deleteKey($keyId);
}
// =========================================================================
// List keys tests
// =========================================================================
public function testListKeys(): void
{
// Create multiple keys
$key1 = $this->createKey(
ID::unique(),
'List Key Alpha',
['users.read'],
);
$this->assertSame(201, $key1['headers']['status-code']);
$key2 = $this->createKey(
ID::unique(),
'List Key Beta',
['databases.read'],
);
$this->assertSame(201, $key2['headers']['status-code']);
$key3 = $this->createKey(
ID::unique(),
'List Key Gamma',
['users.write'],
);
$this->assertSame(201, $key3['headers']['status-code']);
// List all
$list = $this->listKeys(null, true);
$this->assertSame(200, $list['headers']['status-code']);
$this->assertGreaterThanOrEqual(3, $list['body']['total']);
$this->assertGreaterThanOrEqual(3, \count($list['body']['keys']));
$this->assertIsArray($list['body']['keys']);
// Verify structure of returned keys
foreach ($list['body']['keys'] as $key) {
$this->assertArrayHasKey('$id', $key);
$this->assertArrayHasKey('$createdAt', $key);
$this->assertArrayHasKey('$updatedAt', $key);
$this->assertArrayHasKey('name', $key);
$this->assertArrayHasKey('scopes', $key);
$this->assertArrayHasKey('secret', $key);
$this->assertArrayHasKey('expire', $key);
$this->assertArrayHasKey('accessedAt', $key);
$this->assertArrayHasKey('sdks', $key);
}
// Cleanup
$this->deleteKey($key1['body']['$id']);
$this->deleteKey($key2['body']['$id']);
$this->deleteKey($key3['body']['$id']);
}
public function testListKeysWithLimit(): void
{
$key1 = $this->createKey(
ID::unique(),
'Limit Key 1',
['users.read'],
);
$this->assertSame(201, $key1['headers']['status-code']);
$key2 = $this->createKey(
ID::unique(),
'Limit Key 2',
['users.write'],
);
$this->assertSame(201, $key2['headers']['status-code']);
// List with limit 1
$list = $this->listKeys([
Query::limit(1)->toString(),
], true);
$this->assertSame(200, $list['headers']['status-code']);
$this->assertCount(1, $list['body']['keys']);
$this->assertGreaterThanOrEqual(2, $list['body']['total']);
// Cleanup
$this->deleteKey($key1['body']['$id']);
$this->deleteKey($key2['body']['$id']);
}
public function testListKeysWithoutTotal(): void
{
$key = $this->createKey(
ID::unique(),
'No Total Key',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
// List with total=false
$list = $this->listKeys(null, false);
$this->assertSame(200, $list['headers']['status-code']);
$this->assertSame(0, $list['body']['total']);
$this->assertGreaterThanOrEqual(1, \count($list['body']['keys']));
// Cleanup
$this->deleteKey($key['body']['$id']);
}
public function testListKeysCursorPagination(): void
{
$key1 = $this->createKey(
ID::unique(),
'Cursor Key 1',
['users.read'],
);
$this->assertSame(201, $key1['headers']['status-code']);
$key2 = $this->createKey(
ID::unique(),
'Cursor Key 2',
['users.write'],
);
$this->assertSame(201, $key2['headers']['status-code']);
// Get first page with limit 1
$page1 = $this->listKeys([
Query::limit(1)->toString(),
], true);
$this->assertSame(200, $page1['headers']['status-code']);
$this->assertCount(1, $page1['body']['keys']);
$cursorId = $page1['body']['keys'][0]['$id'];
// Get next page using cursor
$page2 = $this->listKeys([
Query::limit(1)->toString(),
Query::cursorAfter(new Document(['$id' => $cursorId]))->toString(),
], true);
$this->assertSame(200, $page2['headers']['status-code']);
$this->assertCount(1, $page2['body']['keys']);
$this->assertNotEquals($cursorId, $page2['body']['keys'][0]['$id']);
// Cleanup
$this->deleteKey($key1['body']['$id']);
$this->deleteKey($key2['body']['$id']);
}
public function testListKeysWithoutAuthentication(): void
{
$response = $this->listKeys(null, null, false);
$this->assertSame(401, $response['headers']['status-code']);
}
public function testListKeysInvalidCursor(): void
{
$list = $this->listKeys([
Query::cursorAfter(new Document(['$id' => 'non-existent-id']))->toString(),
], true);
$this->assertSame(400, $list['headers']['status-code']);
}
// =========================================================================
// Delete key tests
// =========================================================================
public function testDeleteKey(): void
{
$key = $this->createKey(
ID::unique(),
'Delete Key',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
$keyId = $key['body']['$id'];
// Verify it exists
$get = $this->getKey($keyId);
$this->assertSame(200, $get['headers']['status-code']);
// Delete
$delete = $this->deleteKey($keyId);
$this->assertSame(204, $delete['headers']['status-code']);
$this->assertEmpty($delete['body']);
// Verify it no longer exists
$get = $this->getKey($keyId);
$this->assertSame(404, $get['headers']['status-code']);
$this->assertSame('key_not_found', $get['body']['type']);
}
public function testDeleteKeyNotFound(): void
{
$delete = $this->deleteKey('non-existent-id');
$this->assertSame(404, $delete['headers']['status-code']);
$this->assertSame('key_not_found', $delete['body']['type']);
}
public function testDeleteKeyWithoutAuthentication(): void
{
$key = $this->createKey(
ID::unique(),
'Delete Auth Key',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
$keyId = $key['body']['$id'];
// Attempt DELETE without authentication
$response = $this->deleteKey($keyId, false);
$this->assertSame(401, $response['headers']['status-code']);
// Verify it still exists
$get = $this->getKey($keyId);
$this->assertSame(200, $get['headers']['status-code']);
// Cleanup
$this->deleteKey($keyId);
}
public function testDeleteKeyRemovedFromList(): void
{
$key = $this->createKey(
ID::unique(),
'Delete List Key',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
$keyId = $key['body']['$id'];
// Get list count before delete
$listBefore = $this->listKeys(null, true);
$this->assertSame(200, $listBefore['headers']['status-code']);
$countBefore = $listBefore['body']['total'];
// Delete
$delete = $this->deleteKey($keyId);
$this->assertSame(204, $delete['headers']['status-code']);
// Get list count after delete
$listAfter = $this->listKeys(null, true);
$this->assertSame(200, $listAfter['headers']['status-code']);
$this->assertSame($countBefore - 1, $listAfter['body']['total']);
// Verify the deleted key is not in the list
$ids = \array_column($listAfter['body']['keys'], '$id');
$this->assertNotContains($keyId, $ids);
}
public function testDeleteKeyDoubleDelete(): void
{
$key = $this->createKey(
ID::unique(),
'Double Delete Key',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
$keyId = $key['body']['$id'];
// First delete succeeds
$delete = $this->deleteKey($keyId);
$this->assertSame(204, $delete['headers']['status-code']);
// Second delete returns 404
$delete = $this->deleteKey($keyId);
$this->assertSame(404, $delete['headers']['status-code']);
$this->assertSame('key_not_found', $delete['body']['type']);
}
// =========================================================================
// Helpers
// =========================================================================
/**
* @param array<string>|null $scopes
*/
protected function createKey(string $keyId, ?string $name, ?array $scopes = null, ?string $expire = null, bool $authenticated = true): mixed
{
$params = [
'keyId' => $keyId,
'scopes' => $scopes,
];
if ($name !== null) {
$params['name'] = $name;
}
if ($expire !== null) {
$params['expire'] = $expire;
}
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
];
if ($authenticated) {
$headers = array_merge($headers, $this->getHeaders());
}
return $this->client->call(Client::METHOD_POST, '/project/keys', $headers, $params);
}
/**
* @param array<string>|null $scopes
*/
protected function updateKey(string $keyId, ?string $name = null, ?array $scopes = null, ?string $expire = null, bool $authenticated = true): mixed
{
$params = [];
if ($name !== null) {
$params['name'] = $name;
}
if ($scopes !== null) {
$params['scopes'] = $scopes;
}
if ($expire !== null) {
$params['expire'] = $expire;
}
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
];
if ($authenticated) {
$headers = array_merge($headers, $this->getHeaders());
}
return $this->client->call(Client::METHOD_PUT, '/project/keys/' . $keyId, $headers, $params);
}
protected function getKey(string $keyId, bool $authenticated = true): mixed
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
];
if ($authenticated) {
$headers = array_merge($headers, $this->getHeaders());
}
return $this->client->call(Client::METHOD_GET, '/project/keys/' . $keyId, $headers);
}
/**
* @param array<string>|null $queries
*/
protected function listKeys(?array $queries, ?bool $total, bool $authenticated = true): mixed
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
];
if ($authenticated) {
$headers = array_merge($headers, $this->getHeaders());
}
return $this->client->call(Client::METHOD_GET, '/project/keys', $headers, [
'queries' => $queries,
'total' => $total,
]);
}
protected function deleteKey(string $keyId, bool $authenticated = true): mixed
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
];
if ($authenticated) {
$headers = array_merge($headers, $this->getHeaders());
}
return $this->client->call(Client::METHOD_DELETE, '/project/keys/' . $keyId, $headers);
}
}
@@ -1,14 +0,0 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
class KeysConsoleClientTest extends Scope
{
use KeysBase;
use ProjectCustom;
use SideConsole;
}
@@ -1,14 +0,0 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
class KeysCustomServerTest extends Scope
{
use KeysBase;
use ProjectCustom;
use SideServer;
}
-224
View File
@@ -1,224 +0,0 @@
<?php
namespace Tests\E2E\Services\Project;
trait LabelsBase
{
// Update labels tests
public function testUpdateLabels(): void
{
$response = $this->updateLabels(['frontend', 'backend']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['labels']);
$this->assertCount(2, $response['body']['labels']);
$this->assertContains('frontend', $response['body']['labels']);
$this->assertContains('backend', $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsReplace(): void
{
$response = $this->updateLabels(['alpha', 'beta']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(2, $response['body']['labels']);
$this->assertContains('alpha', $response['body']['labels']);
$this->assertContains('beta', $response['body']['labels']);
// Replace with new labels
$response = $this->updateLabels(['gamma', 'delta', 'epsilon']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(3, $response['body']['labels']);
$this->assertContains('gamma', $response['body']['labels']);
$this->assertContains('delta', $response['body']['labels']);
$this->assertContains('epsilon', $response['body']['labels']);
$this->assertNotContains('alpha', $response['body']['labels']);
$this->assertNotContains('beta', $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsEmpty(): void
{
// Set some labels first
$response = $this->updateLabels(['toRemove']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['labels']);
// Clear all labels
$response = $this->updateLabels([]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['labels']);
$this->assertCount(0, $response['body']['labels']);
}
public function testUpdateLabelsDeduplicated(): void
{
$response = $this->updateLabels(['duplicate', 'duplicate', 'unique']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(2, $response['body']['labels']);
$this->assertContains('duplicate', $response['body']['labels']);
$this->assertContains('unique', $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsSingleLabel(): void
{
$response = $this->updateLabels(['solo']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['labels']);
$this->assertContains('solo', $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsWithoutAuthentication(): void
{
$response = $this->updateLabels(['unauthorized'], false);
$this->assertSame(401, $response['headers']['status-code']);
}
public function testUpdateLabelsInvalidLabelTooLong(): void
{
$response = $this->updateLabels([str_repeat('a', 37)]);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsInvalidLabelCharacters(): void
{
$response = $this->updateLabels(['invalid-label!']);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsAlphanumericOnly(): void
{
$response = $this->updateLabels(['ABC123', 'lowercase', 'UPPERCASE', '0123456789']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(4, $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsMaxLength(): void
{
$label = str_repeat('a', 36);
$response = $this->updateLabels([$label]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['labels']);
$this->assertContains($label, $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsIdempotent(): void
{
$labels = ['stable', 'production'];
$first = $this->updateLabels($labels);
$this->assertSame(200, $first['headers']['status-code']);
$second = $this->updateLabels($labels);
$this->assertSame(200, $second['headers']['status-code']);
$this->assertSame($first['body']['labels'], $second['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsDeduplicatedOrder(): void
{
$response = $this->updateLabels(['b', 'a', 'b']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(2, $response['body']['labels']);
$this->assertSame('b', $response['body']['labels'][0]);
$this->assertSame('a', $response['body']['labels'][1]);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsInvalidHyphen(): void
{
$response = $this->updateLabels(['my-label']);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsInvalidUnderscore(): void
{
$response = $this->updateLabels(['my_label']);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsInvalidSpace(): void
{
$response = $this->updateLabels(['my label']);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsInvalidEmptyString(): void
{
$response = $this->updateLabels(['']);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsResponseModel(): void
{
$response = $this->updateLabels(['test']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertArrayHasKey('$id', $response['body']);
$this->assertArrayHasKey('name', $response['body']);
$this->assertArrayHasKey('labels', $response['body']);
$this->assertIsArray($response['body']['labels']);
$this->assertContains('test', $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
// Helpers
/**
* @param array<string> $labels
*/
protected function updateLabels(array $labels, bool $authenticated = true): mixed
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
];
if ($authenticated) {
$headers = array_merge($headers, $this->getHeaders());
}
return $this->client->call(\Tests\E2E\Client::METHOD_PUT, '/project/labels', $headers, [
'labels' => $labels,
]);
}
}
@@ -1,14 +0,0 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
class LabelsConsoleClientTest extends Scope
{
use LabelsBase;
use ProjectCustom;
use SideConsole;
}
@@ -1,14 +0,0 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
class LabelsCustomServerTest extends Scope
{
use LabelsBase;
use ProjectCustom;
use SideServer;
}

Some files were not shown because too many files have changed in this diff Show More