Merge pull request #8741 from appwrite/feat-adding-coroutines

Feat adding coroutines
This commit is contained in:
Christy Jacob 2024-10-08 07:35:38 +04:00 committed by GitHub
commit 0c3fed9ef1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
111 changed files with 5779 additions and 5124 deletions

16
.github/workflows/codeql-phpstan.yml vendored Normal file
View file

@ -0,0 +1,16 @@
name: "CodeQL"
on: [pull_request]
jobs:
lint:
name: CodeQL
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Run CodeQL
run: |
docker run --rm -v $PWD:/app composer sh -c \
"composer install --profile --ignore-platform-reqs && composer check"

View file

@ -16,22 +16,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v2
- name: Build Appwrite
uses: docker/build-push-action@v6
uses: docker/build-push-action@v3
with:
context: .
push: false
tags: ${{ env.IMAGE }}
load: true
cache-from: type=gha,scope=appwrite
cache-to: type=gha,mode=max,scope=appwrite
cache-from: type=gha
cache-to: type=gha,mode=max
outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar
build-args: |
DEBUG=false
@ -39,13 +39,12 @@ jobs:
VERSION=dev
- name: Cache Docker Image
uses: actions/cache@v4
uses: actions/cache@v3
with:
key: ${{ env.CACHE_KEY }}
restore-keys: |
appwrite-dev-
path: /tmp/${{ env.IMAGE }}.tar
unit_test:
name: Unit Test
runs-on: ubuntu-latest
@ -53,10 +52,10 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Load Cache
uses: actions/cache@v4
uses: actions/cache@v3
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
@ -83,10 +82,10 @@ jobs:
needs: setup
steps:
- name: checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Load Cache
uses: actions/cache@v4
uses: actions/cache@v3
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
@ -131,10 +130,10 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Load Cache
uses: actions/cache@v4
uses: actions/cache@v3
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
@ -158,9 +157,9 @@ jobs:
needs: setup
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Load Cache
uses: actions/cache@v4
uses: actions/cache@v3
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar

View file

@ -7,214 +7,134 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Func;
use Appwrite\Platform\Appwrite;
use Appwrite\Runtimes\Runtimes;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\CLI\CLI;
use Swoole\Runtime;
use Utopia\CLI\Adapters\Swoole as SwooleCLI;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\DI\Dependency;
use Utopia\Logger\Log;
use Utopia\Platform\Service;
use Utopia\Pools\Group;
use Utopia\Queue\Connection;
use Utopia\Registry\Registry;
use Utopia\System\System;
global $registry, $container;
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
// overwriting runtimes to be architectur agnostic for CLI
Config::setParam('runtimes', (new Runtimes('v4'))->getAll(supported: false));
// require controllers after overwriting runtimes
require_once __DIR__ . '/controllers/general.php';
Authorization::disable();
/**
* @var Registry $registry
* @var Container $container
*/
$context = new Dependency();
$register = new Dependency();
$logError = new Dependency();
$queueForDeletes = new Dependency();
$queueForFunctions = new Dependency();
$queueForCertificates = new Dependency();
CLI::setResource('register', fn () => $register);
$context
->setName('context')
->setCallback(fn () => $container);
CLI::setResource('cache', function ($pools) {
$list = Config::getParam('pools-cache', []);
$adapters = [];
$register
->setName('register')
->setCallback(function () use (&$registry): Registry {
return $registry;
});
foreach ($list as $value) {
$adapters[] = $pools
->get($value)
->pop()
->getResource()
;
}
$queueForFunctions
->setName('queueForFunctions')
->inject('queue')
->setCallback(function (Connection $queue) {
return new Func($queue);
});
return new Cache(new Sharding($adapters));
}, ['pools']);
CLI::setResource('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
$queueForDeletes
->setName('queueForDeletes')
->inject('queue')
->setCallback(function (Connection $queue) {
return new Delete($queue);
});
CLI::setResource('dbForConsole', function ($pools, $cache) {
$sleep = 3;
$maxAttempts = 5;
$attempts = 0;
$ready = false;
$queueForCertificates
->setName('queueForCertificates')
->inject('queue')
->setCallback(function (Connection $queue) {
return new Certificate($queue);
});
do {
$attempts++;
try {
// Prepare database connection
$dbAdapter = $pools
->get('console')
->pop()
->getResource();
$logError
->setName('logError')
->inject('register')
->setCallback(function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
$logger = $register->get('logger');
$dbForConsole = new Database($dbAdapter, $cache);
if ($logger) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$dbForConsole
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console');
$log = new Log();
$log->setNamespace($namespace);
$log->setServer(\gethostname());
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
// Ensure tables exist
$collections = Config::getParam('collections', [])['console'];
$last = \array_key_last($collections);
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', get_class($error));
if (!($dbForConsole->exists($dbForConsole->getDatabase(), $last))) { /** TODO cache ready variable using registry */
throw new Exception('Tables not ready yet.');
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('trace', $error->getTraceAsString());
$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());
}
}
$ready = true;
} catch (\Throwable $err) {
Console::warning($err->getMessage());
$pools->get('console')->reclaim();
sleep($sleep);
}
} while ($attempts < $maxAttempts && !$ready);
Console::warning("Failed: {$error->getMessage()}");
Console::warning($error->getTraceAsString());
};
});
if (!$ready) {
throw new Exception("Console is not ready yet. Please try again later.");
}
return $dbForConsole;
}, ['pools', 'cache']);
CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
}
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()];
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getInternalId());
}
return $database;
}
$dbAdapter = $pools
->get($dsn->getHost())
->pop()
->getResource();
$database = new Database($dbAdapter, $cache);
$databases[$dsn->getHost()] = $database;
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getInternalId());
}
$database
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId());
return $database;
};
}, ['pools', 'dbForConsole', 'cache']);
CLI::setResource('queue', function (Group $pools) {
return $pools->get('queue')->pop()->getResource();
}, ['pools']);
CLI::setResource('queueForFunctions', function (Connection $queue) {
return new Func($queue);
}, ['queue']);
CLI::setResource('queueForDeletes', function (Connection $queue) {
return new Delete($queue);
}, ['queue']);
CLI::setResource('queueForCertificates', function (Connection $queue) {
return new Certificate($queue);
}, ['queue']);
CLI::setResource('logError', function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
$logger = $register->get('logger');
if ($logger) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
$log->setNamespace($namespace);
$log->setServer(\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->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$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());
};
}, ['register']);
$container->set($context);
$container->set($logError);
$container->set($register);
$container->set($queueForDeletes);
$container->set($queueForFunctions);
$container->set($queueForCertificates);
$platform = new Appwrite();
$platform->init(Service::TYPE_TASK);
$platform->init(Service::TYPE_TASK, ['adapter' => new SwooleCLI(1)]);
$cli = $platform->getCli();
$cli
->init()
->inject('authorization')
->action(function (Authorization $authorization) {
$authorization->disable();
});
$cli
->error()
->inject('error')
@ -222,4 +142,6 @@ $cli
Console::error($error->getMessage());
});
$cli->run();
$cli
->setContainer($container)
->run();

View file

@ -254,7 +254,7 @@ return [
'name' => '_APP_WORKER_PER_CORE',
'description' => 'Internal Worker per core for the API, Realtime and Executor containers. Can be configured to optimize performance.',
'introduction' => '0.13.0',
'default' => 6,
'default' => 2,
'required' => false,
'question' => '',
'filter' => ''

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@ use Appwrite\URL\URL as URLParse;
use Appwrite\Utopia\Response;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@ -14,15 +14,17 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Utopia\Fetch\Client;
use Utopia\Http\Http;
use Utopia\Http\Validator\Boolean;
use Utopia\Http\Validator\HexColor;
use Utopia\Http\Validator\Range;
use Utopia\Http\Validator\Text;
use Utopia\Http\Validator\URL;
use Utopia\Http\Validator\WhiteList;
use Utopia\Image\Image;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\HexColor;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
$avatarCallback = function (string $type, string $code, int $width, int $height, int $quality, Response $response) {
@ -61,9 +63,9 @@ $avatarCallback = function (string $type, string $code, int $width, int $height,
unset($image);
};
$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForConsole, ?Logger $logger) {
$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForConsole, ?Logger $logger, Authorization $auth) {
try {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
$user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId));
$sessions = $user->getAttribute('sessions', []);
@ -114,7 +116,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry('')));
Authorization::skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession));
$auth->skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession));
$dbForProject->purgeCachedDocument('users', $user->getId());
} catch (Throwable $err) {
@ -122,7 +124,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro
do {
$previousAccessToken = $gitHubSession->getAttribute('providerAccessToken');
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
$user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId));
$sessions = $user->getAttribute('sessions', []);
$gitHubSession = new Document();
@ -154,11 +156,42 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro
'id' => $githubId
];
} catch (Exception $error) {
if ($logger) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
$log->setNamespace('console');
$log->setServer(\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->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('detailedTrace', $error->getTrace());
$log->setAction('avatarsGetGitHub');
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
$responseCode = $logger->addLog($log);
Console::info('GitHub error log pushed with status code: ' . $responseCode);
}
Console::warning("Failed: {$error->getMessage()}");
Console::warning($error->getTraceAsString());
return [];
}
};
App::get('/v1/avatars/credit-cards/:code')
Http::get('/v1/avatars/credit-cards/:code')
->desc('Get credit card icon')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
@ -178,7 +211,7 @@ App::get('/v1/avatars/credit-cards/:code')
->inject('response')
->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('credit-cards', $code, $width, $height, $quality, $response));
App::get('/v1/avatars/browsers/:code')
Http::get('/v1/avatars/browsers/:code')
->desc('Get browser icon')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
@ -198,7 +231,7 @@ App::get('/v1/avatars/browsers/:code')
->inject('response')
->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('browsers', $code, $width, $height, $quality, $response));
App::get('/v1/avatars/flags/:code')
Http::get('/v1/avatars/flags/:code')
->desc('Get country flag')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
@ -218,7 +251,7 @@ App::get('/v1/avatars/flags/:code')
->inject('response')
->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('flags', $code, $width, $height, $quality, $response));
App::get('/v1/avatars/image')
Http::get('/v1/avatars/image')
->desc('Get image from URL')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
@ -281,7 +314,7 @@ App::get('/v1/avatars/image')
unset($image);
});
App::get('/v1/avatars/favicon')
Http::get('/v1/avatars/favicon')
->desc('Get favicon')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
@ -426,7 +459,7 @@ App::get('/v1/avatars/favicon')
unset($image);
});
App::get('/v1/avatars/qr')
Http::get('/v1/avatars/qr')
->desc('Get QR code')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
@ -466,7 +499,7 @@ App::get('/v1/avatars/qr')
->send($image->output('png', 9));
});
App::get('/v1/avatars/initials')
Http::get('/v1/avatars/initials')
->desc('Get user initials')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
@ -549,8 +582,8 @@ App::get('/v1/avatars/initials')
->file($image->getImageBlob());
});
App::get('/v1/cards/cloud')
->desc('Get front Of Cloud Card')
Http::get('/v1/cards/cloud')
->desc('Get Front Of Cloud Card')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
@ -571,8 +604,9 @@ App::get('/v1/cards/cloud')
->inject('contributors')
->inject('employees')
->inject('logger')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
->inject('authorization')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) use ($getUserGitHub) {
$user = $authorization->skip(fn () => $dbForConsole->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
@ -583,7 +617,7 @@ App::get('/v1/cards/cloud')
$email = $user->getAttribute('email', '');
$createdAt = new \DateTime($user->getCreatedAt());
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger);
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $authorization);
$githubName = $gitHub['name'] ?? '';
$githubId = $gitHub['id'] ?? '';
@ -756,8 +790,8 @@ App::get('/v1/cards/cloud')
->file($baseImage->getImageBlob());
});
App::get('/v1/cards/cloud-back')
->desc('Get back Of Cloud Card')
Http::get('/v1/cards/cloud-back')
->desc('Get Back Of Cloud Card')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
@ -778,8 +812,9 @@ App::get('/v1/cards/cloud-back')
->inject('contributors')
->inject('employees')
->inject('logger')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
->inject('authorization')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) use ($getUserGitHub) {
$user = $authorization->skip(fn () => $dbForConsole->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
@ -789,7 +824,7 @@ App::get('/v1/cards/cloud-back')
$userId = $user->getId();
$email = $user->getAttribute('email', '');
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger);
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $authorization);
$githubId = $gitHub['id'] ?? '';
$isHero = \array_key_exists($email, $heroes);
@ -834,8 +869,8 @@ App::get('/v1/cards/cloud-back')
->file($baseImage->getImageBlob());
});
App::get('/v1/cards/cloud-og')
->desc('Get OG image From Cloud Card')
Http::get('/v1/cards/cloud-og')
->desc('Get OG Image From Cloud Card')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
@ -856,8 +891,9 @@ App::get('/v1/cards/cloud-og')
->inject('contributors')
->inject('employees')
->inject('logger')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
->inject('authorization')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) use ($getUserGitHub) {
$user = $authorization->skip(fn () => $dbForConsole->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
@ -872,7 +908,7 @@ App::get('/v1/cards/cloud-og')
$email = $user->getAttribute('email', '');
$createdAt = new \DateTime($user->getCreatedAt());
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger);
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $authorization);
$githubName = $gitHub['name'] ?? '';
$githubId = $gitHub['id'] ?? '';

View file

@ -2,12 +2,12 @@
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Document;
use Utopia\Http\Http;
use Utopia\Http\Validator\Text;
use Utopia\System\System;
use Utopia\Validator\Text;
App::init()
Http::init()
->groups(['console'])
->inject('project')
->action(function (Document $project) {
@ -17,7 +17,7 @@ App::init()
});
App::get('/v1/console/variables')
Http::get('/v1/console/variables')
->desc('Get variables')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
@ -56,8 +56,8 @@ App::get('/v1/console/variables')
$response->dynamic($variables, Response::MODEL_CONSOLE_VARIABLES);
});
App::post('/v1/console/assistant')
->desc('Ask query')
Http::post('/v1/console/assistant')
->desc('Ask Query')
->groups(['api', 'assistant'])
->label('scope', 'assistant.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Authentication;
use Appwrite\Event\Build;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
@ -20,11 +21,11 @@ use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Deployments;
use Appwrite\Utopia\Database\Validator\Queries\Executions;
use Appwrite\Utopia\Database\Validator\Queries\Functions;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Rule;
use Executor\Executor;
use MaxMind\Db\Reader;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
@ -37,24 +38,25 @@ use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Roles;
use Utopia\Database\Validator\UID;
use Utopia\Http\Http;
use Utopia\Http\Validator\AnyOf;
use Utopia\Http\Validator\ArrayList;
use Utopia\Http\Validator\Assoc;
use Utopia\Http\Validator\Boolean;
use Utopia\Http\Validator\Nullable;
use Utopia\Http\Validator\Range;
use Utopia\Http\Validator\Text;
use Utopia\Http\Validator\WhiteList;
use Utopia\Storage\Device;
use Utopia\Storage\Validator\File;
use Utopia\Storage\Validator\FileExt;
use Utopia\Storage\Validator\FileSize;
use Utopia\Storage\Validator\Upload;
use Utopia\Swoole\Request;
use Utopia\System\System;
use Utopia\Validator\AnyOf;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
@ -133,7 +135,7 @@ $redeployVcs = function (Request $request, Document $function, Document $project
->setTemplate($template);
};
App::post('/v1/functions')
Http::post('/v1/functions')
->groups(['api', 'functions'])
->desc('Create function')
->label('scope', 'functions.write')
@ -171,8 +173,8 @@ App::post('/v1/functions')
->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification(
$plan,
Config::getParam('runtime-specifications', []),
App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT),
App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT)
System::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT),
System::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT)
), 'Runtime specification for the function and builds.', true, ['plan'])
->inject('request')
->inject('response')
@ -183,7 +185,8 @@ App::post('/v1/functions')
->inject('queueForBuilds')
->inject('dbForConsole')
->inject('gitHub')
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
->inject('authorization')
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github, Authorization $authorization) use ($redeployVcs) {
$functionId = ($functionId == 'unique()') ? ID::unique() : $functionId;
$allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', '')));
@ -247,7 +250,7 @@ App::post('/v1/functions')
'specification' => $specification
]));
$schedule = Authorization::skip(
$schedule = $authorization->skip(
fn () => $dbForConsole->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'), // Todo replace with projects region
'resourceType' => 'function',
@ -329,7 +332,7 @@ App::post('/v1/functions')
$routeSubdomain = ID::unique();
$domain = "{$routeSubdomain}.{$functionsDomain}";
$rule = Authorization::skip(
$rule = $authorization->skip(
fn () => $dbForConsole->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
@ -396,7 +399,7 @@ App::post('/v1/functions')
->dynamic($function, Response::MODEL_FUNCTION);
});
App::get('/v1/functions')
Http::get('/v1/functions')
->groups(['api', 'functions'])
->desc('List functions')
->label('scope', 'functions.read')
@ -450,7 +453,7 @@ App::get('/v1/functions')
]), Response::MODEL_FUNCTION_LIST);
});
App::get('/v1/functions/runtimes')
Http::get('/v1/functions/runtimes')
->groups(['api', 'functions'])
->desc('List runtimes')
->label('scope', 'functions.read')
@ -483,7 +486,7 @@ App::get('/v1/functions/runtimes')
]), Response::MODEL_RUNTIME_LIST);
});
App::get('/v1/functions/specifications')
Http::get('/v1/functions/specifications')
->groups(['api', 'functions'])
->desc('List available function runtime specifications')
->label('scope', 'functions.read')
@ -519,7 +522,7 @@ App::get('/v1/functions/specifications')
]), Response::MODEL_SPECIFICATION_LIST);
});
App::get('/v1/functions/:functionId')
Http::get('/v1/functions/:functionId')
->groups(['api', 'functions'])
->desc('Get function')
->label('scope', 'functions.read')
@ -543,7 +546,7 @@ App::get('/v1/functions/:functionId')
$response->dynamic($function, Response::MODEL_FUNCTION);
});
App::get('/v1/functions/:functionId/usage')
Http::get('/v1/functions/:functionId/usage')
->desc('Get function usage')
->groups(['api', 'functions', 'usage'])
->label('scope', 'functions.read')
@ -557,7 +560,8 @@ App::get('/v1/functions/:functionId/usage')
->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $functionId, string $range, Response $response, Database $dbForProject) {
->inject('authorization')
->action(function (string $functionId, string $range, Response $response, Database $dbForProject, Authorization $authorization) {
$function = $dbForProject->getDocument('functions', $functionId);
@ -580,7 +584,7 @@ App::get('/v1/functions/:functionId/usage')
str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS)
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
$authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
@ -647,7 +651,7 @@ App::get('/v1/functions/:functionId/usage')
]), Response::MODEL_USAGE_FUNCTION);
});
App::get('/v1/functions/usage')
Http::get('/v1/functions/usage')
->desc('Get functions usage')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
@ -660,7 +664,8 @@ App::get('/v1/functions/usage')
->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $range, Response $response, Database $dbForProject) {
->inject('authorization')
->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) {
$periods = Config::getParam('usage', []);
$stats = $usage = [];
@ -678,7 +683,7 @@ App::get('/v1/functions/usage')
METRIC_EXECUTIONS_MB_SECONDS,
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
$authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
@ -746,7 +751,7 @@ App::get('/v1/functions/usage')
]), Response::MODEL_USAGE_FUNCTIONS);
});
App::put('/v1/functions/:functionId')
Http::put('/v1/functions/:functionId')
->groups(['api', 'functions'])
->desc('Update function')
->label('scope', 'functions.write')
@ -780,8 +785,8 @@ App::put('/v1/functions/:functionId')
->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification(
$plan,
Config::getParam('runtime-specifications', []),
App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT),
App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT)
System::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT),
System::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT)
), 'Runtime specification for the function and builds.', true, ['plan'])
->inject('request')
->inject('response')
@ -791,7 +796,8 @@ App::put('/v1/functions/:functionId')
->inject('queueForBuilds')
->inject('dbForConsole')
->inject('gitHub')
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
->inject('authorization')
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github, Authorization $authorization) use ($redeployVcs) {
// TODO: If only branch changes, re-deploy
$function = $dbForProject->getDocument('functions', $functionId);
@ -894,7 +900,7 @@ App::put('/v1/functions/:functionId')
// Enforce Cold Start if spec limits change.
if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deployment'))) {
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
try {
$executor->deleteRuntime($project->getId(), $function->getAttribute('deployment'));
} catch (\Throwable $th) {
@ -941,14 +947,14 @@ App::put('/v1/functions/:functionId')
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
$authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForEvents->setParam('functionId', $function->getId());
$response->dynamic($function, Response::MODEL_FUNCTION);
});
App::get('/v1/functions/:functionId/deployments/:deploymentId/download')
Http::get('/v1/functions/:functionId/deployments/:deploymentId/download')
->groups(['api', 'functions'])
->desc('Download deployment')
->label('scope', 'functions.read')
@ -1033,7 +1039,7 @@ App::get('/v1/functions/:functionId/deployments/:deploymentId/download')
}
});
App::patch('/v1/functions/:functionId/deployments/:deploymentId')
Http::patch('/v1/functions/:functionId/deployments/:deploymentId')
->groups(['api', 'functions'])
->desc('Update deployment')
->label('scope', 'functions.write')
@ -1053,7 +1059,8 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId')
->inject('dbForProject')
->inject('queueForEvents')
->inject('dbForConsole')
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForConsole) {
->inject('authorization')
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForConsole, Authorization $authorization) {
$function = $dbForProject->getDocument('functions', $functionId);
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
@ -1086,7 +1093,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId')
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
$authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForEvents
->setParam('functionId', $function->getId())
@ -1095,7 +1102,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId')
$response->dynamic($function, Response::MODEL_FUNCTION);
});
App::delete('/v1/functions/:functionId')
Http::delete('/v1/functions/:functionId')
->groups(['api', 'functions'])
->desc('Delete function')
->label('scope', 'functions.write')
@ -1114,7 +1121,8 @@ App::delete('/v1/functions/:functionId')
->inject('queueForDeletes')
->inject('queueForEvents')
->inject('dbForConsole')
->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForConsole) {
->inject('authorization')
->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForConsole, Authorization $authorization) {
$function = $dbForProject->getDocument('functions', $functionId);
@ -1131,7 +1139,7 @@ App::delete('/v1/functions/:functionId')
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('active', false);
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
$authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
@ -1142,7 +1150,7 @@ App::delete('/v1/functions/:functionId')
$response->noContent();
});
App::post('/v1/functions/:functionId/deployments')
Http::post('/v1/functions/:functionId/deployments')
->groups(['api', 'functions'])
->desc('Create deployment')
->label('scope', 'functions.write')
@ -1361,7 +1369,7 @@ App::post('/v1/functions/:functionId/deployments')
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
});
App::get('/v1/functions/:functionId/deployments')
Http::get('/v1/functions/:functionId/deployments')
->groups(['api', 'functions'])
->desc('List deployments')
->label('scope', 'functions.read')
@ -1438,7 +1446,7 @@ App::get('/v1/functions/:functionId/deployments')
]), Response::MODEL_DEPLOYMENT_LIST);
});
App::get('/v1/functions/:functionId/deployments/:deploymentId')
Http::get('/v1/functions/:functionId/deployments/:deploymentId')
->groups(['api', 'functions'])
->desc('Get deployment')
->label('scope', 'functions.read')
@ -1481,7 +1489,7 @@ App::get('/v1/functions/:functionId/deployments/:deploymentId')
$response->dynamic($deployment, Response::MODEL_DEPLOYMENT);
});
App::delete('/v1/functions/:functionId/deployments/:deploymentId')
Http::delete('/v1/functions/:functionId/deployments/:deploymentId')
->groups(['api', 'functions'])
->desc('Delete deployment')
->label('scope', 'functions.write')
@ -1545,7 +1553,7 @@ App::delete('/v1/functions/:functionId/deployments/:deploymentId')
$response->noContent();
});
App::post('/v1/functions/:functionId/deployments/:deploymentId/build')
Http::post('/v1/functions/:functionId/deployments/:deploymentId/build')
->alias('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
->groups(['api', 'functions'])
->desc('Rebuild deployment')
@ -1568,7 +1576,8 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/build')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('deviceForFunctions')
->action(function (string $functionId, string $deploymentId, string $buildId, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions) {
->inject('authorization')
->action(function (string $functionId, string $deploymentId, string $buildId, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions, Authorization $authorization) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@ -1614,7 +1623,7 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/build')
$response->noContent();
});
App::patch('/v1/functions/:functionId/deployments/:deploymentId/build')
Http::patch('/v1/functions/:functionId/deployments/:deploymentId/build')
->groups(['api', 'functions'])
->desc('Cancel deployment')
->label('scope', 'functions.write')
@ -1632,7 +1641,8 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId/build')
->inject('dbForProject')
->inject('project')
->inject('queueForEvents')
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents) {
->inject('authorization')
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Authorization $authorization) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@ -1645,7 +1655,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId/build')
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
$build = $authorization->skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
if ($build->isEmpty()) {
$buildId = ID::unique();
@ -1687,7 +1697,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId/build')
$dbForProject->purgeCachedDocument('deployments', $deployment->getId());
try {
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
$executor->deleteRuntime($project->getId(), $deploymentId . "-build");
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
@ -1703,7 +1713,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId/build')
$response->dynamic($build, Response::MODEL_BUILD);
});
App::post('/v1/functions/:functionId/executions')
Http::post('/v1/functions/:functionId/executions')
->groups(['api', 'functions'])
->desc('Create execution')
->label('scope', 'execution.write')
@ -1733,7 +1743,9 @@ App::post('/v1/functions/:functionId/executions')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geodb')
->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForConsole, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb) {
->inject('authorization')
->inject('authentication')
->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForConsole, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, Authorization $authorization, Authentication $authentication) {
$async = \strval($async) === 'true' || \strval($async) === '1';
if (!$async && !is_null($scheduledAt)) {
@ -1763,10 +1775,10 @@ App::post('/v1/functions/:functionId/executions')
throw new Exception($validator->getDescription(), 400);
}
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
$function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
@ -1782,7 +1794,7 @@ App::post('/v1/functions/:functionId/executions')
throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
}
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', '')));
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', '')));
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function');
@ -1793,7 +1805,7 @@ App::post('/v1/functions/:functionId/executions')
}
/** Check if build has completed */
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
$build = $authorization->skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
if ($build->isEmpty()) {
throw new Exception(Exception::BUILD_NOT_FOUND);
}
@ -1802,10 +1814,8 @@ App::post('/v1/functions/:functionId/executions')
throw new Exception(Exception::BUILD_NOT_READY);
}
$validator = new Authorization('execute');
if (!$validator->isValid($function->getAttribute('execute'))) { // Check if user has write access to execute function
throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription());
if (!$authorization->isValid(new Input('execute', $function->getAttribute('execute')))) { // Check if user has write access to execute function
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
$jwt = ''; // initialize
@ -1815,7 +1825,7 @@ App::post('/v1/functions/:functionId/executions')
foreach ($sessions as $session) {
/** @var Utopia\Database\Document $session */
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
if ($session->getAttribute('secret') == Auth::hash($authentication->getSecret())) { // If current session delete the cookies too
$current = $session;
}
}
@ -1899,8 +1909,9 @@ App::post('/v1/functions/:functionId/executions')
->setContext('function', $function);
if ($async) {
if (is_null($scheduledAt)) {
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
$queueForFunctions
->setType('http')
->setExecution($execution)
@ -1941,7 +1952,7 @@ App::post('/v1/functions/:functionId/executions')
->setAttribute('scheduleInternalId', $schedule->getInternalId())
->setAttribute('scheduledAt', $scheduledAt);
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
return $response
@ -2027,8 +2038,7 @@ App::post('/v1/functions/:functionId/executions')
runtimeEntrypoint: $command,
cpus: $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT,
logging: $function->getAttribute('logging', true),
requestTimeout: 30
logging: $function->getAttribute('logging', true)
);
$headersFiltered = [];
@ -2069,10 +2079,10 @@ App::post('/v1/functions/:functionId/executions')
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
;
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
@ -2105,7 +2115,7 @@ App::post('/v1/functions/:functionId/executions')
->dynamic($execution, Response::MODEL_EXECUTION);
});
App::get('/v1/functions/:functionId/executions')
Http::get('/v1/functions/:functionId/executions')
->groups(['api', 'functions'])
->desc('List executions')
->label('scope', 'execution.read')
@ -2122,11 +2132,12 @@ App::get('/v1/functions/:functionId/executions')
->inject('response')
->inject('dbForProject')
->inject('mode')
->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) {
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
->inject('authorization')
->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject, string $mode, Authorization $authorization) {
$function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
@ -2169,7 +2180,7 @@ App::get('/v1/functions/:functionId/executions')
$results = $dbForProject->find('executions', $queries);
$total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT);
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
if (!$isPrivilegedUser && !$isAppUser) {
@ -2186,7 +2197,7 @@ App::get('/v1/functions/:functionId/executions')
]), Response::MODEL_EXECUTION_LIST);
});
App::get('/v1/functions/:functionId/executions/:executionId')
Http::get('/v1/functions/:functionId/executions/:executionId')
->groups(['api', 'functions'])
->desc('Get execution')
->label('scope', 'execution.read')
@ -2202,11 +2213,12 @@ App::get('/v1/functions/:functionId/executions/:executionId')
->inject('response')
->inject('dbForProject')
->inject('mode')
->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, string $mode) {
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
->inject('authorization')
->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, string $mode, Authorization $authorization) {
$function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
@ -2222,7 +2234,7 @@ App::get('/v1/functions/:functionId/executions/:executionId')
throw new Exception(Exception::EXECUTION_NOT_FOUND);
}
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
if (!$isPrivilegedUser && !$isAppUser) {
@ -2233,7 +2245,7 @@ App::get('/v1/functions/:functionId/executions/:executionId')
$response->dynamic($execution, Response::MODEL_EXECUTION);
});
App::delete('/v1/functions/:functionId/executions/:executionId')
Http::delete('/v1/functions/:functionId/executions/:executionId')
->groups(['api', 'functions'])
->desc('Delete execution')
->label('scope', 'execution.write')
@ -2252,7 +2264,8 @@ App::delete('/v1/functions/:functionId/executions/:executionId')
->inject('dbForProject')
->inject('dbForConsole')
->inject('queueForEvents')
->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForConsole, Event $queueForEvents) {
->inject('authorization')
->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForConsole, Event $queueForEvents, Authorization $authorization) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@ -2284,12 +2297,12 @@ App::delete('/v1/functions/:functionId/executions/:executionId')
Query::equal('active', [true]),
]);
if ($schedule && !$schedule->isEmpty()) {
if (!$schedule->isEmpty()) {
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('active', false);
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
$authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
}
}
@ -2303,7 +2316,7 @@ App::delete('/v1/functions/:functionId/executions/:executionId')
// Variables
App::post('/v1/functions/:functionId/variables')
Http::post('/v1/functions/:functionId/variables')
->desc('Create variable')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
@ -2322,7 +2335,8 @@ App::post('/v1/functions/:functionId/variables')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $functionId, string $key, string $value, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('authorization')
->action(function (string $functionId, string $key, string $value, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@ -2360,14 +2374,14 @@ App::post('/v1/functions/:functionId/variables')
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
$authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($variable, Response::MODEL_VARIABLE);
});
App::get('/v1/functions/:functionId/variables')
Http::get('/v1/functions/:functionId/variables')
->desc('List variables')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
@ -2394,7 +2408,7 @@ App::get('/v1/functions/:functionId/variables')
]), Response::MODEL_VARIABLE_LIST);
});
App::get('/v1/functions/:functionId/variables/:variableId')
Http::get('/v1/functions/:functionId/variables/:variableId')
->desc('Get variable')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
@ -2433,7 +2447,7 @@ App::get('/v1/functions/:functionId/variables/:variableId')
$response->dynamic($variable, Response::MODEL_VARIABLE);
});
App::put('/v1/functions/:functionId/variables/:variableId')
Http::put('/v1/functions/:functionId/variables/:variableId')
->desc('Update variable')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
@ -2453,7 +2467,8 @@ App::put('/v1/functions/:functionId/variables/:variableId')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('authorization')
->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization) {
$function = $dbForProject->getDocument('functions', $functionId);
@ -2489,12 +2504,12 @@ App::put('/v1/functions/:functionId/variables/:variableId')
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
$authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
$response->dynamic($variable, Response::MODEL_VARIABLE);
});
App::delete('/v1/functions/:functionId/variables/:variableId')
Http::delete('/v1/functions/:functionId/variables/:variableId')
->desc('Delete variable')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
@ -2511,7 +2526,8 @@ App::delete('/v1/functions/:functionId/variables/:variableId')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('authorization')
->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@ -2537,12 +2553,12 @@ App::delete('/v1/functions/:functionId/variables/:variableId')
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
$authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
$response->noContent();
});
App::get('/v1/functions/templates')
Http::get('/v1/functions/templates')
->groups(['api'])
->desc('List function templates')
->label('scope', 'public')
@ -2580,7 +2596,7 @@ App::get('/v1/functions/templates')
]), Response::MODEL_TEMPLATE_FUNCTION_LIST);
});
App::get('/v1/functions/templates/:templateId')
Http::get('/v1/functions/templates/:templateId')
->desc('Get function template')
->label('scope', 'public')
->label('sdk.namespace', 'functions')
@ -2595,9 +2611,10 @@ App::get('/v1/functions/templates/:templateId')
->action(function (string $templateId, Response $response) {
$templates = Config::getParam('function-templates', []);
$template = array_shift(\array_filter($templates, function ($template) use ($templateId) {
$array = \array_filter($templates, function ($template) use ($templateId) {
return $template['id'] === $templateId;
}));
});
$template = array_shift($array);
if (empty($template)) {
throw new Exception(Exception::FUNCTION_TEMPLATE_NOT_FOUND);

View file

@ -14,27 +14,28 @@ use GraphQL\Validator\Rules\DisableIntrospection;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth;
use Swoole\Coroutine\WaitGroup;
use Utopia\App;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Http\Http;
use Utopia\Http\Validator\JSON;
use Utopia\Http\Validator\Text;
use Utopia\System\System;
use Utopia\Validator\JSON;
use Utopia\Validator\Text;
App::init()
Http::init()
->groups(['graphql'])
->inject('project')
->action(function (Document $project) {
->inject('authorization')
->action(function (Document $project, Authorization $authorization) {
if (
array_key_exists('graphql', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['graphql']
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
});
App::get('/v1/graphql')
Http::get('/v1/graphql')
->desc('GraphQL endpoint')
->groups(['graphql'])
->label('scope', 'graphql')
@ -74,7 +75,7 @@ App::get('/v1/graphql')
->json($output);
});
App::post('/v1/graphql/mutation')
Http::post('/v1/graphql/mutation')
->desc('GraphQL endpoint')
->groups(['graphql'])
->label('scope', 'graphql')
@ -119,7 +120,7 @@ App::post('/v1/graphql/mutation')
->json($output);
});
App::post('/v1/graphql')
Http::post('/v1/graphql')
->desc('GraphQL endpoint')
->groups(['graphql'])
->label('scope', 'graphql')
@ -156,7 +157,6 @@ App::post('/v1/graphql')
if (\str_starts_with($type, 'multipart/form-data')) {
$query = parseMultipart($query, $request);
}
$output = execute($schema, $promiseAdapter, $query);
$response
@ -205,7 +205,7 @@ function execute(
$validations[] = new QueryComplexity($maxComplexity);
$validations[] = new QueryDepth($maxDepth);
}
if (App::getMode() === App::MODE_TYPE_PRODUCTION) {
if (Http::getMode() === Http::MODE_TYPE_PRODUCTION) {
$flags = DebugFlag::NONE;
}
@ -306,9 +306,10 @@ function processResult($result, $debugFlags): array
);
}
App::shutdown()
Http::shutdown()
->groups(['schema'])
->inject('project')
->action(function (Document $project) {
Schema::setDirty($project->getId());
->inject('schemaVariable')
->action(function (Document $project, Schema $schemaVariable) {
$schemaVariable->setDirty($project->getId());
});

View file

@ -3,12 +3,19 @@
use Appwrite\ClamAV\Network;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Queue\Connections;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Adapter\MySQL;
use Utopia\Database\Document;
use Utopia\Domains\Validator\PublicDomain;
use Utopia\Pools\Group;
use Utopia\Http\Http;
use Utopia\Http\Validator\Domain;
use Utopia\Http\Validator\Integer;
use Utopia\Http\Validator\Multiple;
use Utopia\Http\Validator\Text;
use Utopia\Http\Validator\WhiteList;
use Utopia\Queue\Client;
use Utopia\Queue\Connection;
use Utopia\Registry\Registry;
@ -16,13 +23,8 @@ use Utopia\Storage\Device;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;
use Utopia\System\System;
use Utopia\Validator\Domain;
use Utopia\Validator\Integer;
use Utopia\Validator\Multiple;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
App::get('/v1/health')
Http::get('/v1/health')
->desc('Get HTTP')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -45,7 +47,7 @@ App::get('/v1/health')
$response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS);
});
App::get('/v1/health/version')
Http::get('/v1/health/version')
->desc('Get version')
->groups(['api', 'health'])
->label('scope', 'public')
@ -57,7 +59,7 @@ App::get('/v1/health/version')
$response->dynamic(new Document([ 'version' => APP_VERSION_STABLE ]), Response::MODEL_HEALTH_VERSION);
});
App::get('/v1/health/db')
Http::get('/v1/health/db')
->desc('Get DB')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -70,21 +72,34 @@ App::get('/v1/health/db')
->label('sdk.response.model', Response::MODEL_HEALTH_STATUS)
->inject('response')
->inject('pools')
->action(function (Response $response, Group $pools) {
->inject('connections')
->action(function (Response $response, array $pools, Connections $connections) {
$output = [];
$configs = [
'Console.DB' => Config::getParam('pools-console'),
'Projects.DB' => Config::getParam('pools-database'),
'console' => Config::getParam('pools-console'),
'database' => Config::getParam('pools-database'),
];
foreach ($configs as $key => $config) {
foreach ($config as $database) {
try {
$adapter = $pools->get($database)->pop()->getResource();
$checkStart = \microtime(true);
try {
$pool = $pools['pools-'.$key.'-'.$database]['pool'];
$dsn = $pools['pools-'.$key.'-'.$database]['dsn'];
$connection = $pool->get();
$connections->add($connection, $pool);
$adapter = match ($dsn->getScheme()) {
'mariadb' => new MariaDB($connection),
'mysql' => new MySQL($connection),
default => null
};
$adapter->setDatabase($dsn->getPath());
$checkStart = \microtime(true);
if ($adapter->ping()) {
$output[] = new Document([
@ -111,7 +126,7 @@ App::get('/v1/health/db')
]), Response::MODEL_HEALTH_STATUS_LIST);
});
App::get('/v1/health/cache')
Http::get('/v1/health/cache')
->desc('Get cache')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -124,7 +139,8 @@ App::get('/v1/health/cache')
->label('sdk.response.model', Response::MODEL_HEALTH_STATUS)
->inject('response')
->inject('pools')
->action(function (Response $response, Group $pools) {
->inject('connections')
->action(function (Response $response, array $pools, Connections $connections) {
$output = [];
@ -134,10 +150,15 @@ App::get('/v1/health/cache')
foreach ($configs as $key => $config) {
foreach ($config as $database) {
$checkStart = \microtime(true);
try {
$adapter = $pools->get($database)->pop()->getResource();
$pool = $pools['pools-cache-' . $database]['pool'];
$dsn = $pools['pools-cache-' . $database]['dsn'];
$connection = $pool->get();
$connections->add($connection, $pool);
$adapter = new Connection\Redis($dsn->getHost(), $dsn->getPort());
$checkStart = \microtime(true);
if ($adapter->ping()) {
$output[] = new Document([
@ -158,6 +179,8 @@ App::get('/v1/health/cache')
'status' => 'fail',
'ping' => \round((\microtime(true) - $checkStart) / 1000)
]);
} finally {
$connections->reclaim();
}
}
}
@ -168,7 +191,7 @@ App::get('/v1/health/cache')
]), Response::MODEL_HEALTH_STATUS_LIST);
});
App::get('/v1/health/queue')
Http::get('/v1/health/queue')
->desc('Get queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -181,7 +204,8 @@ App::get('/v1/health/queue')
->label('sdk.response.model', Response::MODEL_HEALTH_STATUS)
->inject('response')
->inject('pools')
->action(function (Response $response, Group $pools) {
->inject('connections')
->action(function (Response $response, array $pools, Connections $connections) {
$output = [];
@ -190,12 +214,16 @@ App::get('/v1/health/queue')
];
foreach ($configs as $key => $config) {
$checkStart = \microtime(true);
foreach ($config as $database) {
try {
$adapter = $pools->get($database)->pop()->getResource();
$checkStart = \microtime(true);
$pool = $pools['pools-queue-' . $database]['pool'];
$dsn = $pools['pools-queue-' . $database]['dsn'];
$connection = $pool->get();
$connections->add($connection, $pool);
$adapter = new Connection\Redis($dsn->getHost(), $dsn->getPort());
if ($adapter->ping()) {
$output[] = new Document([
'name' => $key . " ($database)",
@ -215,6 +243,8 @@ App::get('/v1/health/queue')
'status' => 'fail',
'ping' => \round((\microtime(true) - $checkStart) / 1000)
]);
} finally {
$connections->reclaim();
}
}
}
@ -225,7 +255,7 @@ App::get('/v1/health/queue')
]), Response::MODEL_HEALTH_STATUS_LIST);
});
App::get('/v1/health/pubsub')
Http::get('/v1/health/pubsub')
->desc('Get pubsub')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -238,7 +268,8 @@ App::get('/v1/health/pubsub')
->label('sdk.response.model', Response::MODEL_HEALTH_STATUS)
->inject('response')
->inject('pools')
->action(function (Response $response, Group $pools) {
->inject('connections')
->action(function (Response $response, array $pools, Connections $connections) {
$output = [];
@ -249,7 +280,12 @@ App::get('/v1/health/pubsub')
foreach ($configs as $key => $config) {
foreach ($config as $database) {
try {
$adapter = $pools->get($database)->pop()->getResource();
$pool = $pools['pools-pubsub-' . $database]['pool'];
$connection = $pool->get();
$connections->add($connection, $pool);
$adapter = new Connection\Redis($connection);
$checkStart = \microtime(true);
@ -272,6 +308,8 @@ App::get('/v1/health/pubsub')
'status' => 'fail',
'ping' => \round((\microtime(true) - $checkStart) / 1000)
]);
} finally {
$connections->reclaim();
}
}
}
@ -282,7 +320,7 @@ App::get('/v1/health/pubsub')
]), Response::MODEL_HEALTH_STATUS_LIST);
});
App::get('/v1/health/time')
Http::get('/v1/health/time')
->desc('Get time')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -339,7 +377,7 @@ App::get('/v1/health/time')
$response->dynamic(new Document($output), Response::MODEL_HEALTH_TIME);
});
App::get('/v1/health/queue/webhooks')
Http::get('/v1/health/queue/webhooks')
->desc('Get webhooks queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -366,7 +404,7 @@ App::get('/v1/health/queue/webhooks')
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/logs')
Http::get('/v1/health/queue/logs')
->desc('Get logs queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -393,7 +431,7 @@ App::get('/v1/health/queue/logs')
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/certificate')
Http::get('/v1/health/certificate')
->desc('Get the SSL certificate for a domain')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -443,7 +481,7 @@ App::get('/v1/health/certificate')
]), Response::MODEL_HEALTH_CERTIFICATE);
}, ['response']);
App::get('/v1/health/queue/certificates')
Http::get('/v1/health/queue/certificates')
->desc('Get certificates queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -470,7 +508,7 @@ App::get('/v1/health/queue/certificates')
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/builds')
Http::get('/v1/health/queue/builds')
->desc('Get builds queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -497,7 +535,7 @@ App::get('/v1/health/queue/builds')
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/databases')
Http::get('/v1/health/queue/databases')
->desc('Get databases queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -525,7 +563,7 @@ App::get('/v1/health/queue/databases')
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/deletes')
Http::get('/v1/health/queue/deletes')
->desc('Get deletes queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -552,7 +590,7 @@ App::get('/v1/health/queue/deletes')
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/mails')
Http::get('/v1/health/queue/mails')
->desc('Get mails queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -579,7 +617,7 @@ App::get('/v1/health/queue/mails')
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/messaging')
Http::get('/v1/health/queue/messaging')
->desc('Get messaging queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -606,7 +644,7 @@ App::get('/v1/health/queue/messaging')
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/migrations')
Http::get('/v1/health/queue/migrations')
->desc('Get migrations queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -633,7 +671,7 @@ App::get('/v1/health/queue/migrations')
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/functions')
Http::get('/v1/health/queue/functions')
->desc('Get functions queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -660,7 +698,7 @@ App::get('/v1/health/queue/functions')
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/usage')
Http::get('/v1/health/queue/usage')
->desc('Get usage queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -687,7 +725,7 @@ App::get('/v1/health/queue/usage')
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
});
App::get('/v1/health/queue/usage-dump')
Http::get('/v1/health/queue/usage-dump')
->desc('Get usage dump queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -714,7 +752,7 @@ App::get('/v1/health/queue/usage-dump')
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
});
App::get('/v1/health/storage/local')
Http::get('/v1/health/storage/local')
->desc('Get local storage')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -757,7 +795,7 @@ App::get('/v1/health/storage/local')
$response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS);
});
App::get('/v1/health/storage')
Http::get('/v1/health/storage')
->desc('Get storage')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -798,7 +836,7 @@ App::get('/v1/health/storage')
$response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS);
});
App::get('/v1/health/anti-virus')
Http::get('/v1/health/anti-virus')
->desc('Get antivirus')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -837,7 +875,7 @@ App::get('/v1/health/anti-virus')
$response->dynamic(new Document($output), Response::MODEL_HEALTH_ANTIVIRUS);
});
App::get('/v1/health/queue/failed/:name')
Http::get('/v1/health/queue/failed/:name')
->desc('Get number of failed queue jobs')
->groups(['api', 'health'])
->label('scope', 'health.read')
@ -878,7 +916,7 @@ App::get('/v1/health/queue/failed/:name')
$response->dynamic(new Document([ 'size' => $failed ]), Response::MODEL_HEALTH_QUEUE);
});
App::get('/v1/health/stats') // Currently only used internally
Http::get('/v1/health/stats') // Currently only used internally
->desc('Get system stats')
->groups(['api', 'health'])
->label('scope', 'root')

View file

@ -3,12 +3,12 @@
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
App::get('/v1/locale')
Http::get('/v1/locale')
->desc('Get user locale')
->groups(['api', 'locale'])
->label('scope', 'locale.read')
@ -68,8 +68,8 @@ App::get('/v1/locale')
$response->dynamic(new Document($output), Response::MODEL_LOCALE);
});
App::get('/v1/locale/codes')
->desc('List locale codes')
Http::get('/v1/locale/codes')
->desc('List Locale Codes')
->groups(['api', 'locale'])
->label('scope', 'locale.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
@ -90,7 +90,7 @@ App::get('/v1/locale/codes')
]), Response::MODEL_LOCALE_CODE_LIST);
});
App::get('/v1/locale/countries')
Http::get('/v1/locale/countries')
->desc('List countries')
->groups(['api', 'locale'])
->label('scope', 'locale.read')
@ -123,7 +123,7 @@ App::get('/v1/locale/countries')
$response->dynamic(new Document(['countries' => $output, 'total' => \count($output)]), Response::MODEL_COUNTRY_LIST);
});
App::get('/v1/locale/countries/eu')
Http::get('/v1/locale/countries/eu')
->desc('List EU countries')
->groups(['api', 'locale'])
->label('scope', 'locale.read')
@ -158,7 +158,7 @@ App::get('/v1/locale/countries/eu')
$response->dynamic(new Document(['countries' => $output, 'total' => \count($output)]), Response::MODEL_COUNTRY_LIST);
});
App::get('/v1/locale/countries/phones')
Http::get('/v1/locale/countries/phones')
->desc('List countries phone codes')
->groups(['api', 'locale'])
->label('scope', 'locale.read')
@ -192,7 +192,7 @@ App::get('/v1/locale/countries/phones')
$response->dynamic(new Document(['phones' => $output, 'total' => \count($output)]), Response::MODEL_PHONE_LIST);
});
App::get('/v1/locale/continents')
Http::get('/v1/locale/continents')
->desc('List continents')
->groups(['api', 'locale'])
->label('scope', 'locale.read')
@ -224,7 +224,7 @@ App::get('/v1/locale/continents')
$response->dynamic(new Document(['continents' => $output, 'total' => \count($output)]), Response::MODEL_CONTINENT_LIST);
});
App::get('/v1/locale/currencies')
Http::get('/v1/locale/currencies')
->desc('List currencies')
->groups(['api', 'locale'])
->label('scope', 'locale.read')
@ -247,7 +247,7 @@ App::get('/v1/locale/currencies')
});
App::get('/v1/locale/languages')
Http::get('/v1/locale/languages')
->desc('List languages')
->groups(['api', 'locale'])
->label('scope', 'locale.read')

View file

@ -20,7 +20,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Targets;
use Appwrite\Utopia\Database\Validator\Queries\Topics;
use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@ -30,25 +29,27 @@ use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\Roles;
use Utopia\Database\Validator\UID;
use Utopia\Http\Http;
use Utopia\Http\Validator\ArrayList;
use Utopia\Http\Validator\Boolean;
use Utopia\Http\Validator\Integer;
use Utopia\Http\Validator\JSON;
use Utopia\Http\Validator\Range;
use Utopia\Http\Validator\Text;
use Utopia\Http\Validator\WhiteList;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\JSON;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use function Swoole\Coroutine\batch;
App::post('/v1/messaging/providers/mailgun')
Http::post('/v1/messaging/providers/mailgun')
->desc('Create Mailgun provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.create')
@ -135,7 +136,7 @@ App::post('/v1/messaging/providers/mailgun')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::post('/v1/messaging/providers/sendgrid')
Http::post('/v1/messaging/providers/sendgrid')
->desc('Create Sendgrid provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.create')
@ -210,7 +211,7 @@ App::post('/v1/messaging/providers/sendgrid')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::post('/v1/messaging/providers/smtp')
Http::post('/v1/messaging/providers/smtp')
->desc('Create SMTP provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.create')
@ -298,7 +299,7 @@ App::post('/v1/messaging/providers/smtp')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::post('/v1/messaging/providers/msg91')
Http::post('/v1/messaging/providers/msg91')
->desc('Create Msg91 provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.create')
@ -374,7 +375,7 @@ App::post('/v1/messaging/providers/msg91')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::post('/v1/messaging/providers/telesign')
Http::post('/v1/messaging/providers/telesign')
->desc('Create Telesign provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.create')
@ -451,7 +452,7 @@ App::post('/v1/messaging/providers/telesign')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::post('/v1/messaging/providers/textmagic')
Http::post('/v1/messaging/providers/textmagic')
->desc('Create Textmagic provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.create')
@ -528,7 +529,7 @@ App::post('/v1/messaging/providers/textmagic')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::post('/v1/messaging/providers/twilio')
Http::post('/v1/messaging/providers/twilio')
->desc('Create Twilio provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.create')
@ -605,7 +606,7 @@ App::post('/v1/messaging/providers/twilio')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::post('/v1/messaging/providers/vonage')
Http::post('/v1/messaging/providers/vonage')
->desc('Create Vonage provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.create')
@ -682,7 +683,7 @@ App::post('/v1/messaging/providers/vonage')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::post('/v1/messaging/providers/fcm')
Http::post('/v1/messaging/providers/fcm')
->desc('Create FCM provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.create')
@ -745,7 +746,7 @@ App::post('/v1/messaging/providers/fcm')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::post('/v1/messaging/providers/apns')
Http::post('/v1/messaging/providers/apns')
->desc('Create APNS provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.create')
@ -831,7 +832,7 @@ App::post('/v1/messaging/providers/apns')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::get('/v1/messaging/providers')
Http::get('/v1/messaging/providers')
->desc('List providers')
->groups(['api', 'messaging'])
->label('scope', 'providers.read')
@ -846,7 +847,8 @@ App::get('/v1/messaging/providers')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('dbForProject')
->inject('response')
->action(function (array $queries, string $search, Database $dbForProject, Response $response) {
->inject('authorization')
->action(function (array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@ -867,7 +869,7 @@ App::get('/v1/messaging/providers')
if ($cursor) {
$providerId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId));
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('providers', $providerId));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Provider '{$providerId}' for the 'cursor' value not found.");
@ -882,7 +884,7 @@ App::get('/v1/messaging/providers')
]), Response::MODEL_PROVIDER_LIST);
});
App::get('/v1/messaging/providers/:providerId/logs')
Http::get('/v1/messaging/providers/:providerId/logs')
->desc('List provider logs')
->groups(['api', 'messaging'])
->label('scope', 'providers.read')
@ -970,7 +972,7 @@ App::get('/v1/messaging/providers/:providerId/logs')
]), Response::MODEL_LOG_LIST);
});
App::get('/v1/messaging/providers/:providerId')
Http::get('/v1/messaging/providers/:providerId')
->desc('Get provider')
->groups(['api', 'messaging'])
->label('scope', 'providers.read')
@ -994,7 +996,7 @@ App::get('/v1/messaging/providers/:providerId')
$response->dynamic($provider, Response::MODEL_PROVIDER);
});
App::patch('/v1/messaging/providers/mailgun/:providerId')
Http::patch('/v1/messaging/providers/mailgun/:providerId')
->desc('Update Mailgun provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.update')
@ -1100,7 +1102,7 @@ App::patch('/v1/messaging/providers/mailgun/:providerId')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::patch('/v1/messaging/providers/sendgrid/:providerId')
Http::patch('/v1/messaging/providers/sendgrid/:providerId')
->desc('Update Sendgrid provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.update')
@ -1191,7 +1193,7 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::patch('/v1/messaging/providers/smtp/:providerId')
Http::patch('/v1/messaging/providers/smtp/:providerId')
->desc('Update SMTP provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.update')
@ -1313,7 +1315,7 @@ App::patch('/v1/messaging/providers/smtp/:providerId')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::patch('/v1/messaging/providers/msg91/:providerId')
Http::patch('/v1/messaging/providers/msg91/:providerId')
->desc('Update Msg91 provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.update')
@ -1393,7 +1395,7 @@ App::patch('/v1/messaging/providers/msg91/:providerId')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::patch('/v1/messaging/providers/telesign/:providerId')
Http::patch('/v1/messaging/providers/telesign/:providerId')
->desc('Update Telesign provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.update')
@ -1475,7 +1477,7 @@ App::patch('/v1/messaging/providers/telesign/:providerId')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::patch('/v1/messaging/providers/textmagic/:providerId')
Http::patch('/v1/messaging/providers/textmagic/:providerId')
->desc('Update Textmagic provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.update')
@ -1557,7 +1559,7 @@ App::patch('/v1/messaging/providers/textmagic/:providerId')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::patch('/v1/messaging/providers/twilio/:providerId')
Http::patch('/v1/messaging/providers/twilio/:providerId')
->desc('Update Twilio provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.update')
@ -1639,7 +1641,7 @@ App::patch('/v1/messaging/providers/twilio/:providerId')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::patch('/v1/messaging/providers/vonage/:providerId')
Http::patch('/v1/messaging/providers/vonage/:providerId')
->desc('Update Vonage provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.update')
@ -1721,7 +1723,7 @@ App::patch('/v1/messaging/providers/vonage/:providerId')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::patch('/v1/messaging/providers/fcm/:providerId')
Http::patch('/v1/messaging/providers/fcm/:providerId')
->desc('Update FCM provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.update')
@ -1790,7 +1792,7 @@ App::patch('/v1/messaging/providers/fcm/:providerId')
});
App::patch('/v1/messaging/providers/apns/:providerId')
Http::patch('/v1/messaging/providers/apns/:providerId')
->desc('Update APNS provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.update')
@ -1885,7 +1887,7 @@ App::patch('/v1/messaging/providers/apns/:providerId')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::delete('/v1/messaging/providers/:providerId')
Http::delete('/v1/messaging/providers/:providerId')
->desc('Delete provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.delete')
@ -1920,7 +1922,7 @@ App::delete('/v1/messaging/providers/:providerId')
->noContent();
});
App::post('/v1/messaging/topics')
Http::post('/v1/messaging/topics')
->desc('Create topic')
->groups(['api', 'messaging'])
->label('audits.event', 'topic.create')
@ -1963,7 +1965,7 @@ App::post('/v1/messaging/topics')
->dynamic($topic, Response::MODEL_TOPIC);
});
App::get('/v1/messaging/topics')
Http::get('/v1/messaging/topics')
->desc('List topics')
->groups(['api', 'messaging'])
->label('scope', 'topics.read')
@ -1978,7 +1980,8 @@ App::get('/v1/messaging/topics')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('dbForProject')
->inject('response')
->action(function (array $queries, string $search, Database $dbForProject, Response $response) {
->inject('authorization')
->action(function (array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@ -1999,7 +2002,7 @@ App::get('/v1/messaging/topics')
if ($cursor) {
$topicId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Topic '{$topicId}' for the 'cursor' value not found.");
@ -2014,7 +2017,7 @@ App::get('/v1/messaging/topics')
]), Response::MODEL_TOPIC_LIST);
});
App::get('/v1/messaging/topics/:topicId/logs')
Http::get('/v1/messaging/topics/:topicId/logs')
->desc('List topic logs')
->groups(['api', 'messaging'])
->label('scope', 'topics.read')
@ -2031,7 +2034,8 @@ App::get('/v1/messaging/topics/:topicId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $topicId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('authorization')
->action(function (string $topicId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) {
$topic = $dbForProject->getDocument('topics', $topicId);
if ($topic->isEmpty()) {
@ -2103,7 +2107,7 @@ App::get('/v1/messaging/topics/:topicId/logs')
]), Response::MODEL_LOG_LIST);
});
App::get('/v1/messaging/topics/:topicId')
Http::get('/v1/messaging/topics/:topicId')
->desc('Get topic')
->groups(['api', 'messaging'])
->label('scope', 'topics.read')
@ -2128,7 +2132,7 @@ App::get('/v1/messaging/topics/:topicId')
->dynamic($topic, Response::MODEL_TOPIC);
});
App::patch('/v1/messaging/topics/:topicId')
Http::patch('/v1/messaging/topics/:topicId')
->desc('Update topic')
->groups(['api', 'messaging'])
->label('audits.event', 'topic.update')
@ -2172,7 +2176,7 @@ App::patch('/v1/messaging/topics/:topicId')
->dynamic($topic, Response::MODEL_TOPIC);
});
App::delete('/v1/messaging/topics/:topicId')
Http::delete('/v1/messaging/topics/:topicId')
->desc('Delete topic')
->groups(['api', 'messaging'])
->label('audits.event', 'topic.delete')
@ -2212,7 +2216,7 @@ App::delete('/v1/messaging/topics/:topicId')
->noContent();
});
App::post('/v1/messaging/topics/:topicId/subscribers')
Http::post('/v1/messaging/topics/:topicId/subscribers')
->desc('Create subscriber')
->groups(['api', 'messaging'])
->label('audits.event', 'subscriber.create')
@ -2232,28 +2236,27 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Response $response) {
->inject('authorization')
->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Response $response, Authorization $authorization) {
$subscriberId = $subscriberId == 'unique()' ? ID::unique() : $subscriberId;
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND);
}
$validator = new Authorization('subscribe');
if (!$validator->isValid($topic->getAttribute('subscribe'))) {
throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription());
if (!$authorization->isValid(new Input('subscribe', $topic->getAttribute('subscribe')))) {
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId));
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$subscriber = new Document([
'$id' => $subscriberId,
@ -2286,7 +2289,7 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE),
};
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute(
$authorization->skip(fn () => $dbForProject->increaseDocumentAttribute(
'topics',
$topicId,
$totalAttribute,
@ -2308,7 +2311,7 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
->dynamic($subscriber, Response::MODEL_SUBSCRIBER);
});
App::get('/v1/messaging/topics/:topicId/subscribers')
Http::get('/v1/messaging/topics/:topicId/subscribers')
->desc('List subscribers')
->groups(['api', 'messaging'])
->label('scope', 'subscribers.read')
@ -2324,7 +2327,8 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('dbForProject')
->inject('response')
->action(function (string $topicId, array $queries, string $search, Database $dbForProject, Response $response) {
->inject('authorization')
->action(function (string $topicId, array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@ -2335,7 +2339,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
$queries[] = Query::search('search', $search);
}
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND);
@ -2353,7 +2357,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
if ($cursor) {
$subscriberId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId));
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Subscriber '{$subscriberId}' for the 'cursor' value not found.");
@ -2364,10 +2368,10 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
$subscribers = $dbForProject->find('subscribers', $queries);
$subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject) {
return function () use ($subscriber, $dbForProject) {
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject, $authorization) {
return function () use ($subscriber, $dbForProject, $authorization) {
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
$user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
return $subscriber
->setAttribute('target', $target)
@ -2382,7 +2386,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
]), Response::MODEL_SUBSCRIBER_LIST);
});
App::get('/v1/messaging/subscribers/:subscriberId/logs')
Http::get('/v1/messaging/subscribers/:subscriberId/logs')
->desc('List subscriber logs')
->groups(['api', 'messaging'])
->label('scope', 'subscribers.read')
@ -2399,7 +2403,8 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $subscriberId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('authorization')
->action(function (string $subscriberId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) {
$subscriber = $dbForProject->getDocument('subscribers', $subscriberId);
if ($subscriber->isEmpty()) {
@ -2471,7 +2476,7 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
]), Response::MODEL_LOG_LIST);
});
App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
Http::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
->desc('Get subscriber')
->groups(['api', 'messaging'])
->label('scope', 'subscribers.read')
@ -2486,8 +2491,9 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
->param('subscriberId', '', new UID(), 'Subscriber ID.')
->inject('dbForProject')
->inject('response')
->action(function (string $topicId, string $subscriberId, Database $dbForProject, Response $response) {
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
->inject('authorization')
->action(function (string $topicId, string $subscriberId, Database $dbForProject, Response $response, Authorization $authorization) {
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND);
@ -2499,8 +2505,8 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
throw new Exception(Exception::SUBSCRIBER_NOT_FOUND);
}
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
$user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$subscriber
->setAttribute('target', $target)
@ -2510,7 +2516,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
->dynamic($subscriber, Response::MODEL_SUBSCRIBER);
});
App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
Http::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
->desc('Delete subscriber')
->groups(['api', 'messaging'])
->label('audits.event', 'subscriber.delete')
@ -2529,8 +2535,9 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Response $response) {
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
->inject('authorization')
->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Response $response, Authorization $authorization) {
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND);
@ -2553,7 +2560,7 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE),
};
Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute(
$authorization->skip(fn () => $dbForProject->decreaseDocumentAttribute(
'topics',
$topicId,
$totalAttribute,
@ -2569,7 +2576,7 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
->noContent();
});
App::post('/v1/messaging/messages/email')
Http::post('/v1/messaging/messages/email')
->desc('Create email')
->groups(['api', 'messaging'])
->label('audits.event', 'message.create')
@ -2721,7 +2728,7 @@ App::post('/v1/messaging/messages/email')
->dynamic($message, Response::MODEL_MESSAGE);
});
App::post('/v1/messaging/messages/sms')
Http::post('/v1/messaging/messages/sms')
->desc('Create SMS')
->groups(['api', 'messaging'])
->label('audits.event', 'message.create')
@ -2837,7 +2844,7 @@ App::post('/v1/messaging/messages/sms')
->dynamic($message, Response::MODEL_MESSAGE);
});
App::post('/v1/messaging/messages/push')
Http::post('/v1/messaging/messages/push')
->desc('Create push notification')
->groups(['api', 'messaging'])
->label('audits.event', 'message.create')
@ -3013,7 +3020,7 @@ App::post('/v1/messaging/messages/push')
->dynamic($message, Response::MODEL_MESSAGE);
});
App::get('/v1/messaging/messages')
Http::get('/v1/messaging/messages')
->desc('List messages')
->groups(['api', 'messaging'])
->label('scope', 'messages.read')
@ -3028,7 +3035,8 @@ App::get('/v1/messaging/messages')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('dbForProject')
->inject('response')
->action(function (array $queries, string $search, Database $dbForProject, Response $response) {
->inject('authorization')
->action(function (array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@ -3049,7 +3057,7 @@ App::get('/v1/messaging/messages')
if ($cursor) {
$messageId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('messages', $messageId));
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('messages', $messageId));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Message '{$messageId}' for the 'cursor' value not found.");
@ -3064,7 +3072,7 @@ App::get('/v1/messaging/messages')
]), Response::MODEL_MESSAGE_LIST);
});
App::get('/v1/messaging/messages/:messageId/logs')
Http::get('/v1/messaging/messages/:messageId/logs')
->desc('List message logs')
->groups(['api', 'messaging'])
->label('scope', 'messages.read')
@ -3081,7 +3089,8 @@ App::get('/v1/messaging/messages/:messageId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $messageId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('authorization')
->action(function (string $messageId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@ -3153,7 +3162,7 @@ App::get('/v1/messaging/messages/:messageId/logs')
]), Response::MODEL_LOG_LIST);
});
App::get('/v1/messaging/messages/:messageId/targets')
Http::get('/v1/messaging/messages/:messageId/targets')
->desc('List message targets')
->groups(['api', 'messaging'])
->label('scope', 'messages.read')
@ -3218,7 +3227,7 @@ App::get('/v1/messaging/messages/:messageId/targets')
]), Response::MODEL_TARGET_LIST);
});
App::get('/v1/messaging/messages/:messageId')
Http::get('/v1/messaging/messages/:messageId')
->desc('Get message')
->groups(['api', 'messaging'])
->label('scope', 'messages.read')
@ -3242,7 +3251,7 @@ App::get('/v1/messaging/messages/:messageId')
$response->dynamic($message, Response::MODEL_MESSAGE);
});
App::patch('/v1/messaging/messages/email/:messageId')
Http::patch('/v1/messaging/messages/email/:messageId')
->desc('Update email')
->groups(['api', 'messaging'])
->label('audits.event', 'message.update')
@ -3442,7 +3451,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
->dynamic($message, Response::MODEL_MESSAGE);
});
App::patch('/v1/messaging/messages/sms/:messageId')
Http::patch('/v1/messaging/messages/sms/:messageId')
->desc('Update SMS')
->groups(['api', 'messaging'])
->label('audits.event', 'message.update')
@ -3597,7 +3606,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
->dynamic($message, Response::MODEL_MESSAGE);
});
App::patch('/v1/messaging/messages/push/:messageId')
Http::patch('/v1/messaging/messages/push/:messageId')
->desc('Update push notification')
->groups(['api', 'messaging'])
->label('audits.event', 'message.update')
@ -3835,7 +3844,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
->dynamic($message, Response::MODEL_MESSAGE);
});
App::delete('/v1/messaging/messages/:messageId')
Http::delete('/v1/messaging/messages/:messageId')
->desc('Delete message')
->groups(['api', 'messaging'])
->label('audits.event', 'message.delete')

View file

@ -9,7 +9,6 @@ use Appwrite\Role;
use Appwrite\Utopia\Database\Validator\Queries\Migrations;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
@ -17,21 +16,22 @@ use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
use Utopia\Http\Http;
use Utopia\Http\Validator\ArrayList;
use Utopia\Http\Validator\Host;
use Utopia\Http\Validator\Integer;
use Utopia\Http\Validator\Text;
use Utopia\Http\Validator\URL;
use Utopia\Http\Validator\WhiteList;
use Utopia\Migration\Sources\Appwrite;
use Utopia\Migration\Sources\Firebase;
use Utopia\Migration\Sources\NHost;
use Utopia\Migration\Sources\Supabase;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Host;
use Utopia\Validator\Integer;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
include_once __DIR__ . '/../shared/api.php';
App::post('/v1/migrations/appwrite')
Http::post('/v1/migrations/appwrite')
->groups(['api', 'migrations'])
->desc('Migrate Appwrite data')
->label('scope', 'migrations.write')
@ -86,7 +86,7 @@ App::post('/v1/migrations/appwrite')
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/firebase/oauth')
Http::post('/v1/migrations/firebase/oauth')
->groups(['api', 'migrations'])
->desc('Migrate Firebase data (OAuth)')
->label('scope', 'migrations.write')
@ -120,7 +120,7 @@ App::post('/v1/migrations/firebase/oauth')
Query::equal('provider', ['firebase']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity === false || $identity->isEmpty()) {
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
@ -189,7 +189,7 @@ App::post('/v1/migrations/firebase/oauth')
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/firebase')
Http::post('/v1/migrations/firebase')
->groups(['api', 'migrations'])
->desc('Migrate Firebase data (Service Account)')
->label('scope', 'migrations.write')
@ -250,7 +250,7 @@ App::post('/v1/migrations/firebase')
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/supabase')
Http::post('/v1/migrations/supabase')
->groups(['api', 'migrations'])
->desc('Migrate Supabase data')
->label('scope', 'migrations.write')
@ -311,7 +311,7 @@ App::post('/v1/migrations/supabase')
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/nhost')
Http::post('/v1/migrations/nhost')
->groups(['api', 'migrations'])
->desc('Migrate NHost data')
->label('scope', 'migrations.write')
@ -374,7 +374,7 @@ App::post('/v1/migrations/nhost')
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::get('/v1/migrations')
Http::get('/v1/migrations')
->groups(['api', 'migrations'])
->desc('List migrations')
->label('scope', 'migrations.read')
@ -427,7 +427,7 @@ App::get('/v1/migrations')
]), Response::MODEL_MIGRATION_LIST);
});
App::get('/v1/migrations/:migrationId')
Http::get('/v1/migrations/:migrationId')
->groups(['api', 'migrations'])
->desc('Get migration')
->label('scope', 'migrations.read')
@ -451,7 +451,7 @@ App::get('/v1/migrations/:migrationId')
$response->dynamic($migration, Response::MODEL_MIGRATION);
});
App::get('/v1/migrations/appwrite/report')
Http::get('/v1/migrations/appwrite/report')
->groups(['api', 'migrations'])
->desc('Generate a report on Appwrite data')
->label('scope', 'migrations.write')
@ -493,7 +493,7 @@ App::get('/v1/migrations/appwrite/report')
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
});
App::get('/v1/migrations/firebase/report')
Http::get('/v1/migrations/firebase/report')
->groups(['api', 'migrations'])
->desc('Generate a report on Firebase data')
->label('scope', 'migrations.write')
@ -540,7 +540,7 @@ App::get('/v1/migrations/firebase/report')
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
});
App::get('/v1/migrations/firebase/report/oauth')
Http::get('/v1/migrations/firebase/report/oauth')
->groups(['api', 'migrations'])
->desc('Generate a report on Firebase data using OAuth')
->label('scope', 'migrations.write')
@ -569,7 +569,7 @@ App::get('/v1/migrations/firebase/report/oauth')
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity === false || $identity->isEmpty()) {
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
@ -631,7 +631,7 @@ App::get('/v1/migrations/firebase/report/oauth')
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
});
App::get('/v1/migrations/firebase/connect')
Http::get('/v1/migrations/firebase/connect')
->desc('Authorize with Firebase')
->groups(['api', 'migrations'])
->label('scope', 'migrations.write')
@ -673,7 +673,7 @@ App::get('/v1/migrations/firebase/connect')
->redirect($url);
});
App::get('/v1/migrations/firebase/redirect')
Http::get('/v1/migrations/firebase/redirect')
->desc('Capture and receive data on Firebase authorization')
->groups(['api', 'migrations'])
->label('scope', 'public')
@ -744,7 +744,7 @@ App::get('/v1/migrations/firebase/redirect')
Query::equal('providerEmail', [$email]),
]);
if ($identity !== false && !$identity->isEmpty()) {
if (!$identity->isEmpty()) {
if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
@ -785,8 +785,8 @@ App::get('/v1/migrations/firebase/redirect')
->redirect($redirect);
});
App::get('/v1/migrations/firebase/projects')
->desc('List Firebase projects')
Http::get('/v1/migrations/firebase/projects')
->desc('List Firebase Projects')
->groups(['api', 'migrations'])
->label('scope', 'migrations.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
@ -813,7 +813,7 @@ App::get('/v1/migrations/firebase/projects')
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity === false || $identity->isEmpty()) {
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
@ -874,8 +874,8 @@ App::get('/v1/migrations/firebase/projects')
]), Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST);
});
App::get('/v1/migrations/firebase/deauthorize')
->desc('Revoke Appwrite\'s authorization to access Firebase projects')
Http::get('/v1/migrations/firebase/deauthorize')
->desc('Revoke Appwrite\'s authorization to access Firebase Projects')
->groups(['api', 'migrations'])
->label('scope', 'migrations.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
@ -893,7 +893,7 @@ App::get('/v1/migrations/firebase/deauthorize')
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity === false || $identity->isEmpty()) {
if ($identity->isEmpty()) {
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase'); //TODO: Replace with USER_IDENTITY_NOT_FOUND
}
@ -902,7 +902,7 @@ App::get('/v1/migrations/firebase/deauthorize')
$response->noContent();
});
App::get('/v1/migrations/supabase/report')
Http::get('/v1/migrations/supabase/report')
->groups(['api', 'migrations'])
->desc('Generate a report on Supabase Data')
->label('scope', 'migrations.write')
@ -945,7 +945,7 @@ App::get('/v1/migrations/supabase/report')
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
});
App::get('/v1/migrations/nhost/report')
Http::get('/v1/migrations/nhost/report')
->groups(['api', 'migrations'])
->desc('Generate a report on NHost Data')
->label('scope', 'migrations.write')
@ -988,7 +988,7 @@ App::get('/v1/migrations/nhost/report')
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
});
App::patch('/v1/migrations/:migrationId')
Http::patch('/v1/migrations/:migrationId')
->groups(['api', 'migrations'])
->desc('Retry migration')
->label('scope', 'migrations.write')
@ -1033,7 +1033,7 @@ App::patch('/v1/migrations/:migrationId')
$response->noContent();
});
App::delete('/v1/migrations/:migrationId')
Http::delete('/v1/migrations/:migrationId')
->groups(['api', 'migrations'])
->desc('Delete migration')
->label('scope', 'migrations.write')

View file

@ -2,7 +2,6 @@
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
@ -13,10 +12,11 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DateTimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\Http\Http;
use Utopia\Http\Validator\Text;
use Utopia\Http\Validator\WhiteList;
App::get('/v1/project/usage')
Http::get('/v1/project/usage')
->desc('Get project usage stats')
->groups(['api', 'usage'])
->label('scope', 'projects.read')
@ -31,7 +31,8 @@ App::get('/v1/project/usage')
->param('period', '1d', new WhiteList(['1h', '1d']), 'Period used', true)
->inject('response')
->inject('dbForProject')
->action(function (string $startDate, string $endDate, string $period, Response $response, Database $dbForProject) {
->inject('authorization')
->action(function (string $startDate, string $endDate, string $period, Response $response, Database $dbForProject, Authorization $authorization) {
$stats = $total = $usage = [];
$format = 'Y-m-d 00:00:00';
$firstDay = (new DateTime($startDate))->format($format);
@ -78,7 +79,7 @@ App::get('/v1/project/usage')
'1d' => 'Y-m-d\T00:00:00.000P',
};
Authorization::skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) {
$authorization->skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) {
foreach ($metrics['total'] as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
@ -296,8 +297,6 @@ App::get('/v1/project/usage')
'buildsStorageTotal' => $total[METRIC_BUILDS_STORAGE],
'deploymentsStorageTotal' => $total[METRIC_DEPLOYMENTS_STORAGE],
'executionsBreakdown' => $executionsBreakdown,
'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown,
'buildsMbSecondsBreakdown' => $buildsMbSecondsBreakdown,
'bucketsBreakdown' => $bucketsBreakdown,
'databasesStorageBreakdown' => $databasesStorageBreakdown,
'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown,
@ -308,8 +307,8 @@ App::get('/v1/project/usage')
// Variables
App::post('/v1/project/variables')
->desc('Create variable')
Http::post('/v1/project/variables')
->desc('Create Variable')
->groups(['api'])
->label('scope', 'projects.write')
->label('audits.event', 'variable.create')
@ -363,8 +362,8 @@ App::post('/v1/project/variables')
->dynamic($variable, Response::MODEL_VARIABLE);
});
App::get('/v1/project/variables')
->desc('List variables')
Http::get('/v1/project/variables')
->desc('List Variables')
->groups(['api'])
->label('scope', 'projects.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
@ -388,8 +387,8 @@ App::get('/v1/project/variables')
]), Response::MODEL_VARIABLE_LIST);
});
App::get('/v1/project/variables/:variableId')
->desc('Get variable')
Http::get('/v1/project/variables/:variableId')
->desc('Get Variable')
->groups(['api'])
->label('scope', 'projects.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
@ -412,8 +411,8 @@ App::get('/v1/project/variables/:variableId')
$response->dynamic($variable, Response::MODEL_VARIABLE);
});
App::put('/v1/project/variables/:variableId')
->desc('Update variable')
Http::put('/v1/project/variables/:variableId')
->desc('Update Variable')
->groups(['api'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
@ -458,8 +457,8 @@ App::put('/v1/project/variables/:variableId')
$response->dynamic($variable, Response::MODEL_VARIABLE);
});
App::delete('/v1/project/variables/:variableId')
->desc('Delete variable')
Http::delete('/v1/project/variables/:variableId')
->desc('Delete Variable')
->groups(['api'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])

View file

@ -13,14 +13,16 @@ use Appwrite\Network\Validator\Origin;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\ProjectId;
use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Appwrite\Utopia\Queue\Connections;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\Abuse\Adapters\Database\TimeLimit;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Adapter\MySQL;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
@ -30,24 +32,25 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Validator\PublicDomain;
use Utopia\DSN\DSN;
use Utopia\Http\Http;
use Utopia\Http\Validator\ArrayList;
use Utopia\Http\Validator\Boolean;
use Utopia\Http\Validator\Hostname;
use Utopia\Http\Validator\Integer;
use Utopia\Http\Validator\Multiple;
use Utopia\Http\Validator\Range;
use Utopia\Http\Validator\Text;
use Utopia\Http\Validator\URL;
use Utopia\Http\Validator\WhiteList;
use Utopia\Locale\Locale;
use Utopia\Pools\Group;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Hostname;
use Utopia\Validator\Integer;
use Utopia\Validator\Multiple;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
App::init()
Http::init()
->groups(['projects'])
->inject('project')
->action(function (Document $project) {
@ -56,7 +59,7 @@ App::init()
}
});
App::post('/v1/projects')
Http::post('/v1/projects')
->desc('Create project')
->groups(['api', 'projects'])
->label('audits.event', 'projects.create')
@ -86,8 +89,9 @@ App::post('/v1/projects')
->inject('cache')
->inject('pools')
->inject('hooks')
->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Request $request, Response $response, Database $dbForConsole, Cache $cache, Group $pools, Hooks $hooks) {
->inject('authorization')
->inject('connections')
->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Request $request, Response $response, Database $dbForConsole, Cache $cache, array $pools, Hooks $hooks, Authorization $authorization, Connections $connections) {
$team = $dbForConsole->getDocument('teams', $teamId);
if ($team->isEmpty()) {
@ -189,8 +193,21 @@ App::post('/v1/projects')
$dsn = new DSN('mysql://' . $dsn);
}
$adapter = $pools->get($dsn->getHost())->pop()->getResource();
$pool = $pools['pools-database-' . $dsn->getHost()]['pool'];
$connectionDsn = $pools['pools-database-' . $dsn->getHost()]['dsn'];
$connection = $pool->get();
$connections->add($connection, $pool);
$adapter = match ($connectionDsn->getScheme()) {
'mariadb' => new MariaDB($connection),
'mysql' => new MySQL($connection),
default => null
};
$adapter->setDatabase($connectionDsn->getPath());
$dbForProject = new Database($adapter, $cache);
$dbForProject->setAuthorization($authorization);
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$dbForProject
@ -208,10 +225,8 @@ App::post('/v1/projects')
$audit = new Audit($dbForProject);
$audit->setup();
$abuse = new TimeLimit('', 0, 1, $dbForProject);
$abuse->setup();
/** @var array $collections */
$collections = Config::getParam('collections', [])['projects'] ?? [];
@ -234,17 +249,17 @@ App::post('/v1/projects')
// Collection already exists
}
}
$connections->reclaim();
// Hook allowing instant project mirroring during migration
// Outside of migration, hook is not registered and has no effect
$hooks->trigger('afterProjectCreation', [ $project, $pools, $cache ]);
$hooks->trigger('afterProjectCreation', [$project, $pools, $cache]);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($project, Response::MODEL_PROJECT);
});
App::get('/v1/projects')
Http::get('/v1/projects')
->desc('List projects')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
@ -259,7 +274,6 @@ App::get('/v1/projects')
->inject('response')
->inject('dbForConsole')
->action(function (array $queries, string $search, Response $response, Database $dbForConsole) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@ -297,7 +311,7 @@ App::get('/v1/projects')
]), Response::MODEL_PROJECT_LIST);
});
App::get('/v1/projects/:projectId')
Http::get('/v1/projects/:projectId')
->desc('Get project')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
@ -311,7 +325,6 @@ App::get('/v1/projects/:projectId')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -321,7 +334,7 @@ App::get('/v1/projects/:projectId')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId')
Http::patch('/v1/projects/:projectId')
->desc('Update project')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -345,31 +358,34 @@ App::patch('/v1/projects/:projectId')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('name', $name)
->setAttribute('description', $description)
->setAttribute('logo', $logo)
->setAttribute('url', $url)
->setAttribute('legalName', $legalName)
->setAttribute('legalCountry', $legalCountry)
->setAttribute('legalState', $legalState)
->setAttribute('legalCity', $legalCity)
->setAttribute('legalAddress', $legalAddress)
->setAttribute('legalTaxId', $legalTaxId)
->setAttribute('search', implode(' ', [$projectId, $name])));
$project = $dbForConsole->updateDocument(
'projects',
$project->getId(),
$project
->setAttribute('name', $name)
->setAttribute('description', $description)
->setAttribute('logo', $logo)
->setAttribute('url', $url)
->setAttribute('legalName', $legalName)
->setAttribute('legalCountry', $legalCountry)
->setAttribute('legalState', $legalState)
->setAttribute('legalCity', $legalCity)
->setAttribute('legalAddress', $legalAddress)
->setAttribute('legalTaxId', $legalTaxId)
->setAttribute('search', implode(' ', [$projectId, $name]))
);
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/team')
->desc('Update project team')
Http::patch('/v1/projects/:projectId/team')
->desc('Update Project Team')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
@ -383,7 +399,6 @@ App::patch('/v1/projects/:projectId/team')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $teamId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
$team = $dbForConsole->getDocument('teams', $teamId);
@ -436,7 +451,7 @@ App::patch('/v1/projects/:projectId/team')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/service')
Http::patch('/v1/projects/:projectId/service')
->desc('Update service status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -452,7 +467,6 @@ App::patch('/v1/projects/:projectId/service')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $service, bool $status, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -467,7 +481,7 @@ App::patch('/v1/projects/:projectId/service')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/service/all')
Http::patch('/v1/projects/:projectId/service/all')
->desc('Update all service status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -482,7 +496,6 @@ App::patch('/v1/projects/:projectId/service/all')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, bool $status, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -501,7 +514,7 @@ App::patch('/v1/projects/:projectId/service/all')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/api')
Http::patch('/v1/projects/:projectId/api')
->desc('Update API status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -517,7 +530,6 @@ App::patch('/v1/projects/:projectId/api')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $api, bool $status, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -532,7 +544,7 @@ App::patch('/v1/projects/:projectId/api')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/api/all')
Http::patch('/v1/projects/:projectId/api/all')
->desc('Update all API status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -547,7 +559,6 @@ App::patch('/v1/projects/:projectId/api/all')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, bool $status, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -566,7 +577,7 @@ App::patch('/v1/projects/:projectId/api/all')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/oauth2')
Http::patch('/v1/projects/:projectId/oauth2')
->desc('Update project OAuth2')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -584,7 +595,6 @@ App::patch('/v1/projects/:projectId/oauth2')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $provider, ?string $appId, ?string $secret, ?bool $enabled, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -610,7 +620,7 @@ App::patch('/v1/projects/:projectId/oauth2')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/auth/session-alerts')
Http::patch('/v1/projects/:projectId/auth/session-alerts')
->desc('Update project sessions emails')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -641,7 +651,7 @@ App::patch('/v1/projects/:projectId/auth/session-alerts')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/auth/limit')
Http::patch('/v1/projects/:projectId/auth/limit')
->desc('Update project users limit')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -656,7 +666,6 @@ App::patch('/v1/projects/:projectId/auth/limit')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -666,13 +675,17 @@ App::patch('/v1/projects/:projectId/auth/limit')
$auths = $project->getAttribute('auths', []);
$auths['limit'] = $limit;
$dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$dbForConsole->updateDocument(
'projects',
$project->getId(),
$project
->setAttribute('auths', $auths)
);
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/auth/duration')
Http::patch('/v1/projects/:projectId/auth/duration')
->desc('Update project authentication duration')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -687,7 +700,6 @@ App::patch('/v1/projects/:projectId/auth/duration')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, int $duration, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -697,13 +709,17 @@ App::patch('/v1/projects/:projectId/auth/duration')
$auths = $project->getAttribute('auths', []);
$auths['duration'] = $duration;
$dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$dbForConsole->updateDocument(
'projects',
$project->getId(),
$project
->setAttribute('auths', $auths)
);
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/auth/:method')
Http::patch('/v1/projects/:projectId/auth/:method')
->desc('Update project auth method status. Use this endpoint to enable or disable a given auth method for this project.')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -719,10 +735,9 @@ App::patch('/v1/projects/:projectId/auth/:method')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $method, bool $status, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
$auth = Config::getParam('auth')[$method] ?? [];
$authKey = $auth['key'] ?? '';
$authConfig = Config::getParam('auth')[$method] ?? [];
$authKey = $authConfig['key'] ?? '';
$status = ($status === '1' || $status === 'true' || $status === 1 || $status === true);
if ($project->isEmpty()) {
@ -737,7 +752,7 @@ App::patch('/v1/projects/:projectId/auth/:method')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/auth/password-history')
Http::patch('/v1/projects/:projectId/auth/password-history')
->desc('Update authentication password history. Use this endpoint to set the number of password history to save and 0 to disable password history.')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -752,7 +767,6 @@ App::patch('/v1/projects/:projectId/auth/password-history')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -762,13 +776,17 @@ App::patch('/v1/projects/:projectId/auth/password-history')
$auths = $project->getAttribute('auths', []);
$auths['passwordHistory'] = $limit;
$dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$dbForConsole->updateDocument(
'projects',
$project->getId(),
$project
->setAttribute('auths', $auths)
);
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/auth/password-dictionary')
Http::patch('/v1/projects/:projectId/auth/password-dictionary')
->desc('Update authentication password dictionary status. Use this endpoint to enable or disable the dicitonary check for user password')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -783,7 +801,6 @@ App::patch('/v1/projects/:projectId/auth/password-dictionary')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, bool $enabled, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -793,13 +810,17 @@ App::patch('/v1/projects/:projectId/auth/password-dictionary')
$auths = $project->getAttribute('auths', []);
$auths['passwordDictionary'] = $enabled;
$dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$dbForConsole->updateDocument(
'projects',
$project->getId(),
$project
->setAttribute('auths', $auths)
);
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/auth/personal-data')
Http::patch('/v1/projects/:projectId/auth/personal-data')
->desc('Enable or disable checking user passwords for similarity with their personal data.')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -814,7 +835,6 @@ App::patch('/v1/projects/:projectId/auth/personal-data')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, bool $enabled, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -824,13 +844,17 @@ App::patch('/v1/projects/:projectId/auth/personal-data')
$auths = $project->getAttribute('auths', []);
$auths['personalDataCheck'] = $enabled;
$dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$dbForConsole->updateDocument(
'projects',
$project->getId(),
$project
->setAttribute('auths', $auths)
);
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/auth/max-sessions')
Http::patch('/v1/projects/:projectId/auth/max-sessions')
->desc('Update project user sessions limit')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -845,7 +869,6 @@ App::patch('/v1/projects/:projectId/auth/max-sessions')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -855,13 +878,17 @@ App::patch('/v1/projects/:projectId/auth/max-sessions')
$auths = $project->getAttribute('auths', []);
$auths['maxSessions'] = $limit;
$dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$dbForConsole->updateDocument(
'projects',
$project->getId(),
$project
->setAttribute('auths', $auths)
);
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/auth/mock-numbers')
Http::patch('/v1/projects/:projectId/auth/mock-numbers')
->desc('Update the mock numbers for the project')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -900,7 +927,7 @@ App::patch('/v1/projects/:projectId/auth/mock-numbers')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::delete('/v1/projects/:projectId')
Http::delete('/v1/projects/:projectId')
->desc('Delete project')
->groups(['api', 'projects'])
->label('audits.event', 'projects.delete')
@ -936,7 +963,7 @@ App::delete('/v1/projects/:projectId')
// Webhooks
App::post('/v1/projects/:projectId/webhooks')
Http::post('/v1/projects/:projectId/webhooks')
->desc('Create webhook')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -957,14 +984,13 @@ App::post('/v1/projects/:projectId/webhooks')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$security = (bool) filter_var($security, FILTER_VALIDATE_BOOLEAN);
$security = (bool)filter_var($security, FILTER_VALIDATE_BOOLEAN);
$webhook = new Document([
'$id' => ID::unique(),
@ -994,7 +1020,7 @@ App::post('/v1/projects/:projectId/webhooks')
->dynamic($webhook, Response::MODEL_WEBHOOK);
});
App::get('/v1/projects/:projectId/webhooks')
Http::get('/v1/projects/:projectId/webhooks')
->desc('List webhooks')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
@ -1008,7 +1034,6 @@ App::get('/v1/projects/:projectId/webhooks')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1026,7 +1051,7 @@ App::get('/v1/projects/:projectId/webhooks')
]), Response::MODEL_WEBHOOK_LIST);
});
App::get('/v1/projects/:projectId/webhooks/:webhookId')
Http::get('/v1/projects/:projectId/webhooks/:webhookId')
->desc('Get webhook')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
@ -1041,7 +1066,6 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1053,14 +1077,14 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($webhook === false || $webhook->isEmpty()) {
if ($webhook->isEmpty()) {
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
}
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
});
App::put('/v1/projects/:projectId/webhooks/:webhookId')
Http::put('/v1/projects/:projectId/webhooks/:webhookId')
->desc('Update webhook')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -1082,7 +1106,6 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $webhookId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1096,7 +1119,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($webhook === false || $webhook->isEmpty()) {
if ($webhook->isEmpty()) {
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
}
@ -1119,7 +1142,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
});
App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature')
Http::patch('/v1/projects/:projectId/webhooks/:webhookId/signature')
->desc('Update webhook signature key')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -1134,7 +1157,6 @@ App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1146,7 +1168,7 @@ App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($webhook === false || $webhook->isEmpty()) {
if ($webhook->isEmpty()) {
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
}
@ -1158,7 +1180,7 @@ App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature')
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
});
App::delete('/v1/projects/:projectId/webhooks/:webhookId')
Http::delete('/v1/projects/:projectId/webhooks/:webhookId')
->desc('Delete webhook')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -1172,7 +1194,6 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1184,7 +1205,7 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($webhook === false || $webhook->isEmpty()) {
if ($webhook->isEmpty()) {
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
}
@ -1197,7 +1218,7 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId')
// Keys
App::post('/v1/projects/:projectId/keys')
Http::post('/v1/projects/:projectId/keys')
->desc('Create key')
->groups(['api', 'projects'])
->label('scope', 'keys.write')
@ -1214,7 +1235,6 @@ App::post('/v1/projects/:projectId/keys')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1247,7 +1267,7 @@ App::post('/v1/projects/:projectId/keys')
->dynamic($key, Response::MODEL_KEY);
});
App::get('/v1/projects/:projectId/keys')
Http::get('/v1/projects/:projectId/keys')
->desc('List keys')
->groups(['api', 'projects'])
->label('scope', 'keys.read')
@ -1261,7 +1281,6 @@ App::get('/v1/projects/:projectId/keys')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1279,7 +1298,7 @@ App::get('/v1/projects/:projectId/keys')
]), Response::MODEL_KEY_LIST);
});
App::get('/v1/projects/:projectId/keys/:keyId')
Http::get('/v1/projects/:projectId/keys/:keyId')
->desc('Get key')
->groups(['api', 'projects'])
->label('scope', 'keys.read')
@ -1294,7 +1313,6 @@ App::get('/v1/projects/:projectId/keys/:keyId')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $keyId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1306,14 +1324,14 @@ App::get('/v1/projects/:projectId/keys/:keyId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($key === false || $key->isEmpty()) {
if ($key->isEmpty()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
$response->dynamic($key, Response::MODEL_KEY);
});
App::put('/v1/projects/:projectId/keys/:keyId')
Http::put('/v1/projects/:projectId/keys/:keyId')
->desc('Update key')
->groups(['api', 'projects'])
->label('scope', 'keys.write')
@ -1331,7 +1349,6 @@ App::put('/v1/projects/:projectId/keys/:keyId')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1343,7 +1360,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($key === false || $key->isEmpty()) {
if ($key->isEmpty()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
@ -1359,7 +1376,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
$response->dynamic($key, Response::MODEL_KEY);
});
App::delete('/v1/projects/:projectId/keys/:keyId')
Http::delete('/v1/projects/:projectId/keys/:keyId')
->desc('Delete key')
->groups(['api', 'projects'])
->label('scope', 'keys.write')
@ -1373,7 +1390,6 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $keyId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1385,7 +1401,7 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($key === false || $key->isEmpty()) {
if ($key->isEmpty()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
@ -1398,7 +1414,7 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
// JWT Keys
App::post('/v1/projects/:projectId/jwts')
Http::post('/v1/projects/:projectId/jwts')
->groups(['api', 'projects'])
->desc('Create JWT')
->label('scope', 'projects.write')
@ -1433,7 +1449,7 @@ App::post('/v1/projects/:projectId/jwts')
// Platforms
App::post('/v1/projects/:projectId/platforms')
Http::post('/v1/projects/:projectId/platforms')
->desc('Create platform')
->groups(['api', 'projects'])
->label('audits.event', 'platforms.create')
@ -1484,7 +1500,7 @@ App::post('/v1/projects/:projectId/platforms')
->dynamic($platform, Response::MODEL_PLATFORM);
});
App::get('/v1/projects/:projectId/platforms')
Http::get('/v1/projects/:projectId/platforms')
->desc('List platforms')
->groups(['api', 'projects'])
->label('scope', 'platforms.read')
@ -1498,7 +1514,6 @@ App::get('/v1/projects/:projectId/platforms')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1516,7 +1531,7 @@ App::get('/v1/projects/:projectId/platforms')
]), Response::MODEL_PLATFORM_LIST);
});
App::get('/v1/projects/:projectId/platforms/:platformId')
Http::get('/v1/projects/:projectId/platforms/:platformId')
->desc('Get platform')
->groups(['api', 'projects'])
->label('scope', 'platforms.read')
@ -1531,7 +1546,6 @@ App::get('/v1/projects/:projectId/platforms/:platformId')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $platformId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1543,14 +1557,14 @@ App::get('/v1/projects/:projectId/platforms/:platformId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($platform === false || $platform->isEmpty()) {
if ($platform->isEmpty()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
$response->dynamic($platform, Response::MODEL_PLATFORM);
});
App::put('/v1/projects/:projectId/platforms/:platformId')
Http::put('/v1/projects/:projectId/platforms/:platformId')
->desc('Update platform')
->groups(['api', 'projects'])
->label('scope', 'platforms.write')
@ -1580,7 +1594,7 @@ App::put('/v1/projects/:projectId/platforms/:platformId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($platform === false || $platform->isEmpty()) {
if ($platform->isEmpty()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
@ -1597,7 +1611,7 @@ App::put('/v1/projects/:projectId/platforms/:platformId')
$response->dynamic($platform, Response::MODEL_PLATFORM);
});
App::delete('/v1/projects/:projectId/platforms/:platformId')
Http::delete('/v1/projects/:projectId/platforms/:platformId')
->desc('Delete platform')
->groups(['api', 'projects'])
->label('audits.event', 'platforms.delete')
@ -1612,7 +1626,6 @@ App::delete('/v1/projects/:projectId/platforms/:platformId')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $platformId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1624,7 +1637,7 @@ App::delete('/v1/projects/:projectId/platforms/:platformId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($platform === false || $platform->isEmpty()) {
if ($platform->isEmpty()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
@ -1637,7 +1650,7 @@ App::delete('/v1/projects/:projectId/platforms/:platformId')
// CUSTOM SMTP and Templates
App::patch('/v1/projects/:projectId/smtp')
Http::patch('/v1/projects/:projectId/smtp')
->desc('Update SMTP')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -1660,7 +1673,6 @@ App::patch('/v1/projects/:projectId/smtp')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, bool $enabled, string $senderName, string $senderEmail, string $replyTo, string $host, int $port, string $username, string $password, string $secure, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1727,7 +1739,7 @@ App::patch('/v1/projects/:projectId/smtp')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::post('/v1/projects/:projectId/smtp/tests')
Http::post('/v1/projects/:projectId/smtp/tests')
->desc('Create SMTP test')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -1786,7 +1798,7 @@ App::post('/v1/projects/:projectId/smtp/tests')
return $response->noContent();
});
App::get('/v1/projects/:projectId/templates/sms/:type/:locale')
Http::get('/v1/projects/:projectId/templates/sms/:type/:locale')
->desc('Get custom SMS template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -1802,7 +1814,6 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
$project = $dbForConsole->getDocument('projects', $projectId);
@ -1812,7 +1823,7 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale')
}
$templates = $project->getAttribute('templates', []);
$template = $templates['sms.' . $type . '-' . $locale] ?? null;
$template = $templates['sms.' . $type . '-' . $locale] ?? null;
if (is_null($template)) {
$template = [
@ -1827,7 +1838,7 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale')
});
App::get('/v1/projects/:projectId/templates/email/:type/:locale')
Http::get('/v1/projects/:projectId/templates/email/:type/:locale')
->desc('Get custom email template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -1843,7 +1854,6 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1851,7 +1861,7 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale')
}
$templates = $project->getAttribute('templates', []);
$template = $templates['email.' . $type . '-' . $locale] ?? null;
$template = $templates['email.' . $type . '-' . $locale] ?? null;
$localeObj = new Locale($locale);
if (is_null($template)) {
@ -1859,7 +1869,7 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale')
$message
->setParam('{{hello}}', $localeObj->getText("emails.{$type}.hello"))
->setParam('{{footer}}', $localeObj->getText("emails.{$type}.footer"))
->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body'), escapeHtml: false)
->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body'), escape: false)
->setParam('{{thanks}}', $localeObj->getText("emails.{$type}.thanks"))
->setParam('{{signature}}', $localeObj->getText("emails.{$type}.signature"))
->setParam('{{direction}}', $localeObj->getText('settings.direction'));
@ -1879,7 +1889,7 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale')
$response->dynamic(new Document($template), Response::MODEL_EMAIL_TEMPLATE);
});
App::patch('/v1/projects/:projectId/templates/sms/:type/:locale')
Http::patch('/v1/projects/:projectId/templates/sms/:type/:locale')
->desc('Update custom SMS template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -1896,7 +1906,6 @@ App::patch('/v1/projects/:projectId/templates/sms/:type/:locale')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, string $message, Response $response, Database $dbForConsole) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
$project = $dbForConsole->getDocument('projects', $projectId);
@ -1919,7 +1928,7 @@ App::patch('/v1/projects/:projectId/templates/sms/:type/:locale')
]), Response::MODEL_SMS_TEMPLATE);
});
App::patch('/v1/projects/:projectId/templates/email/:type/:locale')
Http::patch('/v1/projects/:projectId/templates/email/:type/:locale')
->desc('Update custom email templates')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -1940,7 +1949,6 @@ App::patch('/v1/projects/:projectId/templates/email/:type/:locale')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -1969,7 +1977,7 @@ App::patch('/v1/projects/:projectId/templates/email/:type/:locale')
]), Response::MODEL_EMAIL_TEMPLATE);
});
App::delete('/v1/projects/:projectId/templates/sms/:type/:locale')
Http::delete('/v1/projects/:projectId/templates/sms/:type/:locale')
->desc('Reset custom SMS template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -1985,7 +1993,6 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
$project = $dbForConsole->getDocument('projects', $projectId);
@ -1995,7 +2002,7 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale')
}
$templates = $project->getAttribute('templates', []);
$template = $templates['sms.' . $type . '-' . $locale] ?? null;
$template = $templates['sms.' . $type . '-' . $locale] ?? null;
if (is_null($template)) {
throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION);
@ -2012,7 +2019,7 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale')
]), Response::MODEL_SMS_TEMPLATE);
});
App::delete('/v1/projects/:projectId/templates/email/:type/:locale')
Http::delete('/v1/projects/:projectId/templates/email/:type/:locale')
->desc('Reset custom email template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -2028,7 +2035,6 @@ App::delete('/v1/projects/:projectId/templates/email/:type/:locale')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -2036,7 +2042,7 @@ App::delete('/v1/projects/:projectId/templates/email/:type/:locale')
}
$templates = $project->getAttribute('templates', []);
$template = $templates['email.' . $type . '-' . $locale] ?? null;
$template = $templates['email.' . $type . '-' . $locale] ?? null;
if (is_null($template)) {
throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION);

View file

@ -7,7 +7,6 @@ use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Utopia\Database\Validator\Queries\Rules;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
@ -15,13 +14,14 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Utopia\Http\Http;
use Utopia\Http\Validator\Domain as ValidatorDomain;
use Utopia\Http\Validator\Text;
use Utopia\Http\Validator\WhiteList;
use Utopia\Logger\Log;
use Utopia\System\System;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
App::post('/v1/proxy/rules')
Http::post('/v1/proxy/rules')
->groups(['api', 'proxy'])
->desc('Create rule')
->label('scope', 'rules.write')
@ -63,7 +63,7 @@ App::post('/v1/proxy/rules')
Query::equal('domain', [$domain]),
]);
if ($document && !$document->isEmpty()) {
if (!$document->isEmpty()) {
if ($document->getAttribute('projectId') === $project->getId()) {
$resourceType = $document->getAttribute('resourceType');
$resourceId = $document->getAttribute('resourceId');
@ -147,7 +147,7 @@ App::post('/v1/proxy/rules')
->dynamic($rule, Response::MODEL_PROXY_RULE);
});
App::get('/v1/proxy/rules')
Http::get('/v1/proxy/rules')
->groups(['api', 'proxy'])
->desc('List rules')
->label('scope', 'rules.read')
@ -210,7 +210,7 @@ App::get('/v1/proxy/rules')
]), Response::MODEL_PROXY_RULE_LIST);
});
App::get('/v1/proxy/rules/:ruleId')
Http::get('/v1/proxy/rules/:ruleId')
->groups(['api', 'proxy'])
->desc('Get rule')
->label('scope', 'rules.read')
@ -239,7 +239,7 @@ App::get('/v1/proxy/rules/:ruleId')
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
});
App::delete('/v1/proxy/rules/:ruleId')
Http::delete('/v1/proxy/rules/:ruleId')
->groups(['api', 'proxy'])
->desc('Delete rule')
->label('scope', 'rules.write')
@ -276,8 +276,8 @@ App::delete('/v1/proxy/rules/:ruleId')
$response->noContent();
});
App::patch('/v1/proxy/rules/:ruleId/verification')
->desc('Update rule verification status')
Http::patch('/v1/proxy/rules/:ruleId/verification')
->desc('Update Rule Verification Status')
->groups(['api', 'proxy'])
->label('scope', 'rules.write')
->label('event', 'rules.[ruleId].update')

View file

@ -11,8 +11,8 @@ use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Buckets;
use Appwrite\Utopia\Database\Validator\Queries\Files;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
@ -23,8 +23,16 @@ use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Utopia\Http\Http;
use Utopia\Http\Validator\ArrayList;
use Utopia\Http\Validator\Boolean;
use Utopia\Http\Validator\HexColor;
use Utopia\Http\Validator\Range;
use Utopia\Http\Validator\Text;
use Utopia\Http\Validator\WhiteList;
use Utopia\Image\Image;
use Utopia\Storage\Compression\Algorithms\GZIP;
use Utopia\Storage\Compression\Algorithms\Zstd;
@ -35,16 +43,9 @@ use Utopia\Storage\Validator\File;
use Utopia\Storage\Validator\FileExt;
use Utopia\Storage\Validator\FileSize;
use Utopia\Storage\Validator\Upload;
use Utopia\Swoole\Request;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\HexColor;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
App::post('/v1/storage/buckets')
Http::post('/v1/storage/buckets')
->desc('Create bucket')
->groups(['api', 'storage'])
->label('scope', 'buckets.write')
@ -142,7 +143,7 @@ App::post('/v1/storage/buckets')
->dynamic($bucket, Response::MODEL_BUCKET);
});
App::get('/v1/storage/buckets')
Http::get('/v1/storage/buckets')
->desc('List buckets')
->groups(['api', 'storage'])
->label('scope', 'buckets.read')
@ -196,7 +197,7 @@ App::get('/v1/storage/buckets')
]), Response::MODEL_BUCKET_LIST);
});
App::get('/v1/storage/buckets/:bucketId')
Http::get('/v1/storage/buckets/:bucketId')
->desc('Get bucket')
->groups(['api', 'storage'])
->label('scope', 'buckets.read')
@ -221,7 +222,7 @@ App::get('/v1/storage/buckets/:bucketId')
$response->dynamic($bucket, Response::MODEL_BUCKET);
});
App::put('/v1/storage/buckets/:bucketId')
Http::put('/v1/storage/buckets/:bucketId')
->desc('Update bucket')
->groups(['api', 'storage'])
->label('scope', 'buckets.write')
@ -288,7 +289,7 @@ App::put('/v1/storage/buckets/:bucketId')
$response->dynamic($bucket, Response::MODEL_BUCKET);
});
App::delete('/v1/storage/buckets/:bucketId')
Http::delete('/v1/storage/buckets/:bucketId')
->desc('Delete bucket')
->groups(['api', 'storage'])
->label('scope', 'buckets.write')
@ -329,7 +330,7 @@ App::delete('/v1/storage/buckets/:bucketId')
$response->noContent();
});
App::post('/v1/storage/buckets/:bucketId/files')
Http::post('/v1/storage/buckets/:bucketId/files')
->alias('/v1/storage/files', ['bucketId' => 'default'])
->desc('Create file')
->groups(['api', 'storage'])
@ -361,19 +362,19 @@ App::post('/v1/storage/buckets/:bucketId/files')
->inject('mode')
->inject('deviceForFiles')
->inject('deviceForLocal')
->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, string $mode, Device $deviceForFiles, Device $deviceForLocal) {
->inject('authorization')
->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, string $mode, Device $deviceForFiles, Device $deviceForLocal, Authorization $authorization) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$validator = new Authorization(Database::PERMISSION_CREATE);
if (!$validator->isValid($bucket->getCreate())) {
if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
@ -397,7 +398,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
}
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
@ -410,7 +411,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
$permission->getIdentifier(),
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
if (!$authorization->isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')');
}
}
@ -630,11 +631,10 @@ App::post('/v1/storage/buckets/:bucketId/files')
* However as with chunk upload even if we are updating, we are essentially creating a file
* adding it's new chunk so we validate create permission instead of update
*/
$validator = new Authorization(Database::PERMISSION_CREATE);
if (!$validator->isValid($bucket->getCreate())) {
if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
$file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
}
} else {
if ($file->isEmpty()) {
@ -669,11 +669,10 @@ App::post('/v1/storage/buckets/:bucketId/files')
* However as with chunk upload even if we are updating, we are essentially creating a file
* adding it's new chunk so we validate create permission instead of update
*/
$validator = new Authorization(Database::PERMISSION_CREATE);
if (!$validator->isValid($bucket->getCreate())) {
if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
$file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
}
}
@ -690,7 +689,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
->dynamic($file, Response::MODEL_FILE);
});
App::get('/v1/storage/buckets/:bucketId/files')
Http::get('/v1/storage/buckets/:bucketId/files')
->alias('/v1/storage/files', ['bucketId' => 'default'])
->desc('List files')
->groups(['api', 'storage'])
@ -708,19 +707,19 @@ App::get('/v1/storage/buckets/:bucketId/files')
->inject('response')
->inject('dbForProject')
->inject('mode')
->action(function (string $bucketId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
->inject('authorization')
->action(function (string $bucketId, array $queries, string $search, Response $response, Database $dbForProject, string $mode, Authorization $authorization) {
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
$valid = $validator->isValid($bucket->getRead());
$valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead()));
if (!$fileSecurity && !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
@ -749,7 +748,7 @@ App::get('/v1/storage/buckets/:bucketId/files')
if ($fileSecurity && !$valid) {
$cursorDocument = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
} else {
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
}
if ($cursorDocument->isEmpty()) {
@ -765,8 +764,8 @@ App::get('/v1/storage/buckets/:bucketId/files')
$files = $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries);
$total = $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT);
} else {
$files = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries));
$total = Authorization::skip(fn () => $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT));
$files = $authorization->skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries));
$total = $authorization->skip(fn () => $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT));
}
$response->dynamic(new Document([
@ -775,7 +774,7 @@ App::get('/v1/storage/buckets/:bucketId/files')
]), Response::MODEL_FILE_LIST);
});
App::get('/v1/storage/buckets/:bucketId/files/:fileId')
Http::get('/v1/storage/buckets/:bucketId/files/:fileId')
->alias('/v1/storage/files/:fileId', ['bucketId' => 'default'])
->desc('Get file')
->groups(['api', 'storage'])
@ -792,19 +791,19 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
->inject('response')
->inject('dbForProject')
->inject('mode')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
->inject('authorization')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode, Authorization $authorization) {
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
$valid = $validator->isValid($bucket->getRead());
$valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead()));
if (!$fileSecurity && !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
@ -812,7 +811,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
if ($fileSecurity && !$valid) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
} else {
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
}
if ($file->isEmpty()) {
@ -822,7 +821,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
$response->dynamic($file, Response::MODEL_FILE);
});
App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
Http::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->alias('/v1/storage/files/:fileId/preview', ['bucketId' => 'default'])
->desc('Get file preview')
->groups(['api', 'storage'])
@ -857,24 +856,24 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->inject('mode')
->inject('deviceForFiles')
->inject('deviceForLocal')
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceForFiles, Device $deviceForLocal) {
->inject('authorization')
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceForFiles, Device $deviceForLocal, Authorization $authorization) {
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
}
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
$valid = $validator->isValid($bucket->getRead());
$valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead()));
if (!$fileSecurity && !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
@ -882,7 +881,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
if ($fileSecurity && !$valid) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
} else {
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
}
if ($file->isEmpty()) {
@ -994,7 +993,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
unset($image);
});
App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
Http::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
->alias('/v1/storage/files/:fileId/download', ['bucketId' => 'default'])
->desc('Get file for download')
->groups(['api', 'storage'])
@ -1013,20 +1012,20 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
->inject('dbForProject')
->inject('mode')
->inject('deviceForFiles')
->action(function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, string $mode, Device $deviceForFiles) {
->inject('authorization')
->action(function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, string $mode, Device $deviceForFiles, Authorization $authorization) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
$valid = $validator->isValid($bucket->getRead());
$valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead()));
if (!$fileSecurity && !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
@ -1034,7 +1033,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
if ($fileSecurity && !$valid) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
} else {
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
}
if ($file->isEmpty()) {
@ -1134,7 +1133,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
}
});
App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
Http::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
->alias('/v1/storage/files/:fileId/view', ['bucketId' => 'default'])
->desc('Get file for view')
->groups(['api', 'storage'])
@ -1153,19 +1152,19 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
->inject('dbForProject')
->inject('mode')
->inject('deviceForFiles')
->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, string $mode, Device $deviceForFiles) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
->inject('authorization')
->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, string $mode, Device $deviceForFiles, Authorization $authorization) {
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
$valid = $validator->isValid($bucket->getRead());
$valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead()));
if (!$fileSecurity && !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
@ -1173,7 +1172,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
if ($fileSecurity && !$valid) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
} else {
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
}
if ($file->isEmpty()) {
@ -1286,7 +1285,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
}
});
App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
Http::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
->desc('Get file for push notification')
->groups(['api', 'storage'])
->label('scope', 'public')
@ -1302,8 +1301,9 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
->inject('project')
->inject('mode')
->inject('deviceForFiles')
->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Document $project, string $mode, Device $deviceForFiles) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
->inject('authorization')
->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Document $project, string $mode, Device $deviceForFiles, Authorization $authorization) {
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
@ -1321,14 +1321,14 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
@ -1439,7 +1439,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
}
});
App::put('/v1/storage/buckets/:bucketId/files/:fileId')
Http::put('/v1/storage/buckets/:bucketId/files/:fileId')
->alias('/v1/storage/files/:fileId', ['bucketId' => 'default'])
->desc('Update file')
->groups(['api', 'storage'])
@ -1466,26 +1466,26 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
->inject('user')
->inject('mode')
->inject('queueForEvents')
->action(function (string $bucketId, string $fileId, ?string $name, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $queueForEvents) {
->inject('authorization')
->action(function (string $bucketId, string $fileId, ?string $name, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $queueForEvents, Authorization $authorization) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_UPDATE);
$valid = $validator->isValid($bucket->getUpdate());
$valid = $authorization->isValid(new Input(Database::PERMISSION_UPDATE, $bucket->getUpdate()));
if (!$fileSecurity && !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
// Read permission should not be required for update
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
@ -1499,7 +1499,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
]);
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles) && !\is_null($permissions)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
@ -1512,7 +1512,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
$permission->getIdentifier(),
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
if (!$authorization->isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')');
}
}
@ -1532,7 +1532,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
if ($fileSecurity && !$valid) {
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
} else {
$file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
$file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
}
$queueForEvents
@ -1544,8 +1544,8 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
$response->dynamic($file, Response::MODEL_FILE);
});
App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->desc('Delete file')
Http::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->desc('Delete File')
->groups(['api', 'storage'])
->label('scope', 'files.write')
->label('event', 'buckets.[bucketId].files.[fileId].delete')
@ -1568,32 +1568,32 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->inject('mode')
->inject('deviceForFiles')
->inject('queueForDeletes')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceForFiles, Delete $queueForDeletes) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
->inject('authorization')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceForFiles, Delete $queueForDeletes, Authorization $authorization) {
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_DELETE);
$valid = $validator->isValid($bucket->getDelete());
$valid = $authorization->isValid(new Input(Database::PERMISSION_DELETE, $bucket->getDelete()));
if (!$fileSecurity && !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
// Read permission should not be required for delete
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
// Make sure we don't delete the file before the document permission check occurs
if ($fileSecurity && !$valid && !$validator->isValid($file->getDelete())) {
if ($fileSecurity && !$valid && !$authorization->isValid(new Input(Database::PERMISSION_DELETE, $file->getDelete()))) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
@ -1617,7 +1617,7 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
if ($fileSecurity && !$valid) {
$deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId);
} else {
$deleted = Authorization::skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId));
$deleted = $authorization->skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId));
}
if (!$deleted) {
@ -1637,7 +1637,7 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
$response->noContent();
});
App::get('/v1/storage/usage')
Http::get('/v1/storage/usage')
->desc('Get storage usage stats')
->groups(['api', 'storage'])
->label('scope', 'files.read')
@ -1650,7 +1650,8 @@ App::get('/v1/storage/usage')
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $range, Response $response, Database $dbForProject) {
->inject('authorization')
->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) {
$periods = Config::getParam('usage', []);
$stats = $usage = [];
@ -1662,7 +1663,7 @@ App::get('/v1/storage/usage')
];
$total = [];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
$authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
@ -1716,7 +1717,7 @@ App::get('/v1/storage/usage')
]), Response::MODEL_USAGE_STORAGE);
});
App::get('/v1/storage/:bucketId/usage')
Http::get('/v1/storage/:bucketId/usage')
->desc('Get bucket usage stats')
->groups(['api', 'storage'])
->label('scope', 'files.read')
@ -1730,7 +1731,8 @@ App::get('/v1/storage/:bucketId/usage')
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $bucketId, string $range, Response $response, Database $dbForProject) {
->inject('authorization')
->action(function (string $bucketId, string $range, Response $response, Database $dbForProject, Authorization $authorization) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
@ -1746,8 +1748,7 @@ App::get('/v1/storage/:bucketId/usage')
str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE),
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
$authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),

View file

@ -1,6 +1,7 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Auth\Authentication;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
@ -18,7 +19,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Teams;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Utopia\Database\Database;
@ -37,15 +37,16 @@ use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Http;
use Utopia\Http\Validator\ArrayList;
use Utopia\Http\Validator\Assoc;
use Utopia\Http\Validator\Host;
use Utopia\Http\Validator\Text;
use Utopia\Http\Validator\WhiteList;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Host;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
App::post('/v1/teams')
Http::post('/v1/teams')
->desc('Create team')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].create')
@ -66,15 +67,16 @@ App::post('/v1/teams')
->inject('user')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
->inject('authorization')
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization) {
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
$isAppUser = Auth::isAppUser($authorization->getRoles());
$teamId = $teamId == 'unique()' ? ID::unique() : $teamId;
try {
$team = Authorization::skip(fn () => $dbForProject->createDocument('teams', new Document([
$team = $authorization->skip(fn () => $dbForProject->createDocument('teams', new Document([
'$id' => $teamId,
'$permissions' => [
Permission::read(Role::team($teamId)),
@ -133,7 +135,7 @@ App::post('/v1/teams')
->dynamic($team, Response::MODEL_TEAM);
});
App::get('/v1/teams')
Http::get('/v1/teams')
->desc('List teams')
->groups(['api', 'teams'])
->label('scope', 'teams.read')
@ -191,7 +193,7 @@ App::get('/v1/teams')
]), Response::MODEL_TEAM_LIST);
});
App::get('/v1/teams/:teamId')
Http::get('/v1/teams/:teamId')
->desc('Get team')
->groups(['api', 'teams'])
->label('scope', 'teams.read')
@ -218,7 +220,7 @@ App::get('/v1/teams/:teamId')
$response->dynamic($team, Response::MODEL_TEAM);
});
App::get('/v1/teams/:teamId/prefs')
Http::get('/v1/teams/:teamId/prefs')
->desc('Get team preferences')
->groups(['api', 'teams'])
->label('scope', 'teams.read')
@ -246,7 +248,7 @@ App::get('/v1/teams/:teamId/prefs')
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
App::put('/v1/teams/:teamId')
Http::put('/v1/teams/:teamId')
->desc('Update name')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].update')
@ -289,7 +291,7 @@ App::put('/v1/teams/:teamId')
$response->dynamic($team, Response::MODEL_TEAM);
});
App::put('/v1/teams/:teamId/prefs')
Http::put('/v1/teams/:teamId/prefs')
->desc('Update preferences')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].update.prefs')
@ -325,7 +327,7 @@ App::put('/v1/teams/:teamId/prefs')
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
App::delete('/v1/teams/:teamId')
Http::delete('/v1/teams/:teamId')
->desc('Delete team')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].delete')
@ -374,7 +376,7 @@ App::delete('/v1/teams/:teamId')
$response->noContent();
});
App::post('/v1/teams/:teamId/memberships')
Http::post('/v1/teams/:teamId/memberships')
->desc('Create team membership')
->groups(['api', 'teams', 'auth'])
->label('event', 'teams.[teamId].memberships.[membershipId].create')
@ -416,9 +418,10 @@ App::post('/v1/teams/:teamId/memberships')
->inject('queueForMails')
->inject('queueForMessaging')
->inject('queueForEvents')
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents) {
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
->inject('authorization')
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, Authorization $authorization) {
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
$url = htmlentities($url);
if (empty($url)) {
@ -430,8 +433,8 @@ App::post('/v1/teams/:teamId/memberships')
if (empty($userId) && empty($email) && empty($phone)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'At least one of userId, email, or phone is required');
}
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
$isAppUser = Auth::isAppUser($authorization->getRoles());
if (!$isPrivilegedUser && !$isAppUser && empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED);
@ -460,17 +463,17 @@ App::post('/v1/teams/:teamId/memberships')
$name = empty($name) ? $invitee->getAttribute('name', '') : $name;
} elseif (!empty($email)) {
$invitee = $dbForProject->findOne('users', [Query::equal('email', [$email])]); // Get user by email address
if (!empty($invitee) && !empty($phone) && $invitee->getAttribute('phone', '') !== $phone) {
if ($invitee->isEmpty() && !empty($phone) && $invitee->getAttribute('phone', '') !== $phone) {
throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given email and phone doesn\'t match', 409);
}
} elseif (!empty($phone)) {
$invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]);
if (!empty($invitee) && !empty($email) && $invitee->getAttribute('email', '') !== $email) {
if ($invitee->isEmpty() && !empty($email) && $invitee->getAttribute('email', '') !== $email) {
throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given phone and email doesn\'t match', 409);
}
}
if (empty($invitee)) { // Create new user if no user with same email found
if (!isset($invitee) || $invitee->isEmpty()) { // Create new user if no user with same email found
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
if (!$isPrivilegedUser && !$isAppUser && $limit !== 0 && $project->getId() !== 'console') { // check users limit, console invites are allways allowed.
@ -491,7 +494,7 @@ App::post('/v1/teams/:teamId/memberships')
try {
$userId = ID::unique();
$invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([
$invitee = $authorization->skip(fn () => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$permissions' => [
Permission::read(Role::any()),
@ -522,12 +525,13 @@ App::post('/v1/teams/:teamId/memberships')
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name]),
])));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
}
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
$isOwner = $authorization->isRole('team:' . $team->getId() . '/owner');
if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server)
throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to send invitations for this team');
@ -559,12 +563,12 @@ App::post('/v1/teams/:teamId/memberships')
if ($isPrivilegedUser || $isAppUser) { // Allow admin to create membership
try {
$membership = Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership));
$membership = $authorization->skip(fn () => $dbForProject->createDocument('memberships', $membership));
} catch (Duplicate $th) {
throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS);
}
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
$authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
$dbForProject->purgeCachedDocument('users', $invitee->getId());
} else {
@ -586,7 +590,7 @@ App::post('/v1/teams/:teamId/memberships')
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
$message
->setParam('{{body}}', $body, escapeHtml: false)
->setParam('{{body}}', $body, escape: false)
->setParam('{{hello}}', $locale->getText("emails.invitation.hello"))
->setParam('{{footer}}', $locale->getText("emails.invitation.footer"))
->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks"))
@ -704,7 +708,7 @@ App::post('/v1/teams/:teamId/memberships')
);
});
App::get('/v1/teams/:teamId/memberships')
Http::get('/v1/teams/:teamId/memberships')
->desc('List team memberships')
->groups(['api', 'teams'])
->label('scope', 'teams.read')
@ -807,7 +811,7 @@ App::get('/v1/teams/:teamId/memberships')
]), Response::MODEL_MEMBERSHIP_LIST);
});
App::get('/v1/teams/:teamId/memberships/:membershipId')
Http::get('/v1/teams/:teamId/memberships/:membershipId')
->desc('Get team membership')
->groups(['api', 'teams'])
->label('scope', 'teams.read')
@ -863,7 +867,7 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
$response->dynamic($membership, Response::MODEL_MEMBERSHIP);
});
App::patch('/v1/teams/:teamId/memberships/:membershipId')
Http::patch('/v1/teams/:teamId/memberships/:membershipId')
->desc('Update membership')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].memberships.[membershipId].update')
@ -895,7 +899,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->inject('user')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
->inject('authorization')
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
@ -912,9 +917,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
throw new Exception(Exception::USER_NOT_FOUND);
}
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
$isAppUser = Auth::isAppUser($authorization->getRoles());
$isOwner = $authorization->isRole('team:' . $team->getId() . '/owner');
if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server)
throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to modify roles');
@ -945,7 +950,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
);
});
App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
Http::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->desc('Update team membership status')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].memberships.[membershipId].update.status')
@ -971,7 +976,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->inject('project')
->inject('geodb')
->inject('queueForEvents')
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents) {
->inject('authorization')
->inject('authentication')
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Authorization $authorization, Authentication $authentication) {
$protocol = $request->getProtocol();
$membership = $dbForProject->getDocument('memberships', $membershipId);
@ -980,7 +987,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
throw new Exception(Exception::MEMBERSHIP_NOT_FOUND);
}
$team = Authorization::skip(fn () => $dbForProject->getDocument('teams', $teamId));
$team = $authorization->skip(fn () => $dbForProject->getDocument('teams', $teamId));
if ($team->isEmpty()) {
throw new Exception(Exception::TEAM_NOT_FOUND);
@ -1015,11 +1022,11 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->setAttribute('confirm', true)
;
Authorization::skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
$authorization->skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
// Log user in
Authorization::setRole(Role::user($user->getId())->toString());
$authorization->addRole(Role::user($user->getId())->toString());
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
@ -1049,13 +1056,13 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$dbForProject->purgeCachedDocument('users', $user->getId());
Authorization::setRole(Role::user($userId)->toString());
$authorization->addRole(Role::user($userId)->toString());
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
$dbForProject->purgeCachedDocument('users', $user->getId());
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
$authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
$queueForEvents
->setParam('userId', $user->getId())
@ -1065,13 +1072,13 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
if (!Config::getParam('domainVerification')) {
$response
->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]))
->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $secret)]))
;
}
$response
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
;
$response->dynamic(
@ -1083,7 +1090,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
);
});
App::delete('/v1/teams/:teamId/memberships/:membershipId')
Http::delete('/v1/teams/:teamId/memberships/:membershipId')
->desc('Delete team membership')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].memberships.[membershipId].delete')
@ -1101,7 +1108,8 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, Event $queueForEvents) {
->inject('authorization')
->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) {
$membership = $dbForProject->getDocument('memberships', $membershipId);
@ -1136,7 +1144,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
$dbForProject->purgeCachedDocument('users', $user->getId());
if ($membership->getAttribute('confirm')) { // Count only confirmed members
Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0));
$authorization->skip(fn () => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0));
}
$queueForEvents
@ -1149,7 +1157,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
$response->noContent();
});
App::get('/v1/teams/:teamId/logs')
Http::get('/v1/teams/:teamId/logs')
->desc('List team logs')
->groups(['api', 'teams'])
->label('scope', 'teams.read')
@ -1166,7 +1174,8 @@ App::get('/v1/teams/:teamId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $teamId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('authorization')
->action(function (string $teamId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) {
$team = $dbForProject->getDocument('teams', $teamId);

View file

@ -22,7 +22,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Users;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Utopia\Database\Database;
@ -39,15 +38,16 @@ use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Http;
use Utopia\Http\Validator\ArrayList;
use Utopia\Http\Validator\Assoc;
use Utopia\Http\Validator\Boolean;
use Utopia\Http\Validator\Integer;
use Utopia\Http\Validator\Range;
use Utopia\Http\Validator\Text;
use Utopia\Http\Validator\WhiteList;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
/** TODO: Remove function when we move to using utopia/platform */
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks): Document
@ -63,7 +63,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
if (!$identityWithMatchingEmail->isEmpty()) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
}
@ -140,7 +140,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
$existingTarget = $dbForProject->findOne('targets', [
Query::equal('identifier', [$email]),
]);
if ($existingTarget) {
if (!$existingTarget->isEmpty()) {
$user->setAttribute('targets', $existingTarget, Document::SET_TYPE_APPEND);
}
}
@ -164,7 +164,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
$existingTarget = $dbForProject->findOne('targets', [
Query::equal('identifier', [$phone]),
]);
if ($existingTarget) {
if (!$existingTarget->isEmpty()) {
$user->setAttribute('targets', $existingTarget, Document::SET_TYPE_APPEND);
}
}
@ -180,7 +180,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
return $user;
}
App::post('/v1/users')
Http::post('/v1/users')
->desc('Create user')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
@ -211,7 +211,7 @@ App::post('/v1/users')
->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/bcrypt')
Http::post('/v1/users/bcrypt')
->desc('Create user with bcrypt password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
@ -242,7 +242,7 @@ App::post('/v1/users/bcrypt')
->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/md5')
Http::post('/v1/users/md5')
->desc('Create user with MD5 password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
@ -273,7 +273,7 @@ App::post('/v1/users/md5')
->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/argon2')
Http::post('/v1/users/argon2')
->desc('Create user with Argon2 password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
@ -304,7 +304,7 @@ App::post('/v1/users/argon2')
->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/sha')
Http::post('/v1/users/sha')
->desc('Create user with SHA password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
@ -342,7 +342,7 @@ App::post('/v1/users/sha')
->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/phpass')
Http::post('/v1/users/phpass')
->desc('Create user with PHPass password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
@ -373,7 +373,7 @@ App::post('/v1/users/phpass')
->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/scrypt')
Http::post('/v1/users/scrypt')
->desc('Create user with Scrypt password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
@ -417,7 +417,7 @@ App::post('/v1/users/scrypt')
->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/scrypt-modified')
Http::post('/v1/users/scrypt-modified')
->desc('Create user with Scrypt modified password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
@ -451,7 +451,7 @@ App::post('/v1/users/scrypt-modified')
->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/:userId/targets')
Http::post('/v1/users/:userId/targets')
->desc('Create user target')
->groups(['api', 'users'])
->label('audits.event', 'target.create')
@ -540,7 +540,7 @@ App::post('/v1/users/:userId/targets')
->dynamic($target, Response::MODEL_TARGET);
});
App::get('/v1/users')
Http::get('/v1/users')
->desc('List users')
->groups(['api', 'users'])
->label('scope', 'users.read')
@ -594,7 +594,7 @@ App::get('/v1/users')
]), Response::MODEL_USER_LIST);
});
App::get('/v1/users/:userId')
Http::get('/v1/users/:userId')
->desc('Get user')
->groups(['api', 'users'])
->label('scope', 'users.read')
@ -619,7 +619,7 @@ App::get('/v1/users/:userId')
$response->dynamic($user, Response::MODEL_USER);
});
App::get('/v1/users/:userId/prefs')
Http::get('/v1/users/:userId/prefs')
->desc('Get user preferences')
->groups(['api', 'users'])
->label('scope', 'users.read')
@ -646,7 +646,7 @@ App::get('/v1/users/:userId/prefs')
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
App::get('/v1/users/:userId/targets/:targetId')
Http::get('/v1/users/:userId/targets/:targetId')
->desc('Get user target')
->groups(['api', 'users'])
->label('scope', 'targets.read')
@ -678,7 +678,7 @@ App::get('/v1/users/:userId/targets/:targetId')
$response->dynamic($target, Response::MODEL_TARGET);
});
App::get('/v1/users/:userId/sessions')
Http::get('/v1/users/:userId/sessions')
->desc('List user sessions')
->groups(['api', 'users'])
->label('scope', 'users.read')
@ -719,7 +719,7 @@ App::get('/v1/users/:userId/sessions')
]), Response::MODEL_SESSION_LIST);
});
App::get('/v1/users/:userId/memberships')
Http::get('/v1/users/:userId/memberships')
->desc('List user memberships')
->groups(['api', 'users'])
->label('scope', 'users.read')
@ -758,7 +758,7 @@ App::get('/v1/users/:userId/memberships')
]), Response::MODEL_MEMBERSHIP_LIST);
});
App::get('/v1/users/:userId/logs')
Http::get('/v1/users/:userId/logs')
->desc('List user logs')
->groups(['api', 'users'])
->label('scope', 'users.read')
@ -775,7 +775,8 @@ App::get('/v1/users/:userId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $userId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('authorization')
->action(function (string $userId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) {
$user = $dbForProject->getDocument('users', $userId);
@ -847,7 +848,7 @@ App::get('/v1/users/:userId/logs')
]), Response::MODEL_LOG_LIST);
});
App::get('/v1/users/:userId/targets')
Http::get('/v1/users/:userId/targets')
->desc('List user targets')
->groups(['api', 'users'])
->label('scope', 'targets.read')
@ -902,7 +903,7 @@ App::get('/v1/users/:userId/targets')
]), Response::MODEL_TARGET_LIST);
});
App::get('/v1/users/identities')
Http::get('/v1/users/identities')
->desc('List identities')
->groups(['api', 'users'])
->label('scope', 'users.read')
@ -956,7 +957,7 @@ App::get('/v1/users/identities')
]), Response::MODEL_IDENTITY_LIST);
});
App::patch('/v1/users/:userId/status')
Http::patch('/v1/users/:userId/status')
->desc('Update user status')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.status')
@ -992,7 +993,7 @@ App::patch('/v1/users/:userId/status')
$response->dynamic($user, Response::MODEL_USER);
});
App::put('/v1/users/:userId/labels')
Http::put('/v1/users/:userId/labels')
->desc('Update user labels')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.labels')
@ -1029,7 +1030,7 @@ App::put('/v1/users/:userId/labels')
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/verification/phone')
Http::patch('/v1/users/:userId/verification/phone')
->desc('Update phone verification')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.verification')
@ -1064,7 +1065,7 @@ App::patch('/v1/users/:userId/verification/phone')
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/name')
Http::patch('/v1/users/:userId/name')
->desc('Update name')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.name')
@ -1101,7 +1102,7 @@ App::patch('/v1/users/:userId/name')
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/password')
Http::patch('/v1/users/:userId/password')
->desc('Update password')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.password')
@ -1178,7 +1179,7 @@ App::patch('/v1/users/:userId/password')
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/email')
Http::patch('/v1/users/:userId/email')
->desc('Update email')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.email')
@ -1214,7 +1215,7 @@ App::patch('/v1/users/:userId/email')
Query::equal('providerEmail', [$email]),
Query::notEqual('userInternalId', $user->getInternalId()),
]);
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
if (!$identityWithMatchingEmail->isEmpty()) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
@ -1222,7 +1223,7 @@ App::patch('/v1/users/:userId/email')
Query::equal('identifier', [$email]),
]);
if ($target instanceof Document && !$target->isEmpty()) {
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
}
@ -1273,7 +1274,7 @@ App::patch('/v1/users/:userId/email')
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/phone')
Http::patch('/v1/users/:userId/phone')
->desc('Update phone')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.phone')
@ -1312,7 +1313,7 @@ App::patch('/v1/users/:userId/phone')
Query::equal('identifier', [$number]),
]);
if ($target instanceof Document && !$target->isEmpty()) {
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
}
@ -1356,7 +1357,7 @@ App::patch('/v1/users/:userId/phone')
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/verification')
Http::patch('/v1/users/:userId/verification')
->desc('Update email verification')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.verification')
@ -1391,7 +1392,7 @@ App::patch('/v1/users/:userId/verification')
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/prefs')
Http::patch('/v1/users/:userId/prefs')
->desc('Update user preferences')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.prefs')
@ -1424,7 +1425,7 @@ App::patch('/v1/users/:userId/prefs')
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
App::patch('/v1/users/:userId/targets/:targetId')
Http::patch('/v1/users/:userId/targets/:targetId')
->desc('Update user target')
->groups(['api', 'users'])
->label('audits.event', 'target.update')
@ -1518,7 +1519,7 @@ App::patch('/v1/users/:userId/targets/:targetId')
->dynamic($target, Response::MODEL_TARGET);
});
App::patch('/v1/users/:userId/mfa')
Http::patch('/v1/users/:userId/mfa')
->desc('Update MFA')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.mfa')
@ -1556,7 +1557,7 @@ App::patch('/v1/users/:userId/mfa')
$response->dynamic($user, Response::MODEL_USER);
});
App::get('/v1/users/:userId/mfa/factors')
Http::get('/v1/users/:userId/mfa/factors')
->desc('List factors')
->groups(['api', 'users'])
->label('scope', 'users.read')
@ -1589,7 +1590,7 @@ App::get('/v1/users/:userId/mfa/factors')
$response->dynamic($factors, Response::MODEL_MFA_FACTORS);
});
App::get('/v1/users/:userId/mfa/recovery-codes')
Http::get('/v1/users/:userId/mfa/recovery-codes')
->desc('Get MFA recovery codes')
->groups(['api', 'users'])
->label('scope', 'users.read')
@ -1624,7 +1625,7 @@ App::get('/v1/users/:userId/mfa/recovery-codes')
$response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES);
});
App::patch('/v1/users/:userId/mfa/recovery-codes')
Http::patch('/v1/users/:userId/mfa/recovery-codes')
->desc('Create MFA recovery codes')
->groups(['api', 'users'])
->label('event', 'users.[userId].create.mfa.recovery-codes')
@ -1670,7 +1671,7 @@ App::patch('/v1/users/:userId/mfa/recovery-codes')
$response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES);
});
App::put('/v1/users/:userId/mfa/recovery-codes')
Http::put('/v1/users/:userId/mfa/recovery-codes')
->desc('Regenerate MFA recovery codes')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.mfa.recovery-codes')
@ -1715,7 +1716,7 @@ App::put('/v1/users/:userId/mfa/recovery-codes')
$response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES);
});
App::delete('/v1/users/:userId/mfa/authenticators/:type')
Http::delete('/v1/users/:userId/mfa/authenticators/:type')
->desc('Delete authenticator')
->groups(['api', 'users'])
->label('event', 'users.[userId].delete.mfa')
@ -1757,7 +1758,7 @@ App::delete('/v1/users/:userId/mfa/authenticators/:type')
$response->noContent();
});
App::post('/v1/users/:userId/sessions')
Http::post('/v1/users/:userId/sessions')
->desc('Create session')
->groups(['api', 'users'])
->label('event', 'users.[userId].sessions.[sessionId].create')
@ -1827,7 +1828,7 @@ App::post('/v1/users/:userId/sessions')
->dynamic($session, Response::MODEL_SESSION);
});
App::post('/v1/users/:userId/tokens')
Http::post('/v1/users/:userId/tokens')
->desc('Create token')
->groups(['api', 'users'])
->label('event', 'users.[userId].tokens.[tokenId].create')
@ -1884,7 +1885,7 @@ App::post('/v1/users/:userId/tokens')
->dynamic($token, Response::MODEL_TOKEN);
});
App::delete('/v1/users/:userId/sessions/:sessionId')
Http::delete('/v1/users/:userId/sessions/:sessionId')
->desc('Delete user session')
->groups(['api', 'users'])
->label('event', 'users.[userId].sessions.[sessionId].delete')
@ -1927,7 +1928,7 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
$response->noContent();
});
App::delete('/v1/users/:userId/sessions')
Http::delete('/v1/users/:userId/sessions')
->desc('Delete user sessions')
->groups(['api', 'users'])
->label('event', 'users.[userId].sessions.delete')
@ -1969,7 +1970,7 @@ App::delete('/v1/users/:userId/sessions')
$response->noContent();
});
App::delete('/v1/users/:userId')
Http::delete('/v1/users/:userId')
->desc('Delete user')
->groups(['api', 'users'])
->label('event', 'users.[userId].delete')
@ -2011,7 +2012,7 @@ App::delete('/v1/users/:userId')
$response->noContent();
});
App::delete('/v1/users/:userId/targets/:targetId')
Http::delete('/v1/users/:userId/targets/:targetId')
->desc('Delete user target')
->groups(['api', 'users'])
->label('audits.event', 'target.delete')
@ -2062,7 +2063,7 @@ App::delete('/v1/users/:userId/targets/:targetId')
$response->noContent();
});
App::delete('/v1/users/identities/:identityId')
Http::delete('/v1/users/identities/:identityId')
->desc('Delete identity')
->groups(['api', 'users'])
->label('event', 'users.[userId].identities.[identityId].delete')
@ -2097,7 +2098,7 @@ App::delete('/v1/users/identities/:identityId')
return $response->noContent();
});
App::post('/v1/users/:userId/jwts')
Http::post('/v1/users/:userId/jwts')
->desc('Create user JWT')
->groups(['api', 'users'])
->label('scope', 'users.write')
@ -2147,7 +2148,7 @@ App::post('/v1/users/:userId/jwts')
])]), Response::MODEL_JWT);
});
App::get('/v1/users/usage')
Http::get('/v1/users/usage')
->desc('Get users usage stats')
->groups(['api', 'users'])
->label('scope', 'users.read')
@ -2160,8 +2161,8 @@ App::get('/v1/users/usage')
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->inject('register')
->action(function (string $range, Response $response, Database $dbForProject) {
->inject('authorization')
->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) {
$periods = Config::getParam('usage', []);
$stats = $usage = [];
@ -2171,7 +2172,7 @@ App::get('/v1/users/usage')
METRIC_SESSIONS,
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
$authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $count => $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),

View file

@ -8,7 +8,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Installations;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\Comment;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
@ -32,17 +31,17 @@ use Utopia\Detector\Adapter\Python;
use Utopia\Detector\Adapter\Ruby;
use Utopia\Detector\Adapter\Swift;
use Utopia\Detector\Detector;
use Utopia\Http\Http;
use Utopia\Http\Validator\Boolean;
use Utopia\Http\Validator\Host;
use Utopia\Http\Validator\Text;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\Host;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
use function Swoole\Coroutine\batch;
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForConsole, Build $queueForBuilds, callable $getProjectDB, Request $request) {
$errors = [];
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForConsole, Build $queueForBuilds, callable $getProjectDB, Request $request, Authorization $auth) {
foreach ($repositories as $resource) {
try {
$resourceType = $resource->getAttribute('resourceType');
@ -52,11 +51,11 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
$projectId = $resource->getAttribute('projectId');
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
$project = $auth->skip(fn () => $dbForConsole->getDocument('projects', $projectId));
$dbForProject = $getProjectDB($project);
$functionId = $resource->getAttribute('resourceId');
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
$function = $auth->skip(fn () => $dbForProject->getDocument('functions', $functionId));
$functionInternalId = $function->getInternalId();
$deploymentId = ID::unique();
@ -102,14 +101,14 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = '';
if (!empty($providerPullRequestId) && $function->getAttribute('providerSilentMode', false) === false) {
$latestComment = Authorization::skip(fn () => $dbForConsole->findOne('vcsComments', [
if (!empty($providerPullRequestId) && $function->getAttribute('providerSilentMode', false)) {
$latestComment = $auth->skip(fn () => $dbForConsole->findOne('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerPullRequestId', [$providerPullRequestId]),
Query::orderDesc('$createdAt'),
]));
if ($latestComment !== false && !$latestComment->isEmpty()) {
if (!$latestComment->isEmpty()) {
$latestCommentId = $latestComment->getAttribute('providerCommentId', '');
$comment = new Comment();
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
@ -124,7 +123,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
if (!empty($latestCommentId)) {
$teamId = $project->getAttribute('teamId', '');
$latestComment = Authorization::skip(fn () => $dbForConsole->createDocument('vcsComments', new Document([
$latestComment = $auth->skip(fn () => $dbForConsole->createDocument('vcsComments', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
@ -145,7 +144,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
}
} elseif (!empty($providerBranch)) {
$latestComments = Authorization::skip(fn () => $dbForConsole->find('vcsComments', [
$latestComments = $auth->skip(fn () => $dbForConsole->find('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerBranch', [$providerBranch]),
Query::orderDesc('$createdAt'),
@ -262,8 +261,8 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
};
App::get('/v1/vcs/github/authorize')
->desc('Install GitHub app')
Http::get('/v1/vcs/github/authorize')
->desc('Install GitHub App')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
->label('sdk.namespace', 'vcs')
@ -304,8 +303,8 @@ App::get('/v1/vcs/github/authorize')
->redirect($url);
});
App::get('/v1/vcs/github/callback')
->desc('Capture installation and authorization from GitHub app')
Http::get('/v1/vcs/github/callback')
->desc('Capture installation and authorization from GitHub App')
->groups(['api', 'vcs'])
->label('scope', 'public')
->label('error', __DIR__ . '/../../views/general/error.phtml')
@ -370,7 +369,7 @@ App::get('/v1/vcs/github/callback')
$identity = $dbForConsole->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if ($identity !== false && !$identity->isEmpty()) {
if (!$identity->isEmpty()) {
if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
@ -417,7 +416,7 @@ App::get('/v1/vcs/github/callback')
Query::equal('projectInternalId', [$projectInternalId])
]);
if ($installation === false || $installation->isEmpty()) {
if ($installation->isEmpty()) {
$teamId = $project->getAttribute('teamId', '');
$installation = new Document([
@ -464,7 +463,7 @@ App::get('/v1/vcs/github/callback')
->redirect($redirectSuccess);
});
App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents')
Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents')
->desc('Get files and directories of a VCS repository')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
@ -525,7 +524,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
]), Response::MODEL_VCS_CONTENT_LIST);
});
App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection')
Http::post('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection')
->desc('Detect runtime settings from source code')
->groups(['api', 'vcs'])
->label('scope', 'vcs.write')
@ -597,8 +596,8 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:pr
$response->dynamic(new Document($detection), Response::MODEL_DETECTION);
});
App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
->desc('List repositories')
Http::get('/v1/vcs/github/installations/:installationId/providerRepositories')
->desc('List Repositories')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
->label('sdk.namespace', 'vcs')
@ -692,7 +691,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
]), Response::MODEL_PROVIDER_REPOSITORY_LIST);
});
App::post('/v1/vcs/github/installations/:installationId/providerRepositories')
Http::post('/v1/vcs/github/installations/:installationId/providerRepositories')
->desc('Create repository')
->groups(['api', 'vcs'])
->label('scope', 'vcs.write')
@ -725,7 +724,7 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories')
Query::equal('provider', ['github']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity === false || $identity->isEmpty()) {
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
@ -793,7 +792,7 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories')
$response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY);
});
App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId')
Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId')
->desc('Get repository')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
@ -842,8 +841,8 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
$response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY);
});
App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/branches')
->desc('List repository branches')
Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/branches')
->desc('List Repository Branches')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
->label('sdk.namespace', 'vcs')
@ -891,8 +890,8 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
]), Response::MODEL_BRANCH_LIST);
});
App::post('/v1/vcs/github/events')
->desc('Create event')
Http::post('/v1/vcs/github/events')
->desc('Create Event')
->groups(['api', 'vcs'])
->label('scope', 'public')
->inject('gitHub')
@ -901,8 +900,9 @@ App::post('/v1/vcs/github/events')
->inject('dbForConsole')
->inject('getProjectDB')
->inject('queueForBuilds')
->inject('authorization')
->action(
function (GitHub $github, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
function (GitHub $github, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $authorization) use ($createGitDeployments) {
$payload = $request->getRawPayload();
$signatureRemote = $request->getHeader('x-hub-signature-256', '');
$signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', '');
@ -936,14 +936,14 @@ App::post('/v1/vcs/github/events')
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
//find functionId from functions table
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::limit(100),
]));
// create new deployment only on push and not when branch is created
if (!$providerBranchCreated) {
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForConsole, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $authorization);
}
} elseif ($event == $github::EVENT_INSTALLATION) {
if ($parsedPayload["action"] == "deleted") {
@ -956,13 +956,13 @@ App::post('/v1/vcs/github/events')
]);
foreach ($installations as $installation) {
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [
Query::equal('installationInternalId', [$installation->getInternalId()]),
Query::limit(1000)
]));
foreach ($repositories as $repository) {
Authorization::skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId()));
$authorization->skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId()));
}
$dbForConsole->deleteDocument('installations', $installation->getId());
@ -994,12 +994,12 @@ App::post('/v1/vcs/github/events')
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
$providerCommitMessage = $commitDetails["commitMessage"] ?? '';
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $authorization);
} elseif ($parsedPayload["action"] == "closed") {
// Allowed external contributions cleanup
@ -1008,7 +1008,7 @@ App::post('/v1/vcs/github/events')
$external = $parsedPayload["external"] ?? true;
if ($external) {
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
@ -1019,7 +1019,7 @@ App::post('/v1/vcs/github/events')
if (\in_array($providerPullRequestId, $providerPullRequestIds)) {
$providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]);
$repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds);
$repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository));
$repository = $authorization->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository));
}
}
}
@ -1030,7 +1030,7 @@ App::post('/v1/vcs/github/events')
}
);
App::get('/v1/vcs/installations')
Http::get('/v1/vcs/installations')
->desc('List installations')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
@ -1090,7 +1090,7 @@ App::get('/v1/vcs/installations')
]), Response::MODEL_INSTALLATION_LIST);
});
App::get('/v1/vcs/installations/:installationId')
Http::get('/v1/vcs/installations/:installationId')
->desc('Get installation')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
@ -1119,8 +1119,8 @@ App::get('/v1/vcs/installations/:installationId')
$response->dynamic($installation, Response::MODEL_INSTALLATION);
});
App::delete('/v1/vcs/installations/:installationId')
->desc('Delete installation')
Http::delete('/v1/vcs/installations/:installationId')
->desc('Delete Installation')
->groups(['api', 'vcs'])
->label('scope', 'vcs.write')
->label('sdk.namespace', 'vcs')
@ -1152,7 +1152,7 @@ App::delete('/v1/vcs/installations/:installationId')
$response->noContent();
});
App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositoryId')
Http::patch('/v1/vcs/github/installations/:installationId/repositories/:repositoryId')
->desc('Authorize external deployment')
->groups(['api', 'vcs'])
->label('scope', 'vcs.write')
@ -1172,14 +1172,15 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
->inject('dbForConsole')
->inject('getProjectDB')
->inject('queueForBuilds')
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
->inject('authorization')
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $authorization) use ($createGitDeployments) {
$installation = $dbForConsole->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$repository = Authorization::skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [
$repository = $authorization->skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [
Query::equal('projectInternalId', [$project->getInternalId()])
]));
@ -1196,7 +1197,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
// TODO: Delete from array when PR is closed
$repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository));
$repository = $authorization->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository));
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
@ -1220,7 +1221,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
$providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? '';
$providerCommitHash = $pullRequestResponse['head']['sha'] ?? '';
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForConsole, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $authorization);
$response->noContent();
});

View file

@ -1,7 +1,5 @@
<?php
require_once __DIR__ . '/../init.php';
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Event\Certificate;
@ -9,6 +7,7 @@ use Appwrite\Event\Event;
use Appwrite\Event\Usage;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Queue\Connections;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
use Appwrite\Utopia\Request\Filters\V17 as RequestV17;
@ -20,8 +19,6 @@ use Appwrite\Utopia\Response\Filters\V18 as ResponseV18;
use Appwrite\Utopia\View;
use Executor\Executor;
use MaxMind\Db\Reader;
use Swoole\Http\Request as SwooleRequest;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
@ -31,33 +28,35 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Domains\Domain;
use Utopia\DSN\DSN;
use Utopia\Http\Http;
use Utopia\Http\Route;
use Utopia\Http\Validator\Hostname;
use Utopia\Http\Validator\Text;
use Utopia\Locale\Locale;
use Utopia\Logger\Adapter\Sentry;
use Utopia\Logger\Log;
use Utopia\Logger\Log\User;
use Utopia\Logger\Logger;
use Utopia\System\System;
use Utopia\Validator\Hostname;
use Utopia\Validator\Text;
Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(App $utopia, Database $dbForConsole, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage, Reader $geodb)
function router(Database $dbForConsole, callable $getProjectDB, Request $request, Response $response, Route $route, Event $queueForEvents, Usage $queueForUsage, Reader $geodb, Authorization $auth)
{
$utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml');
$route?->label('error', __DIR__ . '/../views/general/error.phtml');
$host = $request->getHostname() ?? '';
$route = Authorization::skip(
$rule = $auth->skip(
fn () => $dbForConsole->find('rules', [
Query::equal('domain', [$host]),
Query::limit(1)
])
)[0] ?? null;
if ($route === null) {
if ($rule === null) {
if ($host === System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.');
}
@ -73,12 +72,12 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
}
// Act as API - no Proxy logic
$utopia->getRoute()?->label('error', '');
$route?->label('error', '');
return false;
}
$projectId = $route->getAttribute('projectId');
$project = Authorization::skip(
$projectId = $rule->getAttribute('projectId');
$project = $auth->skip(
fn () => $dbForConsole->getDocument('projects', $projectId)
);
if (array_key_exists('proxy', $project->getAttribute('services', []))) {
@ -89,16 +88,16 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
}
// Skip Appwrite Router for ACME challenge. Nessessary for certificate generation
$path = ($swooleRequest->server['request_uri'] ?? '/');
$path = ($request->getURI() ?? '/');
if (\str_starts_with($path, '/.well-known/acme-challenge')) {
return false;
}
$type = $route->getAttribute('resourceType');
$type = $rule->getAttribute('resourceType');
if ($type === 'function') {
$utopia->getRoute()?->label('sdk.namespace', 'functions');
$utopia->getRoute()?->label('sdk.method', 'createExecution');
$route->label('sdk.namespace', 'functions');
$route->label('sdk.method', 'createExecution');
if (System::getEnv('_APP_OPTIONS_FUNCTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
if ($request->getProtocol() !== 'https') {
@ -110,26 +109,25 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
}
}
$functionId = $route->getAttribute('resourceId');
$projectId = $route->getAttribute('projectId');
$functionId = $rule->getAttribute('resourceId');
$projectId = $rule->getAttribute('projectId');
$path = ($swooleRequest->server['request_uri'] ?? '/');
$query = ($swooleRequest->server['query_string'] ?? '');
$path = ($request->getURI() ?? '/');
$query = ($request->getQueryString() ?? '');
if (!empty($query)) {
$path .= '?' . $query;
}
$body = $swooleRequest->getContent() ?? '';
$method = $swooleRequest->server['request_method'];
$body = $request->getRawPayload() ?? '';
$method = $request->getMethod();
$requestHeaders = $request->getHeaders();
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
$project = $auth->skip(fn () => $dbForConsole->getDocument('projects', $projectId));
$dbForProject = $getProjectDB($project);
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
$function = $auth->skip(fn () => $dbForProject->getDocument('functions', $functionId));
if ($function->isEmpty() || !$function->getAttribute('enabled')) {
throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND);
@ -145,7 +143,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
}
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', '')));
$deployment = $auth->skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', '')));
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function');
@ -156,7 +154,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
}
/** Check if build has completed */
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
$build = $auth->skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
if ($build->isEmpty()) {
throw new AppwriteException(AppwriteException::BUILD_NOT_FOUND);
}
@ -217,7 +215,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
'deploymentInternalId' => $deployment->getInternalId(),
'deploymentId' => $deployment->getId(),
'trigger' => 'http', // http / schedule / event
'status' => 'processing', // waiting / processing / completed / failed
'status' => 'processing', // waiting / processing / completed / failed
'responseStatusCode' => 0,
'responseHeaders' => [],
'requestPath' => $path,
@ -313,7 +311,6 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
cpus: $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT,
logging: $function->getAttribute('logging', true),
requestTimeout: 30
);
$headersFiltered = [];
@ -331,7 +328,6 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$execution->setAttribute('logs', $executionResponse['logs']);
$execution->setAttribute('errors', $executionResponse['errors']);
$execution->setAttribute('duration', $executionResponse['duration']);
} catch (\Throwable $th) {
$durationEnd = \microtime(true);
@ -366,7 +362,8 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
->trigger()
;
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));
/** @var Document $execution */
$execution = $auth->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
$execution->setAttribute('logs', '');
@ -398,18 +395,15 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
return true;
} elseif ($type === 'api') {
$utopia->getRoute()?->label('error', '');
$route?->label('error', '');
return false;
} else {
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type);
}
$utopia->getRoute()?->label('error', '');
return false;
}
/*
App::init()
Http::init()
->groups(['api'])
->inject('project')
->inject('mode')
@ -420,7 +414,7 @@ App::init()
});
*/
App::init()
Http::init()
->groups(['database', 'functions', 'storage', 'messaging'])
->inject('project')
->inject('request')
@ -433,12 +427,11 @@ App::init()
}
});
App::init()
Http::init()
->groups(['api', 'web'])
->inject('utopia')
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('route')
->inject('console')
->inject('project')
->inject('dbForConsole')
@ -450,7 +443,27 @@ App::init()
->inject('queueForUsage')
->inject('queueForEvents')
->inject('queueForCertificates')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, Usage $queueForUsage, Event $queueForEvents, Certificate $queueForCertificates) {
->inject('authorization')
->action(function (
Request $request,
Response $response,
Route $route,
Document $console,
Document $project,
Database $dbForConsole,
$getProjectDB,
Locale $locale,
array $localeCodes,
array $clients,
/**
* @disregard P1009 Undefined type
*/
Reader $geodb,
Usage $queueForUsage,
Event $queueForEvents,
Certificate $queueForCertificates,
Authorization $authorization
) {
/*
* Appwrite Router
*/
@ -458,7 +471,7 @@ App::init()
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain) {
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) {
if (router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization)) {
return;
}
}
@ -466,8 +479,8 @@ App::init()
/*
* Request format
*/
$route = $utopia->getRoute();
Request::setRoute($route);
//$route = $utopia->getRoute();
//Request::setRoute($route);
if ($route === null) {
return $response
@ -499,7 +512,7 @@ App::init()
} elseif (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) {
Console::warning('Skipping SSL certificates generation on ACME challenge.');
} else {
Authorization::disable();
$authorization->disable();
$envDomain = System::getEnv('_APP_DOMAIN', '');
$mainDomain = null;
@ -507,7 +520,7 @@ App::init()
$mainDomain = $envDomain;
} else {
$domainDocument = $dbForConsole->findOne('rules', [Query::orderAsc('$id')]);
$mainDomain = $domainDocument ? $domainDocument->getAttribute('domain') : $domain->get();
$mainDomain = !$domainDocument->isEmpty() ? $domainDocument->getAttribute('domain') : $domain->get();
}
if ($mainDomain !== $domain->get()) {
@ -517,7 +530,7 @@ App::init()
Query::equal('domain', [$domain->get()])
]);
if (!$domainDocument) {
if ($domainDocument->isEmpty()) {
$domainDocument = new Document([
'domain' => $domain->get(),
'resourceType' => 'api',
@ -538,12 +551,12 @@ App::init()
}
$domains[$domain->get()] = true;
Authorization::reset(); // ensure authorization is re-enabled
$authorization->reset(); // ensure authorization is re-enabled
}
Config::setParam('domains', $domains);
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
$localeParam = (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
if (\in_array($localeParam, $localeCodes)) {
$locale->setDefault($localeParam);
}
@ -571,7 +584,7 @@ App::init()
Config::setParam(
'domainVerification',
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
$endDomain->getRegisterable() !== ''
$endDomain->getRegisterable() !== ''
);
$isLocalHost = $request->getHostname() === 'localhost' || $request->getHostname() === 'localhost:' . $request->getPort();
@ -586,8 +599,8 @@ App::init()
? null
: (
$isConsoleProject && $isConsoleRootSession
? '.' . $selfDomain->getRegisterable()
: '.' . $request->getHostname()
? '.' . $selfDomain->getRegisterable()
: '.' . $request->getHostname()
)
);
@ -606,7 +619,7 @@ App::init()
$response->addFilter(new ResponseV18());
}
if (version_compare($responseFormat, APP_VERSION_STABLE, '>')) {
$response->addHeader('X-Appwrite-Warning', "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is ". APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks");
$response->addHeader('X-Appwrite-Warning', "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is " . APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks");
}
}
@ -617,7 +630,9 @@ App::init()
* @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
*/
if (System::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
if ($request->getProtocol() !== 'https' && ($swooleRequest->header['host'] ?? '') !== 'localhost' && ($swooleRequest->header['host'] ?? '') !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations
if ($request->getProtocol() !== 'https' // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations
&& ($request->getHeader('host') ?? '') !== 'localhost'
&& ($request->getHeader('host') ?? '') !== APP_HOSTNAME_INTERNAL) {
if ($request->getMethod() !== Request::METHOD_GET) {
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.');
}
@ -657,9 +672,8 @@ App::init()
}
});
App::options()
->inject('utopia')
->inject('swooleRequest')
Http::options()
->inject('route')
->inject('request')
->inject('response')
->inject('dbForConsole')
@ -667,7 +681,8 @@ App::options()
->inject('queueForEvents')
->inject('queueForUsage')
->inject('geodb')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) {
->inject('authorization')
->action(function (Route $route, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb, Authorization $authorization) {
/*
* Appwrite Router
*/
@ -675,7 +690,7 @@ App::options()
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain) {
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) {
if (router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization)) {
return;
}
}
@ -692,18 +707,25 @@ App::options()
->noContent();
});
App::error()
Http::error()
->inject('error')
->inject('utopia')
->inject('user')
->inject('route')
->inject('request')
->inject('response')
->inject('project')
->inject('logger')
->inject('log')
->inject('authorization')
->inject('connections')
->inject('queueForUsage')
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Usage $queueForUsage) {
->action(function (Throwable $error, Document $user, ?Route $route, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Authorization $authorization, Connections $connections, Usage $queueForUsage) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$route = $utopia->getRoute();
if (is_null($route)) {
$route = new Route($request->getMethod(), $request->getURI());
}
$class = \get_class($error);
$code = $error->getCode();
$message = $error->getMessage();
@ -724,9 +746,9 @@ App::error()
Console::error('[Error] File: ' . $file);
Console::error('[Error] Line: ' . $line);
}
switch ($class) {
case 'Utopia\Exception':
case 'Utopia\Servers\Exception':
case 'Utopia\Http\Exception':
$error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error);
switch ($code) {
case 400:
@ -771,35 +793,36 @@ App::error()
} else {
$publish = $error->getCode() === 0 || $error->getCode() >= 500;
}
if ($error->getCode() >= 400 && $error->getCode() < 500) {
// Register error logger
$providerName = System::getEnv('_APP_EXPERIMENT_LOGGING_PROVIDER', '');
$providerConfig = System::getEnv('_APP_EXPERIMENT_LOGGING_CONFIG', '');
try {
$loggingProvider = new DSN($providerConfig ?? '');
$providerName = $loggingProvider->getScheme();
if (!(empty($providerName) || empty($providerConfig))) {
try {
$loggingProvider = new DSN($providerConfig);
$providerName = $loggingProvider->getScheme();
if (!empty($providerName) && $providerName === 'sentry') {
$key = $loggingProvider->getPassword();
$projectId = $loggingProvider->getUser() ?? '';
$host = 'https://' . $loggingProvider->getHost();
if (!empty($providerName) && $providerName === 'sentry') {
$key = $loggingProvider->getPassword();
$projectId = $loggingProvider->getUser() ?? '';
$host = 'https://' . $loggingProvider->getHost();
$adapter = new Sentry($projectId, $key, $host);
$logger = new Logger($adapter);
$logger->setSample(0.04);
$publish = true;
} else {
throw new \Exception('Invalid experimental logging provider');
$adapter = new Sentry($projectId, $key, $host);
$logger = new Logger($adapter);
$logger->setSample(0.04);
$publish = true;
} else {
throw new \Exception('Invalid experimental logging provider');
}
} catch (\Throwable $th) {
Console::warning('Failed to initialize logging provider: ' . $th->getMessage());
}
} catch (\Throwable $th) {
Console::warning('Failed to initialize logging provider: ' . $th->getMessage());
}
}
if ($publish && $project->getId() !== 'console') {
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
if (!Auth::isPrivilegedUser($authorization->getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
@ -818,14 +841,7 @@ App::error()
}
if ($logger && $publish) {
try {
/** @var Utopia\Database\Document $user */
$user = $utopia->getResource('user');
} catch (\Throwable) {
// All good, user is optional information for logger
}
if ($logger && ($publish || $error->getCode() === 0)) {
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
}
@ -855,7 +871,7 @@ App::error()
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('roles', Authorization::getRoles());
$log->addExtra('roles', $authorization->getRoles());
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
$log->setAction($action);
@ -873,7 +889,7 @@ App::error()
/** Wrap all exceptions inside Appwrite\Extend\Exception */
if (!($error instanceof AppwriteException)) {
$error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error);
$error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, (int)$code, $error);
}
switch ($code) { // Don't show 500 errors!
@ -893,14 +909,14 @@ App::error()
break;
default:
$code = 500; // All other errors get the generic 500 server error status code
$message = 'Server Error';
$message = (Http::getMode() === Http::MODE_TYPE_DEVELOPMENT) ? $message : 'Server Error';
}
//$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised
$type = $error->getType();
$output = ((App::isDevelopment())) ? [
$output = ((Http::isDevelopment())) ? [
'message' => $message,
'code' => $code,
'file' => $file,
@ -928,7 +944,7 @@ App::error()
$layout
->setParam('title', $project->getAttribute('name') . ' - Error')
->setParam('development', App::isDevelopment())
->setParam('development', Http::isDevelopment())
->setParam('projectName', $project->getAttribute('name'))
->setParam('projectURL', $project->getAttribute('url'))
->setParam('message', $output['message'] ?? '')
@ -939,18 +955,18 @@ App::error()
$response->html($layout->render());
}
$connections->reclaim();
$response->dynamic(
new Document($output),
$utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR
Http::isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR
);
});
App::get('/robots.txt')
Http::get('/robots.txt')
->desc('Robots.txt File')
->label('scope', 'public')
->label('docs', false)
->inject('utopia')
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('dbForConsole')
@ -958,7 +974,9 @@ App::get('/robots.txt')
->inject('queueForEvents')
->inject('queueForUsage')
->inject('geodb')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) {
->inject('route')
->inject('authorization')
->action(function (Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb, ?Route $route, Authorization $authorization) {
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
@ -966,16 +984,17 @@ App::get('/robots.txt')
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb);
if (is_null($route)) {
$route = new Route($request->getMethod(), $request->getURI());
}
router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization);
}
});
App::get('/humans.txt')
Http::get('/humans.txt')
->desc('Humans.txt File')
->label('scope', 'public')
->label('docs', false)
->inject('utopia')
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('dbForConsole')
@ -983,7 +1002,9 @@ App::get('/humans.txt')
->inject('queueForEvents')
->inject('queueForUsage')
->inject('geodb')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) {
->inject('route')
->inject('authorization')
->action(function (Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb, Route $route, Authorization $authorization) {
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
@ -991,11 +1012,11 @@ App::get('/humans.txt')
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb);
router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization);
}
});
App::get('/.well-known/acme-challenge/*')
Http::get('/.well-known/acme-challenge/*')
->desc('SSL Verification')
->label('scope', 'public')
->label('docs', false)
@ -1045,16 +1066,32 @@ App::get('/.well-known/acme-challenge/*')
$response->text($content);
});
include_once __DIR__ . '/shared/api.php';
include_once __DIR__ . '/shared/api/auth.php';
App::wildcard()
Http::wildcard()
->groups(['api'])
->label('scope', 'global')
->action(function () {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
});
foreach (Config::getParam('services', []) as $service) {
include_once $service['controller'];
}
include_once 'mock.php';
include_once 'shared/api.php';
include_once 'shared/api/auth.php';
include_once 'api/account.php';
include_once 'api/avatars.php';
include_once 'api/console.php';
include_once 'api/databases.php';
include_once 'api/functions.php';
include_once 'api/graphql.php';
include_once 'api/health.php';
include_once 'api/locale.php';
include_once 'api/messaging.php';
include_once 'api/migrations.php';
include_once 'api/project.php';
include_once 'api/projects.php';
include_once 'api/proxy.php';
include_once 'api/storage.php';
include_once 'api/teams.php';
include_once 'api/users.php';
include_once 'api/vcs.php';
include_once 'web/console.php';
include_once 'web/home.php';

View file

@ -3,9 +3,7 @@
global $utopia, $request, $response;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
@ -13,13 +11,15 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\UID;
use Utopia\Http\Http;
use Utopia\Http\Route;
use Utopia\Http\Validator\Host;
use Utopia\Http\Validator\Text;
use Utopia\Http\Validator\WhiteList;
use Utopia\System\System;
use Utopia\Validator\Host;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
App::get('/v1/mock/tests/general/oauth2')
Http::get('/v1/mock/tests/general/oauth2')
->desc('OAuth Login')
->groups(['mock'])
->label('scope', 'public')
@ -35,7 +35,7 @@ App::get('/v1/mock/tests/general/oauth2')
$response->redirect($redirectURI . '?' . \http_build_query(['code' => 'abcdef', 'state' => $state]));
});
App::get('/v1/mock/tests/general/oauth2/token')
Http::get('/v1/mock/tests/general/oauth2/token')
->desc('OAuth2 Token')
->groups(['mock'])
->label('scope', 'public')
@ -81,7 +81,7 @@ App::get('/v1/mock/tests/general/oauth2/token')
}
});
App::get('/v1/mock/tests/general/oauth2/user')
Http::get('/v1/mock/tests/general/oauth2/user')
->desc('OAuth2 User')
->groups(['mock'])
->label('scope', 'public')
@ -101,7 +101,7 @@ App::get('/v1/mock/tests/general/oauth2/user')
]);
});
App::get('/v1/mock/tests/general/oauth2/success')
Http::get('/v1/mock/tests/general/oauth2/success')
->desc('OAuth2 Success')
->groups(['mock'])
->label('scope', 'public')
@ -114,7 +114,7 @@ App::get('/v1/mock/tests/general/oauth2/success')
]);
});
App::get('/v1/mock/tests/general/oauth2/failure')
Http::get('/v1/mock/tests/general/oauth2/failure')
->desc('OAuth2 Failure')
->groups(['mock'])
->label('scope', 'public')
@ -129,7 +129,7 @@ App::get('/v1/mock/tests/general/oauth2/failure')
]);
});
App::patch('/v1/mock/functions-v2')
Http::patch('/v1/mock/functions-v2')
->desc('Update Function Version to V2 (outdated code syntax)')
->groups(['mock', 'api', 'functions'])
->label('scope', 'functions.write')
@ -155,7 +155,7 @@ App::patch('/v1/mock/functions-v2')
$response->noContent();
});
App::post('/v1/mock/api-key-unprefixed')
Http::post('/v1/mock/api-key-unprefixed')
->desc('Create API Key (without standard prefix)')
->groups(['mock', 'api', 'projects'])
->label('scope', 'public')
@ -204,7 +204,7 @@ App::post('/v1/mock/api-key-unprefixed')
->dynamic($key, Response::MODEL_KEY);
});
App::get('/v1/mock/github/callback')
Http::get('/v1/mock/github/callback')
->desc('Create installation document using GitHub installation id')
->groups(['mock', 'api', 'vcs'])
->label('scope', 'public')
@ -264,15 +264,13 @@ App::get('/v1/mock/github/callback')
]);
});
App::shutdown()
Http::shutdown()
->groups(['mock'])
->inject('utopia')
->inject('route')
->inject('response')
->inject('request')
->action(function (App $utopia, Response $response, Request $request) {
->action(function (Route $route, Response $response) {
$result = [];
$route = $utopia->getRoute();
$path = APP_STORAGE_CACHE . '/tests.json';
$tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : [];

View file

@ -15,11 +15,11 @@ use Appwrite\Event\Usage;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Utopia\Queue\Connections;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\Database\TimeLimit;
use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
@ -28,8 +28,11 @@ use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Http\Http;
use Utopia\Http\Route;
use Utopia\Http\Validator\WhiteList;
use Utopia\System\System;
use Utopia\Validator\WhiteList;
$parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) {
preg_match_all('/{(.*?)}/', $label, $matches);
@ -150,9 +153,9 @@ $databaseListener = function (string $event, Document $document, Document $proje
}
};
App::init()
Http::init()
->groups(['api'])
->inject('utopia')
->inject('route')
->inject('request')
->inject('dbForConsole')
->inject('project')
@ -161,9 +164,8 @@ App::init()
->inject('servers')
->inject('mode')
->inject('team')
->action(function (App $utopia, Request $request, Database $dbForConsole, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team) {
$route = $utopia->getRoute();
->inject('authorization')
->action(function (Route $route, Request $request, Database $dbForConsole, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, Authorization $authorization) {
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
@ -221,8 +223,8 @@ App::init()
$role = Auth::USER_ROLE_APPS;
$scopes = \array_merge($roles[$role]['scopes'], $tokenScopes);
Authorization::setRole(Auth::USER_ROLE_APPS);
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
$authorization->addRole(Auth::USER_ROLE_APPS);
$authorization->setDefaultStatus(false); // Cancel security segmentation for API keys.
}
} elseif ($keyType === API_KEY_STANDARD) {
// No underline means no prefix. Backwards compatibility.
@ -247,8 +249,8 @@ App::init()
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
}
Authorization::setRole(Auth::USER_ROLE_APPS);
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
$authorization->addRole(Auth::USER_ROLE_APPS);
$authorization->setDefaultStatus(false); // Cancel security segmentation for API keys.
$accessedAt = $key->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
@ -294,15 +296,15 @@ App::init()
foreach ($adminRoles as $role) {
$scopes = \array_merge($scopes, $roles[$role]['scopes']);
}
Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
$authorization->setDefaultStatus(false); // Cancel security segmentation for admin users.
}
$scopes = \array_unique($scopes);
Authorization::setRole($role);
foreach (Auth::getRoles($user) as $authRole) {
Authorization::setRole($authRole);
$authorization->addRole($role);
foreach (Auth::getRoles($user, $authorization) as $authRole) {
$authorization->addRole($authRole);
}
/** Do not allow access to disabled services */
@ -311,7 +313,7 @@ App::init()
if (
array_key_exists($service, $project->getAttribute('services', []))
&& !$project->getAttribute('services', [])[$service]
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles()))
) {
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
}
@ -346,9 +348,9 @@ App::init()
}
});
App::init()
Http::init()
->groups(['api'])
->inject('utopia')
->inject('route')
->inject('request')
->inject('response')
->inject('project')
@ -362,14 +364,12 @@ App::init()
->inject('queueForUsage')
->inject('dbForProject')
->inject('mode')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, string $mode) use ($databaseListener) {
$route = $utopia->getRoute();
->inject('authorization')
->action(function (Route $route, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, string $mode, Authorization $authorization) use ($databaseListener) {
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['rest']
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@ -399,7 +399,7 @@ App::init()
$closestLimit = null;
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
@ -463,7 +463,7 @@ App::init()
$useCache = $route->getLabel('cache', false);
if ($useCache) {
$key = md5($request->getURI() . '*' . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER);
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
@ -476,19 +476,18 @@ App::init()
if ($type === 'bucket') {
$bucketId = $parts[1] ?? null;
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
$valid = $validator->isValid($bucket->getRead());
$valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead()));
if (!$fileSecurity && !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
@ -499,7 +498,7 @@ App::init()
if ($fileSecurity && !$valid) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
} else {
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
}
if ($file->isEmpty()) {
@ -523,7 +522,7 @@ App::init()
}
});
App::init()
Http::init()
->groups(['session'])
->inject('user')
->inject('request')
@ -543,14 +542,12 @@ App::init()
* Delete older sessions if the number of sessions have crossed
* the session limit set for the project
*/
App::shutdown()
Http::shutdown()
->groups(['session'])
->inject('utopia')
->inject('request')
->inject('response')
->inject('project')
->inject('dbForProject')
->action(function (App $utopia, Request $request, Response $response, Document $project, Database $dbForProject) {
->action(function (Response $response, Document $project, Database $dbForProject) {
$sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT;
$session = $response->getPayload();
$userId = $session['userId'] ?? '';
@ -577,9 +574,9 @@ App::shutdown()
$dbForProject->purgeCachedDocument('users', $userId);
});
App::shutdown()
Http::shutdown()
->groups(['api'])
->inject('utopia')
->inject('route')
->inject('request')
->inject('response')
->inject('project')
@ -595,7 +592,29 @@ App::shutdown()
->inject('queueForFunctions')
->inject('mode')
->inject('dbForConsole')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Database $dbForProject, Func $queueForFunctions, string $mode, Database $dbForConsole) use ($parseLabel) {
->inject('authorization')
->action(function (
Route $route,
Request $request,
Response $response,
Document $project,
Document $user,
Event $queueForEvents,
Audit $queueForAudits,
Usage $queueForUsage,
Delete $queueForDeletes,
EventDatabase $queueForDatabase,
Build $queueForBuilds,
Messaging $queueForMessaging,
Database $dbForProject,
Func $queueForFunctions,
string $mode,
Database $dbForConsole,
Authorization $authorization,
) use ($parseLabel) {
if (!empty($user) && !$user->isEmpty() && empty($user->getInternalId())) {
$user = $authorization->skip(fn () => $dbForProject->getDocument('users', $user->getId()));
}
$responsePayload = $response->getPayload();
@ -655,7 +674,6 @@ App::shutdown()
}
}
$route = $utopia->getRoute();
$requestParams = $route->getParamsValues();
/**
@ -725,11 +743,11 @@ App::shutdown()
$key = md5($request->getURI() . '*' . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER);
$signature = md5($data['payload']);
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
$accessedAt = $cacheLog->getAttribute('accessedAt', '');
$now = DateTime::now();
if ($cacheLog->isEmpty()) {
Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([
$authorization->skip(fn () => $dbForProject->createDocument('cache', new Document([
'$id' => $key,
'resource' => $resource,
'resourceType' => $resourceType,
@ -739,7 +757,7 @@ App::shutdown()
])));
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
$cacheLog->setAttribute('accessedAt', $now);
Authorization::skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
}
if ($signature !== $cacheLog->getAttribute('signature')) {
@ -751,10 +769,8 @@ App::shutdown()
}
}
if ($project->getId() !== 'console') {
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
if (!Auth::isPrivilegedUser($authorization->getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
@ -779,7 +795,7 @@ App::shutdown()
$accessedAt = $project->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
Authorization::skip(fn () => $dbForConsole->updateDocument('projects', $project->getId(), $project));
$authorization->skip(fn () => $dbForConsole->updateDocument('projects', $project->getId(), $project));
}
}
@ -800,10 +816,16 @@ App::shutdown()
}
});
App::init()
Http::init()
->groups(['usage'])
->action(function () {
if (System::getEnv('_APP_USAGE_STATS', 'enabled') !== 'enabled') {
throw new Exception(Exception::GENERAL_USAGE_DISABLED);
}
});
Http::shutdown()
->inject('connections')
->action(function (Connections $connections) {
$connections->reclaim();
});

View file

@ -4,13 +4,14 @@ use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Request;
use MaxMind\Db\Reader;
use Utopia\App;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Http\Http;
use Utopia\Http\Route;
use Utopia\System\System;
App::init()
Http::init()
->groups(['mfaProtected'])
->inject('session')
->action(function (Document $session) {
@ -29,13 +30,14 @@ App::init()
}
});
App::init()
Http::init()
->groups(['auth'])
->inject('utopia')
->inject('route')
->inject('request')
->inject('project')
->inject('geodb')
->action(function (App $utopia, Request $request, Document $project, Reader $geodb) {
->inject('authorization')
->action(function (Route $route, Request $request, Document $project, Reader $geodb, Authorization $authorization) {
$denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', '');
if (!empty($denylist && $project->getId() === 'console')) {
$countries = explode(',', $denylist);
@ -46,15 +48,17 @@ App::init()
}
}
$route = $utopia->match($request);
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
$isAppUser = Auth::isAppUser($authorization->getRoles());
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
return;
}
if ($route->getLabel('sdk.namespace', '') === 'graphql') { // Skip for graphQL recursive call
return;
}
$auths = $project->getAttribute('auths', []);
switch ($route->getLabel('auth.type', '')) {
case 'emailPassword':

View file

@ -2,9 +2,9 @@
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Http\Http;
App::init()
Http::init()
->groups(['web'])
->inject('request')
->inject('response')
@ -16,7 +16,7 @@ App::init()
;
});
App::get('/')
Http::get('/')
->alias('auth/*')
->alias('/invite')
->alias('/login')

View file

@ -1,10 +1,10 @@
<?php
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Http\Http;
App::get('/versions')
Http::get('/versions')
->desc('Get Version')
->groups(['home', 'web'])
->label('scope', 'public')

View file

@ -1,335 +1,242 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/init.php';
require_once __DIR__ . '/controllers/general.php';
use Appwrite\Utopia\Queue\Connections;
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 Utopia\Abuse\Adapters\Database\TimeLimit;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Adapter\MySQL;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Log\User;
use Utopia\Pools\Group;
use Utopia\Swoole\Files;
use Utopia\Http\Adapter\Swoole\Server;
use Utopia\Http\Http;
use Utopia\System\System;
$http = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
mode: SWOOLE_PROCESS,
);
global $registry, $container;
$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
$workerNumber = swoole_cpu_num() * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$http
->set([
'worker_num' => $workerNumber,
'open_http2_protocol' => true,
'http_compression' => true,
'http_compression_level' => 6,
'package_max_length' => $payloadSize,
'buffer_output_size' => $payloadSize,
]);
$server = new Server('0.0.0.0', '80', [
'open_http2_protocol' => true,
'http_compression' => true,
'http_compression_level' => 6,
'package_max_length' => $payloadSize,
'buffer_output_size' => $payloadSize,
// Server
// 'log_level' => 0,
'dispatch_mode' => 2,
'worker_num' => $workerNumber,
'reactor_num' => swoole_cpu_num() * 2,
'open_cpu_affinity' => true,
// Coroutine
'enable_coroutine' => true,
'send_yield' => true,
'tcp_fastopen' => true,
]);
$http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) {
Console::success('Worker ' . ++$workerId . ' started successfully');
});
$http->on(Constant::EVENT_BEFORE_RELOAD, function ($server, $workerId) {
Console::success('Starting reload...');
});
$http->on(Constant::EVENT_AFTER_RELOAD, function ($server, $workerId) {
Console::success('Reload completed...');
});
include __DIR__ . '/controllers/general.php';
$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $register) {
$app = new App('UTC');
go(function () use ($register, $app) {
$pools = $register->get('pools');
/** @var Group $pools */
App::setResource('pools', fn () => $pools);
// wait for database to be ready
$attempts = 0;
$max = 10;
$sleep = 1;
do {
try {
$attempts++;
$dbForConsole = $app->getResource('dbForConsole');
/** @var Utopia\Database\Database $dbForConsole */
break; // leave the do-while if successful
} catch (\Throwable $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= $max) {
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
}
sleep($sleep);
}
} while ($attempts < $max);
Console::success('[Setup] - Server database init started...');
$http = new Http($server, $container, 'UTC');
$http->setRequestClass(Request::class);
$http->setResponseClass(Response::class);
Http::onStart()
->inject('authorization')
->inject('cache')
->inject('pools')
->inject('connections')
->action(function (Authorization $authorization, Cache $cache, array $pools, Connections $connections) {
try {
Console::success('[Setup] - Creating database: appwrite...');
$dbForConsole->create();
// wait for database to be ready
$attempts = 0;
$max = 15;
$sleep = 2;
do {
try {
$attempts++;
$pool = $pools['pools-console-console']['pool'];
$dsn = $pools['pools-console-console']['dsn'];
$connection = $pool->get();
$connections->add($connection, $pool);
$adapter = match ($dsn->getScheme()) {
'mariadb' => new MariaDB($connection),
'mysql' => new MySQL($connection),
default => null
};
$adapter->setDatabase($dsn->getPath());
$dbForConsole = new Database($adapter, $cache);
$dbForConsole->setAuthorization($authorization);
$dbForConsole
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS);
$dbForConsole->ping();
break; // leave the do-while if successful
} catch (\Throwable $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= $max) {
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
}
sleep($sleep);
}
} while ($attempts < $max);
Console::success('[Setup] - Server database init started...');
try {
Console::success('[Setup] - Creating database: appwrite...');
$dbForConsole->create();
} catch (\Throwable $e) {
Console::success('[Setup] - Skip: metadata table already exists');
return true;
}
if ($dbForConsole->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForConsole);
$audit->setup();
}
if ($dbForConsole->getCollection(TimeLimit::COLLECTION)->isEmpty()) {
$abuse = new TimeLimit("", 0, 1, $dbForConsole);
$abuse->setup();
}
/** @var array $collections */
$collections = Config::getParam('collections', []);
$consoleCollections = $collections['console'];
foreach ($consoleCollections as $key => $collection) {
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
}
if (!$dbForConsole->getCollection($key)->isEmpty()) {
continue;
}
Console::success('[Setup] - Creating collection: ' . $collection['$id'] . '...');
$attributes = [];
$indexes = [];
foreach ($collection['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => ID::custom($attribute['$id']),
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
'signed' => $attribute['signed'],
'array' => $attribute['array'],
'filters' => $attribute['filters'],
'default' => $attribute['default'] ?? null,
'format' => $attribute['format'] ?? ''
]);
}
foreach ($collection['indexes'] as $index) {
$indexes[] = new Document([
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
'orders' => $index['orders'],
]);
}
$dbForConsole->createCollection($key, $attributes, $indexes);
}
if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDatabase(), 'bucket_1')) {
Console::success('[Setup] - Creating default bucket...');
$dbForConsole->createDocument('buckets', new Document([
'$id' => ID::custom('default'),
'$collection' => ID::custom('buckets'),
'name' => 'Default',
'maximumFileSize' => (int) System::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB
'allowedFileExtensions' => [],
'enabled' => true,
'compression' => 'gzip',
'encryption' => true,
'antivirus' => true,
'fileSecurity' => true,
'$permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'search' => 'buckets Default',
]));
$bucket = $dbForConsole->getDocument('buckets', 'default');
Console::success('[Setup] - Creating files collection for default bucket...');
$files = $collections['buckets']['files'] ?? [];
if (empty($files)) {
throw new Exception('Files collection is not configured.');
}
$attributes = [];
$indexes = [];
foreach ($files['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => ID::custom($attribute['$id']),
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
'signed' => $attribute['signed'],
'array' => $attribute['array'],
'filters' => $attribute['filters'],
'default' => $attribute['default'] ?? null,
'format' => $attribute['format'] ?? ''
]);
}
foreach ($files['indexes'] as $index) {
$indexes[] = new Document([
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
'orders' => $index['orders'],
]);
}
$dbForConsole->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
}
$connections->reclaim();
Console::success('[Setup] - Server database init completed...');
Console::success('Server started successfully');
} catch (\Throwable $e) {
Console::success('[Setup] - Skip: metadata table already exists');
Console::warning('Database not ready: ' . $e->getMessage());
exit(1);
}
if ($dbForConsole->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForConsole);
$audit->setup();
}
if ($dbForConsole->getCollection(TimeLimit::COLLECTION)->isEmpty()) {
$adapter = new TimeLimit("", 0, 1, $dbForConsole);
$adapter->setup();
}
/** @var array $collections */
$collections = Config::getParam('collections', []);
$consoleCollections = $collections['console'];
foreach ($consoleCollections as $key => $collection) {
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
}
if (!$dbForConsole->getCollection($key)->isEmpty()) {
continue;
}
Console::success('[Setup] - Creating collection: ' . $collection['$id'] . '...');
$attributes = [];
$indexes = [];
foreach ($collection['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => ID::custom($attribute['$id']),
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
'signed' => $attribute['signed'],
'array' => $attribute['array'],
'filters' => $attribute['filters'],
'default' => $attribute['default'] ?? null,
'format' => $attribute['format'] ?? ''
]);
}
foreach ($collection['indexes'] as $index) {
$indexes[] = new Document([
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
'orders' => $index['orders'],
]);
}
$dbForConsole->createCollection($key, $attributes, $indexes);
}
if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDatabase(), 'bucket_1')) {
Console::success('[Setup] - Creating default bucket...');
$dbForConsole->createDocument('buckets', new Document([
'$id' => ID::custom('default'),
'$collection' => ID::custom('buckets'),
'name' => 'Default',
'maximumFileSize' => (int) System::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB
'allowedFileExtensions' => [],
'enabled' => true,
'compression' => 'gzip',
'encryption' => true,
'antivirus' => true,
'fileSecurity' => true,
'$permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'search' => 'buckets Default',
]));
$bucket = $dbForConsole->getDocument('buckets', 'default');
Console::success('[Setup] - Creating files collection for default bucket...');
$files = $collections['buckets']['files'] ?? [];
if (empty($files)) {
throw new Exception('Files collection is not configured.');
}
$attributes = [];
$indexes = [];
foreach ($files['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => ID::custom($attribute['$id']),
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
'signed' => $attribute['signed'],
'array' => $attribute['array'],
'filters' => $attribute['filters'],
'default' => $attribute['default'] ?? null,
'format' => $attribute['format'] ?? ''
]);
}
foreach ($files['indexes'] as $index) {
$indexes[] = new Document([
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
'orders' => $index['orders'],
]);
}
$dbForConsole->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
}
$pools->reclaim();
Console::success('[Setup] - Server database init completed...');
});
Console::success('Server started successfully (max payload is ' . number_format($payloadSize) . ' bytes)');
Console::info("Master pid {$http->master_pid}, manager pid {$http->manager_pid}");
// listen ctrl + c
Process::signal(2, function () use ($http) {
Console::log('Stop by Ctrl+C');
$http->shutdown();
Http::init()
->inject('authorization')
->action(function (Authorization $authorization) {
$authorization->cleanRoles();
$authorization->addRole(Role::any()->toString());
});
});
$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register) {
App::setResource('swooleRequest', fn () => $swooleRequest);
App::setResource('swooleResponse', fn () => $swooleResponse);
$request = new Request($swooleRequest);
$response = new Response($swooleResponse);
if (Files::isFileLoaded($request->getURI())) {
$time = (60 * 60 * 24 * 365 * 2); // 45 days cache
$response
->setContentType(Files::getFileMimeType($request->getURI()))
->addHeader('Cache-Control', 'public, max-age=' . $time)
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time) . ' GMT') // 45 days cache
->send(Files::getFileContents($request->getURI()));
return;
}
$app = new App('UTC');
$pools = $register->get('pools');
App::setResource('pools', fn () => $pools);
try {
Authorization::cleanRoles();
Authorization::setRole(Role::any()->toString());
$app->run($request, $response);
} catch (\Throwable $th) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$logger = $app->getResource("logger");
if ($logger) {
try {
/** @var Utopia\Database\Document $user */
$user = $app->getResource('user');
} catch (\Throwable $_th) {
// All good, user is optional information for logger
}
$route = $app->getRoute();
$log = $app->getResource("log");
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
}
$log->setNamespace("http");
$log->setServer(\gethostname());
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($th->getMessage());
$log->addTag('method', $route->getMethod());
$log->addTag('url', $route->getPath());
$log->addTag('verboseType', get_class($th));
$log->addTag('code', $th->getCode());
// $log->addTag('projectId', $project->getId()); // TODO: Figure out how to get ProjectID, if it becomes relevant
$log->addTag('hostname', $request->getHostname());
$log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')));
$log->addExtra('file', $th->getFile());
$log->addExtra('line', $th->getLine());
$log->addExtra('trace', $th->getTraceAsString());
$log->addExtra('roles', Authorization::getRoles());
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
$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::error('[Error] Type: ' . get_class($th));
Console::error('[Error] Message: ' . $th->getMessage());
Console::error('[Error] File: ' . $th->getFile());
Console::error('[Error] Line: ' . $th->getLine());
$swooleResponse->setStatusCode(500);
$output = ((App::isDevelopment())) ? [
'message' => 'Error: ' . $th->getMessage(),
'code' => 500,
'file' => $th->getFile(),
'line' => $th->getLine(),
'trace' => $th->getTrace(),
'version' => $version,
] : [
'message' => 'Error: Server Error',
'code' => 500,
'version' => $version,
];
$swooleResponse->end(\json_encode($output));
} finally {
$pools->reclaim();
}
});
$http->start();

File diff suppressed because it is too large Load diff

37
app/init/config.php Normal file
View file

@ -0,0 +1,37 @@
<?php
use Utopia\Config\Config;
Config::load('events', __DIR__ . '/../config/events.php');
Config::load('auth', __DIR__ . '/../config/auth.php');
Config::load('apis', __DIR__ . '/../config/apis.php');
Config::load('errors', __DIR__ . '/../config/errors.php');
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php');
Config::load('platforms', __DIR__ . '/../config/platforms.php');
Config::load('collections', __DIR__ . '/../config/collections.php');
Config::load('runtimes', __DIR__ . '/../config/runtimes.php');
Config::load('runtimes-v2', __DIR__ . '/../config/runtimes-v2.php');
Config::load('usage', __DIR__ . '/../config/usage.php');
Config::load('roles', __DIR__ . '/../config/roles.php'); // User roles and scopes
Config::load('scopes', __DIR__ . '/../config/scopes.php'); // User roles and scopes
Config::load('services', __DIR__ . '/../config/services.php'); // List of services
Config::load('variables', __DIR__ . '/../config/variables.php'); // List of env variables
Config::load('regions', __DIR__ . '/../config/regions.php'); // List of available regions
Config::load('avatar-browsers', __DIR__ . '/../config/avatars/browsers.php');
Config::load('avatar-credit-cards', __DIR__ . '/../config/avatars/credit-cards.php');
Config::load('avatar-flags', __DIR__ . '/../config/avatars/flags.php');
Config::load('locale-codes', __DIR__ . '/../config/locale/codes.php');
Config::load('locale-currencies', __DIR__ . '/../config/locale/currencies.php');
Config::load('locale-eu', __DIR__ . '/../config/locale/eu.php');
Config::load('locale-languages', __DIR__ . '/../config/locale/languages.php');
Config::load('locale-phones', __DIR__ . '/../config/locale/phones.php');
Config::load('locale-countries', __DIR__ . '/../config/locale/countries.php');
Config::load('locale-continents', __DIR__ . '/../config/locale/continents.php');
Config::load('locale-templates', __DIR__ . '/../config/locale/templates.php');
Config::load('storage-logos', __DIR__ . '/../config/storage/logos.php');
Config::load('storage-mimes', __DIR__ . '/../config/storage/mimes.php');
Config::load('storage-inputs', __DIR__ . '/../config/storage/inputs.php');
Config::load('storage-outputs', __DIR__ . '/../config/storage/outputs.php');
Config::load('runtime-specifications', __DIR__ . '/../config/runtimes/specifications.php');
Config::load('function-templates', __DIR__ . '/../config/function-templates.php');

202
app/init/constants.php Normal file
View file

@ -0,0 +1,202 @@
<?php
use Appwrite\Functions\Specification;
const APP_NAME = 'Appwrite';
const APP_DOMAIN = 'appwrite.io';
const APP_EMAIL_TEAM = 'team@localhost.test'; // Default email address
const APP_EMAIL_SECURITY = ''; // Default security email address
const APP_USERAGENT = APP_NAME . '-Server v%s. Please report abuse at %s';
const APP_MODE_DEFAULT = 'default';
const APP_MODE_ADMIN = 'admin';
const APP_PAGING_LIMIT = 12;
const APP_LIMIT_COUNT = 5000;
const APP_LIMIT_USERS = 10_000;
const APP_LIMIT_USER_PASSWORD_HISTORY = 20;
const APP_LIMIT_USER_SESSIONS_MAX = 100;
const APP_LIMIT_USER_SESSIONS_DEFAULT = 10;
const APP_LIMIT_ANTIVIRUS = 20_000_000; //20MB
const APP_LIMIT_ENCRYPTION = 20_000_000; //20MB
const APP_LIMIT_COMPRESSION = 20_000_000; //20MB
const APP_LIMIT_ARRAY_PARAMS_SIZE = 100; // Default maximum of how many elements can there be in API parameter that expects array value
const APP_LIMIT_ARRAY_LABELS_SIZE = 1000; // Default maximum of how many labels elements can there be in API parameter that expects array value
const APP_LIMIT_ARRAY_ELEMENT_SIZE = 4096; // Default maximum length of element in array parameter represented by maximum URL length.
const APP_LIMIT_SUBQUERY = 1000;
const APP_LIMIT_SUBSCRIBERS_SUBQUERY = 1_000_000;
const APP_LIMIT_WRITE_RATE_DEFAULT = 60; // Default maximum write rate per rate period
const APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT = 60; // Default maximum write rate period in seconds
const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return in list API calls
const APP_KEY_ACCESS = 24 * 60 * 60; // 24 hours
const APP_USER_ACCESS = 24 * 60 * 60; // 24 hours
const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4318;
const APP_VERSION_STABLE = '1.6.0';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
const APP_DATABASE_ATTRIBUTE_DATETIME = 'datetime';
const APP_DATABASE_ATTRIBUTE_URL = 'url';
const APP_DATABASE_ATTRIBUTE_INT_RANGE = 'intRange';
const APP_DATABASE_ATTRIBUTE_FLOAT_RANGE = 'floatRange';
const APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH = 1_073_741_824; // 2^32 bits / 4 bits per char
const APP_DATABASE_TIMEOUT_MILLISECONDS = 15_000;
const APP_STORAGE_UPLOADS = '/storage/uploads';
const APP_STORAGE_FUNCTIONS = '/storage/functions';
const APP_STORAGE_BUILDS = '/storage/builds';
const APP_STORAGE_CACHE = '/storage/cache';
const APP_STORAGE_CERTIFICATES = '/storage/certificates';
const APP_STORAGE_CONFIG = '/storage/config';
const APP_STORAGE_READ_BUFFER = 20 * (1000 * 1000); //20MB other names `APP_STORAGE_MEMORY_LIMIT`, `APP_STORAGE_MEMORY_BUFFER`, `APP_STORAGE_READ_LIMIT`, `APP_STORAGE_BUFFER_LIMIT`
const APP_SOCIAL_TWITTER = 'https://twitter.com/appwrite';
const APP_SOCIAL_TWITTER_HANDLE = 'appwrite';
const APP_SOCIAL_FACEBOOK = 'https://www.facebook.com/appwrite.io';
const APP_SOCIAL_LINKEDIN = 'https://www.linkedin.com/company/appwrite';
const APP_SOCIAL_INSTAGRAM = 'https://www.instagram.com/appwrite.io';
const APP_SOCIAL_GITHUB = 'https://github.com/appwrite';
const APP_SOCIAL_DISCORD = 'https://appwrite.io/discord';
const APP_SOCIAL_DISCORD_CHANNEL = '564160730845151244';
const APP_SOCIAL_DEV = 'https://dev.to/appwrite';
const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite';
const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=1';
const APP_HOSTNAME_INTERNAL = 'appwrite';
const APP_FUNCTION_SPECIFICATION_DEFAULT = Specification::S_05VCPU_512MB;
const APP_FUNCTION_CPUS_DEFAULT = 0.5;
const APP_FUNCTION_MEMORY_DEFAULT = 512;
const APP_PLATFORM_SERVER = 'server';
const APP_PLATFORM_CLIENT = 'client';
const APP_PLATFORM_CONSOLE = 'console';
const DATABASE_SHARED_TABLES = 'database_db_fra1_self_hosted_16_0';
// Database Reconnect
const DATABASE_RECONNECT_SLEEP = 2;
const DATABASE_RECONNECT_MAX_ATTEMPTS = 10;
// Database Worker Types
const DATABASE_TYPE_CREATE_ATTRIBUTE = 'createAttribute';
const DATABASE_TYPE_CREATE_INDEX = 'createIndex';
const DATABASE_TYPE_DELETE_ATTRIBUTE = 'deleteAttribute';
const DATABASE_TYPE_DELETE_INDEX = 'deleteIndex';
const DATABASE_TYPE_DELETE_COLLECTION = 'deleteCollection';
const DATABASE_TYPE_DELETE_DATABASE = 'deleteDatabase';
// Build Worker Types
const BUILD_TYPE_DEPLOYMENT = 'deployment';
const BUILD_TYPE_RETRY = 'retry';
// Deletion Types
const DELETE_TYPE_DATABASES = 'databases';
const DELETE_TYPE_DOCUMENT = 'document';
const DELETE_TYPE_COLLECTIONS = 'collections';
const DELETE_TYPE_PROJECTS = 'projects';
const DELETE_TYPE_FUNCTIONS = 'functions';
const DELETE_TYPE_DEPLOYMENTS = 'deployments';
const DELETE_TYPE_USERS = 'users';
const DELETE_TYPE_TEAM_PROJECTS = 'teams_projects';
const DELETE_TYPE_EXECUTIONS = 'executions';
const DELETE_TYPE_AUDIT = 'audit';
const DELETE_TYPE_ABUSE = 'abuse';
const DELETE_TYPE_USAGE = 'usage';
const DELETE_TYPE_REALTIME = 'realtime';
const DELETE_TYPE_BUCKETS = 'buckets';
const DELETE_TYPE_INSTALLATIONS = 'installations';
const DELETE_TYPE_RULES = 'rules';
const DELETE_TYPE_SESSIONS = 'sessions';
const DELETE_TYPE_CACHE_BY_TIMESTAMP = 'cacheByTimeStamp';
const DELETE_TYPE_CACHE_BY_RESOURCE = 'cacheByResource';
const DELETE_TYPE_SCHEDULES = 'schedules';
const DELETE_TYPE_TOPIC = 'topic';
const DELETE_TYPE_TARGET = 'target';
const DELETE_TYPE_EXPIRED_TARGETS = 'invalid_targets';
const DELETE_TYPE_SESSION_TARGETS = 'session_targets';
// Message types
const MESSAGE_SEND_TYPE_INTERNAL = 'internal';
const MESSAGE_SEND_TYPE_EXTERNAL = 'external';
// Mail Types
const MAIL_TYPE_VERIFICATION = 'verification';
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
const MAIL_TYPE_RECOVERY = 'recovery';
const MAIL_TYPE_INVITATION = 'invitation';
const MAIL_TYPE_CERTIFICATE = 'certificate';
// Auth Types
const APP_AUTH_TYPE_SESSION = 'Session';
const APP_AUTH_TYPE_JWT = 'JWT';
const APP_AUTH_TYPE_KEY = 'Key';
const APP_AUTH_TYPE_ADMIN = 'Admin';
// Response related
const MAX_OUTPUT_CHUNK_SIZE = 10 * 1024 * 1024; // 10MB
// Function headers
const FUNCTION_ALLOWLIST_HEADERS_REQUEST = ['content-type', 'agent', 'content-length', 'host'];
const FUNCTION_ALLOWLIST_HEADERS_RESPONSE = ['content-type', 'content-length'];
// Message types
const MESSAGE_TYPE_EMAIL = 'email';
const MESSAGE_TYPE_SMS = 'sms';
const MESSAGE_TYPE_PUSH = 'push';
// API key types
const API_KEY_STANDARD = 'standard';
const API_KEY_DYNAMIC = 'dynamic';
// Usage metrics
const METRIC_TEAMS = 'teams';
const METRIC_USERS = 'users';
const METRIC_AUTH_METHOD_PHONE = 'auth.method.phone';
const METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE = METRIC_AUTH_METHOD_PHONE . '.{countryCode}';
const METRIC_MESSAGES = 'messages';
const METRIC_MESSAGES_SENT = METRIC_MESSAGES . '.sent';
const METRIC_MESSAGES_FAILED = METRIC_MESSAGES . '.failed';
const METRIC_MESSAGES_TYPE = METRIC_MESSAGES . '.{type}';
const METRIC_MESSAGES_TYPE_SENT = METRIC_MESSAGES . '.{type}.sent';
const METRIC_MESSAGES_TYPE_FAILED = METRIC_MESSAGES . '.{type}.failed';
const METRIC_MESSAGES_TYPE_PROVIDER = METRIC_MESSAGES . '.{type}.{provider}';
const METRIC_MESSAGES_TYPE_PROVIDER_SENT = METRIC_MESSAGES . '.{type}.{provider}.sent';
const METRIC_MESSAGES_TYPE_PROVIDER_FAILED = METRIC_MESSAGES . '.{type}.{provider}.failed';
const METRIC_SESSIONS = 'sessions';
const METRIC_DATABASES = 'databases';
const METRIC_COLLECTIONS = 'collections';
const METRIC_DATABASES_STORAGE = 'databases.storage';
const METRIC_DATABASE_ID_COLLECTIONS = '{databaseInternalId}.collections';
const METRIC_DATABASE_ID_STORAGE = '{databaseInternalId}.databases.storage';
const METRIC_DOCUMENTS = 'documents';
const METRIC_DATABASE_ID_DOCUMENTS = '{databaseInternalId}.documents';
const METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS = '{databaseInternalId}.{collectionInternalId}.documents';
const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE = '{databaseInternalId}.{collectionInternalId}.databases.storage';
const METRIC_BUCKETS = 'buckets';
const METRIC_FILES = 'files';
const METRIC_FILES_STORAGE = 'files.storage';
const METRIC_BUCKET_ID_FILES = '{bucketInternalId}.files';
const METRIC_BUCKET_ID_FILES_STORAGE = '{bucketInternalId}.files.storage';
const METRIC_FUNCTIONS = 'functions';
const METRIC_DEPLOYMENTS = 'deployments';
const METRIC_DEPLOYMENTS_STORAGE = 'deployments.storage';
const METRIC_BUILDS = 'builds';
const METRIC_BUILDS_SUCCESS = 'builds.success';
const METRIC_BUILDS_FAILED = 'builds.failed';
const METRIC_BUILDS_STORAGE = 'builds.storage';
const METRIC_BUILDS_COMPUTE = 'builds.compute';
const METRIC_BUILDS_COMPUTE_SUCCESS = 'builds.compute.success';
const METRIC_BUILDS_COMPUTE_FAILED = 'builds.compute.failed';
const METRIC_BUILDS_MB_SECONDS = 'builds.mbSeconds';
const METRIC_FUNCTION_ID_BUILDS_COMPUTE_SUCCESS = '{functionInternalId}.builds.compute.success';
const METRIC_FUNCTION_ID_BUILDS_COMPUTE_FAILED = '{functionInternalId}.builds.compute.failed';
const METRIC_FUNCTION_ID_BUILDS = '{functionInternalId}.builds';
const METRIC_FUNCTION_ID_BUILDS_SUCCESS = '{functionInternalId}.builds.success';
const METRIC_FUNCTION_ID_BUILDS_FAILED = '{functionInternalId}.builds.failed';
const METRIC_FUNCTION_ID_BUILDS_STORAGE = '{functionInternalId}.builds.storage';
const METRIC_FUNCTION_ID_BUILDS_COMPUTE = '{functionInternalId}.builds.compute';
const METRIC_FUNCTION_ID_DEPLOYMENTS = '{resourceType}.{resourceInternalId}.deployments';
const METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE = '{resourceType}.{resourceInternalId}.deployments.storage';
const METRIC_FUNCTION_ID_BUILDS_MB_SECONDS = '{functionInternalId}.builds.mbSeconds';
const METRIC_EXECUTIONS = 'executions';
const METRIC_EXECUTIONS_COMPUTE = 'executions.compute';
const METRIC_EXECUTIONS_MB_SECONDS = 'executions.mbSeconds';
const METRIC_FUNCTION_ID_EXECUTIONS = '{functionInternalId}.executions';
const METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE = '{functionInternalId}.executions.compute';
const METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS = '{functionInternalId}.executions.mbSeconds';
const METRIC_NETWORK_REQUESTS = 'network.requests';
const METRIC_NETWORK_INBOUND = 'network.inbound';
const METRIC_NETWORK_OUTBOUND = 'network.outbound';

View file

@ -0,0 +1,397 @@
<?php
use Appwrite\OpenSSL\OpenSSL;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\System\System;
Database::addFilter(
'casting',
function (mixed $value) {
return json_encode(['value' => $value], JSON_PRESERVE_ZERO_FRACTION);
},
function (mixed $value) {
if (is_null($value)) {
return;
}
return json_decode($value, true)['value'];
}
);
Database::addFilter(
'enum',
function (mixed $value, Document $attribute) {
if ($attribute->isSet('elements')) {
$attribute->removeAttribute('elements');
}
return $value;
},
function (mixed $value, Document $attribute) {
$formatOptions = \json_decode($attribute->getAttribute('formatOptions', '[]'), true);
if (isset($formatOptions['elements'])) {
$attribute->setAttribute('elements', $formatOptions['elements']);
}
return $value;
}
);
Database::addFilter(
'range',
function (mixed $value, Document $attribute) {
if ($attribute->isSet('min')) {
$attribute->removeAttribute('min');
}
if ($attribute->isSet('max')) {
$attribute->removeAttribute('max');
}
return $value;
},
function (mixed $value, Document $attribute) {
$formatOptions = json_decode($attribute->getAttribute('formatOptions', '[]'), true);
if (isset($formatOptions['min']) || isset($formatOptions['max'])) {
$attribute
->setAttribute('min', $formatOptions['min'])
->setAttribute('max', $formatOptions['max'])
;
}
return $value;
}
);
Database::addFilter(
'subQueryAttributes',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
$attributes = $database->find('attributes', [
Query::equal('collectionInternalId', [$document->getInternalId()]),
Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]),
Query::limit($database->getLimitForAttributes()),
]);
foreach ($attributes as $attribute) {
if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) {
$options = $attribute->getAttribute('options');
foreach ($options as $key => $value) {
$attribute->setAttribute($key, $value);
}
$attribute->removeAttribute('options');
}
}
return $attributes;
}
);
Database::addFilter(
'subQueryIndexes',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
->find('indexes', [
Query::equal('collectionInternalId', [$document->getInternalId()]),
Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]),
Query::limit($database->getLimitForIndexes()),
]);
}
);
Database::addFilter(
'subQueryPlatforms',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
->find('platforms', [
Query::equal('projectInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
}
);
Database::addFilter(
'subQueryKeys',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
->find('keys', [
Query::equal('projectInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
}
);
Database::addFilter(
'subQueryWebhooks',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
->find('webhooks', [
Query::equal('projectInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
}
);
Database::addFilter(
'subQuerySessions',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database->find('sessions', [
Query::equal('userInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
Database::addFilter(
'subQueryTokens',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database
->find('tokens', [
Query::equal('userInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
Database::addFilter(
'subQueryChallenges',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database
->find('challenges', [
Query::equal('userInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
Database::addFilter(
'subQueryAuthenticators',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database
->find('authenticators', [
Query::equal('userInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
Database::addFilter(
'subQueryMemberships',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database
->find('memberships', [
Query::equal('userInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
Database::addFilter(
'subQueryVariables',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
->find('variables', [
Query::equal('resourceInternalId', [$document->getInternalId()]),
Query::equal('resourceType', ['function']),
Query::limit(APP_LIMIT_SUBQUERY),
]);
}
);
Database::addFilter(
'encrypt',
function (mixed $value) {
$key = System::getEnv('_APP_OPENSSL_KEY_V1');
$iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
$tag = null;
return json_encode([
'data' => OpenSSL::encrypt($value, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag),
'method' => OpenSSL::CIPHER_AES_128_GCM,
'iv' => \bin2hex($iv),
'tag' => \bin2hex($tag ?? ''),
'version' => '1',
]);
},
function (mixed $value) {
if (is_null($value)) {
return;
}
$value = json_decode($value, true);
$key = System::getEnv('_APP_OPENSSL_KEY_V' . $value['version']);
return OpenSSL::decrypt($value['data'], $value['method'], $key, 0, hex2bin($value['iv']), hex2bin($value['tag']));
}
);
Database::addFilter(
'subQueryProjectVariables',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
->find('variables', [
Query::equal('resourceType', ['project']),
Query::limit(APP_LIMIT_SUBQUERY)
]);
}
);
Database::addFilter(
'userSearch',
function (mixed $value, Document $user) {
$searchValues = [
$user->getId(),
$user->getAttribute('email', ''),
$user->getAttribute('name', ''),
$user->getAttribute('phone', '')
];
foreach ($user->getAttribute('labels', []) as $label) {
$searchValues[] = 'label:' . $label;
}
$search = implode(' ', \array_filter($searchValues));
return $search;
},
function (mixed $value) {
return $value;
}
);
Database::addFilter(
'subQueryTargets',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database
->find('targets', [
Query::equal('userInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY)
]));
}
);
Database::addFilter(
'subQueryTopicTargets',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
$targetIds = $database->getAuthorization()->skip(fn () => \array_map(
fn ($document) => $document->getAttribute('targetInternalId'),
$database->find('subscribers', [
Query::equal('topicInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBSCRIBERS_SUBQUERY)
])
));
if (\count($targetIds) > 0) {
return $database->find('targets', [
Query::equal('$internalId', $targetIds)
]);
}
return [];
}
);
Database::addFilter(
'providerSearch',
function (mixed $value, Document $provider) {
$searchValues = [
$provider->getId(),
$provider->getAttribute('name', ''),
$provider->getAttribute('provider', ''),
$provider->getAttribute('type', '')
];
$search = \implode(' ', \array_filter($searchValues));
return $search;
},
function (mixed $value) {
return $value;
}
);
Database::addFilter(
'topicSearch',
function (mixed $value, Document $topic) {
$searchValues = [
$topic->getId(),
$topic->getAttribute('name', ''),
$topic->getAttribute('description', ''),
];
$search = \implode(' ', \array_filter($searchValues));
return $search;
},
function (mixed $value) {
return $value;
}
);
Database::addFilter(
'messageSearch',
function (mixed $value, Document $message) {
$searchValues = [
$message->getId(),
$message->getAttribute('description', ''),
$message->getAttribute('status', ''),
];
$data = \json_decode($message->getAttribute('data', []), true);
$providerType = $message->getAttribute('providerType', '');
if ($providerType === MESSAGE_TYPE_EMAIL) {
$searchValues = \array_merge($searchValues, [$data['subject'], MESSAGE_TYPE_EMAIL]);
} elseif ($providerType === MESSAGE_TYPE_SMS) {
$searchValues = \array_merge($searchValues, [$data['content'], MESSAGE_TYPE_SMS]);
} else {
$searchValues = \array_merge($searchValues, [$data['title'], MESSAGE_TYPE_PUSH]);
}
$search = \implode(' ', \array_filter($searchValues));
return $search;
},
function (mixed $value) {
return $value;
}
);

View file

@ -0,0 +1,43 @@
<?php
use Appwrite\Network\Validator\Email;
use Utopia\Database\Database;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Structure;
use Utopia\Http\Validator\IP;
use Utopia\Http\Validator\Range;
use Utopia\Http\Validator\URL;
use Utopia\Http\Validator\WhiteList;
Structure::addFormat(APP_DATABASE_ATTRIBUTE_EMAIL, function () {
return new Email();
}, Database::VAR_STRING);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_DATETIME, function () {
return new DatetimeValidator();
}, Database::VAR_DATETIME);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_ENUM, function ($attribute) {
$elements = $attribute['formatOptions']['elements'];
return new WhiteList($elements, true);
}, Database::VAR_STRING);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_IP, function () {
return new IP();
}, Database::VAR_STRING);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_URL, function () {
return new URL();
}, Database::VAR_STRING);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function ($attribute) {
$min = $attribute['formatOptions']['min'] ?? -INF;
$max = $attribute['formatOptions']['max'] ?? INF;
return new Range($min, $max, Range::TYPE_INTEGER);
}, Database::VAR_INTEGER);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function ($attribute) {
$min = $attribute['formatOptions']['min'] ?? -INF;
$max = $attribute['formatOptions']['max'] ?? INF;
return new Range($min, $max, Range::TYPE_FLOAT);
}, Database::VAR_FLOAT);

23
app/init/locale.php Normal file
View file

@ -0,0 +1,23 @@
<?php
use Utopia\Config\Config;
use Utopia\Locale\Locale;
Locale::$exceptions = false;
$locales = Config::getParam('locale-codes', []);
foreach ($locales as $locale) {
$code = $locale['code'];
$path = __DIR__ . '/../config/locale/translations/' . $code . '.json';
if (!\file_exists($path)) {
$path = __DIR__ . '/../config/locale/translations/' . \substr($code, 0, 2) . '.json'; // if `ar-ae` doesn't exist, look for `ar`
if (!\file_exists($path)) {
$path = __DIR__ . '/../config/locale/translations/en.json'; // if none translation exists, use default from `en.json`
}
}
Locale::setLanguageFromJSON($code, $path);
}

1091
app/init/resources.php Normal file

File diff suppressed because it is too large Load diff

View file

@ -5,136 +5,47 @@ use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Queue\Connections;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Swoole\Http\Request as SwooleRequest;
use Swoole\Http\Response as SwooleHttpResponse;
use Swoole\Http\Response as SwooleResponse;
use Swoole\Runtime;
use Swoole\Table;
use Swoole\Timer;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\Database\TimeLimit;
use Utopia\App;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\DI\Container;
use Utopia\DI\Dependency;
use Utopia\Http\Adapter\Swoole\Request as UtopiaRequest;
use Utopia\Http\Adapter\Swoole\Response as HttpResponse;
use Utopia\Http\Adapter\Swoole\Response as UtopiaResponse;
use Utopia\Http\Http;
use Utopia\Logger\Log;
use Utopia\Pools\Connection;
use Utopia\Registry\Registry;
use Utopia\System\System;
use Utopia\WebSocket\Adapter;
use Utopia\WebSocket\Server;
/**
* @var \Utopia\Registry\Registry $register
* @var Registry $registry
* @var Container $container
*/
global $registry, $container;
require_once __DIR__ . '/init.php';
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
// Allows overriding
if (!function_exists('getConsoleDB')) {
function getConsoleDB(): Database
{
global $register;
/** @var \Utopia\Pools\Group $pools */
$pools = $register->get('pools');
$dbAdapter = $pools
->get('console')
->pop()
->getResource()
;
$database = new Database($dbAdapter, getCache());
$database
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', '_console');
return $database;
}
}
// Allows overriding
if (!function_exists('getProjectDB')) {
function getProjectDB(Document $project): Database
{
global $register;
/** @var \Utopia\Pools\Group $pools */
$pools = $register->get('pools');
if ($project->isEmpty() || $project->getId() === 'console') {
return getConsoleDB();
}
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 = $pools
->get($dsn->getHost())
->pop()
->getResource();
$database = new Database($adapter, getCache());
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getInternalId());
}
$database
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId());
return $database;
}
}
// Allows overriding
if (!function_exists('getCache')) {
function getCache(): Cache
{
global $register;
$pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */
$list = Config::getParam('pools-cache', []);
$adapters = [];
foreach ($list as $value) {
$adapters[] = $pools
->get($value)
->pop()
->getResource()
;
}
return new Cache(new Sharding($adapters));
}
}
if (!function_exists('getRealtime')) {
function getRealtime(): Realtime
{
@ -166,8 +77,8 @@ $adapter
$server = new Server($adapter);
$logError = function (Throwable $error, string $action) use ($register) {
$logger = $register->get('logger');
$logError = function (Throwable $error, string $action) use ($registry) {
$logger = $registry->get('logger');
if ($logger && !$error instanceof Exception) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
@ -207,16 +118,16 @@ $logError = function (Throwable $error, string $action) use ($register) {
$server->error($logError);
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) {
$server->onStart(function () use ($stats, $container, $containerId, &$statsDocument, $logError) {
sleep(5); // wait for the initial database schema to be ready
Console::success('Server started successfully');
$authorization = $container->get('authorization');
/**
* Create document for this worker to share stats across Containers.
*/
go(function () use ($register, $containerId, &$statsDocument) {
go(function () use ($container, $containerId, &$statsDocument) {
$attempts = 0;
$database = getConsoleDB();
$database = $container->get('dbForConsole');
do {
try {
@ -230,14 +141,15 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
'value' => '{}'
]);
$statsDocument = Authorization::skip(fn () => $database->createDocument('realtime', $document));
$authorization = $container->get('authorization');
$statsDocument = $authorization->skip(fn () => $database->createDocument('realtime', $document));
break;
} catch (Throwable) {
Console::warning("Collection not ready. Retrying connection ({$attempts})...");
sleep(DATABASE_RECONNECT_SLEEP);
}
} while (true);
$register->get('pools')->reclaim();
($container->get('connections'))->reclaim();
});
/**
@ -245,7 +157,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
*/
// TODO: Remove this if check once it doesn't cause issues for cloud
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') {
Timer::tick(5000, function () use ($register, $stats, &$statsDocument, $logError) {
Timer::tick(5000, function () use ($container, $stats, &$statsDocument, $logError, $authorization) {
$payload = [];
foreach ($stats as $projectId => $value) {
$payload[$projectId] = $stats->get($projectId, 'connectionsTotal');
@ -255,40 +167,43 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
}
try {
$database = getConsoleDB();
$database = $container->get('dbForConsole');
$statsDocument
->setAttribute('timestamp', DateTime::now())
->setAttribute('value', json_encode($payload));
Authorization::skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
$authorization->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
} catch (Throwable $th) {
call_user_func($logError, $th, "updateWorkerDocument");
} finally {
$register->get('pools')->reclaim();
($container->get('connections'))->reclaim();
$container->refresh('dbForConsole');
}
});
}
});
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) {
$server->onWorkerStart(function (int $workerId) use ($server, $container, $stats, $realtime, $logError) {
Console::success('Worker ' . $workerId . ' started successfully');
$attempts = 0;
$start = time();
Timer::tick(5000, function () use ($server, $register, $realtime, $stats, $logError) {
$authorization = $container->get('authorization');
Timer::tick(5000, function () use ($server, $container, $realtime, $stats, $logError, $authorization) {
/**
* Sending current connections to project channels on the console project every 5 seconds.
*/
// TODO: Remove this if check once it doesn't cause issues for cloud
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') {
if ($realtime->hasSubscriber('console', Role::users()->toString(), 'project')) {
$database = getConsoleDB();
$database = $container->get('dbForConsole');
$payload = [];
$list = Authorization::skip(fn () => $database->find('realtime', [
$list = $authorization->skip(fn () => $database->find('realtime', [
Query::greaterThan('timestamp', DateTime::addSeconds(new \DateTime(), -15)),
]));
@ -328,8 +243,8 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
'data' => $event['data']
]));
}
$register->get('pools')->reclaim();
($container->get('connections'))->reclaim();
$container->refresh('dbForConsole');
}
}
/**
@ -359,13 +274,26 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
while ($attempts < 300) {
try {
if ($attempts > 0) {
Console::error('Pub/sub connection lost (lasted ' . (time() - $start) . ' seconds, worker: ' . $workerId . ').
Attempting restart in 5 seconds (attempt #' . $attempts . ')');
Console::error(
'Pub/sub connection lost (lasted ' . (time() - $start) . ' seconds, worker: ' . $workerId . ').
Attempting restart in 5 seconds (attempt #' . $attempts . ')'
);
sleep(5); // 5 sec delay between connection attempts
}
$start = time();
$redis = $register->get('pools')->get('pubsub')->pop()->getResource(); /** @var Redis $redis */
$pools = $container->get('pools');
$pool = $pools['pools-pubsub-pubsub']['pool'];
/** @var Connections $connections */
$connections = $container->get('connections');
$connection = $pool->get();
$connections->add($connection, $pool);
$redis = $connection;
/** @var Redis $redis */
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
if ($redis->ping(true)) {
@ -375,7 +303,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
Console::error('Pub/sub failed (worker: ' . $workerId . ')');
}
$redis->subscribe(['realtime'], function (Redis $redis, string $channel, string $payload) use ($server, $workerId, $stats, $register, $realtime) {
$redis->subscribe(['realtime'], function (Redis $redis, string $channel, string $payload) use ($server, $workerId, $stats, $realtime, $authorization, $container) {
$event = json_decode($payload, true);
if ($event['permissionsChanged'] && isset($event['userId'])) {
@ -384,25 +312,24 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) {
$connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId]));
$consoleDatabase = getConsoleDB();
$project = Authorization::skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
$database = getProjectDB($project);
$dbForConsole = $container->get('dbForConsole');
$user = $database->getDocument('users', $userId);
$project = $authorization->skip(fn () => $dbForConsole->getDocument('projects', $projectId));
$dbForProject = $container->get('getProjectDB')($project);
$roles = Auth::getRoles($user);
$user = $dbForProject->getDocument('users', $userId);
$roles = Auth::getRoles($user, $authorization);
$channels = $realtime->connections[$connection]['channels'];
$realtime->unsubscribe($connection);
$realtime->subscribe($projectId, $connection, $roles, $channels);
$register->get('pools')->reclaim();
}
}
$receivers = $realtime->getSubscribers($event);
if (App::isDevelopment() && !empty($receivers)) {
if (Http::isDevelopment() && !empty($receivers)) {
Console::log("[Debug][Worker {$workerId}] Receivers: " . count($receivers));
Console::log("[Debug][Worker {$workerId}] Receivers Connection IDs: " . json_encode($receivers));
Console::log("[Debug][Worker {$workerId}] Event: " . $payload);
@ -428,30 +355,40 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
sleep(DATABASE_RECONNECT_SLEEP);
continue;
} finally {
$register->get('pools')->reclaim();
($container->get('connections'))->reclaim();
$container->refresh('dbForConsole');
}
}
Console::error('Failed to restart pub/sub...');
});
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $logError) {
$app = new App('UTC');
$request = new Request($request);
$response = new Response(new SwooleResponse());
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $container, $stats, &$realtime, $logError) {
$authorization = $container->get('authorization');
$request = new Request(new UtopiaRequest($request));
$response = new Response(new UtopiaResponse(new SwooleResponse()));
$requestInjection = new Dependency();
$responseInjection = new Dependency();
$requestInjection->setName('request')->setCallback(fn () => $request);
$responseInjection->setName('response')->setCallback(fn () => $response);
$container->set($requestInjection);
$container->set($responseInjection);
Console::info("Connection open (user: {$connection})");
App::setResource('pools', fn () => $register->get('pools'));
App::setResource('request', fn () => $request);
App::setResource('response', fn () => $response);
try {
/** @var Document $project */
$project = $app->getResource('project');
$project = $container->refresh('project')->get('project');
$container->refresh('dbForProject');
/*
* Project Check
* Project Check
*/
if (empty($project->getId())) {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing or unknown project ID');
@ -460,15 +397,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
if (
array_key_exists('realtime', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['realtime']
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
$dbForProject = getProjectDB($project);
$console = $app->getResource('console'); /** @var Document $console */
$user = $app->getResource('user'); /** @var Document $user */
$dbForProject = $container->get('getProjectDB')($project);
/** @var Document $console */
$console = $container->get('console');
/** @var Document $user */
$user = $container->refresh('user')->get('user');
/*
* Abuse Check
*
@ -497,7 +435,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
}
$roles = Auth::getRoles($user);
$authorization = $container->get('authorization');
$roles = Auth::getRoles($user, $authorization);
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
@ -546,25 +485,33 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$server->send([$connection], json_encode($response));
$server->close($connection, $code);
if (App::isDevelopment()) {
if (Http::isDevelopment()) {
Console::error('[Error] Connection Error');
Console::error('[Error] Code: ' . $response['data']['code']);
Console::error('[Error] Message: ' . $response['data']['message']);
}
} finally {
$register->get('pools')->reclaim();
$connections = $container->get('connections');
$connections->reclaim();
}
});
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
$server->onWorkerStop(function (int $workerId) use ($container) {
$connections = $container->get('connections');
$connections->reclaim();
});
$server->onMessage(function (int $connection, string $message) use ($server, $container, $realtime, $containerId) {
try {
$response = new Response(new SwooleResponse());
$response = new Response(new HttpResponse(new SwooleHttpResponse()));
$projectId = $realtime->connections[$connection]['projectId'];
$database = getConsoleDB();
$database = $container->get('dbForConsole');
$authorization = $container->get('authorization');
$authentication = $container->get('authentication');
if ($projectId !== 'console') {
$project = Authorization::skip(fn () => $database->getDocument('projects', $projectId));
$database = getProjectDB($project);
$project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
$database = $container->get('getProjectDB')($project);
} else {
$project = null;
}
@ -602,20 +549,21 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
}
$session = Auth::decodeSession($message['data']['session']);
Auth::$unique = $session['id'] ?? '';
Auth::$secret = $session['secret'] ?? '';
$user = $database->getDocument('users', Auth::$unique);
$authentication->setUnique($session['id'] ?? '');
$authentication->setSecret($session['secret'] ?? '');
$user = $database->getDocument('users', $authentication->getUnique());
if (
empty($user->getId()) // Check a document has been found in the DB
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) // Validate user has valid login token
|| !Auth::sessionVerify($user->getAttribute('sessions', []), $authentication->getSecret()) // Validate user has valid login token
) {
// cookie not valid
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.');
}
$roles = Auth::getRoles($user);
$roles = Auth::getRoles($user, $authorization);
$channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId());
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels);
@ -649,7 +597,8 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$server->close($connection, $th->getCode());
}
} finally {
$register->get('pools')->reclaim();
($container->get('connections'))->reclaim();
$container->refresh('dbForConsole');
}
});

View file

@ -11,7 +11,8 @@ $httpsPort = $this->getParam('httpsPort', '');
$version = $this->getParam('version', '');
$organization = $this->getParam('organization', '');
$image = $this->getParam('image', '');
?>services:
?>
services:
traefik:
image: traefik:2.11
container_name: appwrite-traefik
@ -853,7 +854,7 @@ $image = $this->getParam('image', '');
- MYSQL_USER=${_APP_DB_USER}
- MYSQL_PASSWORD=${_APP_DB_PASS}
- MARIADB_AUTO_UPGRADE=1
command: 'mysqld --innodb-flush-method=fsync'
command: 'mysqld --innodb-flush-method=fsync --max_connections=5000'
redis:
image: redis:7.2.4-alpine

View file

@ -2,278 +2,107 @@
require_once __DIR__ . '/init.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\Usage;
use Appwrite\Event\UsageDump;
use Appwrite\Platform\Appwrite;
use Appwrite\Utopia\Queue\Connections;
use Swoole\Runtime;
use Utopia\App;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\DI\Dependency;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Service;
use Utopia\Pools\Group;
use Utopia\Queue\Connection;
use Utopia\Queue\Message;
use Utopia\Queue\Server;
use Utopia\Registry\Registry;
use Utopia\Queue\Worker;
use Utopia\Storage\Device\Local;
use Utopia\System\System;
Authorization::disable();
global $registry, $container;
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
Server::setResource('register', fn () => $register);
$project = new Dependency();
$register = new Dependency();
$dbForProject = new Dependency();
$abuseRetention = new Dependency();
$deviceForCache = new Dependency();
$auditRetention = new Dependency();
$queueForUsageDump = new Dependency();
$executionRetention = new Dependency();
$deviceForLocalFiles = new Dependency();
Server::setResource('dbForConsole', function (Cache $cache, Registry $register) {
$pools = $register->get('pools');
$database = $pools
->get('console')
->pop()
->getResource();
$register
->setName('register')
->setCallback(fn () => $registry);
$adapter = new Database($database, $cache);
$adapter->setNamespace('_console');
$project
->setName('project')
->inject('message')
->inject('dbForConsole')
->setCallback(function (Message $message, Database $dbForConsole) {
$payload = $message->getPayload() ?? [];
$project = new Document($payload['project'] ?? []);
return $adapter;
}, ['cache', 'register']);
Server::setResource('project', function (Message $message, Database $dbForConsole) {
$payload = $message->getPayload() ?? [];
$project = new Document($payload['project'] ?? []);
if ($project->getId() === 'console' || $project->isEmpty() || ! empty($project->getInternalId())) {
return $project;
}
return $dbForConsole->getDocument('projects', $project->getId());
}, ['message', 'dbForConsole']);
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForConsole) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
}
$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 = $pools
->get($dsn->getHost())
->pop()
->getResource();
$database = new Database($adapter, $cache);
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 ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getInternalId());
}
return $database;
}, ['cache', 'register', 'message', 'project', 'dbForConsole']);
Server::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases): Database {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
if ($project->getId() === 'console' || $project->isEmpty() || ! empty($project->getInternalId())) {
return $project;
}
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'));
}
return $dbForConsole->getDocument('projects', $project->getId());
});
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
$abuseRetention
->setName('abuseRetention')
->setCallback(function () {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400));
});
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getInternalId());
}
$auditRetention
->setName('auditRetention')
->setCallback(function () {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600));
});
return $database;
}
$executionRetention
->setName('executionRetention')
->setCallback(function () {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600));
});
$dbAdapter = $pools
->get($dsn->getHost())
->pop()
->getResource();
$queueForUsageDump
->setName('queueForUsageDump')
->inject('queue')
->setCallback(function (Connection $queue) {
return new UsageDump($queue);
});
$database = new Database($dbAdapter, $cache);
$deviceForCache
->setName('deviceForCache')
->inject('project')
->setCallback(function (Document $project) {
return getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId());
});
$databases[$dsn->getHost()] = $database;
$deviceForLocalFiles
->setName('deviceForLocalFiles')
->inject('project')
->setCallback(function (Document $project) {
return new Local(APP_STORAGE_UPLOADS . '/app-' . $project->getId());
});
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getInternalId());
}
$container->set($project);
$container->set($register);
$container->set($dbForProject);
$container->set($abuseRetention);
$container->set($auditRetention);
$container->set($deviceForCache);
$container->set($queueForUsageDump);
$container->set($executionRetention);
$container->set($deviceForLocalFiles);
return $database;
};
}, ['pools', 'dbForConsole', 'cache']);
Server::setResource('abuseRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400));
});
Server::setResource('auditRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600));
});
Server::setResource('executionRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600));
});
Server::setResource('cache', function (Registry $register) {
$pools = $register->get('pools');
$list = Config::getParam('pools-cache', []);
$adapters = [];
foreach ($list as $value) {
$adapters[] = $pools
->get($value)
->pop()
->getResource()
;
}
return new Cache(new Sharding($adapters));
}, ['register']);
Server::setResource('log', fn () => new Log());
Server::setResource('queueForUsage', function (Connection $queue) {
return new Usage($queue);
}, ['queue']);
Server::setResource('queueForUsageDump', function (Connection $queue) {
return new UsageDump($queue);
}, ['queue']);
Server::setResource('queue', function (Group $pools) {
return $pools->get('queue')->pop()->getResource();
}, ['pools']);
Server::setResource('queueForDatabase', function (Connection $queue) {
return new EventDatabase($queue);
}, ['queue']);
Server::setResource('queueForMessaging', function (Connection $queue) {
return new Messaging($queue);
}, ['queue']);
Server::setResource('queueForMails', function (Connection $queue) {
return new Mail($queue);
}, ['queue']);
Server::setResource('queueForBuilds', function (Connection $queue) {
return new Build($queue);
}, ['queue']);
Server::setResource('queueForDeletes', function (Connection $queue) {
return new Delete($queue);
}, ['queue']);
Server::setResource('queueForEvents', function (Connection $queue) {
return new Event($queue);
}, ['queue']);
Server::setResource('queueForAudits', function (Connection $queue) {
return new Audit($queue);
}, ['queue']);
Server::setResource('queueForFunctions', function (Connection $queue) {
return new Func($queue);
}, ['queue']);
Server::setResource('queueForCertificates', function (Connection $queue) {
return new Certificate($queue);
}, ['queue']);
Server::setResource('queueForMigrations', function (Connection $queue) {
return new Migration($queue);
}, ['queue']);
Server::setResource('logger', function (Registry $register) {
return $register->get('logger');
}, ['register']);
Server::setResource('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
Server::setResource('deviceForFunctions', function (Document $project) {
return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId());
}, ['project']);
Server::setResource('deviceForFiles', function (Document $project) {
return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId());
}, ['project']);
Server::setResource('deviceForBuilds', function (Document $project) {
return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId());
}, ['project']);
Server::setResource('deviceForCache', function (Document $project) {
return getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId());
}, ['project']);
$pools = $register->get('pools');
$platform = new Appwrite();
$args = $platform->getEnv('argv');
@ -292,6 +121,13 @@ if (\str_starts_with($workerName, 'databases')) {
}
try {
$connection = new Connection\Redis(
System::getEnv('_APP_REDIS_HOST', 'redis'),
System::getEnv('_APP_REDIS_PORT', '6379'),
System::getEnv('_APP_REDIS_USER', ''),
System::getEnv('_APP_REDIS_PASS', '')
);
/**
* Any worker can be configured with the following env vars:
* - _APP_WORKERS_NUM The total number of worker processes
@ -300,32 +136,35 @@ try {
*/
$platform->init(Service::TYPE_WORKER, [
'workersNum' => System::getEnv('_APP_WORKERS_NUM', 1),
'connection' => $pools->get('queue')->pop()->getResource(),
'connection' => $connection,
'workerName' => strtolower($workerName) ?? null,
'queueName' => $queueName
]);
} catch (\Throwable $e) {
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
}
$worker = $platform->getWorker();
$worker
->shutdown()
->inject('pools')
->action(function (Group $pools) {
$pools->reclaim();
Worker::init()
->inject('authorization')
->action(function (Authorization $authorization) {
$authorization->disable();
});
$worker
->error()
Worker::shutdown()
->inject('connections')
->action(function (Connections $connections) {
$connections->reclaim();
});
Worker::error()
->inject('error')
->inject('logger')
->inject('log')
->inject('pools')
->inject('connections')
->inject('project')
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project) use ($queueName) {
$pools->reclaim();
->inject('authorization')
->action(function (Throwable $error, ?Logger $logger, Log $log, Connections $connections, Document $project, Authorization $authorization) use ($queueName) {
$connections->reclaim();
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
if ($logger) {
@ -341,7 +180,7 @@ $worker
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('roles', Authorization::getRoles());
$log->addExtra('roles', $authorization->getRoles());
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
@ -360,9 +199,7 @@ $worker
Console::error('[Error] Line: ' . $error->getLine());
});
$worker->workerStart()
->action(function () use ($workerName) {
Console::info("Worker $workerName started");
});
$worker->start();
$platform
->getWorker()
->setContainer($container)
->start();

View file

@ -4,6 +4,7 @@
"description": "End to end backend server for frontend and mobile apps.",
"type": "project",
"license": "BSD-3-Clause",
"minimum-stability": "stable",
"authors": [
{
"name": "Eldad Fux",
@ -14,7 +15,8 @@
"test": "vendor/bin/phpunit",
"lint": "vendor/bin/pint --test",
"format": "vendor/bin/pint",
"bench": "vendor/bin/phpbench run --report=benchmark"
"bench": "vendor/bin/phpbench run --report=benchmark",
"check": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit 1G app src tests"
},
"autoload": {
"psr-4": {
@ -45,33 +47,33 @@
"ext-sockets": "*",
"appwrite/php-runtimes": "0.15.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "0.43.0",
"utopia-php/analytics": "0.10.*",
"utopia-php/audit": "0.43.0",
"utopia-php/abuse": "0.46.*",
"utopia-php/analytics": "0.13.*",
"utopia-php/audit": "0.46.*",
"utopia-php/cache": "0.10.*",
"utopia-php/cli": "0.15.*",
"utopia-php/cli": "0.19.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.53.5",
"utopia-php/domains": "0.5.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
"utopia-php/database": "0.55.1",
"utopia-php/domains": "0.6.*",
"utopia-php/dsn": "0.2.*",
"utopia-php/framework": "1.0.*",
"utopia-php/fetch": "0.2.*",
"utopia-php/image": "0.7.*",
"utopia-php/locale": "0.4.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.12.*",
"utopia-php/migration": "0.6.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/migration": "0.7.*",
"utopia-php/orchestration": "0.15.*",
"utopia-php/platform": "0.8.*",
"utopia-php/view": "0.2.*",
"utopia-php/pools": "0.5.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/queue": "0.7.*",
"utopia-php/queue": "0.8.*",
"utopia-php/registry": "0.5.*",
"utopia-php/storage": "0.18.*",
"utopia-php/swoole": "0.8.*",
"utopia-php/storage": "0.19.*",
"utopia-php/system": "0.8.*",
"utopia-php/vcs": "0.8.*",
"utopia-php/websocket": "0.1.*",
"utopia-php/vcs": "0.9.*",
"utopia-php/websocket": "0.2.*",
"matomo/device-detector": "6.1.*",
"dragonmantank/cron-expression": "3.3.2",
"phpmailer/phpmailer": "6.9.1",
@ -88,7 +90,8 @@
"swoole/ide-helper": "5.1.2",
"textalk/websocket": "1.5.7",
"laravel/pint": "^1.14",
"phpbench/phpbench": "^1.2"
"phpbench/phpbench": "^1.2",
"phpstan/phpstan": "1.8.*"
},
"provide": {
"ext-phpiredis": "*"

517
composer.lock generated
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": "1884e3a2966762c4a955842426b64f6c",
"content-hash": "b808f001c3ff4c0396cf51f46e8b314a",
"packages": [
{
"name": "adhocore/jwt",
@ -1430,16 +1430,16 @@
},
{
"name": "utopia-php/abuse",
"version": "0.43.0",
"version": "0.46.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
"reference": "6346a3b4c5177a43160035a7289e30fdfb0790d6"
"reference": "ab09ecf6ceac5420020e6046ec651c155005d3c3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/6346a3b4c5177a43160035a7289e30fdfb0790d6",
"reference": "6346a3b4c5177a43160035a7289e30fdfb0790d6",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/ab09ecf6ceac5420020e6046ec651c155005d3c3",
"reference": "ab09ecf6ceac5420020e6046ec651c155005d3c3",
"shasum": ""
},
"require": {
@ -1447,7 +1447,7 @@
"ext-pdo": "*",
"ext-redis": "*",
"php": ">=8.0",
"utopia-php/database": "0.53.*"
"utopia-php/database": "0.55.*"
},
"require-dev": {
"laravel/pint": "1.5.*",
@ -1475,27 +1475,28 @@
],
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
"source": "https://github.com/utopia-php/abuse/tree/0.43.0"
"source": "https://github.com/utopia-php/abuse/tree/0.46.0"
},
"time": "2024-08-30T05:17:23+00:00"
"time": "2024-10-07T19:09:29+00:00"
},
{
"name": "utopia-php/analytics",
"version": "0.10.2",
"version": "0.13.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/analytics.git",
"reference": "14c805114736f44c26d6d24b176a2f8b93d86a1f"
"reference": "3dace02af5d4190623f88fb6e02f5559a99f14c4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/analytics/zipball/14c805114736f44c26d6d24b176a2f8b93d86a1f",
"reference": "14c805114736f44c26d6d24b176a2f8b93d86a1f",
"url": "https://api.github.com/repos/utopia-php/analytics/zipball/3dace02af5d4190623f88fb6e02f5559a99f14c4",
"reference": "3dace02af5d4190623f88fb6e02f5559a99f14c4",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/cli": "^0.15.0"
"utopia-php/cli": "0.19.*",
"utopia-php/system": "0.8.*"
},
"require-dev": {
"laravel/pint": "dev-main",
@ -1521,27 +1522,27 @@
],
"support": {
"issues": "https://github.com/utopia-php/analytics/issues",
"source": "https://github.com/utopia-php/analytics/tree/0.10.2"
"source": "https://github.com/utopia-php/analytics/tree/0.13.0"
},
"time": "2023-03-22T12:01:09+00:00"
"time": "2024-09-05T16:19:26+00:00"
},
{
"name": "utopia-php/audit",
"version": "0.43.0",
"version": "0.46.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e"
"reference": "660bb322ca1e6a9fa121610a85930e65863e1e5d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e",
"reference": "cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/660bb322ca1e6a9fa121610a85930e65863e1e5d",
"reference": "660bb322ca1e6a9fa121610a85930e65863e1e5d",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/database": "0.53.*"
"utopia-php/database": "0.55.*"
},
"require-dev": {
"laravel/pint": "1.5.*",
@ -1568,9 +1569,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/0.43.0"
"source": "https://github.com/utopia-php/audit/tree/0.46.0"
},
"time": "2024-08-30T05:17:36+00:00"
"time": "2024-10-07T19:10:12+00:00"
},
{
"name": "utopia-php/cache",
@ -1624,27 +1625,29 @@
},
{
"name": "utopia-php/cli",
"version": "0.15.0",
"version": "0.19.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/cli.git",
"reference": "ccb7c8125ffe0254fef8f25744bfa376eb7bd0ea"
"reference": "f8af1d6087f498bc1f0191750a118d357ded9948"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/cli/zipball/ccb7c8125ffe0254fef8f25744bfa376eb7bd0ea",
"reference": "ccb7c8125ffe0254fef8f25744bfa376eb7bd0ea",
"url": "https://api.github.com/repos/utopia-php/cli/zipball/f8af1d6087f498bc1f0191750a118d357ded9948",
"reference": "f8af1d6087f498bc1f0191750a118d357ded9948",
"shasum": ""
},
"require": {
"php": ">=7.4",
"utopia-php/framework": "0.*.*"
"utopia-php/di": "0.1.*",
"utopia-php/framework": "1.0.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.3",
"squizlabs/php_codesniffer": "^3.6",
"vimeo/psalm": "4.0.1"
"swoole/ide-helper": "4.8.8"
},
"type": "library",
"autoload": {
@ -1667,9 +1670,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/cli/issues",
"source": "https://github.com/utopia-php/cli/tree/0.15.0"
"source": "https://github.com/utopia-php/cli/tree/0.19.0"
},
"time": "2023-03-01T05:55:14+00:00"
"time": "2024-09-05T15:46:56+00:00"
},
{
"name": "utopia-php/config",
@ -1724,16 +1727,16 @@
},
{
"name": "utopia-php/database",
"version": "0.53.5",
"version": "0.55.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "689ba22063bf46def385da8695ba7a921e81e38d"
"reference": "6f44079999491feb0ef85686f6b2ac7ef54db828"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/689ba22063bf46def385da8695ba7a921e81e38d",
"reference": "689ba22063bf46def385da8695ba7a921e81e38d",
"url": "https://api.github.com/repos/utopia-php/database/zipball/6f44079999491feb0ef85686f6b2ac7ef54db828",
"reference": "6f44079999491feb0ef85686f6b2ac7ef54db828",
"shasum": ""
},
"require": {
@ -1741,7 +1744,7 @@
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/cache": "0.10.*",
"utopia-php/framework": "0.33.*",
"utopia-php/framework": "1.0.*",
"utopia-php/mongo": "0.3.*"
},
"require-dev": {
@ -1752,7 +1755,7 @@
"phpunit/phpunit": "9.6.*",
"rregeer/phpunit-coverage-check": "0.3.*",
"swoole/ide-helper": "5.1.3",
"utopia-php/cli": "0.14.*"
"utopia-php/cli": "0.19.*"
},
"type": "library",
"autoload": {
@ -1774,30 +1777,79 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.53.5"
"source": "https://github.com/utopia-php/database/tree/0.55.1"
},
"time": "2024-09-24T08:43:10+00:00"
"time": "2024-10-07T18:52:26+00:00"
},
{
"name": "utopia-php/domains",
"version": "0.5.0",
"name": "utopia-php/di",
"version": "0.1.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/domains.git",
"reference": "bf07f60326f8389f378ddf6fcde86217e5cfe18c"
"url": "https://github.com/utopia-php/di.git",
"reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/domains/zipball/bf07f60326f8389f378ddf6fcde86217e5cfe18c",
"reference": "bf07f60326f8389f378ddf6fcde86217e5cfe18c",
"url": "https://api.github.com/repos/utopia-php/di/zipball/22490c95f7ac3898ed1c33f1b1b5dd577305ee31",
"reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31",
"shasum": ""
},
"require": {
"php": ">=8.2"
},
"require-dev": {
"laravel/pint": "^1.2",
"phpbench/phpbench": "^1.2",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.5.25",
"swoole/ide-helper": "4.8.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Utopia\\": "src/",
"Tests\\E2E\\": "tests/e2e"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A simple and lite library for managing dependency injections",
"keywords": [
"framework",
"http",
"php",
"upf"
],
"support": {
"issues": "https://github.com/utopia-php/di/issues",
"source": "https://github.com/utopia-php/di/tree/0.1.0"
},
"time": "2024-08-08T14:35:19+00:00"
},
{
"name": "utopia-php/domains",
"version": "0.6.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/domains.git",
"reference": "5c70b0f524deeb1fccc3962ad1da650ae2c94cda"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/domains/zipball/5c70b0f524deeb1fccc3962ad1da650ae2c94cda",
"reference": "5c70b0f524deeb1fccc3962ad1da650ae2c94cda",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/framework": "0.*.*"
"utopia-php/framework": "1.0.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
"phpstan/phpstan": "1.9.x-dev",
"phpunit/phpunit": "^9.3"
},
"type": "library",
@ -1834,9 +1886,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/domains/issues",
"source": "https://github.com/utopia-php/domains/tree/0.5.0"
"source": "https://github.com/utopia-php/domains/tree/0.6.0"
},
"time": "2024-01-03T22:04:27+00:00"
"time": "2024-09-05T16:21:11+00:00"
},
{
"name": "utopia-php/dsn",
@ -1926,26 +1978,30 @@
},
{
"name": "utopia-php/framework",
"version": "0.33.8",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
"reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5"
"reference": "fc63ec61c720190a5ea5bb484c615145850951e6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/a7f577540a25cb90896fef2b64767bf8d700f3c5",
"reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5",
"url": "https://api.github.com/repos/utopia-php/http/zipball/fc63ec61c720190a5ea5bb484c615145850951e6",
"reference": "fc63ec61c720190a5ea5bb484c615145850951e6",
"shasum": ""
},
"require": {
"php": ">=8.0"
"ext-swoole": "*",
"php": ">=8.0",
"utopia-php/servers": "0.1.*"
},
"require-dev": {
"ext-xdebug": "*",
"laravel/pint": "^1.2",
"phpbench/phpbench": "^1.2",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.5.25"
"phpunit/phpunit": "^9.5.25",
"swoole/ide-helper": "4.8.3"
},
"type": "library",
"autoload": {
@ -1957,17 +2013,18 @@
"license": [
"MIT"
],
"description": "A simple, light and advanced PHP framework",
"description": "A simple, light and advanced PHP HTTP framework",
"keywords": [
"framework",
"http",
"php",
"upf"
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
"source": "https://github.com/utopia-php/http/tree/0.33.8"
"source": "https://github.com/utopia-php/http/tree/1.0.2"
},
"time": "2024-08-15T14:10:09+00:00"
"time": "2024-09-10T09:04:19+00:00"
},
{
"name": "utopia-php/image",
@ -2175,16 +2232,16 @@
},
{
"name": "utopia-php/migration",
"version": "0.6.4",
"version": "0.7.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "e43ef283f1386084e11d1ffe093fb6c6d7a6ce6c"
"reference": "aa996ebfd3223fca2aa7d8b9aa31457c95344084"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/e43ef283f1386084e11d1ffe093fb6c6d7a6ce6c",
"reference": "e43ef283f1386084e11d1ffe093fb6c6d7a6ce6c",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/aa996ebfd3223fca2aa7d8b9aa31457c95344084",
"reference": "aa996ebfd3223fca2aa7d8b9aa31457c95344084",
"shasum": ""
},
"require": {
@ -2192,17 +2249,16 @@
"ext-curl": "*",
"ext-openssl": "*",
"php": "8.3.*",
"utopia-php/database": "0.53.*",
"utopia-php/database": "0.55.*",
"utopia-php/dsn": "0.2.*",
"utopia-php/framework": "0.33.*",
"utopia-php/storage": "0.18.*"
"utopia-php/storage": "0.19.*"
},
"require-dev": {
"ext-pdo": "*",
"laravel/pint": "1.17.*",
"phpstan/phpstan": "1.11.*",
"phpunit/phpunit": "11.2.*",
"utopia-php/cli": "0.16.*",
"utopia-php/cli": "0.19.*",
"vlucas/phpdotenv": "5.6.*"
},
"type": "library",
@ -2225,9 +2281,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/0.6.4"
"source": "https://github.com/utopia-php/migration/tree/0.7.0"
},
"time": "2024-10-02T15:16:36+00:00"
"time": "2024-10-07T19:00:18+00:00"
},
{
"name": "utopia-php/mongo",
@ -2291,26 +2347,26 @@
},
{
"name": "utopia-php/orchestration",
"version": "0.9.1",
"version": "0.15.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/orchestration.git",
"reference": "55f43513b3f940a3f4f9c2cde7682d0c2581beb0"
"reference": "cd55650ba5f13118c3580048e6dd86b604f9a5b3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/orchestration/zipball/55f43513b3f940a3f4f9c2cde7682d0c2581beb0",
"reference": "55f43513b3f940a3f4f9c2cde7682d0c2581beb0",
"url": "https://api.github.com/repos/utopia-php/orchestration/zipball/cd55650ba5f13118c3580048e6dd86b604f9a5b3",
"reference": "cd55650ba5f13118c3580048e6dd86b604f9a5b3",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/cli": "0.15.*"
"utopia-php/cli": "0.19.*"
},
"require-dev": {
"laravel/pint": "^1.2",
"phpunit/phpunit": "^9.3",
"vimeo/psalm": "4.0.1"
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.3"
},
"type": "library",
"autoload": {
@ -2335,31 +2391,32 @@
],
"support": {
"issues": "https://github.com/utopia-php/orchestration/issues",
"source": "https://github.com/utopia-php/orchestration/tree/0.9.1"
"source": "https://github.com/utopia-php/orchestration/tree/0.15.0"
},
"time": "2023-03-17T15:05:06+00:00"
"time": "2024-09-05T16:28:02+00:00"
},
{
"name": "utopia-php/platform",
"version": "0.7.0",
"version": "0.8.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/platform.git",
"reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52"
"reference": "95d57f38a4001c7189a66885c485ac635d305234"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/beeea0f2c9bce14a6869fc5c87a1047cdecb5c52",
"reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/95d57f38a4001c7189a66885c485ac635d305234",
"reference": "95d57f38a4001c7189a66885c485ac635d305234",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-redis": "*",
"php": ">=8.0",
"utopia-php/cli": "0.15.*",
"utopia-php/framework": "0.33.*",
"utopia-php/queue": "0.7.*"
"utopia-php/cli": "0.19.*",
"utopia-php/framework": "1.0.*",
"utopia-php/queue": "0.8.*",
"utopia-php/servers": "0.1.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
@ -2385,9 +2442,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/platform/issues",
"source": "https://github.com/utopia-php/platform/tree/0.7.0"
"source": "https://github.com/utopia-php/platform/tree/0.8.1"
},
"time": "2024-05-08T17:00:55+00:00"
"time": "2024-09-06T02:33:27+00:00"
},
{
"name": "utopia-php/pools",
@ -2495,22 +2552,23 @@
},
{
"name": "utopia-php/queue",
"version": "0.7.0",
"version": "0.8.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/queue.git",
"reference": "917565256eb94bcab7246f7a746b1a486813761b"
"reference": "a518b271f8c158d6e66e36972f767189111033c2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/917565256eb94bcab7246f7a746b1a486813761b",
"reference": "917565256eb94bcab7246f7a746b1a486813761b",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/a518b271f8c158d6e66e36972f767189111033c2",
"reference": "a518b271f8c158d6e66e36972f767189111033c2",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/cli": "0.15.*",
"utopia-php/framework": "0.*.*"
"utopia-php/cli": "0.19.*",
"utopia-php/di": "0.1.*",
"utopia-php/servers": "0.1.*"
},
"require-dev": {
"laravel/pint": "^0.2.3",
@ -2520,6 +2578,7 @@
"workerman/workerman": "^4.0"
},
"suggest": {
"ext-redis": "Needed to support Redis connections",
"ext-swoole": "Needed to support Swoole.",
"workerman/workerman": "Needed to support Workerman."
},
@ -2550,9 +2609,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/queue/issues",
"source": "https://github.com/utopia-php/queue/tree/0.7.0"
"source": "https://github.com/utopia-php/queue/tree/0.8.0"
},
"time": "2024-01-17T19:00:43+00:00"
"time": "2024-09-05T16:33:01+00:00"
},
{
"name": "utopia-php/registry",
@ -2607,17 +2666,70 @@
"time": "2021-03-10T10:45:22+00:00"
},
{
"name": "utopia-php/storage",
"version": "0.18.5",
"name": "utopia-php/servers",
"version": "0.1.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/storage.git",
"reference": "7d355c5e3ccc8ecebc0266f8ddd30088a43be919"
"url": "https://github.com/utopia-php/servers.git",
"reference": "fd5c8d32778f265256c1936372a071b944f5ba8a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/storage/zipball/7d355c5e3ccc8ecebc0266f8ddd30088a43be919",
"reference": "7d355c5e3ccc8ecebc0266f8ddd30088a43be919",
"url": "https://api.github.com/repos/utopia-php/servers/zipball/fd5c8d32778f265256c1936372a071b944f5ba8a",
"reference": "fd5c8d32778f265256c1936372a071b944f5ba8a",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/di": "0.1.*"
},
"require-dev": {
"laravel/pint": "^0.2.3",
"phpstan/phpstan": "^1.8",
"phpunit/phpunit": "^9.5.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Utopia\\Servers\\": "src/Servers"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Team Appwrite",
"email": "team@appwrite.io"
}
],
"description": "A base library for building Utopia style servers.",
"keywords": [
"framework",
"php",
"servers",
"upf",
"utopia"
],
"support": {
"issues": "https://github.com/utopia-php/servers/issues",
"source": "https://github.com/utopia-php/servers/tree/0.1.1"
},
"time": "2024-09-06T02:25:56+00:00"
},
{
"name": "utopia-php/storage",
"version": "0.19.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/storage.git",
"reference": "5013b894a776874d6010753fc9349df2225c69af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/storage/zipball/5013b894a776874d6010753fc9349df2225c69af",
"reference": "5013b894a776874d6010753fc9349df2225c69af",
"shasum": ""
},
"require": {
@ -2629,8 +2741,8 @@
"ext-zlib": "*",
"ext-zstd": "*",
"php": ">=8.0",
"utopia-php/framework": "0.*.*",
"utopia-php/system": "0.*.*"
"utopia-php/framework": "1.0.*",
"utopia-php/system": "0.8.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
@ -2657,60 +2769,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/storage/issues",
"source": "https://github.com/utopia-php/storage/tree/0.18.5"
"source": "https://github.com/utopia-php/storage/tree/0.19.0"
},
"time": "2024-09-04T08:57:27+00:00"
},
{
"name": "utopia-php/swoole",
"version": "0.8.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/swoole.git",
"reference": "5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4",
"reference": "5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4",
"shasum": ""
},
"require": {
"ext-swoole": "*",
"php": ">=8.0",
"utopia-php/framework": "0.33.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.3",
"swoole/ide-helper": "5.0.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Utopia\\Swoole\\": "src/Swoole"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "An extension for Utopia Framework to work with PHP Swoole as a PHP FPM alternative",
"keywords": [
"framework",
"http",
"php",
"server",
"swoole",
"upf",
"utopia"
],
"support": {
"issues": "https://github.com/utopia-php/swoole/issues",
"source": "https://github.com/utopia-php/swoole/tree/0.8.2"
},
"time": "2024-02-01T14:54:12+00:00"
"time": "2024-09-05T17:00:24+00:00"
},
{
"name": "utopia-php/system",
@ -2770,23 +2831,24 @@
},
{
"name": "utopia-php/vcs",
"version": "0.8.2",
"version": "0.9.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
"reference": "eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18"
"reference": "673abe2fef0750a841a4fa8fa6f99d4a602c68e7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18",
"reference": "eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/673abe2fef0750a841a4fa8fa6f99d4a602c68e7",
"reference": "673abe2fef0750a841a4fa8fa6f99d4a602c68e7",
"shasum": ""
},
"require": {
"adhocore/jwt": "^1.1",
"php": ">=8.0",
"utopia-php/cache": "^0.10.0",
"utopia-php/framework": "0.*.*"
"utopia-php/cache": "0.10.*",
"utopia-php/framework": "1.0.*",
"utopia-php/system": "0.8.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
@ -2813,32 +2875,76 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
"source": "https://github.com/utopia-php/vcs/tree/0.8.2"
"source": "https://github.com/utopia-php/vcs/tree/0.9.0"
},
"time": "2024-08-13T14:36:30+00:00"
"time": "2024-09-05T16:44:48+00:00"
},
{
"name": "utopia-php/websocket",
"version": "0.1.0",
"name": "utopia-php/view",
"version": "0.2.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/websocket.git",
"reference": "51fcb86171400d8aa40d76c54593481fd273dab5"
"url": "https://github.com/utopia-php/view.git",
"reference": "6ee55e83bc014c39ed6b69390f6d399116f65e88"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/websocket/zipball/51fcb86171400d8aa40d76c54593481fd273dab5",
"reference": "51fcb86171400d8aa40d76c54593481fd273dab5",
"url": "https://api.github.com/repos/utopia-php/view/zipball/6ee55e83bc014c39ed6b69390f6d399116f65e88",
"reference": "6ee55e83bc014c39ed6b69390f6d399116f65e88",
"shasum": ""
},
"require": {
"php": ">=8.0"
},
"require-dev": {
"laravel/pint": "^1.2",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.5.25"
},
"type": "library",
"autoload": {
"psr-4": {
"Utopia\\View\\": "src/View"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A simple, light and advanced PHP rendering engine",
"keywords": [
"php",
"view"
],
"support": {
"issues": "https://github.com/utopia-php/view/issues",
"source": "https://github.com/utopia-php/view/tree/0.2.0"
},
"time": "2024-04-01T17:21:29+00:00"
},
{
"name": "utopia-php/websocket",
"version": "0.2.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/websocket.git",
"reference": "e9d0919b321744a61f12563f5791c47ba9f57810"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/websocket/zipball/e9d0919b321744a61f12563f5791c47ba9f57810",
"reference": "e9d0919b321744a61f12563f5791c47ba9f57810",
"shasum": ""
},
"require": {
"php": ">=8.0"
},
"require-dev": {
"laravel/pint": "^1.15",
"phpstan/phpstan": "^1.8",
"phpunit/phpunit": "^9.5.5",
"swoole/ide-helper": "4.6.6",
"swoole/ide-helper": "5.1.2",
"textalk/websocket": "1.5.2",
"vimeo/psalm": "^4.8.1",
"workerman/workerman": "^4.0"
},
"type": "library",
@ -2851,16 +2957,6 @@
"license": [
"MIT"
],
"authors": [
{
"name": "Eldad Fux",
"email": "eldad@appwrite.io"
},
{
"name": "Torsten Dittmann",
"email": "torsten@appwrite.io"
}
],
"description": "A simple abstraction for WebSocket servers.",
"keywords": [
"framework",
@ -2871,9 +2967,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/websocket/issues",
"source": "https://github.com/utopia-php/websocket/tree/0.1.0"
"source": "https://github.com/utopia-php/websocket/tree/0.2.0"
},
"time": "2021-12-20T10:50:09+00:00"
"time": "2024-04-09T08:28:11+00:00"
},
{
"name": "webmozart/assert",
@ -4239,6 +4335,65 @@
},
"time": "2024-09-26T07:23:32+00:00"
},
{
"name": "phpstan/phpstan",
"version": "1.8.11",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "46e223dd68a620da18855c23046ddb00940b4014"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/46e223dd68a620da18855c23046ddb00940b4014",
"reference": "46e223dd68a620da18855c23046ddb00940b4014",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
"keywords": [
"dev",
"static analysis"
],
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/1.8.11"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://github.com/phpstan",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
"type": "tidelift"
}
],
"time": "2022-10-24T15:45:13+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.32",

View file

@ -682,6 +682,7 @@ services:
entrypoint: maintenance
<<: *x-logging
container_name: appwrite-task-maintenance
restart: unless-stopped
image: appwrite-dev
networks:
- appwrite
@ -782,6 +783,7 @@ services:
entrypoint: schedule-functions
<<: *x-logging
container_name: appwrite-task-scheduler-functions
restart: unless-stopped
image: appwrite-dev
networks:
- appwrite
@ -810,6 +812,7 @@ services:
entrypoint: schedule-executions
<<: *x-logging
container_name: appwrite-task-scheduler-executions
restart: unless-stopped
image: appwrite-dev
networks:
- appwrite
@ -837,6 +840,7 @@ services:
entrypoint: schedule-messages
<<: *x-logging
container_name: appwrite-task-scheduler-messages
restart: unless-stopped
image: appwrite-dev
networks:
- appwrite
@ -956,7 +960,7 @@ services:
- MYSQL_USER=${_APP_DB_USER}
- MYSQL_PASSWORD=${_APP_DB_PASS}
- MARIADB_AUTO_UPGRADE=1
command: "mysqld --innodb-flush-method=fsync"
command: 'mysqld --innodb-flush-method=fsync --max_connections=5000'
redis:
image: redis:7.2.4-alpine

View file

@ -8,7 +8,7 @@ Setting an alias allows the route to be also accessible from the alias URL.
The first parameter specifies the alias URL, the second parameter specifies default values for route parameters.
```php
App::post('/v1/storage/buckets/:bucketId/files')
Http::post('/v1/storage/buckets/:bucketId/files')
->alias('/v1/storage/files', ['bucketId' => 'default'])
```
@ -17,7 +17,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
Used as an abstract description of the route.
```php
App::post('/v1/storage/buckets/:bucketId/files')
Http::post('/v1/storage/buckets/:bucketId/files')
->desc('Create File')
```
@ -26,14 +26,14 @@ App::post('/v1/storage/buckets/:bucketId/files')
Groups array is used to group one or more routes with one or more hooks functionality.
```php
App::post('/v1/storage/buckets/:bucketId/files')
Http::post('/v1/storage/buckets/:bucketId/files')
->groups(['api'])
```
In the above example groups() is used to define the current route as part of the routes that shares a common init middleware hook.
```php
App::init()
Http::init()
->groups(['api'])
->action(
some code.....
@ -52,7 +52,7 @@ Appwrite uses different labels to achieve different things, for example:
- scope - Defines the route permissions scope.
```php
App::post('/v1/storage/buckets/:bucketId/files')
Http::post('/v1/storage/buckets/:bucketId/files')
->label('scope', 'files.write')
```
@ -66,7 +66,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
- audits.resource - Signals the extraction part of the resource.
```php
App::post('/v1/account/create')
Http::post('/v1/account/create')
->label('audits.event', 'account.create')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
@ -84,7 +84,7 @@ App::post('/v1/account/create')
* sdk.offline.response.key - JSON property name that has the ID. Defaults to $id
```php
App::post('/v1/account/jwt')
Http::post('/v1/account/jwt')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createJWT')
@ -100,7 +100,7 @@ App::post('/v1/account/jwt')
- cache.resource - Identifies the cached resource.
```php
App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
Http::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->label('cache', true)
->label('cache.resource', 'file/{request.fileId}')
```
@ -115,7 +115,7 @@ When using the example below, we configure the abuse mechanism to allow this key
constructed from the combination of the ip, http method, url, userId to hit the route maximum 60 times in 1 hour (60 seconds \* 60 minutes).
```php
App::post('/v1/storage/buckets/:bucketId/files')
Http::post('/v1/storage/buckets/:bucketId/files')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', 60)
->label('abuse-time', 3600)
@ -127,7 +127,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
Placeholders marked as `[]` are parsed and replaced with their real values.
```php
App::post('/v1/storage/buckets/:bucketId/files')
Http::post('/v1/storage/buckets/:bucketId/files')
->label('event', 'buckets.[bucketId].files.[fileId].create')
```
@ -145,7 +145,7 @@ As the name implies, `param()` is used to define a request parameter.
- An array of injections
```php
App::get('/v1/account/logs')
Http::get('/v1/account/logs')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
```
@ -154,14 +154,14 @@ App::get('/v1/account/logs')
inject is used to inject dependencies pre-bounded to the app.
```php
App::post('/v1/storage/buckets/:bucketId/files')
Http::post('/v1/storage/buckets/:bucketId/files')
->inject('user')
```
In the example above, the user object is injected into the route pre-bounded using `App::setResource()`.
In the example above, the user object is injected into the route pre-bounded using `Http::setResource()`.
```php
App::setResource('user', function() {
Http::setResource('user', function() {
some code...
});
```
@ -170,7 +170,7 @@ some code...
Action populates the actual route code and has to be very clear and understandable. A good route stays simple and doesn't contain complex logic. An action is where we describe our business needs in code, and combine different libraries to work together and tell our story.
```php
App::post('/v1/account/sessions/anonymous')
Http::post('/v1/account/sessions/anonymous')
->action(function (Request $request) {
some code...
});

11
phpstan.neon Normal file
View file

@ -0,0 +1,11 @@
parameters:
level: 0
scanDirectories:
- vendor/swoole/ide-helper
excludePaths:
- tests/resources
ignoreErrors:
- '#Parameter \$geodb of anonymous function has invalid type MaxMind\\Db\\Reader\.#'
- '#Parameter \$geodb of function router\(\) has invalid type MaxMind\\Db\\Reader\.#'
- '#Instantiated class MaxMind\\Db\\Reader not found.#'
- '#Function scrypt not found\.#'

View file

@ -91,37 +91,6 @@ class Auth
*/
public const MFA_RECENT_DURATION = 1800; // 30 mins
/**
* @var string
*/
public static $cookieName = 'a_session';
/**
* User Unique ID.
*
* @var string
*/
public static $unique = '';
/**
* User Secret Key.
*
* @var string
*/
public static $secret = '';
/**
* Set Cookie Name.
*
* @param $string
*
* @return string
*/
public static function setCookieName($string)
{
return self::$cookieName = $string;
}
/**
* Encode Session.
*
@ -439,13 +408,14 @@ class Auth
* Returns all roles for a user.
*
* @param Document $user
* @param Authorization $auth
* @return array<string>
*/
public static function getRoles(Document $user): array
public static function getRoles(Document $user, Authorization $auth): array
{
$roles = [];
if (!self::isPrivilegedUser(Authorization::getRoles()) && !self::isAppUser(Authorization::getRoles())) {
if (!self::isPrivilegedUser($auth->getRoles()) && !self::isAppUser($auth->getRoles())) {
if ($user->getId()) {
$roles[] = Role::user($user->getId())->toString();
$roles[] = Role::users()->toString();

View file

@ -0,0 +1,57 @@
<?php
namespace Appwrite\Auth;
class Authentication
{
/**
* User Unique ID.
*
* @var string
*/
private $unique = '';
/**
* User Secret Key.
*
* @var string
*/
private $secret = '';
/**
* @var string
*/
private $cookieName = 'a_session';
public function setCookieName($string): string
{
return $this->cookieName = $string;
}
public function getCookieName(): string
{
return $this->cookieName;
}
public function getUnique(): string
{
return $this->unique;
}
public function setUnique(string $unique): void
{
$this->unique = $unique;
}
public function getSecret(): string
{
return $this->secret;
}
public function setSecret(string $secret): void
{
$this->secret = $secret;
}
}

View file

@ -2,8 +2,8 @@
namespace Appwrite\Auth\Validator;
use Utopia\Validator;
use Utopia\Validator\Text;
use Utopia\Http\Validator;
use Utopia\Http\Validator\Text;
/**
* MockNumber.
@ -52,6 +52,7 @@ class MockNumber extends Validator
return false;
}
$this->message = '';
return true;
}

View file

@ -2,7 +2,7 @@
namespace Appwrite\Auth\Validator;
use Utopia\Validator;
use Utopia\Http\Validator;
/**
* Password.

View file

@ -2,7 +2,7 @@
namespace Appwrite\Auth\Validator;
use Utopia\Validator;
use Utopia\Http\Validator;
/**
* Phone.

View file

@ -175,9 +175,9 @@ class Func extends Event
*
* @return string
*/
public function getData(): string
public function getBody(): string
{
return $this->data;
return $this->body;
}
/**

View file

@ -3,7 +3,7 @@
namespace Appwrite\Event\Validator;
use Utopia\Config\Config;
use Utopia\Validator;
use Utopia\Http\Validator;
class Event extends Validator
{

View file

@ -2,7 +2,7 @@
namespace Appwrite\Functions\Validator;
use Utopia\Validator;
use Utopia\Http\Validator;
/**
* Headers.

View file

@ -2,7 +2,7 @@
namespace Appwrite\Functions\Validator;
use Utopia\Validator\Text;
use Utopia\Http\Validator\Text;
class Payload extends Text
{

View file

@ -2,7 +2,7 @@
namespace Appwrite\Functions\Validator;
use Utopia\Validator;
use Utopia\Http\Validator;
class RuntimeSpecification extends Validator
{

View file

@ -6,9 +6,12 @@ use Appwrite\GraphQL\Exception as GQLException;
use Appwrite\Promises\Swoole;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\DI\Container;
use Utopia\Exception;
use Utopia\Route;
use Utopia\Http\Http;
use Utopia\Http\Request as UtopiaHttpRequest;
use Utopia\Http\Response as UtopiaHttpResponse;
use Utopia\Http\Route;
use Utopia\System\System;
class Resolvers
@ -16,24 +19,21 @@ class Resolvers
/**
* Create a resolver for a given API {@see Route}.
*
* @param App $utopia
* @param Http $http
* @param ?Route $route
* @return callable
*/
public static function api(
App $utopia,
public function api(
Http $http,
?Route $route,
UtopiaHttpRequest $request,
UtopiaHttpResponse $response,
Container $container,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $route, $args, $context, $info) {
/** @var App $utopia */
/** @var Response $response */
/** @var Request $request */
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$resolver = $this;
return fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($http, $route, $args, $context, $container, $info, $request, $response, $resolver) {
$path = $route->getPath();
foreach ($args as $key => $value) {
if (\str_contains($path, '/:' . $key)) {
@ -46,14 +46,13 @@ class Resolvers
switch ($route->getMethod()) {
case 'GET':
$request->setQueryString($args);
$request->setQuery($args);
break;
default:
$request->setPayload($args);
break;
}
self::resolve($utopia, $request, $response, $resolve, $reject);
$resolver->resolve($http, $request, $response, $container, $resolve, $reject);
}
);
}
@ -61,20 +60,20 @@ class Resolvers
/**
* Create a resolver for a document in a specified database and collection with a specific method type.
*
* @param App $utopia
* @param Http $http
* @param string $databaseId
* @param string $collectionId
* @param string $methodType
* @return callable
*/
public static function document(
App $utopia,
public function document(
Http $http,
string $databaseId,
string $collectionId,
string $methodType,
): callable {
return [self::class, 'document' . \ucfirst($methodType)](
$utopia,
$http,
$databaseId,
$collectionId
);
@ -83,28 +82,28 @@ class Resolvers
/**
* Create a resolver for getting a document in a specified database and collection.
*
* @param App $utopia
* @param Http $http
* @param string $databaseId
* @param string $collectionId
* @param callable $url
* @return callable
*/
public static function documentGet(
App $utopia,
public function documentGet(
Http $http,
string $databaseId,
string $collectionId,
callable $url,
UtopiaHttpRequest $request,
UtopiaHttpResponse $response,
Container $container,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$resolver = $this;
return fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $type, $args, $container, $request, $response, $resolver) {
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
$resolver->resolve($http, $request, $response, $container, $resolve, $reject);
}
);
}
@ -112,35 +111,35 @@ class Resolvers
/**
* Create a resolver for listing documents in a specified database and collection.
*
* @param App $utopia
* @param Http $http
* @param string $databaseId
* @param string $collectionId
* @param callable $url
* @param callable $params
* @return callable
*/
public static function documentList(
App $utopia,
public function documentList(
Http $http,
string $databaseId,
string $collectionId,
callable $url,
callable $params,
UtopiaHttpRequest $request,
UtopiaHttpResponse $response,
Container $container,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$resolver = $this;
return fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $params, $type, $args, $container, $request, $response, $resolver) {
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setQueryString($params($databaseId, $collectionId, $args));
$request->setQuery($params($databaseId, $collectionId, $args));
$beforeResolve = function ($payload) {
return $payload['documents'];
};
self::resolve($utopia, $request, $response, $resolve, $reject, $beforeResolve);
$resolver->resolve($http, $request, $response, $container, $resolve, $reject, $beforeResolve);
}
);
}
@ -148,31 +147,31 @@ class Resolvers
/**
* Create a resolver for creating a document in a specified database and collection.
*
* @param App $utopia
* @param Http $http
* @param string $databaseId
* @param string $collectionId
* @param callable $url
* @param callable $params
* @return callable
*/
public static function documentCreate(
App $utopia,
public function documentCreate(
Http $http,
string $databaseId,
string $collectionId,
callable $url,
callable $params,
UtopiaHttpRequest $request,
UtopiaHttpResponse $response,
Container $container,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$resolver = $this;
return fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $params, $type, $args, $container, $request, $response, $resolver) {
$request->setMethod('POST');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
$resolver->resolve($http, $request, $response, $container, $resolve, $reject);
}
);
}
@ -180,31 +179,31 @@ class Resolvers
/**
* Create a resolver for updating a document in a specified database and collection.
*
* @param App $utopia
* @param Http $http
* @param string $databaseId
* @param string $collectionId
* @param callable $url
* @param callable $params
* @return callable
*/
public static function documentUpdate(
App $utopia,
public function documentUpdate(
Http $http,
string $databaseId,
string $collectionId,
callable $url,
callable $params,
UtopiaHttpRequest $request,
UtopiaHttpResponse $response,
Container $container,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$resolver = $this;
return fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $params, $type, $args, $container, $request, $response, $resolver) {
$request->setMethod('PATCH');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
$resolver->resolve($http, $request, $response, $container, $resolve, $reject);
}
);
}
@ -212,34 +211,34 @@ class Resolvers
/**
* Create a resolver for deleting a document in a specified database and collection.
*
* @param App $utopia
* @param Http $http
* @param string $databaseId
* @param string $collectionId
* @param callable $url
* @return callable
*/
public static function documentDelete(
App $utopia,
public function documentDelete(
Http $http,
string $databaseId,
string $collectionId,
callable $url,
UtopiaHttpRequest $request,
UtopiaHttpResponse $response,
Container $container,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$resolver = $this;
return fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $type, $args, $container, $request, $response, $resolver) {
$request->setMethod('DELETE');
$request->setURI($url($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
$resolver->resolve($http, $request, $response, $container, $resolve, $reject);
}
);
}
/**
* @param App $utopia
* @param Http $http
* @param Request $request
* @param Response $response
* @param callable $resolve
@ -249,10 +248,11 @@ class Resolvers
* @return void
* @throws Exception
*/
private static function resolve(
App $utopia,
private function resolve(
Http $http,
Request $request,
Response $response,
Container $context,
callable $resolve,
callable $reject,
?callable $beforeResolve = null,
@ -263,14 +263,16 @@ class Resolvers
$request->removeHeader('content-type');
}
$request = clone $request;
$utopia->setResource('request', static fn () => $request);
$response->setContentType(Response::CONTENT_TYPE_NULL);
try {
$route = $utopia->match($request, fresh: true);
$context
->refresh('cache')
->refresh('dbForProject')
->refresh('dbForConsole')
->refresh('getProjectDb');
$utopia->execute($route, $request, $response);
$http->run(clone $context);
} catch (\Throwable $e) {
if ($beforeReject) {
$e = $beforeReject($e);
@ -285,10 +287,12 @@ class Resolvers
if ($beforeReject) {
$payload = $beforeReject($payload);
}
$reject(new GQLException(
message: $payload['message'],
code: $response->getStatusCode()
));
$reject(
new GQLException(
message: $payload['message'],
code: $response->getStatusCode()
)
);
return;
}

View file

@ -3,54 +3,63 @@
namespace Appwrite\GraphQL;
use Appwrite\GraphQL\Types\Mapper;
use Appwrite\Utopia\Response\Models;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema as GQLSchema;
use Utopia\App;
use Utopia\DI\Container;
use Utopia\Exception;
use Utopia\Route;
use Utopia\Http\Adapter\Swoole\Response as UtopiaSwooleResponse;
use Utopia\Http\Http;
use Utopia\Http\Request;
use Utopia\Http\Response as UtopiaHttpResponse;
use Utopia\Http\Route;
class Schema
{
protected static ?GQLSchema $schema = null;
protected static array $dirty = [];
protected ?GQLSchema $schema = null;
protected array $dirty = [];
/**
*
* @param App $utopia
* @param callable $complexity Function to calculate complexity
* @param callable $attributes Function to get attributes
* @param array $urls Array of functions to get urls for specific method types
* @param array $params Array of functions to build parameters for specific method types
* @param Http $http
* @param callable $complexity Function to calculate complexity
* @param callable $attributes Function to get attributes
* @param array $urls Array of functions to get urls for specific method types
* @param array $params Array of functions to build parameters for specific method types
* @return GQLSchema
* @throws Exception
*/
public static function build(
App $utopia,
public function build(
Http $http,
Request $request,
UtopiaHttpResponse $response,
Container $container,
callable $complexity,
callable $attributes,
array $urls,
array $params,
): GQLSchema {
App::setResource('utopia:graphql', static function () use ($utopia) {
return $utopia;
});
if (!empty(self::$schema)) {
return self::$schema;
if (!empty($this->schema)) {
return $this->schema;
}
$api = static::api(
$utopia,
$api = $this->api(
$http,
$request,
$response,
$container,
$complexity
);
//$collections = static::collections(
// $utopia,
// $complexity,
// $attributes,
// $urls,
// $params,
//);
// $collections = $this->collections(
// $http,
// $complexity,
// $request,
// $response,
// $attributes,
// $urls,
// $params,
// );
$queries = \array_merge_recursive(
$api['query'],
@ -64,7 +73,7 @@ class Schema
\ksort($queries);
\ksort($mutations);
return static::$schema = new GQLSchema([
return $this->schema = new GQLSchema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => $queries
@ -80,21 +89,23 @@ class Schema
* This function iterates all API routes and builds a GraphQL
* schema defining types and resolvers for all response models.
*
* @param App $utopia
* @param Http $http
* @param Request $request
* @param UtopiaSwooleResponse $response
* @param callable $complexity
* @return array
* @throws Exception
* @throws \Exception
*/
protected static function api(App $utopia, callable $complexity): array
protected function api(Http $http, Request $request, UtopiaHttpResponse $response, Container $container, callable $complexity): array
{
Mapper::init($utopia
->getResource('response')
->getModels());
Mapper::init(Models::getModels());
$mapper = new Mapper();
$queries = [];
$mutations = [];
foreach ($utopia->getRoutes() as $routes) {
foreach ($http->getRoutes() as $routes) {
foreach ($routes as $route) {
/** @var Route $route */
@ -106,7 +117,7 @@ class Schema
continue;
}
foreach (Mapper::route($utopia, $route, $complexity) as $field) {
foreach ($mapper->route($http, $route, $request, $response, $container, $complexity) as $field) {
switch ($route->getMethod()) {
case 'GET':
$queries[$name] = $field;
@ -134,7 +145,7 @@ class Schema
* Iterates all of a projects attributes and builds GraphQL
* queries and mutations for the collections they make up.
*
* @param App $utopia
* @param Http $http
* @param callable $complexity
* @param callable $attributes
* @param array $urls
@ -143,7 +154,7 @@ class Schema
* @throws \Exception
*/
protected static function collections(
App $utopia,
Http $http,
callable $complexity,
callable $attributes,
array $urls,
@ -194,36 +205,36 @@ class Schema
$queryFields[$collectionId . 'Get'] = [
'type' => $objectType,
'args' => Mapper::args('id'),
'resolve' => Resolvers::documentGet(
$utopia,
/*'resolve' => Resolvers::documentGet(
$http,
$databaseId,
$collectionId,
$urls['get'],
)
)*/
];
$queryFields[$collectionId . 'List'] = [
'type' => Type::listOf($objectType),
'args' => Mapper::args('list'),
'resolve' => Resolvers::documentList(
$utopia,
/*'resolve' => Resolvers::documentList(
$http,
$databaseId,
$collectionId,
$urls['list'],
$params['list'],
),
),*/
'complexity' => $complexity,
];
$mutationFields[$collectionId . 'Create'] = [
'type' => $objectType,
'args' => $attributes,
'resolve' => Resolvers::documentCreate(
$utopia,
/*'resolve' => Resolvers::documentCreate(
$http,
$databaseId,
$collectionId,
$urls['create'],
$params['create'],
)
)*/
];
$mutationFields[$collectionId . 'Update'] = [
'type' => $objectType,
@ -234,23 +245,23 @@ class Schema
$attributes
)
),
'resolve' => Resolvers::documentUpdate(
$utopia,
/*'resolve' => Resolvers::documentUpdate(
$http,
$databaseId,
$collectionId,
$urls['update'],
$params['update'],
)
)*/
];
$mutationFields[$collectionId . 'Delete'] = [
'type' => Mapper::model('none'),
'args' => Mapper::args('id'),
'resolve' => Resolvers::documentDelete(
$utopia,
/*'resolve' => Resolvers::documentDelete(
$http,
$databaseId,
$collectionId,
$urls['delete'],
)
)*/
];
}
$offset += $limit;
@ -262,8 +273,8 @@ class Schema
];
}
public static function setDirty(string $projectId): void
public function setDirty(string $projectId): void
{
self::$dirty[$projectId] = true;
$this->dirty[$projectId] = true;
}
}

View file

@ -8,10 +8,13 @@ use Exception;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use Utopia\App;
use Utopia\Route;
use Utopia\Validator;
use Utopia\Validator\Nullable;
use Utopia\DI\Container;
use Utopia\Http\Adapter\Swoole\Response as UtopiaSwooleResponse;
use Utopia\Http\Http;
use Utopia\Http\Request;
use Utopia\Http\Route;
use Utopia\Http\Validator;
use Utopia\Http\Validator\Nullable;
class Mapper
{
@ -75,12 +78,15 @@ class Mapper
return self::$args[$key] ?? [];
}
public static function route(
App $utopia,
public function route(
Http $http,
Route $route,
Request $request,
UtopiaSwooleResponse $response,
Container $container,
callable $complexity
): iterable {
foreach (self::$blacklist as $blacklist) {
foreach (static::$blacklist as $blacklist) {
if (\str_starts_with($route->getPath(), $blacklist)) {
return;
}
@ -102,7 +108,7 @@ class Mapper
$list = true;
}
$parameterType = Mapper::param(
$utopia,
$container,
$parameter['validator'],
!$parameter['optional'],
$parameter['injections']
@ -117,7 +123,7 @@ class Mapper
'type' => $type,
'description' => $description,
'args' => $params,
'resolve' => Resolvers::api($utopia, $route)
'resolve' => (new Resolvers())->api($http, $route, $request, $response, $container)
];
if ($list) {
@ -206,7 +212,7 @@ class Mapper
/**
* Map a {@see Route} parameter to a GraphQL Type
*
* @param App $utopia
* @param Container $container
* @param Validator|callable $validator
* @param bool $required
* @param array $injections
@ -214,13 +220,13 @@ class Mapper
* @throws Exception
*/
public static function param(
App $utopia,
Container $container,
Validator|callable $validator,
bool $required,
array $injections
): Type {
$validator = \is_callable($validator)
? \call_user_func_array($validator, $utopia->getResources($injections))
? \call_user_func_array($validator, array_map(fn ($injection) => $container->get($injection), $injections))
: $validator;
$isNullable = $validator instanceof Nullable;
@ -233,20 +239,20 @@ class Mapper
case 'Appwrite\Network\Validator\CNAME':
case 'Appwrite\Task\Validator\Cron':
case 'Appwrite\Utopia\Database\Validator\CustomId':
case 'Utopia\Validator\Domain':
case 'Utopia\Http\Validator\Domain':
case 'Appwrite\Network\Validator\Email':
case 'Appwrite\Event\Validator\Event':
case 'Appwrite\Event\Validator\FunctionEvent':
case 'Utopia\Validator\HexColor':
case 'Utopia\Validator\Host':
case 'Utopia\Validator\IP':
case 'Utopia\Http\Validator\HexColor':
case 'Utopia\Http\Validator\Host':
case 'Utopia\Http\Validator\IP':
case 'Utopia\Database\Validator\Key':
case 'Utopia\Validator\Origin':
case 'Utopia\Http\Validator\Origin':
case 'Appwrite\Auth\Validator\Password':
case 'Utopia\Validator\Text':
case 'Utopia\Http\Validator\Text':
case 'Utopia\Database\Validator\UID':
case 'Utopia\Validator\URL':
case 'Utopia\Validator\WhiteList':
case 'Utopia\Http\Validator\URL':
case 'Utopia\Http\Validator\WhiteList':
default:
$type = Type::string();
break;
@ -274,29 +280,29 @@ class Mapper
case 'Appwrite\Utopia\Database\Validator\Queries\Variables':
$type = Type::listOf(Type::string());
break;
case 'Utopia\Validator\Boolean':
case 'Utopia\Http\Validator\Boolean':
$type = Type::boolean();
break;
case 'Utopia\Validator\ArrayList':
case 'Utopia\Http\Validator\ArrayList':
$type = Type::listOf(self::param(
$utopia,
$container,
$validator->getValidator(),
$required,
$injections
));
break;
case 'Utopia\Validator\Integer':
case 'Utopia\Validator\Numeric':
case 'Utopia\Validator\Range':
case 'Utopia\Http\Validator\Integer':
case 'Utopia\Http\Validator\Numeric':
case 'Utopia\Http\Validator\Range':
$type = Type::int();
break;
case 'Utopia\Validator\FloatValidator':
case 'Utopia\Http\Validator\FloatValidator':
$type = Type::float();
break;
case 'Utopia\Validator\Assoc':
case 'Utopia\Http\Validator\Assoc':
$type = Types::assoc();
break;
case 'Utopia\Validator\JSON':
case 'Utopia\Http\Validator\JSON':
$type = Types::json();
break;
case 'Utopia\Storage\Validator\File':

View file

@ -98,10 +98,10 @@ abstract class Migration
*/
protected array $collections;
public function __construct()
public function __construct(Authorization $auth)
{
Authorization::disable();
Authorization::setDefaultStatus(false);
$auth->disable();
$auth->setDefaultStatus(false);
$this->collections = Config::getParam('collections', []);
@ -176,25 +176,23 @@ abstract class Migration
Console::log('Migrating Collection ' . $collection['$id'] . ':');
foreach ($this->documentsIterator($collection['$id']) as $document) {
go(function (Document $document, callable $callback) {
if (empty($document->getId()) || empty($document->getCollection())) {
return;
}
if (empty($document->getId()) || empty($document->getCollection())) {
return;
}
$old = $document->getArrayCopy();
$new = call_user_func($callback, $document);
$old = $document->getArrayCopy();
$new = call_user_func($callback, $document);
if (is_null($new) || $new->getArrayCopy() == $old) {
return;
}
if (is_null($new) || $new->getArrayCopy() == $old) {
return;
}
try {
$this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document);
} catch (\Throwable $th) {
Console::error('Failed to update document: ' . $th->getMessage());
return;
}
}, $document, $callback);
try {
$this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document);
} catch (\Throwable $th) {
Console::error('Failed to update document: ' . $th->getMessage());
return;
}
}
}
}

View file

@ -2,7 +2,7 @@
namespace Appwrite\Network\Validator;
use Utopia\Validator;
use Utopia\Http\Validator;
class CNAME extends Validator
{

View file

@ -2,14 +2,14 @@
namespace Appwrite\Network\Validator;
use Utopia\Validator;
use Utopia\Http\Validator;
/**
* Email
*
* Validate that an variable is a valid email address
*
* @package Utopia\Validator
* @package Utopia\Http\Validator
*/
class Email extends Validator
{

View file

@ -2,8 +2,8 @@
namespace Appwrite\Network\Validator;
use Utopia\Validator;
use Utopia\Validator\Hostname;
use Utopia\Http\Validator;
use Utopia\Http\Validator\Hostname;
class Origin extends Validator
{

View file

@ -3,13 +3,17 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\ClamAV\Network;
use Utopia\App;
use Appwrite\Utopia\Queue\Connections;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Adapter\MySQL;
use Utopia\Domains\Domain;
use Utopia\DSN\DSN;
use Utopia\Http\Http;
use Utopia\Logger\Logger;
use Utopia\Platform\Action;
use Utopia\Queue\Connection\Redis;
use Utopia\Registry\Registry;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;
@ -27,10 +31,11 @@ class Doctor extends Action
$this
->desc('Validate server health')
->inject('register')
->callback(fn (Registry $register) => $this->action($register));
->inject('connections')
->callback(fn (Registry $register, Connections $connections) => $this->action($register, $connections));
}
public function action(Registry $register): void
public function action(Registry $register, Connections $connections): void
{
Console::log(" __ ____ ____ _ _ ____ __ ____ ____ __ __
/ _\ ( _ \( _ \/ )( \( _ \( )(_ _)( __) ( )/ \
@ -125,22 +130,41 @@ class Doctor extends Action
//throw $th;
}
$pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */
/** @var array $pools */
$pools = $register->get('pools');
$configs = [
'Console.DB' => Config::getParam('pools-console'),
'Projects.DB' => Config::getParam('pools-database'),
'Console.DB' => [
'prefix' => 'console',
'databases' => Config::getParam('pools-console')
],
'Database.DB' => [
'prefix' => 'database',
'databases' => Config::getParam('pools-database')
],
];
foreach ($configs as $key => $config) {
foreach ($config as $database) {
foreach ($config['databases'] as $database) {
try {
$adapter = $pools->get($database)->pop()->getResource();
$pool = $pools['pools-' . $config['prefix'] . '-' . $database]['pool'];
$dsn = $pools['pools-' . $config['prefix'] . '-' . $database]['dsn'];
$connection = $pool->get();
$connections->add($connection, $pool);
$adapter = match ($dsn->getScheme()) {
'mariadb' => new MariaDB($connection),
'mysql' => new MySQL($connection),
default => null
};
$adapter->setDatabase($dsn->getPath());
if ($adapter->ping()) {
Console::success('🟢 ' . str_pad("{$key}({$database})", 50, '.') . 'connected');
Console::success('🟢 ' . str_pad("$key({$database})", 50, '.') . 'connected');
} else {
Console::error('🔴 ' . str_pad("{$key}({$database})", 47, '.') . 'disconnected');
Console::error('🔴 ' . str_pad("$key({$database})", 47, '.') . 'disconnected');
}
} catch (\Throwable $th) {
Console::error('🔴 ' . str_pad("{$key}.({$database})", 47, '.') . 'disconnected');
@ -148,25 +172,39 @@ class Doctor extends Action
}
}
$pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */
/** @var array $pools */
$pools = $register->get('pools');
$configs = [
'Cache' => Config::getParam('pools-cache'),
'Queue' => Config::getParam('pools-queue'),
'PubSub' => Config::getParam('pools-pubsub'),
'Cache' => [
'prefix' => 'cache',
'databases' => Config::getParam('pools-cache')
],
'Queue' => [
'prefix' => 'queue',
'databases' => Config::getParam('pools-queue')
],
'PubSub' => [
'prefix' => 'pubsub',
'databases' => Config::getParam('pools-pubsub')
],
];
foreach ($configs as $key => $config) {
foreach ($config as $pool) {
foreach ($config['databases'] as $database) {
try {
$adapter = $pools->get($pool)->pop()->getResource();
$pool = $pools['pools-' . $config['prefix'] . '-' . $database]['pool'];
$dsn = $pools['pools-' . $config['prefix'] . '-' . $database]['dsn'];
$connection = $pool->get();
$connections->add($connection, $pool);
$adapter = new Redis($dsn->getHost(), $dsn->getPort());
if ($adapter->ping()) {
Console::success('🟢 ' . str_pad("{$key}({$pool})", 50, '.') . 'connected');
Console::success('🟢 ' . str_pad("{$key}({$database})", 50, '.') . 'connected');
} else {
Console::error('🔴 ' . str_pad("{$key}({$pool})", 47, '.') . 'disconnected');
Console::error('🔴 ' . str_pad("{$key}({$database})", 47, '.') . 'disconnected');
}
} catch (\Throwable $th) {
Console::error('🔴 ' . str_pad("{$key}({$pool})", 47, '.') . 'disconnected');
Console::error('🔴 ' . str_pad("{$key}({$database})", 47, '.') . 'disconnected');
}
}
}
@ -258,7 +296,7 @@ class Doctor extends Action
}
try {
if (App::isProduction()) {
if (Http::isProduction()) {
Console::log('');
$version = \json_decode(@\file_get_contents(System::getEnv('_APP_HOME', 'http://localhost') . '/version'), true);

View file

@ -8,9 +8,9 @@ use Appwrite\Docker\Env;
use Appwrite\Utopia\View;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Http\Validator\Boolean;
use Utopia\Http\Validator\Text;
use Utopia\Platform\Action;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Install extends Action
{
@ -213,8 +213,7 @@ class Install extends Action
}
$env = '';
$stdout = '';
$stderr = '';
$output = '';
foreach ($input as $key => $value) {
if ($value) {
@ -225,13 +224,13 @@ class Install extends Action
$exit = 0;
if (!$noStart) {
Console::log("Running \"docker compose up -d --remove-orphans --renew-anon-volumes\"");
$exit = Console::execute("$env docker compose --project-directory $this->path up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr);
$exit = Console::execute("$env docker compose --project-directory $this->path up -d --remove-orphans --renew-anon-volumes", '', $output);
}
if ($exit !== 0) {
$message = 'Failed to install Appwrite dockers';
Console::error($message);
Console::error($stderr);
Console::error($output);
Console::exit($exit);
} else {
$message = 'Appwrite installed successfully';

View file

@ -10,10 +10,10 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Http\Validator\Text;
use Utopia\Platform\Action;
use Utopia\Registry\Registry;
use Utopia\System\System;
use Utopia\Validator\Text;
class Migrate extends Action
{
@ -30,12 +30,15 @@ class Migrate extends Action
->desc('Migrate Appwrite to new version')
/** @TODO APP_VERSION_STABLE needs to be defined */
->param('version', APP_VERSION_STABLE, new Text(8), 'Version to migrate to.', true)
->inject('cache')
->inject('dbForConsole')
->inject('getProjectDB')
->inject('register')
->callback(function ($version, $dbForConsole, $getProjectDB, Registry $register) {
\Co\run(function () use ($version, $dbForConsole, $getProjectDB, $register) {
$this->action($version, $dbForConsole, $getProjectDB, $register);
->inject('authorization')
->inject('console')
->callback(function ($version, $dbForConsole, $getProjectDB, Registry $register, Authorization $authorization, Document $console) {
\Co\run(function () use ($version, $dbForConsole, $getProjectDB, $register, $authorization, $console) {
$this->action($version, $dbForConsole, $getProjectDB, $register, $authorization, $console);
});
});
}
@ -58,13 +61,12 @@ class Migrate extends Action
}
}
public function action(string $version, Database $dbForConsole, callable $getProjectDB, Registry $register)
public function action(string $version, Database $dbForConsole, callable $getProjectDB, Registry $register, Authorization $auth, Document $console)
{
Authorization::disable();
$auth->disable();
if (!array_key_exists($version, Migration::$versions)) {
Console::error("Version {$version} not found.");
Console::exit(1);
return;
}
@ -77,12 +79,8 @@ class Migrate extends Action
10
);
$app = new App('UTC');
Console::success('Starting Data Migration to version ' . $version);
$console = $app->getResource('console');
$limit = 30;
$sum = 30;
$offset = 0;
@ -101,7 +99,7 @@ class Migrate extends Action
$class = 'Appwrite\\Migration\\Version\\' . Migration::$versions[$version];
/** @var Migration $migration */
$migration = new $class();
$migration = new $class($auth, );
while (!empty($projects)) {
foreach ($projects as $project) {

View file

@ -3,11 +3,11 @@
namespace Appwrite\Platform\Tasks;
use Utopia\CLI\Console;
use Utopia\Http\Validator\Text;
use Utopia\Http\Validator\WhiteList;
use Utopia\Platform\Action;
use Utopia\Queue\Client;
use Utopia\Queue\Connection;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class QueueCount extends Action
{

View file

@ -3,11 +3,11 @@
namespace Appwrite\Platform\Tasks;
use Utopia\CLI\Console;
use Utopia\Http\Validator\Text;
use Utopia\Http\Validator\Wildcard;
use Utopia\Platform\Action;
use Utopia\Queue\Client;
use Utopia\Queue\Connection;
use Utopia\Validator\Text;
use Utopia\Validator\Wildcard;
class QueueRetry extends Action
{

View file

@ -5,10 +5,10 @@ namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Certificate;
use Utopia\CLI\Console;
use Utopia\Database\Document;
use Utopia\Http\Validator\Boolean;
use Utopia\Http\Validator\Hostname;
use Utopia\Platform\Action;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\Hostname;
class SSL extends Action
{

View file

@ -2,41 +2,45 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\Utopia\Queue\Connections;
use Swoole\Timer;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception;
use Utopia\Database\Query;
use Utopia\Platform\Action;
use Utopia\Pools\Group;
use Utopia\System\System;
use function Swoole\Coroutine\run;
abstract class ScheduleBase extends Action
{
protected const UPDATE_TIMER = 10; //seconds
protected const ENQUEUE_TIMER = 60; //seconds
protected array $schedules = [];
protected Connections $connections;
abstract public static function getName(): string;
abstract public static function getSupportedResource(): string;
abstract public static function getCollectionId(): string;
abstract protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void;
abstract protected function enqueueResources(
array $pools,
callable $getConsoleDB
);
public function __construct()
{
$this->connections = new Connections();
$type = static::getSupportedResource();
$this
->desc("Execute {$type}s scheduled in Appwrite")
->inject('pools')
->inject('dbForConsole')
->inject('getConsoleDB')
->inject('getProjectDB')
->callback(fn (Group $pools, Database $dbForConsole, callable $getProjectDB) => $this->action($pools, $dbForConsole, $getProjectDB));
->callback(fn (array $pools, callable $getConsoleDB, callable $getProjectDB) => $this->action($pools, $getConsoleDB, $getProjectDB));
}
/**
@ -44,11 +48,12 @@ abstract class ScheduleBase extends Action
* 2. Create timer that sync all changes from 'schedules' collection to local copy. Only reading changes thanks to 'resourceUpdatedAt' attribute
* 3. Create timer that prepares coroutines for soon-to-execute schedules. When it's ready, coroutine sleeps until exact time before sending request to worker.
*/
public function action(Group $pools, Database $dbForConsole, callable $getProjectDB): void
public function action(array $pools, callable $getConsoleDB, callable $getProjectDB): void
{
Console::title(\ucfirst(static::getSupportedResource()) . ' scheduler V1');
Console::success(APP_NAME . ' ' . \ucfirst(static::getSupportedResource()) . ' scheduler v1 has started');
[$_, $_, $dbForConsole] = $getConsoleDB();
/**
* Extract only necessary attributes to lower memory used.
*
@ -59,6 +64,12 @@ abstract class ScheduleBase extends Action
$getSchedule = function (Document $schedule) use ($dbForConsole, $getProjectDB): array {
$project = $dbForConsole->getDocument('projects', $schedule->getAttribute('projectId'));
$collectionId = match ($schedule->getAttribute('resourceType')) {
'function' => 'functions',
'message' => 'messages',
'execution' => 'executions'
};
$resource = $getProjectDB($project)->getDocument(
static::getCollectionId(),
$schedule->getAttribute('resourceId')
@ -113,76 +124,73 @@ abstract class ScheduleBase extends Action
$latestDocument = \end($results);
}
$pools->reclaim();
Console::success("{$total} resources were loaded in " . (\microtime(true) - $loadStart) . " seconds");
Console::success("Starting timers at " . DateTime::now());
run(function () use ($dbForConsole, &$lastSyncUpdate, $getSchedule, $pools, $getProjectDB) {
/**
* The timer synchronize $schedules copy with database collection.
*/
Timer::tick(static::UPDATE_TIMER * 1000, function () use ($dbForConsole, &$lastSyncUpdate, $getSchedule, $pools) {
$time = DateTime::now();
$timerStart = \microtime(true);
Timer::tick(static::UPDATE_TIMER * 1000, function () use ($getConsoleDB, &$lastSyncUpdate, $getSchedule, $pools) {
[$connection,$pool, $dbForConsole] = $getConsoleDB();
$connections = new Connections();
$connections->add($connection, $pool);
$limit = 1000;
$sum = $limit;
$total = 0;
$latestDocument = null;
$time = DateTime::now();
$timerStart = \microtime(true);
Console::log("Sync tick: Running at $time");
$limit = 1000;
$sum = $limit;
$total = 0;
$latestDocument = null;
while ($sum === $limit) {
$paginationQueries = [Query::limit($limit)];
Console::log("Sync tick: Running at $time");
if ($latestDocument) {
$paginationQueries[] = Query::cursorAfter($latestDocument);
}
while ($sum === $limit) {
$paginationQueries = [Query::limit($limit)];
$results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
Query::equal('resourceType', [static::getSupportedResource()]),
Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate),
]));
$sum = count($results);
$total = $total + $sum;
foreach ($results as $document) {
$localDocument = $this->schedules[$document->getInternalId()] ?? null;
// Check if resource has been updated since last sync
$org = $localDocument !== null ? \strtotime($localDocument['resourceUpdatedAt']) : null;
$new = \strtotime($document['resourceUpdatedAt']);
if (!$document['active']) {
Console::info("Removing: {$document['resourceType']}::{$document['resourceId']}");
unset($this->schedules[$document->getInternalId()]);
} elseif ($new !== $org) {
Console::info("Updating: {$document['resourceType']}::{$document['resourceId']}");
$this->schedules[$document->getInternalId()] = $getSchedule($document);
}
}
$latestDocument = \end($results);
if ($latestDocument) {
$paginationQueries[] = Query::cursorAfter($latestDocument);
}
$lastSyncUpdate = $time;
$timerEnd = \microtime(true);
$results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
Query::equal('resourceType', [static::getSupportedResource()]),
Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate),
]));
$pools->reclaim();
$sum = count($results);
$total = $total + $sum;
Console::log("Sync tick: {$total} schedules were updated in " . ($timerEnd - $timerStart) . " seconds");
});
foreach ($results as $document) {
$localDocument = $this->schedules[$document->getInternalId()] ?? null;
Timer::tick(
static::ENQUEUE_TIMER * 1000,
fn () => $this->enqueueResources($pools, $dbForConsole, $getProjectDB)
);
// Check if resource has been updated since last sync
$org = $localDocument !== null ? \strtotime($localDocument['resourceUpdatedAt']) : null;
$new = \strtotime($document['resourceUpdatedAt']);
$this->enqueueResources($pools, $dbForConsole, $getProjectDB);
if (!$document['active']) {
Console::info("Removing: {$document['resourceType']}::{$document['resourceId']}");
unset($this->schedules[$document->getInternalId()]);
} elseif ($new !== $org) {
Console::info("Updating: {$document['resourceType']}::{$document['resourceId']}");
$this->schedules[$document->getInternalId()] = $getSchedule($document);
}
}
$latestDocument = \end($results);
}
$lastSyncUpdate = $time;
$timerEnd = \microtime(true);
$connections->reclaim();
Console::log("Sync tick: {$total} schedules were updated in " . ($timerEnd - $timerStart) . " seconds");
});
Timer::tick(
static::ENQUEUE_TIMER * 1000,
fn () => $this->enqueueResources($pools, $getConsoleDB)
);
$this->enqueueResources($pools, $getConsoleDB);
}
}

View file

@ -4,8 +4,7 @@ namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Func;
use Swoole\Coroutine as Co;
use Utopia\Database\Database;
use Utopia\Pools\Group;
use Utopia\Queue\Connection\Redis;
class ScheduleExecutions extends ScheduleBase
{
@ -27,11 +26,16 @@ class ScheduleExecutions extends ScheduleBase
return 'executions';
}
protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void
protected function enqueueResources(array $pools, callable $getConsoleDB): void
{
$queue = $pools->get('queue')->pop();
$connection = $queue->getResource();
$queueForFunctions = new Func($connection);
[$connection,$pool, $dbForConsole] = $getConsoleDB();
$this->connections->add($connection, $pool);
$queuePool = $pools['pools-queue-queue']['pool'];
$queueConnection = $queuePool->get();
$this->connections->add($queueConnection, $queuePool);
$queueForFunctions = new Func(new Redis($queueConnection));
$intervalEnd = (new \DateTime())->modify('+' . self::ENQUEUE_TIMER . ' seconds');
foreach ($this->schedules as $schedule) {
@ -57,7 +61,7 @@ class ScheduleExecutions extends ScheduleBase
$delay = $scheduledAt->getTimestamp() - (new \DateTime())->getTimestamp();
\go(function () use ($queueForFunctions, $schedule, $delay, $data) {
\go(function () use ($queueForFunctions, $schedule, $delay, $data, $dbForConsole) {
Co::sleep($delay);
$queueForFunctions->setType('schedule')
@ -72,16 +76,16 @@ class ScheduleExecutions extends ScheduleBase
->setProject($schedule['project'])
->setUserId($data['userId'] ?? '')
->trigger();
});
$dbForConsole->deleteDocument(
'schedules',
$schedule['$id'],
);
$dbForConsole->deleteDocument(
'schedules',
$schedule['$id'],
);
});
unset($this->schedules[$schedule['$internalId']]);
}
$queue->reclaim();
$this->connections->reclaim();
}
}

View file

@ -5,9 +5,8 @@ namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Func;
use Cron\CronExpression;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Pools\Group;
use Utopia\Queue\Connection\Redis;
class ScheduleFunctions extends ScheduleBase
{
@ -31,7 +30,7 @@ class ScheduleFunctions extends ScheduleBase
return 'functions';
}
protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void
protected function enqueueResources(array $pools, callable $getConsoleDB): void
{
$timerStart = \microtime(true);
$time = DateTime::now();
@ -73,8 +72,11 @@ class ScheduleFunctions extends ScheduleBase
\go(function () use ($delay, $scheduleKeys, $pools) {
\sleep($delay); // in seconds
$queue = $pools->get('queue')->pop();
$connection = $queue->getResource();
$pool = $pools['pools-queue-queue']['pool'];
$connection = $pool->get();
$this->connections->add($connection, $pool);
$queueConnection = new Redis($connection);
foreach ($scheduleKeys as $scheduleKey) {
// Ensure schedule was not deleted
@ -84,7 +86,7 @@ class ScheduleFunctions extends ScheduleBase
$schedule = $this->schedules[$scheduleKey];
$queueForFunctions = new Func($connection);
$queueForFunctions = new Func($queueConnection);
$queueForFunctions
->setType('schedule')
@ -95,7 +97,8 @@ class ScheduleFunctions extends ScheduleBase
->trigger();
}
$queue->reclaim();
$this->connections->reclaim();
// $queue->reclaim(); // TODO: Do in try/catch/finally, or add to connectons resource
});
}

View file

@ -3,8 +3,7 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Messaging;
use Utopia\Database\Database;
use Utopia\Pools\Group;
use Utopia\Queue\Connection\Redis;
class ScheduleMessages extends ScheduleBase
{
@ -26,8 +25,11 @@ class ScheduleMessages extends ScheduleBase
return 'messages';
}
protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void
protected function enqueueResources(array $pools, callable $getConsoleDB): void
{
[$connection,$pool, $dbForConsole] = $getConsoleDB();
$this->connections->add($connection, $pool);
foreach ($this->schedules as $schedule) {
if (!$schedule['active']) {
continue;
@ -40,10 +42,15 @@ class ScheduleMessages extends ScheduleBase
continue;
}
\go(function () use ($schedule, $pools, $dbForConsole) {
$queue = $pools->get('queue')->pop();
$connection = $queue->getResource();
$queueForMessaging = new Messaging($connection);
\go(function () use ($now, $schedule, $pools, $dbForConsole) {
$pool = $pools['pools-queue-queue']['pool'];
$dsn = $pools['pools-queue-queue']['dsn'];
$connection = $pool->get();
$this->connections->add($connection, $pool);
$queueConnection = new Redis($dsn->getHost(), $dsn->getPort());
$queueForMessaging = new Messaging($queueConnection);
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
@ -56,8 +63,7 @@ class ScheduleMessages extends ScheduleBase
$schedule['$id'],
);
$queue->reclaim();
$this->connections->reclaim();
unset($this->schedules[$schedule['$internalId']]);
});
}

View file

@ -5,25 +5,27 @@ namespace Appwrite\Platform\Tasks;
use Appwrite\Specification\Format\OpenAPI3;
use Appwrite\Specification\Format\Swagger2;
use Appwrite\Specification\Specification;
use Appwrite\Utopia\Request as AppwriteRequest;
use Appwrite\Utopia\Response as AppwriteResponse;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Models;
use Exception;
use Swoole\Http\Request as SwooleRequest;
use Swoole\Http\Response as SwooleResponse;
use Utopia\App;
use Swoole\Http\Request as SwooleHttpRequest;
use Swoole\Http\Response as SwooleHttpResponse;
use Utopia\Cache\Adapter\None;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Adapter\MySQL;
use Utopia\Database\Database;
use Utopia\DI\Container;
use Utopia\DI\Dependency;
use Utopia\Http\Adapter\FPM\Server;
use Utopia\Http\Adapter\Swoole\Request;
use Utopia\Http\Adapter\Swoole\Response as HttpResponse;
use Utopia\Http\Http;
use Utopia\Http\Validator\Text;
use Utopia\Http\Validator\WhiteList;
use Utopia\Platform\Action;
use Utopia\Registry\Registry;
use Utopia\Request as UtopiaRequest;
use Utopia\Response as UtopiaResponse;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class Specs extends Action
{
@ -32,37 +34,38 @@ class Specs extends Action
return 'specs';
}
public function getRequest(): UtopiaRequest
{
return new AppwriteRequest(new SwooleRequest());
}
public function getResponse(): UtopiaResponse
{
return new AppwriteResponse(new SwooleResponse());
}
public function __construct()
{
$this
->desc('Generate Appwrite API specifications')
->param('version', 'latest', new Text(16), 'Spec version', true)
->param('mode', 'normal', new WhiteList(['normal', 'mocks']), 'Spec Mode', true)
->inject('register')
->callback(fn (string $version, string $mode, Registry $register) => $this->action($version, $mode, $register));
->inject('context')
->callback(fn (string $version, string $mode, Container $context) => $this->action($version, $mode, $context));
}
public function action(string $version, string $mode, Registry $register): void
public function action(string $version, string $mode, Container $container): void
{
$appRoutes = App::getRoutes();
$response = $this->getResponse();
$appRoutes = Http::getRoutes();
$response = new Response(new HttpResponse(new SwooleHttpResponse()));
$mocks = ($mode === 'mocks');
$requestDependency = new Dependency();
$responseDependency = new Dependency();
$dbForConsole = new Dependency();
$dbForProject = new Dependency();
// Mock dependencies
App::setResource('request', fn () => $this->getRequest());
App::setResource('response', fn () => $response);
App::setResource('dbForConsole', fn () => new Database(new MySQL(''), new Cache(new None())));
App::setResource('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None())));
$requestDependency->setName('request')->setCallback(fn () => new Request(new SwooleHttpRequest()));
$responseDependency->setName('response')->setCallback(fn () => $response);
$dbForConsole->setName('dbForConsole')->setCallback(fn () => new Database(new MySQL(''), new Cache(new None())));
$dbForProject->setName('dbForProject')->setCallback(fn () => new Database(new MySQL(''), new Cache(new None())));
$container
->set($requestDependency)
->set($responseDependency)
->set($dbForProject)
->set($dbForConsole);
$platforms = [
'client' => APP_PLATFORM_CLIENT,
@ -252,7 +255,7 @@ class Specs extends Action
];
}
$models = $response->getModels();
$models = Models::getModels();
foreach ($models as $key => $value) {
if ($platform !== APP_PLATFORM_CONSOLE && !$value->isPublic()) {
@ -260,7 +263,7 @@ class Specs extends Action
}
}
$arguments = [new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0];
$arguments = [new Http(new Server(), $container, 'UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0];
foreach (['swagger2', 'open-api3'] as $format) {
$formatInstance = match ($format) {
'swagger2' => new Swagger2(...$arguments),

View file

@ -3,8 +3,8 @@
namespace Appwrite\Platform\Tasks;
use Utopia\CLI\Console;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Http\Validator\Boolean;
use Utopia\Http\Validator\Text;
class Upgrade extends Install
{

View file

@ -9,6 +9,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Validator\Authorization as ValidatorAuthorization;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
@ -28,7 +29,8 @@ class Audits extends Action
->desc('Audits worker')
->inject('message')
->inject('dbForProject')
->callback(fn ($message, $dbForProject) => $this->action($message, $dbForProject));
->inject('authorization')
->callback(fn ($message, $dbForProject, ValidatorAuthorization $authorization) => $this->action($message, $dbForProject, $authorization));
}
@ -41,7 +43,7 @@ class Audits extends Action
* @throws Authorization
* @throws Structure
*/
public function action(Message $message, Database $dbForProject): void
public function action(Message $message, Database $dbForProject, ValidatorAuthorization $auth): void
{
$payload = $message->getPayload() ?? [];

View file

@ -54,7 +54,8 @@ class Builds extends Action
->inject('dbForProject')
->inject('deviceForFunctions')
->inject('log')
->callback(fn ($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log));
->inject('authorization')
->callback(fn ($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log, Authorization $authorization) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log, $authorization));
}
/**
@ -67,10 +68,11 @@ class Builds extends Action
* @param Database $dbForProject
* @param Device $deviceForFunctions
* @param Log $log
* @param Authorization $auth
* @return void
* @throws \Utopia\Database\Exception
*/
public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void
public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log, Authorization $auth): void
{
$payload = $message->getPayload() ?? [];
@ -92,7 +94,7 @@ class Builds extends Action
case BUILD_TYPE_RETRY:
Console::info('Creating build for deployment: ' . $deployment->getId());
$github = new GitHub($cache);
$this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log);
$this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log, $auth);
break;
default:
@ -113,11 +115,12 @@ class Builds extends Action
* @param Document $deployment
* @param Document $template
* @param Log $log
* @param Authorization $auth
* @return void
* @throws \Utopia\Database\Exception
* @throws Exception
*/
protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void
protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log, Authorization $auth): void
{
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
@ -221,20 +224,18 @@ class Builds extends Action
$templateRootDirectory = \ltrim($templateRootDirectory, '/');
if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateVersion)) {
$stdout = '';
$stderr = '';
$output = '';
// Clone template repo
$tmpTemplateDirectory = '/tmp/builds/' . $buildId . '-template';
$gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory);
$exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr);
$exit = Console::execute($gitCloneCommandForTemplate, '', $output);
if ($exit !== 0) {
throw new \Exception('Unable to clone code repository: ' . $stderr);
throw new \Exception('Unable to clone code repository: ' . $output);
}
// Ensure directories
Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $stdout, $stderr);
Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $output);
$tmpPathFile = $tmpTemplateDirectory . '/code.tar.gz';
@ -245,7 +246,7 @@ class Builds extends Action
}
$tarParamDirectory = \escapeshellarg($tmpTemplateDirectory . (empty($templateRootDirectory) ? '' : '/' . $templateRootDirectory));
Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax
Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $output); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax
$source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions);
@ -254,7 +255,7 @@ class Builds extends Action
throw new \Exception("Unable to move file");
}
Console::execute('rm -rf ' . \escapeshellarg($tmpTemplateDirectory), '', $stdout, $stderr);
Console::execute('rm -rf ' . \escapeshellarg($tmpTemplateDirectory), '', $output);
$directorySize = $deviceForFunctions->getFileSize($source);
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source));
@ -276,6 +277,7 @@ class Builds extends Action
$branchName = $deployment->getAttribute('providerBranch');
$commitHash = $deployment->getAttribute('providerCommitHash', '');
$output = '';
$cloneVersion = $branchName;
$cloneType = GitHub::CLONE_TYPE_BRANCH;
@ -283,22 +285,18 @@ class Builds extends Action
$cloneVersion = $commitHash;
$cloneType = GitHub::CLONE_TYPE_COMMIT;
}
$gitCloneCommand = $github->generateCloneCommand($cloneOwner, $cloneRepository, $cloneVersion, $cloneType, $tmpDirectory, $rootDirectory);
$stdout = '';
$stderr = '';
Console::execute('mkdir -p ' . \escapeshellarg('/tmp/builds/' . $buildId), '', $stdout, $stderr);
Console::execute('mkdir -p ' . \escapeshellarg('/tmp/builds/' . $buildId), '', $output);
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
return;
}
$exit = Console::execute($gitCloneCommand, '', $stdout, $stderr);
$exit = Console::execute($gitCloneCommand, '', $output);
if ($exit !== 0) {
throw new \Exception('Unable to clone code repository: ' . $stderr);
throw new \Exception('Unable to clone code repository: ' . $output);
}
// Local refactoring for function folder with spaces
@ -306,10 +304,10 @@ class Builds extends Action
$rootDirectoryWithoutSpaces = str_replace(' ', '', $rootDirectory);
$from = $tmpDirectory . '/' . $rootDirectory;
$to = $tmpDirectory . '/' . $rootDirectoryWithoutSpaces;
$exit = Console::execute('mv "' . \escapeshellarg($from) . '" "' . \escapeshellarg($to) . '"', '', $stdout, $stderr);
$exit = Console::execute('mv "' . \escapeshellarg($from) . '" "' . \escapeshellarg($to) . '"', '', $output);
if ($exit !== 0) {
throw new \Exception('Unable to move function with spaces' . $stderr);
throw new \Exception('Unable to move function with spaces' . $output);
}
$rootDirectory = $rootDirectoryWithoutSpaces;
}
@ -330,33 +328,33 @@ class Builds extends Action
$tmpTemplateDirectory = '/tmp/builds/' . $buildId . '/template';
$gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory);
$exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr);
$exit = Console::execute($gitCloneCommandForTemplate, '', $output);
if ($exit !== 0) {
throw new \Exception('Unable to clone code repository: ' . $stderr);
throw new \Exception('Unable to clone code repository: ' . $output);
}
// Ensure directories
Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $stdout, $stderr);
Console::execute('mkdir -p ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $stdout, $stderr);
Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $output);
Console::execute('mkdir -p ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $output);
// Merge template into user repo
Console::execute('rsync -av --exclude \'.git\' ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory . '/') . ' ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $stdout, $stderr);
Console::execute('rsync -av --exclude \'.git\' ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory . '/') . ' ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $output);
// Commit and push
$exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . \escapeshellarg($tmpDirectory) . ' && git add . && git commit -m "Create ' . \escapeshellarg($function->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $stdout, $stderr);
$exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . \escapeshellarg($tmpDirectory) . ' && git add . && git commit -m "Create ' . \escapeshellarg($function->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $output);
if ($exit !== 0) {
throw new \Exception('Unable to push code repository: ' . $stderr);
throw new \Exception('Unable to push code repository: ' . $output);
}
$exit = Console::execute('cd ' . \escapeshellarg($tmpDirectory) . ' && git rev-parse HEAD', '', $stdout, $stderr);
$exit = Console::execute('cd ' . \escapeshellarg($tmpDirectory) . ' && git rev-parse HEAD', '', $output);
if ($exit !== 0) {
throw new \Exception('Unable to get vcs commit SHA: ' . $stderr);
throw new \Exception('Unable to get vcs commit SHA: ' . $output);
}
$providerCommitHash = \trim($stdout);
$providerCommitHash = \trim($output);
$authorUrl = "https://github.com/$cloneOwner";
$deployment->setAttribute('providerCommitHash', $providerCommitHash ?? '');
@ -399,7 +397,7 @@ class Builds extends Action
}
$tarParamDirectory = '/tmp/builds/' . $buildId . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory);
Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax
Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $output); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax
$source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions);
@ -408,7 +406,7 @@ class Builds extends Action
throw new \Exception("Unable to move file");
}
Console::execute('rm -rf ' . \escapeshellarg($tmpPath), '', $stdout, $stderr);
Console::execute('rm -rf ' . \escapeshellarg($tmpPath), '', $output);
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source));
@ -663,7 +661,7 @@ class Builds extends Action
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
$auth->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
} catch (\Throwable $th) {
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');

View file

@ -11,7 +11,6 @@ use Appwrite\Template\Template;
use Appwrite\Utopia\Response\Model\Rule;
use Exception;
use Throwable;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@ -22,6 +21,7 @@ use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Domains\Domain;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
@ -126,7 +126,7 @@ class Certificates extends Action
$certificate = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain->get()])]);
// If we don't have certificate for domain yet, let's create new document. At the end we save it
if (!$certificate) {
if ($certificate->isEmpty()) {
$certificate = new Document();
$certificate->setAttribute('domain', $domain->get());
}
@ -216,7 +216,7 @@ class Certificates extends Action
{
// Check if update or insert required
$certificateDocument = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain])]);
if (!empty($certificateDocument) && !$certificateDocument->isEmpty()) {
if (!$certificateDocument->isEmpty()) {
// Merge new data with current data
$certificate = new Document(\array_merge($certificateDocument->getArrayCopy(), $certificate->getArrayCopy()));
$certificate = $dbForConsole->updateDocument('certificates', $certificate->getId(), $certificate);
@ -330,30 +330,26 @@ class Certificates extends Action
*
* @param string $folder Folder into which certificates should be generated
* @param string $domain Domain to generate certificate for
* @return array Named array with keys 'stdout' and 'stderr', both string
* @return string output
* @throws Exception
*/
private function issueCertificate(string $folder, string $domain, string $email): array
private function issueCertificate(string $folder, string $domain, string $email): string
{
$stdout = '';
$stderr = '';
$output = '';
$staging = (App::isProduction()) ? '' : ' --dry-run';
$staging = (Http::isProduction()) ? '' : ' --dry-run';
$exit = Console::execute("certbot certonly -v --webroot --noninteractive --agree-tos{$staging}"
. " --email " . $email
. " --cert-name " . $folder
. " -w " . APP_STORAGE_CERTIFICATES
. " -d {$domain}", '', $stdout, $stderr);
. " -d {$domain}", '', $output);
// Unexpected error, usually 5XX, API limits, ...
if ($exit !== 0) {
throw new Exception('Failed to issue a certificate with message: ' . $stderr);
throw new Exception('Failed to issue a certificate with message: ' . $output);
}
return [
'stdout' => $stdout,
'stderr' => $stderr
];
return $output;
}
/**
@ -381,7 +377,7 @@ class Certificates extends Action
* @return void
* @throws Exception
*/
private function applyCertificateFiles(string $folder, string $domain, array $letsEncryptData): void
private function applyCertificateFiles(string $folder, string $domain, string $letsEncryptData): void
{
// Prepare folder in storage for domain
@ -394,19 +390,19 @@ class Certificates extends Action
// Move generated files
if (!@\rename('/etc/letsencrypt/live/' . $folder . '/cert.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem')) {
throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $letsEncryptData);
}
if (!@\rename('/etc/letsencrypt/live/' . $folder . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/chain.pem')) {
throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $letsEncryptData);
}
if (!@\rename('/etc/letsencrypt/live/' . $folder . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/fullchain.pem')) {
throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $letsEncryptData);
}
if (!@\rename('/etc/letsencrypt/live/' . $folder . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/privkey.pem')) {
throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $letsEncryptData);
}
$config = \implode(PHP_EOL, [
@ -482,7 +478,7 @@ class Certificates extends Action
Query::equal('domain', [$domain]),
]);
if ($rule !== false && !$rule->isEmpty()) {
if (!$rule->isEmpty()) {
$rule->setAttribute('certificateId', $certificateId);
$rule->setAttribute('status', $success ? 'verified' : 'unverified');
$dbForConsole->updateDocument('rules', $rule->getId(), $rule);

View file

@ -22,6 +22,7 @@ use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization as ValidatorAuthorization;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
@ -54,14 +55,15 @@ class Deletes extends Action
->inject('executionRetention')
->inject('auditRetention')
->inject('log')
->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log) => $this->action($message, $dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $abuseRetention, $executionRetention, $auditRetention, $log));
->inject('authorization')
->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log, ValidatorAuthorization $authorization) => $this->action($message, $dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $abuseRetention, $executionRetention, $auditRetention, $log, $authorization));
}
/**
* @throws Exception
* @throws Throwable
*/
public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log): void
public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log, ValidatorAuthorization $auth): void
{
$payload = $message->getPayload() ?? [];
@ -117,7 +119,7 @@ class Deletes extends Action
break;
case DELETE_TYPE_AUDIT:
if (!$project->isEmpty()) {
$this->deleteAuditLogs($project, $getProjectDB, $auditRetention);
$this->deleteAuditLogs($project, $getProjectDB, $auditRetention, $auth);
}
if (!$document->isEmpty()) {
@ -125,7 +127,7 @@ class Deletes extends Action
}
break;
case DELETE_TYPE_ABUSE:
$this->deleteAbuseLogs($project, $getProjectDB, $abuseRetention);
$this->deleteAbuseLogs($project, $getProjectDB, $abuseRetention, $auth);
break;
case DELETE_TYPE_REALTIME:
$this->deleteRealtimeUsage($dbForConsole, $datetime);
@ -699,7 +701,7 @@ class Deletes extends Action
* @return void
* @throws Exception
*/
private function deleteAbuseLogs(Document $project, callable $getProjectDB, string $abuseRetention): void
private function deleteAbuseLogs(Document $project, callable $getProjectDB, string $abuseRetention, ValidatorAuthorization $auth): void
{
$projectId = $project->getId();
$dbForProject = $getProjectDB($project);
@ -720,7 +722,7 @@ class Deletes extends Action
* @return void
* @throws Exception
*/
private function deleteAuditLogs(Document $project, callable $getProjectDB, string $auditRetention): void
private function deleteAuditLogs(Document $project, callable $getProjectDB, string $auditRetention, ValidatorAuthorization $auth): void
{
$projectId = $project->getId();
$dbForProject = $getProjectDB($project);

View file

@ -84,13 +84,13 @@ class Mails extends Action
$bodyTemplate = __DIR__ . '/../../../../app/config/locale/templates/email-base.tpl';
}
$bodyTemplate = Template::fromFile($bodyTemplate);
$bodyTemplate->setParam('{{body}}', $body, escapeHtml: false);
$bodyTemplate->setParam('{{body}}', $body, escape: false);
foreach ($variables as $key => $value) {
// TODO: hotfix for redirect param
$bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: $key !== 'redirect');
$bodyTemplate->setParam('{{' . $key . '}}', $value, escape: $key !== 'redirect');
}
foreach ($this->richTextParams as $key => $value) {
$bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: false);
$bodyTemplate->setParam('{{' . $key . '}}', $value, escape: false);
}
$body = $bodyTemplate->render();

View file

@ -178,7 +178,7 @@ class Messaging extends Action
Query::equal('type', [$providerType]),
]);
if ($default === false || $default->isEmpty()) {
if ($default->isEmpty()) {
$dbForProject->updateDocument('messages', $message->getId(), $message->setAttributes([
'status' => MessageStatus::FAILED,
'deliveryErrors' => ['No enabled provider found.']
@ -275,7 +275,7 @@ class Messaging extends Action
Query::equal('identifier', [$result['recipient']])
]);
if ($target instanceof Document && !$target->isEmpty()) {
if (!$target->isEmpty()) {
$dbForProject->updateDocument(
'targets',
$target->getId(),

View file

@ -12,7 +12,7 @@ abstract class Promise
private mixed $result;
public function __construct(?callable $executor = null)
final public function __construct(?callable $executor = null)
{
if (\is_null($executor)) {
return;

View file

@ -6,23 +6,16 @@ use Swoole\Coroutine\Channel;
class Swoole extends Promise
{
public function __construct(?callable $executor = null)
{
parent::__construct($executor);
}
protected function execute(
callable $executor,
callable $resolve,
callable $reject
): void {
\go(function () use ($executor, $resolve, $reject) {
try {
$executor($resolve, $reject);
} catch (\Throwable $exception) {
$reject($exception);
}
});
try {
$executor($resolve, $reject);
} catch (\Throwable $exception) {
$reject($exception);
}
}
/**

View file

@ -3,13 +3,13 @@
namespace Appwrite\Specification;
use Appwrite\Utopia\Response\Model;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Route;
use Utopia\Http\Http;
use Utopia\Http\Route;
abstract class Format
{
protected App $app;
protected Http $http;
/**
* @var Route[]
@ -50,9 +50,9 @@ abstract class Format
]
];
public function __construct(App $app, array $services, array $routes, array $models, array $keys, int $authCount)
public function __construct(Http $http, array $services, array $routes, array $models, array $keys, int $authCount)
{
$this->app = $app;
$this->http = $http;
$this->services = $services;
$this->routes = $routes;
$this->models = $models;

View file

@ -7,11 +7,11 @@ use Appwrite\Template\Template;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Validator;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
use Utopia\Http\Validator;
use Utopia\Http\Validator\ArrayList;
use Utopia\Http\Validator\Nullable;
use Utopia\Http\Validator\Range;
use Utopia\Http\Validator\WhiteList;
class OpenAPI3 extends Format
{
@ -238,8 +238,11 @@ class OpenAPI3 extends Format
}
if ($route->getLabel('sdk.response.code', 500) === 204) {
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')]['description'] = 'No content';
unset($temp['responses'][(string)$route->getLabel('sdk.response.code', '500')]['schema']);
$labelCode = (string)$route->getLabel('sdk.response.code', '500');
$temp['responses'][$labelCode]['description'] = 'No content';
if (isset($temp['responses'][$labelCode]['schema'])) {
unset($temp['responses'][$labelCode]['schema']);
}
}
if ((!empty($scope))) { // && 'public' != $scope
@ -269,10 +272,10 @@ class OpenAPI3 extends Format
$bodyRequired = [];
foreach ($route->getParams() as $name => $param) { // Set params
/**
* @var \Utopia\Validator $validator
*/
$validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator'];
$injections = array_map(fn ($injection) => $this->http->getContainer()->get($injection), $param['injections'] ?? []);
/** @var Validator $validator */
$validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $injections) : $param['validator'];
$node = [
'name' => $name,
@ -289,11 +292,11 @@ class OpenAPI3 extends Format
switch ((!empty($validator)) ? \get_class($validator) : '') {
case 'Utopia\Database\Validator\UID':
case 'Utopia\Validator\Text':
case 'Utopia\Http\Validator\Text':
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
break;
case 'Utopia\Validator\Boolean':
case 'Utopia\Http\Validator\Boolean':
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = false;
break;
@ -314,15 +317,15 @@ class OpenAPI3 extends Format
$node['schema']['format'] = 'email';
$node['schema']['x-example'] = 'email@example.com';
break;
case 'Utopia\Validator\Host':
case 'Utopia\Validator\URL':
case 'Utopia\Http\Validator\Host':
case 'Utopia\Http\Validator\URL':
$node['schema']['type'] = $validator->getType();
$node['schema']['format'] = 'url';
$node['schema']['x-example'] = 'https://example.com';
break;
case 'Utopia\Validator\JSON':
case 'Utopia\Validator\Mock':
case 'Utopia\Validator\Assoc':
case 'Utopia\Http\Validator\JSON':
case 'Utopia\Http\Validator\Mock':
case 'Utopia\Http\Validator\Assoc':
$param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['schema']['type'] = 'object';
$node['schema']['x-example'] = '{}';
@ -332,7 +335,7 @@ class OpenAPI3 extends Format
$node['schema']['type'] = $validator->getType();
$node['schema']['format'] = 'binary';
break;
case 'Utopia\Validator\ArrayList':
case 'Utopia\Http\Validator\ArrayList':
/** @var ArrayList $validator */
$node['schema']['type'] = 'array';
$node['schema']['items'] = [
@ -394,25 +397,25 @@ class OpenAPI3 extends Format
$node['schema']['format'] = 'phone';
$node['schema']['x-example'] = '+12065550100'; // In the US, 555 is reserved like example.com
break;
case 'Utopia\Validator\Range':
case 'Utopia\Http\Validator\Range':
/** @var Range $validator */
$node['schema']['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType();
$node['schema']['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float';
$node['schema']['x-example'] = $validator->getMin();
break;
case 'Utopia\Validator\Numeric':
case 'Utopia\Validator\Integer':
case 'Utopia\Http\Validator\Numeric':
case 'Utopia\Http\Validator\Integer':
$node['schema']['type'] = $validator->getType();
$node['schema']['format'] = 'int32';
break;
case 'Utopia\Validator\FloatValidator':
case 'Utopia\Http\Validator\FloatValidator':
$node['schema']['type'] = 'number';
$node['schema']['format'] = 'float';
break;
case 'Utopia\Validator\Length':
case 'Utopia\Http\Validator\Length':
$node['schema']['type'] = $validator->getType();
break;
case 'Utopia\Validator\WhiteList':
case 'Utopia\Http\Validator\WhiteList':
/** @var WhiteList $validator */
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = $validator->getList()[0];

View file

@ -7,10 +7,10 @@ use Appwrite\Template\Template;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Validator;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Http\Validator;
use Utopia\Http\Validator\ArrayList;
use Utopia\Http\Validator\Nullable;
use Utopia\Http\Validator\Range;
class Swagger2 extends Format
{
@ -270,8 +270,10 @@ class Swagger2 extends Format
);
foreach ($parameters as $name => $param) { // Set params
$injections = array_map(fn ($injection) => $this->http->getContainer()->get($injection), $param['injections'] ?? []);
/** @var Validator $validator */
$validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator'];
$validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $injections) : $param['validator'];
$node = [
'name' => $name,
@ -306,12 +308,12 @@ class Swagger2 extends Format
}
switch ($class) {
case 'Utopia\Validator\Text':
case 'Utopia\Http\Validator\Text':
case 'Utopia\Database\Validator\UID':
$node['type'] = $validator->getType();
$node['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
break;
case 'Utopia\Validator\Boolean':
case 'Utopia\Http\Validator\Boolean':
$node['type'] = $validator->getType();
$node['x-example'] = false;
break;
@ -332,13 +334,13 @@ class Swagger2 extends Format
$node['format'] = 'email';
$node['x-example'] = 'email@example.com';
break;
case 'Utopia\Validator\Host':
case 'Utopia\Validator\URL':
case 'Utopia\Http\Validator\Host':
case 'Utopia\Http\Validator\URL':
$node['type'] = $validator->getType();
$node['format'] = 'url';
$node['x-example'] = 'https://example.com';
break;
case 'Utopia\Validator\ArrayList':
case 'Utopia\Http\Validator\ArrayList':
/** @var ArrayList $validator */
$node['type'] = 'array';
$node['collectionFormat'] = 'multi';
@ -346,9 +348,9 @@ class Swagger2 extends Format
'type' => $validator->getValidator()->getType(),
];
break;
case 'Utopia\Validator\JSON':
case 'Utopia\Validator\Mock':
case 'Utopia\Validator\Assoc':
case 'Utopia\Http\Validator\JSON':
case 'Utopia\Http\Validator\Mock':
case 'Utopia\Http\Validator\Assoc':
$node['type'] = 'object';
$node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['x-example'] = '{}';
@ -397,26 +399,26 @@ class Swagger2 extends Format
$node['format'] = 'phone';
$node['x-example'] = '+12065550100';
break;
case 'Utopia\Validator\Range':
case 'Utopia\Http\Validator\Range':
/** @var Range $validator */
$node['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType();
$node['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float';
$node['x-example'] = $validator->getMin();
break;
case 'Utopia\Validator\Numeric':
case 'Utopia\Validator\Integer':
case 'Utopia\Http\Validator\Numeric':
case 'Utopia\Http\Validator\Integer':
$node['type'] = $validator->getType();
$node['format'] = 'int32';
break;
case 'Utopia\Validator\FloatValidator':
case 'Utopia\Http\Validator\FloatValidator':
$node['type'] = 'number';
$node['format'] = 'float';
break;
case 'Utopia\Validator\Length':
case 'Utopia\Http\Validator\Length':
$node['type'] = $validator->getType();
break;
case 'Utopia\Validator\WhiteList':
/** @var \Utopia\Validator\WhiteList $validator */
case 'Utopia\Http\Validator\WhiteList':
/** @var \Utopia\Http\Validator\WhiteList $validator */
$node['type'] = $validator->getType();
$node['x-example'] = $validator->getList()[0];

View file

@ -3,7 +3,7 @@
namespace Appwrite\Task\Validator;
use Cron\CronExpression;
use Utopia\Validator;
use Utopia\Http\Validator;
class Cron extends Validator
{

View file

@ -3,7 +3,7 @@
namespace Appwrite\Utopia\Database\Validator;
use Utopia\Database\Validator\UID;
use Utopia\Validator;
use Utopia\Http\Validator;
class CompoundUID extends Validator
{

View file

@ -2,7 +2,7 @@
namespace Appwrite\Utopia\Database\Validator;
use Utopia\Validator;
use Utopia\Http\Validator;
class ProjectId extends Validator
{

View file

@ -0,0 +1,36 @@
<?php
namespace Appwrite\Utopia\Queue;
class Connections
{
/**
* @var array
*/
protected array $connections = [];
/**
* @param mixed $connection
* @return self
*/
public function add(mixed $connection, $pool): self
{
$this->connections[] = ['connection' => $connection, 'pool' => $pool];
return $this;
}
/**
* @return self
*/
public function reclaim(): self
{
foreach ($this->connections as $id => $resource) {
$pool = $resource['pool'];
$connection = $resource['connection'];
$pool->put($connection);
unset($this->connections[$id]);
}
return $this;
}
}

View file

@ -3,11 +3,10 @@
namespace Appwrite\Utopia;
use Appwrite\Utopia\Request\Filter;
use Swoole\Http\Request as SwooleRequest;
use Utopia\Route;
use Utopia\Swoole\Request as UtopiaRequest;
use Utopia\Http\Adapter\Swoole\Request as HttpRequest;
use Utopia\Http\Route;
class Request extends UtopiaRequest
class Request extends HttpRequest
{
/**
* @var array<Filter>
@ -15,9 +14,12 @@ class Request extends UtopiaRequest
private array $filters = [];
private static ?Route $route = null;
public function __construct(SwooleRequest $request)
/**
* Request constructor.
*/
public function __construct(HttpRequest $request)
{
parent::__construct($request);
parent::__construct($request->swoole);
}
/**
@ -113,6 +115,16 @@ class Request extends UtopiaRequest
return self::$route !== null;
}
public function removeHeader(string $key): static
{
if (isset($this->headers[$key])) {
unset($this->headers[$key]);
}
return parent::removeHeader($key);
}
/**
* Get headers
*
@ -122,14 +134,18 @@ class Request extends UtopiaRequest
*/
public function getHeaders(): array
{
if ($this->headers !== null) {
return $this->headers;
}
try {
$headers = $this->generateHeaders();
$this->headers = $this->generateHeaders();
} catch (\Throwable) {
$headers = [];
$this->headers = [];
}
if (empty($this->swoole->cookie)) {
return $headers;
return $this->headers;
}
$cookieHeaders = [];
@ -138,10 +154,10 @@ class Request extends UtopiaRequest
}
if (!empty($cookieHeaders)) {
$headers['cookie'] = \implode('; ', $cookieHeaders);
$this->headers['cookie'] = \implode('; ', $cookieHeaders);
}
return $headers;
return $this->headers;
}
/**

View file

@ -4,122 +4,20 @@ namespace Appwrite\Utopia;
use Appwrite\Utopia\Fetch\BodyMultipart;
use Appwrite\Utopia\Response\Filter;
use Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response\Model\Account;
use Appwrite\Utopia\Response\Model\AlgoArgon2;
use Appwrite\Utopia\Response\Model\AlgoBcrypt;
use Appwrite\Utopia\Response\Model\AlgoMd5;
use Appwrite\Utopia\Response\Model\AlgoPhpass;
use Appwrite\Utopia\Response\Model\AlgoScrypt;
use Appwrite\Utopia\Response\Model\AlgoScryptModified;
use Appwrite\Utopia\Response\Model\AlgoSha;
use Appwrite\Utopia\Response\Model\Any;
use Appwrite\Utopia\Response\Model\Attribute;
use Appwrite\Utopia\Response\Model\AttributeBoolean;
use Appwrite\Utopia\Response\Model\AttributeDatetime;
use Appwrite\Utopia\Response\Model\AttributeEmail;
use Appwrite\Utopia\Response\Model\AttributeEnum;
use Appwrite\Utopia\Response\Model\AttributeFloat;
use Appwrite\Utopia\Response\Model\AttributeInteger;
use Appwrite\Utopia\Response\Model\AttributeIP;
use Appwrite\Utopia\Response\Model\AttributeList;
use Appwrite\Utopia\Response\Model\AttributeRelationship;
use Appwrite\Utopia\Response\Model\AttributeString;
use Appwrite\Utopia\Response\Model\AttributeURL;
use Appwrite\Utopia\Response\Model\AuthProvider;
use Appwrite\Utopia\Response\Model\BaseList;
use Appwrite\Utopia\Response\Model\Branch;
use Appwrite\Utopia\Response\Model\Bucket;
use Appwrite\Utopia\Response\Model\Build;
use Appwrite\Utopia\Response\Model\Collection;
use Appwrite\Utopia\Response\Model\ConsoleVariables;
use Appwrite\Utopia\Response\Model\Continent;
use Appwrite\Utopia\Response\Model\Country;
use Appwrite\Utopia\Response\Model\Currency;
use Appwrite\Utopia\Response\Model\Database;
use Appwrite\Utopia\Response\Model\Deployment;
use Appwrite\Utopia\Response\Model\Detection;
use Appwrite\Utopia\Response\Model\Document as ModelDocument;
use Appwrite\Utopia\Response\Model\Error;
use Appwrite\Utopia\Response\Model\ErrorDev;
use Appwrite\Utopia\Response\Model\Execution;
use Appwrite\Utopia\Response\Model\File;
use Appwrite\Utopia\Response\Model\Func;
use Appwrite\Utopia\Response\Model\Headers;
use Appwrite\Utopia\Response\Model\HealthAntivirus;
use Appwrite\Utopia\Response\Model\HealthCertificate;
use Appwrite\Utopia\Response\Model\HealthQueue;
use Appwrite\Utopia\Response\Model\HealthStatus;
use Appwrite\Utopia\Response\Model\HealthTime;
use Appwrite\Utopia\Response\Model\HealthVersion;
use Appwrite\Utopia\Response\Model\Identity;
use Appwrite\Utopia\Response\Model\Index;
use Appwrite\Utopia\Response\Model\Installation;
use Appwrite\Utopia\Response\Model\JWT;
use Appwrite\Utopia\Response\Model\Key;
use Appwrite\Utopia\Response\Model\Language;
use Appwrite\Utopia\Response\Model\Locale;
use Appwrite\Utopia\Response\Model\LocaleCode;
use Appwrite\Utopia\Response\Model\Log;
use Appwrite\Utopia\Response\Model\Membership;
use Appwrite\Utopia\Response\Model\Message;
use Appwrite\Utopia\Response\Model\Metric;
use Appwrite\Utopia\Response\Model\MetricBreakdown;
use Appwrite\Utopia\Response\Model\MFAChallenge;
use Appwrite\Utopia\Response\Model\MFAFactors;
use Appwrite\Utopia\Response\Model\MFARecoveryCodes;
use Appwrite\Utopia\Response\Model\MFAType;
use Appwrite\Utopia\Response\Model\Migration;
use Appwrite\Utopia\Response\Model\MigrationFirebaseProject;
use Appwrite\Utopia\Response\Model\MigrationReport;
use Appwrite\Utopia\Response\Model\Mock;
use Appwrite\Utopia\Response\Model\MockNumber;
use Appwrite\Utopia\Response\Model\None;
use Appwrite\Utopia\Response\Model\Phone;
use Appwrite\Utopia\Response\Model\Platform;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\Rule;
use Appwrite\Utopia\Response\Model\Runtime;
use Appwrite\Utopia\Response\Model\Session;
use Appwrite\Utopia\Response\Model\Specification;
use Appwrite\Utopia\Response\Model\Subscriber;
use Appwrite\Utopia\Response\Model\Target;
use Appwrite\Utopia\Response\Model\Team;
use Appwrite\Utopia\Response\Model\TemplateEmail;
use Appwrite\Utopia\Response\Model\TemplateFunction;
use Appwrite\Utopia\Response\Model\TemplateRuntime;
use Appwrite\Utopia\Response\Model\TemplateSMS;
use Appwrite\Utopia\Response\Model\TemplateVariable;
use Appwrite\Utopia\Response\Model\Token;
use Appwrite\Utopia\Response\Model\Topic;
use Appwrite\Utopia\Response\Model\UsageBuckets;
use Appwrite\Utopia\Response\Model\UsageCollection;
use Appwrite\Utopia\Response\Model\UsageDatabase;
use Appwrite\Utopia\Response\Model\UsageDatabases;
use Appwrite\Utopia\Response\Model\UsageFunction;
use Appwrite\Utopia\Response\Model\UsageFunctions;
use Appwrite\Utopia\Response\Model\UsageProject;
use Appwrite\Utopia\Response\Model\UsageStorage;
use Appwrite\Utopia\Response\Model\UsageUsers;
use Appwrite\Utopia\Response\Model\User;
use Appwrite\Utopia\Response\Model\Variable;
use Appwrite\Utopia\Response\Model\VcsContent;
use Appwrite\Utopia\Response\Model\Webhook;
use Appwrite\Utopia\Response\Models;
use Exception;
use JsonException;
use Swoole\Http\Response as SwooleHTTPResponse;
// Keep last
use Utopia\Database\Document;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Http\Adapter\Swoole\Response as HttpResponse;
// Keep last
/**
* @method int getStatusCode()
* @method Response setStatusCode(int $code = 200)
*/
class Response extends SwooleResponse
class Response extends HttpResponse
{
// General
public const MODEL_NONE = 'none';
@ -330,162 +228,9 @@ class Response extends SwooleResponse
*
* @param float $time
*/
public function __construct(SwooleHTTPResponse $response)
public function __construct(HttpResponse $response)
{
$this
// General
->setModel(new None())
->setModel(new Any())
->setModel(new Error())
->setModel(new ErrorDev())
// Lists
->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_DOCUMENT))
->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'collections', self::MODEL_COLLECTION))
->setModel(new BaseList('Databases List', self::MODEL_DATABASE_LIST, 'databases', self::MODEL_DATABASE))
->setModel(new BaseList('Indexes List', self::MODEL_INDEX_LIST, 'indexes', self::MODEL_INDEX))
->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER))
->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION))
->setModel(new BaseList('Identities List', self::MODEL_IDENTITY_LIST, 'identities', self::MODEL_IDENTITY))
->setModel(new BaseList('Logs List', self::MODEL_LOG_LIST, 'logs', self::MODEL_LOG))
->setModel(new BaseList('Files List', self::MODEL_FILE_LIST, 'files', self::MODEL_FILE))
->setModel(new BaseList('Buckets List', self::MODEL_BUCKET_LIST, 'buckets', self::MODEL_BUCKET))
->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM))
->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP))
->setModel(new BaseList('Functions List', self::MODEL_FUNCTION_LIST, 'functions', self::MODEL_FUNCTION))
->setModel(new BaseList('Function Templates List', self::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', self::MODEL_TEMPLATE_FUNCTION))
->setModel(new BaseList('Installations List', self::MODEL_INSTALLATION_LIST, 'installations', self::MODEL_INSTALLATION))
->setModel(new BaseList('Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_LIST, 'providerRepositories', self::MODEL_PROVIDER_REPOSITORY))
->setModel(new BaseList('Branches List', self::MODEL_BRANCH_LIST, 'branches', self::MODEL_BRANCH))
->setModel(new BaseList('Runtimes List', self::MODEL_RUNTIME_LIST, 'runtimes', self::MODEL_RUNTIME))
->setModel(new BaseList('Deployments List', self::MODEL_DEPLOYMENT_LIST, 'deployments', self::MODEL_DEPLOYMENT))
->setModel(new BaseList('Executions List', self::MODEL_EXECUTION_LIST, 'executions', self::MODEL_EXECUTION))
->setModel(new BaseList('Builds List', self::MODEL_BUILD_LIST, 'builds', self::MODEL_BUILD)) // Not used anywhere yet
->setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false))
->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false))
->setModel(new BaseList('API Keys List', self::MODEL_KEY_LIST, 'keys', self::MODEL_KEY, true, false))
->setModel(new BaseList('Auth Providers List', self::MODEL_AUTH_PROVIDER_LIST, 'platforms', self::MODEL_AUTH_PROVIDER, true, false))
->setModel(new BaseList('Platforms List', self::MODEL_PLATFORM_LIST, 'platforms', self::MODEL_PLATFORM, true, false))
->setModel(new BaseList('Countries List', self::MODEL_COUNTRY_LIST, 'countries', self::MODEL_COUNTRY))
->setModel(new BaseList('Continents List', self::MODEL_CONTINENT_LIST, 'continents', self::MODEL_CONTINENT))
->setModel(new BaseList('Languages List', self::MODEL_LANGUAGE_LIST, 'languages', self::MODEL_LANGUAGE))
->setModel(new BaseList('Currencies List', self::MODEL_CURRENCY_LIST, 'currencies', self::MODEL_CURRENCY))
->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE))
->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false))
->setModel(new BaseList('Variables List', self::MODEL_VARIABLE_LIST, 'variables', self::MODEL_VARIABLE))
->setModel(new BaseList('Status List', self::MODEL_HEALTH_STATUS_LIST, 'statuses', self::MODEL_HEALTH_STATUS))
->setModel(new BaseList('Rule List', self::MODEL_PROXY_RULE_LIST, 'rules', self::MODEL_PROXY_RULE))
->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE))
->setModel(new BaseList('Provider list', self::MODEL_PROVIDER_LIST, 'providers', self::MODEL_PROVIDER))
->setModel(new BaseList('Message list', self::MODEL_MESSAGE_LIST, 'messages', self::MODEL_MESSAGE))
->setModel(new BaseList('Topic list', self::MODEL_TOPIC_LIST, 'topics', self::MODEL_TOPIC))
->setModel(new BaseList('Subscriber list', self::MODEL_SUBSCRIBER_LIST, 'subscribers', self::MODEL_SUBSCRIBER))
->setModel(new BaseList('Target list', self::MODEL_TARGET_LIST, 'targets', self::MODEL_TARGET))
->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION))
->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT))
->setModel(new BaseList('Specifications List', self::MODEL_SPECIFICATION_LIST, 'specifications', self::MODEL_SPECIFICATION))
->setModel(new BaseList('VCS Content List', self::MODEL_VCS_CONTENT_LIST, 'contents', self::MODEL_VCS_CONTENT))
// Entities
->setModel(new Database())
->setModel(new Collection())
->setModel(new Attribute())
->setModel(new AttributeList())
->setModel(new AttributeString())
->setModel(new AttributeInteger())
->setModel(new AttributeFloat())
->setModel(new AttributeBoolean())
->setModel(new AttributeEmail())
->setModel(new AttributeEnum())
->setModel(new AttributeIP())
->setModel(new AttributeURL())
->setModel(new AttributeDatetime())
->setModel(new AttributeRelationship())
->setModel(new Index())
->setModel(new ModelDocument())
->setModel(new Log())
->setModel(new User())
->setModel(new AlgoMd5())
->setModel(new AlgoSha())
->setModel(new AlgoPhpass())
->setModel(new AlgoBcrypt())
->setModel(new AlgoScrypt())
->setModel(new AlgoScryptModified())
->setModel(new AlgoArgon2())
->setModel(new Account())
->setModel(new Preferences())
->setModel(new Session())
->setModel(new Identity())
->setModel(new Token())
->setModel(new JWT())
->setModel(new Locale())
->setModel(new LocaleCode())
->setModel(new File())
->setModel(new Bucket())
->setModel(new Team())
->setModel(new Membership())
->setModel(new Func())
->setModel(new TemplateFunction())
->setModel(new TemplateRuntime())
->setModel(new TemplateVariable())
->setModel(new Installation())
->setModel(new ProviderRepository())
->setModel(new Detection())
->setModel(new VcsContent())
->setModel(new Branch())
->setModel(new Runtime())
->setModel(new Deployment())
->setModel(new Execution())
->setModel(new Build())
->setModel(new Project())
->setModel(new Webhook())
->setModel(new Key())
->setModel(new MockNumber())
->setModel(new AuthProvider())
->setModel(new Platform())
->setModel(new Variable())
->setModel(new Country())
->setModel(new Continent())
->setModel(new Language())
->setModel(new Currency())
->setModel(new Phone())
->setModel(new HealthAntivirus())
->setModel(new HealthQueue())
->setModel(new HealthStatus())
->setModel(new HealthCertificate())
->setModel(new HealthTime())
->setModel(new HealthVersion())
->setModel(new Metric())
->setModel(new MetricBreakdown())
->setModel(new UsageDatabases())
->setModel(new UsageDatabase())
->setModel(new UsageCollection())
->setModel(new UsageUsers())
->setModel(new UsageStorage())
->setModel(new UsageBuckets())
->setModel(new UsageFunctions())
->setModel(new UsageFunction())
->setModel(new UsageProject())
->setModel(new Headers())
->setModel(new Specification())
->setModel(new Rule())
->setModel(new TemplateSMS())
->setModel(new TemplateEmail())
->setModel(new ConsoleVariables())
->setModel(new MFAChallenge())
->setModel(new MFARecoveryCodes())
->setModel(new MFAType())
->setModel(new MFAFactors())
->setModel(new Provider())
->setModel(new Message())
->setModel(new Topic())
->setModel(new Subscriber())
->setModel(new Target())
->setModel(new Migration())
->setModel(new MigrationReport())
->setModel(new MigrationFirebaseProject())
// Tests (keep last)
->setModel(new Mock());
parent::__construct($response);
parent::__construct($response->swoole);
}
/**
@ -495,49 +240,6 @@ class Response extends SwooleResponse
public const CONTENT_TYPE_NULL = 'null';
public const CONTENT_TYPE_MULTIPART = 'multipart/form-data';
/**
* List of defined output objects
*/
protected $models = [];
/**
* Set Model Object
*
* @return self
*/
public function setModel(Model $instance)
{
$this->models[$instance->getType()] = $instance;
return $this;
}
/**
* Get Model Object
*
* @param string $key
* @return Model
* @throws Exception
*/
public function getModel(string $key): Model
{
if (!isset($this->models[$key])) {
throw new Exception('Undefined model: ' . $key);
}
return $this->models[$key];
}
/**
* Get Models List
*
* @return Model[]
*/
public function getModels(): array
{
return $this->models;
}
public function applyFilters(array $data, string $model): array
{
foreach ($this->filters as $filter) {
@ -605,7 +307,7 @@ class Response extends SwooleResponse
public function output(Document $document, string $model): array
{
$data = clone $document;
$model = $this->getModel($model);
$model = Models::getModel($model);
$output = [];
$data = $model->filter($data);
@ -640,7 +342,7 @@ class Response extends SwooleResponse
if (\is_array($rule['type'])) {
foreach ($rule['type'] as $type) {
$condition = false;
foreach ($this->getModel($type)->conditions as $attribute => $val) {
foreach (Models::getModel($type)->conditions as $attribute => $val) {
$condition = $item->getAttribute($attribute) === $val;
if (!$condition) {
break;
@ -655,7 +357,7 @@ class Response extends SwooleResponse
$ruleType = $rule['type'];
}
if (!array_key_exists($ruleType, $this->models)) {
if (!array_key_exists($ruleType, Models::getModels())) {
throw new Exception('Missing model for rule: ' . $ruleType);
}

View file

@ -0,0 +1,304 @@
<?php
namespace Appwrite\Utopia\Response;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Account;
use Appwrite\Utopia\Response\Model\AlgoArgon2;
use Appwrite\Utopia\Response\Model\AlgoBcrypt;
use Appwrite\Utopia\Response\Model\AlgoMd5;
use Appwrite\Utopia\Response\Model\AlgoPhpass;
use Appwrite\Utopia\Response\Model\AlgoScrypt;
use Appwrite\Utopia\Response\Model\AlgoScryptModified;
use Appwrite\Utopia\Response\Model\AlgoSha;
use Appwrite\Utopia\Response\Model\Any;
use Appwrite\Utopia\Response\Model\Attribute;
use Appwrite\Utopia\Response\Model\AttributeBoolean;
use Appwrite\Utopia\Response\Model\AttributeDatetime;
use Appwrite\Utopia\Response\Model\AttributeEmail;
use Appwrite\Utopia\Response\Model\AttributeEnum;
use Appwrite\Utopia\Response\Model\AttributeFloat;
use Appwrite\Utopia\Response\Model\AttributeInteger;
use Appwrite\Utopia\Response\Model\AttributeIP;
use Appwrite\Utopia\Response\Model\AttributeList;
use Appwrite\Utopia\Response\Model\AttributeRelationship;
use Appwrite\Utopia\Response\Model\AttributeString;
use Appwrite\Utopia\Response\Model\AttributeURL;
use Appwrite\Utopia\Response\Model\AuthProvider;
use Appwrite\Utopia\Response\Model\BaseList;
use Appwrite\Utopia\Response\Model\Branch;
use Appwrite\Utopia\Response\Model\Bucket;
use Appwrite\Utopia\Response\Model\Build;
use Appwrite\Utopia\Response\Model\Collection;
use Appwrite\Utopia\Response\Model\ConsoleVariables;
use Appwrite\Utopia\Response\Model\Continent;
use Appwrite\Utopia\Response\Model\Country;
use Appwrite\Utopia\Response\Model\Currency;
use Appwrite\Utopia\Response\Model\Database;
use Appwrite\Utopia\Response\Model\Deployment;
use Appwrite\Utopia\Response\Model\Detection;
use Appwrite\Utopia\Response\Model\Document as ModelDocument;
use Appwrite\Utopia\Response\Model\Error;
use Appwrite\Utopia\Response\Model\ErrorDev;
use Appwrite\Utopia\Response\Model\Execution;
use Appwrite\Utopia\Response\Model\File;
use Appwrite\Utopia\Response\Model\Func;
use Appwrite\Utopia\Response\Model\Headers;
use Appwrite\Utopia\Response\Model\HealthAntivirus;
use Appwrite\Utopia\Response\Model\HealthCertificate;
use Appwrite\Utopia\Response\Model\HealthQueue;
use Appwrite\Utopia\Response\Model\HealthStatus;
use Appwrite\Utopia\Response\Model\HealthTime;
use Appwrite\Utopia\Response\Model\HealthVersion;
use Appwrite\Utopia\Response\Model\Identity;
use Appwrite\Utopia\Response\Model\Index;
use Appwrite\Utopia\Response\Model\Installation;
use Appwrite\Utopia\Response\Model\JWT;
use Appwrite\Utopia\Response\Model\Key;
use Appwrite\Utopia\Response\Model\Language;
use Appwrite\Utopia\Response\Model\Locale;
use Appwrite\Utopia\Response\Model\LocaleCode;
use Appwrite\Utopia\Response\Model\Log;
use Appwrite\Utopia\Response\Model\Membership;
use Appwrite\Utopia\Response\Model\Message;
use Appwrite\Utopia\Response\Model\Metric;
use Appwrite\Utopia\Response\Model\MetricBreakdown;
use Appwrite\Utopia\Response\Model\MFAChallenge;
use Appwrite\Utopia\Response\Model\MFAFactors;
use Appwrite\Utopia\Response\Model\MFARecoveryCodes;
use Appwrite\Utopia\Response\Model\MFAType;
use Appwrite\Utopia\Response\Model\Migration;
use Appwrite\Utopia\Response\Model\MigrationFirebaseProject;
use Appwrite\Utopia\Response\Model\MigrationReport;
use Appwrite\Utopia\Response\Model\Mock;
use Appwrite\Utopia\Response\Model\MockNumber;
use Appwrite\Utopia\Response\Model\None;
use Appwrite\Utopia\Response\Model\Phone;
use Appwrite\Utopia\Response\Model\Platform;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\Rule;
use Appwrite\Utopia\Response\Model\Runtime;
use Appwrite\Utopia\Response\Model\Session;
use Appwrite\Utopia\Response\Model\Specification;
use Appwrite\Utopia\Response\Model\Subscriber;
use Appwrite\Utopia\Response\Model\Target;
use Appwrite\Utopia\Response\Model\Team;
use Appwrite\Utopia\Response\Model\TemplateEmail;
use Appwrite\Utopia\Response\Model\TemplateFunction;
use Appwrite\Utopia\Response\Model\TemplateRuntime;
use Appwrite\Utopia\Response\Model\TemplateSMS;
use Appwrite\Utopia\Response\Model\TemplateVariable;
use Appwrite\Utopia\Response\Model\Token;
use Appwrite\Utopia\Response\Model\Topic;
use Appwrite\Utopia\Response\Model\UsageBuckets;
use Appwrite\Utopia\Response\Model\UsageCollection;
use Appwrite\Utopia\Response\Model\UsageDatabase;
use Appwrite\Utopia\Response\Model\UsageDatabases;
use Appwrite\Utopia\Response\Model\UsageFunction;
use Appwrite\Utopia\Response\Model\UsageFunctions;
use Appwrite\Utopia\Response\Model\UsageProject;
use Appwrite\Utopia\Response\Model\UsageStorage;
use Appwrite\Utopia\Response\Model\UsageUsers;
use Appwrite\Utopia\Response\Model\User;
use Appwrite\Utopia\Response\Model\Variable;
use Appwrite\Utopia\Response\Model\VcsContent;
use Appwrite\Utopia\Response\Model\Webhook;
use Exception;
// Keep last
class Models
{
public static function init()
{
// General
self::setModel(new None());
self::setModel(new Any());
self::setModel(new Error());
self::setModel(new ErrorDev());
// Lists
self::setModel(new BaseList('Documents List', Response::MODEL_DOCUMENT_LIST, 'documents', Response::MODEL_DOCUMENT));
self::setModel(new BaseList('Collections List', Response::MODEL_COLLECTION_LIST, 'collections', Response::MODEL_COLLECTION));
self::setModel(new BaseList('Databases List', Response::MODEL_DATABASE_LIST, 'databases', Response::MODEL_DATABASE));
self::setModel(new BaseList('Indexes List', Response::MODEL_INDEX_LIST, 'indexes', Response::MODEL_INDEX));
self::setModel(new BaseList('Users List', Response::MODEL_USER_LIST, 'users', Response::MODEL_USER));
self::setModel(new BaseList('Sessions List', Response::MODEL_SESSION_LIST, 'sessions', Response::MODEL_SESSION));
self::setModel(new BaseList('Identities List', Response::MODEL_IDENTITY_LIST, 'identities', Response::MODEL_IDENTITY));
self::setModel(new BaseList('Logs List', Response::MODEL_LOG_LIST, 'logs', Response::MODEL_LOG));
self::setModel(new BaseList('Files List', Response::MODEL_FILE_LIST, 'files', Response::MODEL_FILE));
self::setModel(new BaseList('Buckets List', Response::MODEL_BUCKET_LIST, 'buckets', Response::MODEL_BUCKET));
self::setModel(new BaseList('Teams List', Response::MODEL_TEAM_LIST, 'teams', Response::MODEL_TEAM));
self::setModel(new BaseList('Memberships List', Response::MODEL_MEMBERSHIP_LIST, 'memberships', Response::MODEL_MEMBERSHIP));
self::setModel(new BaseList('Functions List', Response::MODEL_FUNCTION_LIST, 'functions', Response::MODEL_FUNCTION));
self::setModel(new BaseList('Function Templates List', Response::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', Response::MODEL_TEMPLATE_FUNCTION));
self::setModel(new BaseList('Installations List', Response::MODEL_INSTALLATION_LIST, 'installations', Response::MODEL_INSTALLATION));
self::setModel(new BaseList('Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_LIST, 'providerRepositories', Response::MODEL_PROVIDER_REPOSITORY));
self::setModel(new BaseList('Branches List', Response::MODEL_BRANCH_LIST, 'branches', Response::MODEL_BRANCH));
self::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME));
self::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_LIST, 'deployments', Response::MODEL_DEPLOYMENT));
self::setModel(new BaseList('Executions List', Response::MODEL_EXECUTION_LIST, 'executions', Response::MODEL_EXECUTION));
self::setModel(new BaseList('Builds List', Response::MODEL_BUILD_LIST, 'builds', Response::MODEL_BUILD)); // Not used anywhere yet;
self::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, false));
self::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, false));
self::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, false));
self::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false));
self::setModel(new BaseList('Platforms List', Response::MODEL_PLATFORM_LIST, 'platforms', Response::MODEL_PLATFORM, true, false));
self::setModel(new BaseList('Countries List', Response::MODEL_COUNTRY_LIST, 'countries', Response::MODEL_COUNTRY));
self::setModel(new BaseList('Continents List', Response::MODEL_CONTINENT_LIST, 'continents', Response::MODEL_CONTINENT));
self::setModel(new BaseList('Languages List', Response::MODEL_LANGUAGE_LIST, 'languages', Response::MODEL_LANGUAGE));
self::setModel(new BaseList('Currencies List', Response::MODEL_CURRENCY_LIST, 'currencies', Response::MODEL_CURRENCY));
self::setModel(new BaseList('Phones List', Response::MODEL_PHONE_LIST, 'phones', Response::MODEL_PHONE));
self::setModel(new BaseList('Metric List', Response::MODEL_METRIC_LIST, 'metrics', Response::MODEL_METRIC, true, false));
self::setModel(new BaseList('Variables List', Response::MODEL_VARIABLE_LIST, 'variables', Response::MODEL_VARIABLE));
self::setModel(new BaseList('Status List', Response::MODEL_HEALTH_STATUS_LIST, 'statuses', Response::MODEL_HEALTH_STATUS));
self::setModel(new BaseList('Rule List', Response::MODEL_PROXY_RULE_LIST, 'rules', Response::MODEL_PROXY_RULE));
self::setModel(new BaseList('Locale codes list', Response::MODEL_LOCALE_CODE_LIST, 'localeCodes', Response::MODEL_LOCALE_CODE));
self::setModel(new BaseList('Provider list', Response::MODEL_PROVIDER_LIST, 'providers', Response::MODEL_PROVIDER));
self::setModel(new BaseList('Message list', Response::MODEL_MESSAGE_LIST, 'messages', Response::MODEL_MESSAGE));
self::setModel(new BaseList('Topic list', Response::MODEL_TOPIC_LIST, 'topics', Response::MODEL_TOPIC));
self::setModel(new BaseList('Subscriber list', Response::MODEL_SUBSCRIBER_LIST, 'subscribers', Response::MODEL_SUBSCRIBER));
self::setModel(new BaseList('Target list', Response::MODEL_TARGET_LIST, 'targets', Response::MODEL_TARGET));
self::setModel(new BaseList('Migrations List', Response::MODEL_MIGRATION_LIST, 'migrations', Response::MODEL_MIGRATION));
self::setModel(new BaseList('Migrations Firebase Projects List', Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', Response::MODEL_MIGRATION_FIREBASE_PROJECT));
self::setModel(new BaseList('Specifications List', Response::MODEL_SPECIFICATION_LIST, 'specifications', Response::MODEL_SPECIFICATION));
self::setModel(new BaseList('VCS Content List', Response::MODEL_VCS_CONTENT_LIST, 'contents', Response::MODEL_VCS_CONTENT));
// Entities
self::setModel(new Database());
self::setModel(new Collection());
self::setModel(new Attribute());
self::setModel(new AttributeList());
self::setModel(new AttributeString());
self::setModel(new AttributeInteger());
self::setModel(new AttributeFloat());
self::setModel(new AttributeBoolean());
self::setModel(new AttributeEmail());
self::setModel(new AttributeEnum());
self::setModel(new AttributeIP());
self::setModel(new AttributeURL());
self::setModel(new AttributeDatetime());
self::setModel(new AttributeRelationship());
self::setModel(new Index());
self::setModel(new ModelDocument());
self::setModel(new Log());
self::setModel(new User());
self::setModel(new AlgoMd5());
self::setModel(new AlgoSha());
self::setModel(new AlgoPhpass());
self::setModel(new AlgoBcrypt());
self::setModel(new AlgoScrypt());
self::setModel(new AlgoScryptModified());
self::setModel(new AlgoArgon2());
self::setModel(new Account());
self::setModel(new Preferences());
self::setModel(new Session());
self::setModel(new Identity());
self::setModel(new Token());
self::setModel(new JWT());
self::setModel(new Locale());
self::setModel(new LocaleCode());
self::setModel(new File());
self::setModel(new Bucket());
self::setModel(new Team());
self::setModel(new Membership());
self::setModel(new Func());
self::setModel(new TemplateFunction());
self::setModel(new TemplateRuntime());
self::setModel(new TemplateVariable());
self::setModel(new Installation());
self::setModel(new ProviderRepository());
self::setModel(new Detection());
self::setModel(new VcsContent());
self::setModel(new Branch());
self::setModel(new Runtime());
self::setModel(new Deployment());
self::setModel(new Execution());
self::setModel(new Build());
self::setModel(new Project());
self::setModel(new Webhook());
self::setModel(new Key());
self::setModel(new MockNumber());
self::setModel(new AuthProvider());
self::setModel(new Platform());
self::setModel(new Variable());
self::setModel(new Country());
self::setModel(new Continent());
self::setModel(new Language());
self::setModel(new Currency());
self::setModel(new Phone());
self::setModel(new HealthAntivirus());
self::setModel(new HealthQueue());
self::setModel(new HealthStatus());
self::setModel(new HealthCertificate());
self::setModel(new HealthTime());
self::setModel(new HealthVersion());
self::setModel(new Metric());
self::setModel(new MetricBreakdown());
self::setModel(new UsageDatabases());
self::setModel(new UsageDatabase());
self::setModel(new UsageCollection());
self::setModel(new UsageUsers());
self::setModel(new UsageStorage());
self::setModel(new UsageBuckets());
self::setModel(new UsageFunctions());
self::setModel(new UsageFunction());
self::setModel(new UsageProject());
self::setModel(new Headers());
self::setModel(new Specification());
self::setModel(new Rule());
self::setModel(new TemplateSMS());
self::setModel(new TemplateEmail());
self::setModel(new ConsoleVariables());
self::setModel(new MFAChallenge());
self::setModel(new MFARecoveryCodes());
self::setModel(new MFAType());
self::setModel(new MFAFactors());
self::setModel(new Provider());
self::setModel(new Message());
self::setModel(new Topic());
self::setModel(new Subscriber());
self::setModel(new Target());
self::setModel(new Migration());
self::setModel(new MigrationReport());
self::setModel(new MigrationFirebaseProject());
// Tests (keep last)
self::setModel(new Mock());
;
}
/**
* List of defined output objects
* @var Model[]
*/
protected static array $models = [];
/**
* Set Model Object
*/
public static function setModel(Model $instance): void
{
self::$models[$instance->getType()] = $instance;
}
/**
* Get Model Object
*
* @param string $key
* @return Model
* @throws Exception
*/
public static function getModel(string $key): Model
{
if (!isset(self::$models[$key])) {
throw new Exception('Undefined model: ' . $key);
}
return self::$models[$key];
}
public static function getModels(): array
{
return self::$models;
}
}

View file

@ -2,7 +2,7 @@
namespace Appwrite\Utopia;
use Utopia\View as OldView;
use Utopia\View\View as OldView;
class View extends OldView
{

View file

@ -12,7 +12,7 @@ use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Validator\JSON;
use Utopia\Http\Validator\JSON;
trait DatabasesBase
{
@ -2869,7 +2869,7 @@ trait DatabasesBase
$this->assertEquals('Invalid document structure: Attribute "floatRange" has invalid format. Value must be a valid range between 1 and 1', $badFloatRange['body']['message']);
$this->assertEquals('Invalid document structure: Attribute "probability" has invalid format. Value must be a valid range between 0 and 1', $badProbability['body']['message']);
$this->assertEquals('Invalid document structure: Attribute "upperBound" has invalid format. Value must be a valid range between -9,223,372,036,854,775,808 and 10', $tooHigh['body']['message']);
$this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and 9,223,372,036,854,775,807', $tooLow['body']['message']);
$this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and '.\number_format(PHP_INT_MAX), $tooLow['body']['message']);
}
/**

View file

@ -64,9 +64,10 @@ class DatabasesCustomClientTest extends Scope
'required' => true,
]);
$this->assertEquals(202, $response['headers']['status-code']);
sleep(1);
$this->assertEquals(202, $response['headers']['status-code']);
// Document aliases write to update, delete
$document1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([
@ -82,6 +83,7 @@ class DatabasesCustomClientTest extends Scope
]
]);
$this->assertEquals(201, $document1['headers']['status-code']);
$this->assertNotContains(Permission::create(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']);
$this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']);
$this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']);

View file

@ -17,6 +17,14 @@ class DatabasesPermissionsGuestTest extends Scope
use SideClient;
use DatabasesPermissionsScope;
protected Authorization $auth;
public function setUp(): void
{
parent::setUp();
$this->auth = new Authorization();
}
public function createCollection(): array
{
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
@ -111,8 +119,8 @@ class DatabasesPermissionsGuestTest extends Scope
$this->assertEquals(201, $publicResponse['headers']['status-code']);
$this->assertEquals(201, $privateResponse['headers']['status-code']);
$roles = Authorization::getRoles();
Authorization::cleanRoles();
$roles = $this->auth->getRoles();
$this->auth->cleanRoles();
$publicDocuments = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [
'content-type' => 'application/json',
@ -134,7 +142,7 @@ class DatabasesPermissionsGuestTest extends Scope
}
foreach ($roles as $role) {
Authorization::setRole($role);
$this->auth->addRole($role);
}
}
@ -145,8 +153,8 @@ class DatabasesPermissionsGuestTest extends Scope
$privateCollectionId = $data['privateCollectionId'];
$databaseId = $data['databaseId'];
$roles = Authorization::getRoles();
Authorization::cleanRoles();
$roles = $this->auth->getRoles();
$this->auth->cleanRoles();
$publicResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [
'content-type' => 'application/json',
@ -222,7 +230,7 @@ class DatabasesPermissionsGuestTest extends Scope
$this->assertEquals(401, $privateDocument['headers']['status-code']);
foreach ($roles as $role) {
Authorization::setRole($role);
$this->auth->addRole($role);
}
}

View file

@ -11,8 +11,7 @@ trait FunctionsBase
{
use Async;
protected string $stdout = '';
protected string $stderr = '';
protected string $output = '';
protected function setupFunction(mixed $params): string
{
@ -146,8 +145,7 @@ trait FunctionsBase
{
$folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
$tarPath = "$folderPath/code.tar.gz";
Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');

View file

@ -553,7 +553,7 @@ class FunctionsCustomServerTest extends Scope
$folder = 'php-large';
$code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz";
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output);
$chunkSize = 5 * 1024 * 1024;
$handle = @fopen($code, "rb");
@ -1859,10 +1859,11 @@ class FunctionsCustomServerTest extends Scope
'x-appwrite-response-format' => '1.4.0', // Set response format for 1.4 syntax
], $this->getHeaders()),
[
'queries' => [ 'equal("name", ["Test2"])' ]
'queries' => [
Query::equal('name', ['Test2'])->toString(),
]
]
);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['functions']);
$this->assertEquals('Test2', $response['body']['functions'][0]['name']);

View file

@ -1,12 +1,13 @@
<?php
namespace Tests\E2E\Services\Functions;
namespace Tests\E2E\Services\FunctionsSchedule;
use Appwrite\ID;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Tests\E2E\Services\Functions\FunctionsBase;
use Utopia\Database\Helpers\Role;
class FunctionsScheduleTest extends Scope

View file

@ -2502,7 +2502,7 @@ trait Base
$folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
$tarPath = "$folderPath/code.tar.gz";
Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');

View file

@ -183,7 +183,6 @@ class StorageClientTest extends Scope
/**
* @depends testCreateFile
* @param $file
* @return array
* @throws \Exception
*/
public function testGetFileDownload($file)

View file

@ -232,7 +232,6 @@ class StorageServerTest extends Scope
/**
* @depends testCreateFile
* @param $file
* @return array
* @throws \Exception
*/
public function testGetFileDownload($file)

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