Compare commits

..
100 changed files with 1850 additions and 623 deletions
+5 -40
View File
@@ -150,16 +150,8 @@ jobs:
- name: Install dependencies
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
- name: Cache PHPStan result cache
uses: actions/cache@v4
with:
path: .phpstan-cache
key: phpstan-${{ github.sha }}
restore-keys: |
phpstan-
- name: Run PHPStan
run: composer analyze -- --no-progress
run: composer analyze
locale:
name: Checks / Locale
@@ -188,38 +180,11 @@ jobs:
uses: actions/github-script@v8
with:
script: |
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
const allModes = ['dedicated', 'shared_v1', 'shared_v2'];
const allDatabases = ['MongoDB'];
const allModes = ['shared_v1', 'shared_v2'];
const defaultDatabases = ['MongoDB'];
const defaultModes = ['dedicated'];
const pr = context.payload.pull_request;
if (!pr) {
core.setOutput('databases', JSON.stringify(allDatabases));
core.setOutput('modes', JSON.stringify(allModes));
return;
}
const getContent = (ref) => github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: 'composer.lock',
ref,
});
const getDbVersion = (lock) => lock.packages?.find(p => p.name === 'utopia-php/database')?.version;
const [{ data: base }, { data: head }] = await Promise.all([
getContent(pr.base.sha),
getContent(pr.head.sha),
]);
const decode = (content) => JSON.parse(Buffer.from(content, 'base64').toString());
const databaseChanged = getDbVersion(decode(base.content)) !== getDbVersion(decode(head.content));
core.setOutput('databases', JSON.stringify(databaseChanged ? allDatabases : defaultDatabases));
core.setOutput('modes', JSON.stringify(databaseChanged ? allModes : defaultModes));
core.setOutput('databases', JSON.stringify(allDatabases));
core.setOutput('modes', JSON.stringify(allModes));
build:
name: Build
-1
View File
@@ -21,7 +21,6 @@ appwrite.config.json
/app/config/specs/
/docs/examples/
.phpunit.cache
.phpstan-cache
playwright-report
test-results
docker-compose.web-installer.yml
-3
View File
@@ -72,7 +72,6 @@ Before running the installation command, make sure you have [Docker](https://www
```bash
docker run -it --rm \
--publish 20080:20080 \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
@@ -85,7 +84,6 @@ docker run -it --rm \
```cmd
docker run -it --rm ^
--publish 20080:20080 ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
@@ -96,7 +94,6 @@ docker run -it --rm ^
```powershell
docker run -it --rm `
--publish 20080:20080 `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
+3
View File
@@ -31,6 +31,9 @@ class FunctionUseCases
public const DEV_TOOLS = 'dev-tools';
public const AUTH = 'auth';
/**
* @var array<string>
*/
public static function getAll(): array
{
return [
+6 -3
View File
@@ -25,6 +25,9 @@ class SiteUseCases
public const FORMS = 'forms';
public const DASHBOARD = 'dashboard';
/**
* @var array<string>
*/
public static function getAll(): array
{
return [
@@ -249,7 +252,7 @@ return [
'frameworks' => [
getFramework('VITE', [
'providerRootDirectory' => './vite/vitepress',
'fallbackFile' => '404.html',
'outputDirectory' => '404.html',
'installCommand' => 'npm i vitepress && npm install',
'buildCommand' => 'npm run docs:build',
'outputDirectory' => './.vitepress/dist',
@@ -272,7 +275,7 @@ return [
'frameworks' => [
getFramework('VUE', [
'providerRootDirectory' => './vue/vuepress',
'fallbackFile' => '404.html',
'outputDirectory' => '404.html',
'installCommand' => 'npm install',
'buildCommand' => 'npm run build',
'outputDirectory' => './src/.vuepress/dist',
@@ -295,7 +298,7 @@ return [
'frameworks' => [
getFramework('REACT', [
'providerRootDirectory' => './react/docusaurus',
'fallbackFile' => '404.html',
'outputDirectory' => '404.html',
'installCommand' => 'npm install',
'buildCommand' => 'npm run build',
'outputDirectory' => './build',
+7 -12
View File
@@ -695,7 +695,6 @@ Http::delete('/v1/account/sessions')
$protocol = $request->getProtocol();
$sessions = $user->getAttribute('sessions', []);
$currentSession = null;
foreach ($sessions as $session) {/** @var Document $session */
$dbForProject->deleteDocument('sessions', $session->getId());
@@ -717,7 +716,6 @@ Http::delete('/v1/account/sessions')
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
// Use current session for events.
$currentSession = $session;
$queueForEvents
->setPayload($response->output($session, Response::MODEL_SESSION));
@@ -730,11 +728,9 @@ Http::delete('/v1/account/sessions')
$dbForProject->purgeCachedDocument('users', $user->getId());
if ($currentSession instanceof Document) {
$queueForEvents
->setParam('userId', $user->getId())
->setParam('sessionId', $currentSession->getId());
}
$queueForEvents
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId());
$response->noContent();
});
@@ -780,8 +776,7 @@ Http::get('/v1/account/sessions/:sessionId')
->setAttribute('secret', $session->getAttribute('secret', ''))
;
$response->dynamic($session, Response::MODEL_SESSION);
return;
return $response->dynamic($session, Response::MODEL_SESSION);
}
}
@@ -961,7 +956,7 @@ Http::patch('/v1/account/sessions/:sessionId')
->setPayload($response->output($session, Response::MODEL_SESSION))
;
$response->dynamic($session, Response::MODEL_SESSION);
return $response->dynamic($session, Response::MODEL_SESSION);
});
Http::post('/v1/account/sessions/email')
@@ -1993,7 +1988,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
}
if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) {
if (isset($sessionUpgrade) && $sessionUpgrade) {
foreach ($user->getAttribute('targets', []) as $target) {
if ($target->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) {
continue;
@@ -4720,5 +4715,5 @@ Http::delete('/v1/account/identities/:identityId')
->setParam('identityId', $identity->getId())
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
$response->noContent();
return $response->noContent();
});
-1
View File
@@ -231,7 +231,6 @@ Http::get('/v1/locale/continents')
->inject('locale')
->action(function (Response $response, Locale $locale) {
$list = array_keys(Config::getParam('locale-continents'));
$output = [];
foreach ($list as $value) {
$output[] = new Document([
+1 -1
View File
@@ -3566,7 +3566,7 @@ Http::post('/v1/messaging/messages/push')
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$endpoint = "$protocol://{$platform['apiHostname']}/v1";
$scheduleTime = $scheduledAt;
$scheduleTime = $currentScheduledAt ?? $scheduledAt;
if (!\is_null($scheduleTime)) {
$expiry = (new \DateTime($scheduleTime))->add(new \DateInterval('P15D'))->format('U');
} else {
+1 -1
View File
@@ -1570,7 +1570,7 @@ Http::post('/v1/projects/:projectId/smtp/tests')
->trigger();
}
$response->noContent();
return $response->noContent();
});
Http::get('/v1/projects/:projectId/templates/sms/:type/:locale')
+7 -5
View File
@@ -2337,8 +2337,9 @@ Http::post('/v1/users/:userId/sessions')
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION));
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($session, Response::MODEL_SESSION);
return $response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($session, Response::MODEL_SESSION);
});
Http::post('/v1/users/:userId/tokens')
@@ -2401,8 +2402,9 @@ Http::post('/v1/users/:userId/tokens')
->setParam('tokenId', $token->getId())
->setPayload($response->output($token, Response::MODEL_TOKEN));
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($token, Response::MODEL_TOKEN);
return $response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($token, Response::MODEL_TOKEN);
});
Http::delete('/v1/users/:userId/sessions/:sessionId')
@@ -2656,7 +2658,7 @@ Http::delete('/v1/users/identities/:identityId')
->setParam('identityId', $identity->getId())
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
$response->noContent();
return $response->noContent();
});
Http::post('/v1/users/:userId/jwts')
+10 -17
View File
@@ -166,14 +166,14 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if ($request->getMethod() !== Request::METHOD_GET) {
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.', view: $errorView);
}
$response->redirect('https://' . $request->getHostname() . $request->getURI());
return false;
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
}
}
/** @var Database $dbForProject */
$dbForProject = $getProjectDB($project);
/** @var Document $deployment */
if (!empty($rule->getAttribute('deploymentId', ''))) {
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
} else {
@@ -244,7 +244,6 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if ($isPreview && $requirePreview) {
$cookie = $request->getCookie(COOKIE_NAME_PREVIEW, '');
$authorized = false;
$user = new Document();
// Security checks to mark authorized true
if (!empty($cookie)) {
@@ -274,7 +273,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$membershipExists = false;
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
if (!$project->isEmpty() && !$user->isEmpty()) {
if (!$project->isEmpty() && isset($user)) {
$teamId = $project->getAttribute('teamId', '');
$membership = $user->find('teamId', $teamId, 'memberships');
if (!empty($membership)) {
@@ -380,7 +379,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$executionId = ID::unique();
$headers = \array_merge([], $requestHeaders);
$headers['x-appwrite-execution-id'] = $executionId;
$headers['x-appwrite-execution-id'] = $executionId ?? '';
$headers['x-appwrite-user-id'] = '';
$headers['x-appwrite-country-code'] = '';
$headers['x-appwrite-continent-code'] = '';
@@ -460,7 +459,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if ($version === 'v2') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_DATA' => $body,
'APPWRITE_FUNCTION_DATA' => $body ?? '',
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
]);
@@ -530,11 +529,6 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
/** Execute function */
$executionResponse = [
'headers' => [],
'body' => '',
];
try {
$version = match ($type) {
'function' => $resource->getAttribute('version', 'v2'),
@@ -740,7 +734,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$execution->setAttribute('responseBody', $executionResponse['body'] ?? '');
$execution->setAttribute('responseHeaders', $headers);
$body = $execution['responseBody'];
$body = $execution['responseBody'] ?? '';
$contentType = 'text/plain';
foreach ($executionResponse['headers'] as $name => $values) {
@@ -871,9 +865,9 @@ Http::init()
Request::setRoute($route);
if ($route === null) {
$response->setStatusCode(404);
$response->send('Not Found');
return;
return $response
->setStatusCode(404)
->send('Not Found');
}
$requestFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
@@ -979,8 +973,7 @@ Http::init()
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.');
}
$response->redirect('https://' . $request->getHostname() . $request->getURI());
return;
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
}
}
});
+28 -30
View File
@@ -244,38 +244,36 @@ Http::get('/v1/mock/github/callback')
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
}
if (empty($providerInstallationId)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Missing provider installation ID');
if (!empty($providerInstallationId)) {
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId) ?? '';
$projectInternalId = $project->getSequence();
$teamId = $project->getAttribute('teamId', '');
$installation = new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'providerInstallationId' => $providerInstallationId,
'projectId' => $projectId,
'projectInternalId' => $projectInternalId,
'provider' => 'github',
'organization' => $owner,
'personal' => false
]);
$installation = $dbForPlatform->createDocument('installations', $installation);
}
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId) ?? '';
$projectInternalId = $project->getSequence();
$teamId = $project->getAttribute('teamId', '');
$installation = new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'providerInstallationId' => $providerInstallationId,
'projectId' => $projectId,
'projectInternalId' => $projectInternalId,
'provider' => 'github',
'organization' => $owner,
'personal' => false
]);
$installation = $dbForPlatform->createDocument('installations', $installation);
$response->json([
'installationId' => $installation->getId(),
]);
+3 -4
View File
@@ -3,8 +3,6 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/init/span.php';
global $register;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Swoole\Constant;
@@ -33,6 +31,7 @@ use Utopia\Http\Files;
use Utopia\Http\Http;
use Utopia\Logger\Log;
use Utopia\Logger\Log\User;
use Utopia\Pools\Group;
use Utopia\Span\Span;
use Utopia\System\System;
@@ -294,7 +293,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot
go(function () use ($register, $app) {
$pools = $register->get('pools');
/** @var \Utopia\Pools\Group $pools */
/** @var Group $pools */
Http::setResource('pools', fn () => $pools);
/** @var array $collections */
@@ -656,7 +655,7 @@ $http->on(Constant::EVENT_TASK, function () use ($register) {
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
/** @var \Swoole\Table $riskyDomains */
/** @var Table $riskyDomains */
$riskyDomains = $app->getResource('riskyDomains');
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) {
+36 -21
View File
@@ -6,6 +6,7 @@ use Appwrite\Hooks\Hooks;
use Appwrite\PubSub\Adapter\Redis as PubSub;
use Appwrite\URL\URL as AppwriteURL;
use MaxMind\Db\Reader;
use PHPMailer\PHPMailer\PHPMailer;
use Swoole\Database\PDOProxy;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Config\Config;
@@ -24,7 +25,6 @@ use Utopia\Logger\Adapter\LogOwl;
use Utopia\Logger\Adapter\Raygun;
use Utopia\Logger\Adapter\Sentry;
use Utopia\Logger\Logger;
use Utopia\Messaging\Adapter\Email\SMTP;
use Utopia\Mongo\Client as MongoClient;
use Utopia\Pools\Adapter\Stack as StackPool;
use Utopia\Pools\Adapter\Swoole as SwoolePool;
@@ -56,7 +56,7 @@ $register->set('logger', function () {
}
try {
$loggingProvider = new DSN($providerConfig);
$loggingProvider = new DSN($providerConfig ?? '');
$providerName = $loggingProvider->getScheme();
$providerConfig = match ($providerName) {
@@ -76,7 +76,7 @@ $register->set('logger', function () {
};
}
if (empty($providerName)) {
if (empty($providerName) || empty($providerConfig)) {
return;
}
@@ -121,7 +121,7 @@ $register->set('realtimeLogger', function () {
default => ['key' => $loggingProvider->getHost()],
};
if (empty($providerName)) {
if (empty($providerName) || empty($providerConfig)) {
return;
}
@@ -242,8 +242,8 @@ $register->set('pools', function () {
],
];
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
$instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14);
$maxConnections = System::getEnv('_APP_CONNECTIONS_MAX', 151);
$instanceConnections = $maxConnections / System::getEnv('_APP_POOL_CLIENTS', 14);
$multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled';
@@ -308,7 +308,7 @@ $register->set('pools', function () {
]);
});
},
'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) {
'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase, $dsn) {
try {
$mongo = new MongoClient($dsnDatabase, $dsnHost, (int)$dsnPort, $dsnUser, $dsnPass, false);
@$mongo->connect();
@@ -433,20 +433,35 @@ $register->set('db', function () {
});
$register->set('smtp', function () {
$username = System::getEnv('_APP_SMTP_USERNAME', '');
$password = System::getEnv('_APP_SMTP_PASSWORD', '');
return new SMTP(
host: System::getEnv('_APP_SMTP_HOST', 'smtp'),
port: (int) System::getEnv('_APP_SMTP_PORT', 25),
username: $username,
password: $password,
smtpSecure: System::getEnv('_APP_SMTP_SECURE', ''),
smtpAutoTLS: false,
xMailer: 'Appwrite Mailer',
timeout: 10,
keepAlive: true,
timelimit: 30,
);
$mail = new PHPMailer(true);
$mail->isSMTP();
$username = System::getEnv('_APP_SMTP_USERNAME');
$password = System::getEnv('_APP_SMTP_PASSWORD');
$mail->XMailer = 'Appwrite Mailer';
$mail->Host = System::getEnv('_APP_SMTP_HOST', 'smtp');
$mail->Port = System::getEnv('_APP_SMTP_PORT', 25);
$mail->SMTPAuth = !empty($username) && !empty($password);
$mail->Username = $username;
$mail->Password = $password;
$mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', '');
$mail->SMTPAutoTLS = false;
$mail->SMTPKeepAlive = true;
$mail->CharSet = 'UTF-8';
$mail->Timeout = 10; /* Connection timeout */
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
$from = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
$email = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$mail->setFrom($email, $from);
$mail->addReplyTo($email, $from);
$mail->isHTML(true);
return $mail;
});
$register->set('geodb', function () {
return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2025-12.mmdb');
+21 -20
View File
@@ -696,7 +696,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
$cacheKey = \sprintf(
'%s-cache-%s:%s:%s:project:%s:functions:events',
$dbForProject->getCacheName(),
$hostname,
$hostname ?? '',
$dbForProject->getNamespace(),
$dbForProject->getTenant(),
$project->getId()
@@ -888,7 +888,9 @@ Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authori
}, ['pools', 'cache', 'authorization']);
Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
$initializedPools = [];
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization, &$initializedPools): Database {
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
$databaseType = $database->getAttribute('type', '');
@@ -923,30 +925,29 @@ Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Docume
// inside pools authorization needs to be set first
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
// For separate pools (documentsdb/vectorsdb), check their own shared tables config.
// If not configured, use dedicated mode to avoid cross-engine tenant type mismatches.
// When the database uses a separate pool (e.g. vectorsdb on PostgreSQL),
// always use dedicated mode with namespace isolation. Shared tables mode
// can't be used across different engines (e.g. MongoDB UUID tenants are
// incompatible with PostgreSQL's integer _tenant column).
if ($databaseHost !== $dsn->getHost()) {
$dbTypeSharedTables = match ($databaseType) {
DOCUMENTSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))),
VECTORSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))),
default => [],
};
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
if (\in_array($databaseHost, $dbTypeSharedTables)) {
$database
->setSharedTables(true)
->setTenant((int)$project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
$poolKey = $databaseHost . ':' . $database->getNamespace();
if (!isset($initializedPools[$poolKey])) {
try {
$database->create();
} catch (\Utopia\Database\Exception\Duplicate) {
// Schema already exists
}
$initializedPools[$poolKey] = true;
}
} elseif (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant((int)$project->getSequence())
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
+7 -6
View File
@@ -237,6 +237,7 @@ if (!function_exists('getTelemetry')) {
if (!function_exists('triggerStats')) {
function triggerStats(array $event, string $projectId): void
{
return;
}
}
@@ -319,14 +320,14 @@ if (!function_exists('logError')) {
$server->error(logError(...));
$server->onStart(function () use ($stats, $containerId, &$statsDocument) {
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument) {
sleep(5); // wait for the initial database schema to be ready
Console::success('Server started successfully');
/**
* Create document for this worker to share stats across Containers.
*/
go(function () use ($containerId, &$statsDocument) {
go(function () use ($register, $containerId, &$statsDocument) {
$attempts = 0;
$database = getConsoleDB();
@@ -356,7 +357,7 @@ $server->onStart(function () use ($stats, $containerId, &$statsDocument) {
*/
// 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 ($stats, &$statsDocument) {
Timer::tick(5000, function () use ($register, $stats, &$statsDocument) {
$payload = [];
foreach ($stats as $projectId => $value) {
$payload[$projectId] = $stats->get($projectId, 'connectionsTotal');
@@ -395,7 +396,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$attempts = 0;
$start = time();
Timer::tick(5000, function () use ($server, $realtime, $stats) {
Timer::tick(5000, function () use ($server, $register, $realtime, $stats) {
/**
* Sending current connections to project channels on the console project every 5 seconds.
*/
@@ -797,7 +798,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
}
});
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId) {
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
$project = null;
$authorization = null;
@@ -809,7 +810,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
// Get authorization from connection (stored during onOpen)
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
if ($authorization === null) {
$authorization = new Authorization();
$authorization = new Authorization('');
}
$database = getConsoleDB();
+22 -22
View File
@@ -221,7 +221,9 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authoriza
}, ['pools', 'cache', 'authorization']);
Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
$initializedPools = [];
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization, &$initializedPools): Database {
$projectDocument ??= $project;
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
$databaseType = $database->getAttribute('type', '');
@@ -258,30 +260,28 @@ Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
// For separate pools (documentsdb/vectorsdb), check their own shared tables config.
// If not configured, use dedicated mode to avoid cross-engine tenant type mismatches.
// When using a separate pool, always use dedicated mode with namespace isolation.
// Shared tables mode can't be used across different engines (e.g. MongoDB UUID
// tenants are incompatible with PostgreSQL's integer _tenant column).
if ($databaseHost !== $dsn->getHost()) {
$dbTypeSharedTables = match ($databaseType) {
DOCUMENTSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))),
VECTORSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))),
default => [],
};
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $projectDocument->getSequence());
if (\in_array($databaseHost, $dbTypeSharedTables)) {
$database
->setSharedTables(true)
->setTenant((int) $projectDocument->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $projectDocument->getSequence());
$poolKey = $databaseHost . ':' . $database->getNamespace();
if (!isset($initializedPools[$poolKey])) {
try {
$database->create();
} catch (\Utopia\Database\Exception\Duplicate) {
// Schema already exists
}
$initializedPools[$poolKey] = true;
}
} elseif (\in_array($dsn->getHost(), $sharedTables, true)) {
$database
->setSharedTables(true)
->setTenant((int) $projectDocument->getSequence())
->setTenant($projectDocument->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -301,14 +301,14 @@ Server::setResource('abuseRetention', function () {
Server::setResource('auditRetention', function (Document $project) {
if ($project->getId() === 'console') {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
}
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
}, ['project']);
Server::setResource('executionRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
});
Server::setResource('cache', function (Registry $register) {
+1 -1
View File
@@ -72,7 +72,7 @@
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/messaging": "0.20.*",
"utopia-php/migration": "1.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "1.*",
Generated
+8 -8
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": "4fe91e67f343fbe6deac1fdc7eda949f",
"content-hash": "b5261855586680e467168f527e0634ae",
"packages": [
{
"name": "adhocore/jwt",
@@ -4467,23 +4467,23 @@
},
{
"name": "utopia-php/messaging",
"version": "0.22.0",
"version": "0.20.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/messaging.git",
"reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030"
"reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030",
"reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/fcb4c3c46a48008a677957690bd45ec934dd33b0",
"reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-openssl": "*",
"giggsey/libphonenumber-for-php-lite": "9.0.23",
"php": ">=8.1.0",
"php": ">=8.0.0",
"phpmailer/phpmailer": "6.9.1"
},
"require-dev": {
@@ -4512,9 +4512,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/messaging/issues",
"source": "https://github.com/utopia-php/messaging/tree/0.22.0"
"source": "https://github.com/utopia-php/messaging/tree/0.20.1"
},
"time": "2026-04-02T04:09:19+00:00"
"time": "2026-02-06T09:56:06+00:00"
},
{
"name": "utopia-php/migration",
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -1,6 +1,8 @@
includes:
- phpstan-baseline.neon
parameters:
level: 3
tmpDir: .phpstan-cache
paths:
- src
- app
@@ -12,3 +14,4 @@ parameters:
- vendor/swoole/ide-helper
excludePaths:
- tests/resources
+5 -3
View File
@@ -285,7 +285,7 @@ class Event
*
* @param string $key
* @param Document $context
* @return static
* @return self
*/
public function setContext(string $key, Document $context): self
{
@@ -309,7 +309,7 @@ class Event
/**
* Set class used for this event.
* @param string $class
* @return static
* @return self
*/
public function setClass(string $class): self
{
@@ -648,8 +648,10 @@ class Event
*
* @param Event $event
*
* @return self
*
*/
public function from(Event $event): static
public function from(Event $event): self
{
$this->project = $event->getProject();
$this->user = $event->getUser();
-1
View File
@@ -40,7 +40,6 @@ class Usage extends Base
*/
public static function fromArray(array $data): static
{
/** @phpstan-ignore new.static (subclass constructors are backwards-compatible via optional params) */
return new static(
project: new Document($data['project'] ?? []),
metrics: $data['metrics'] ?? [],
+10 -6
View File
@@ -25,7 +25,11 @@ class Resolvers
?Route $route,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
function (callable $resolve, callable $reject) use ($utopia, $route, $args, $context, $info) {
/** @var Http $utopia */
/** @var Response $response */
/** @var Request $request */
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
@@ -92,7 +96,7 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
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);
@@ -123,7 +127,7 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
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);
@@ -159,7 +163,7 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
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);
@@ -191,7 +195,7 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
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);
@@ -221,7 +225,7 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
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);
+75 -76
View File
@@ -98,9 +98,10 @@ class Schema
foreach ($routes as $route) {
/** @var Route $route */
/** @var \Appwrite\SDK\Method $sdk */
$sdk = $route->getLabel('sdk', false);
if ($sdk === false) {
if (empty($sdk)) {
continue;
}
@@ -176,7 +177,7 @@ class Schema
$required = $attr['required'];
$default = $attr['default'];
$escapedKey = str_replace('$', '', $key);
$collections[$databaseId][$collectionId][$escapedKey] = [
$collections[$collectionId][$escapedKey] = [
'type' => Mapper::attribute(
$type,
$array,
@@ -186,82 +187,80 @@ class Schema
];
}
foreach ($collections as $databaseId => $databaseCollections) {
foreach ($databaseCollections as $collectionId => $attributes) {
$objectType = new ObjectType([
'name' => $collectionId,
'fields' => \array_merge(
["_id" => ['type' => Type::string()]],
foreach ($collections as $collectionId => $attributes) {
$objectType = new ObjectType([
'name' => $collectionId,
'fields' => \array_merge(
["_id" => ['type' => Type::string()]],
$attributes
),
]);
$attributes = \array_merge(
$attributes,
Mapper::args('mutate')
);
$queryFields[$collectionId . 'Get'] = [
'type' => $objectType,
'args' => Mapper::args('id'),
'resolve' => Resolvers::documentGet(
$utopia,
$databaseId,
$collectionId,
$urls['get'],
)
];
$queryFields[$collectionId . 'List'] = [
'type' => Type::listOf($objectType),
'args' => Mapper::args('list'),
'resolve' => Resolvers::documentList(
$utopia,
$databaseId,
$collectionId,
$urls['list'],
$params['list'],
),
'complexity' => $complexity,
];
$mutationFields[$collectionId . 'Create'] = [
'type' => $objectType,
'args' => $attributes,
'resolve' => Resolvers::documentCreate(
$utopia,
$databaseId,
$collectionId,
$urls['create'],
$params['create'],
)
];
$mutationFields[$collectionId . 'Update'] = [
'type' => $objectType,
'args' => \array_merge(
Mapper::args('id'),
\array_map(
fn ($attr) => $attr['type'] = Type::getNullableType($attr['type']),
$attributes
),
]);
$attributes = \array_merge(
$attributes,
Mapper::args('mutate')
);
$queryFields[$collectionId . 'Get'] = [
'type' => $objectType,
'args' => Mapper::args('id'),
'resolve' => Resolvers::documentGet(
$utopia,
$databaseId,
$collectionId,
$urls['get'],
)
];
$queryFields[$collectionId . 'List'] = [
'type' => Type::listOf($objectType),
'args' => Mapper::args('list'),
'resolve' => Resolvers::documentList(
$utopia,
$databaseId,
$collectionId,
$urls['list'],
$params['list'],
),
'complexity' => $complexity,
];
$mutationFields[$collectionId . 'Create'] = [
'type' => $objectType,
'args' => $attributes,
'resolve' => Resolvers::documentCreate(
$utopia,
$databaseId,
$collectionId,
$urls['create'],
$params['create'],
)
];
$mutationFields[$collectionId . 'Update'] = [
'type' => $objectType,
'args' => \array_merge(
Mapper::args('id'),
\array_map(
fn ($attr) => $attr['type'] = Type::getNullableType($attr['type']),
$attributes
)
),
'resolve' => Resolvers::documentUpdate(
$utopia,
$databaseId,
$collectionId,
$urls['update'],
$params['update'],
)
];
$mutationFields[$collectionId . 'Delete'] = [
'type' => Mapper::model('none'),
'args' => Mapper::args('id'),
'resolve' => Resolvers::documentDelete(
$utopia,
$databaseId,
$collectionId,
$urls['delete'],
)
];
}
),
'resolve' => Resolvers::documentUpdate(
$utopia,
$databaseId,
$collectionId,
$urls['update'],
$params['update'],
)
];
$mutationFields[$collectionId . 'Delete'] = [
'type' => Mapper::model('none'),
'args' => Mapper::args('id'),
'resolve' => Resolvers::documentDelete(
$utopia,
$databaseId,
$collectionId,
$urls['delete'],
)
];
}
$offset += $limit;
}
+7 -16
View File
@@ -15,13 +15,10 @@ class Types
*
* @return Json
*/
public static function json(): Json
public static function json(): Type
{
if (Registry::has(Json::class)) {
$type = Registry::get(Json::class);
if ($type instanceof Json) {
return $type;
}
return Registry::get(Json::class);
}
$type = new Json();
Registry::set(Json::class, $type);
@@ -31,15 +28,12 @@ class Types
/**
* Get the JSON type.
*
* @return Assoc
* @return Json
*/
public static function assoc(): Assoc
public static function assoc(): Type
{
if (Registry::has(Assoc::class)) {
$type = Registry::get(Assoc::class);
if ($type instanceof Assoc) {
return $type;
}
return Registry::get(Assoc::class);
}
$type = new Assoc();
Registry::set(Assoc::class, $type);
@@ -51,13 +45,10 @@ class Types
*
* @return InputFile
*/
public static function inputFile(): InputFile
public static function inputFile(): Type
{
if (Registry::has(InputFile::class)) {
$type = Registry::get(InputFile::class);
if ($type instanceof InputFile) {
return $type;
}
return Registry::get(InputFile::class);
}
$type = new InputFile();
Registry::set(InputFile::class, $type);
+3 -1
View File
@@ -273,9 +273,11 @@ class Mapper
case \Appwrite\Auth\Validator\Password::class:
case \Appwrite\Event\Validator\Event::class:
case \Appwrite\Event\Validator\FunctionEvent::class:
case \Appwrite\Network\Validator\CNAME::class:
case \Utopia\Emails\Validator\Email::class:
case \Appwrite\Network\Validator\Redirect::class:
case \Appwrite\Network\Validator\DNS::class:
case \Appwrite\Network\Validator\Origin::class:
case \Appwrite\Task\Validator\Cron::class:
case \Appwrite\Utopia\Database\Validator\CustomId::class:
case \Utopia\Database\Validator\Key::class:
@@ -284,7 +286,7 @@ class Mapper
case \Utopia\Validator\HexColor::class:
case \Utopia\Validator\Host::class:
case \Utopia\Validator\IP::class:
case \Appwrite\Network\Validator\Origin::class:
case \Utopia\Validator\Origin::class:
case \Utopia\Validator\Text::class:
case \Utopia\Validator\URL::class:
case \Utopia\Validator\WhiteList::class:
-25
View File
@@ -13,7 +13,6 @@ use Utopia\Database\Exception\Limit;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Database\PDO;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
abstract class Migration
@@ -205,30 +204,6 @@ abstract class Migration
}
}
/**
* @param array<Query> $queries
* @return \Generator<int, Document>
* @throws Exception
*/
protected function documentsIterator(string $collection, array $queries = []): \Generator
{
$offset = 0;
do {
$documents = $this->dbForProject->find($collection, [
...$queries,
Query::limit($this->limit),
Query::offset($offset),
]);
foreach ($documents as $document) {
yield $document;
}
$offset += \count($documents);
} while (\count($documents) === $this->limit);
}
/**
* Creates collection from the config collection.
*
+5 -4
View File
@@ -1224,7 +1224,7 @@ class V15 extends Migration
* @param \Utopia\Database\Document $document
* @return \Utopia\Database\Document
*/
protected function fixDocument(Document $document): Document
protected function fixDocument(Document $document)
{
switch ($document->getCollection()) {
case 'cache':
@@ -1234,7 +1234,7 @@ class V15 extends Migration
* skipping migration for 'cache' and 'variables'.
* 'users' already migrated.
*/
return $document;
return;
case '_metadata':
/**
@@ -1480,6 +1480,7 @@ class V15 extends Migration
* Filter from the 'encrypt' filter.
*
* @param string $value
* @return string|false
*/
protected function encryptFilter(string $value): string
{
@@ -1491,8 +1492,8 @@ class V15 extends Migration
'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),
'tag' => \bin2hex($tag ?? ''),
'version' => '1',
]) ?: '';
]);
}
}
+1 -1
View File
@@ -452,7 +452,7 @@ class V20 extends Migration
Query::equal('period', ['1d']),
]);
$value = $query;
$value = $query ?? 0;
$this->createInfMetric($to, $value);
}
+1 -1
View File
@@ -48,7 +48,7 @@ final class Cors
/**
* Build CORS headers for a given request origin.
*
* @return array<string, int|string>
* @return array<string,string>
*/
public function headers(string $origin): array
{
+1 -1
View File
@@ -18,7 +18,7 @@ class OpenSSL
*
* @return string
*/
public static function encrypt($data, $method, $key, $options = 0, $iv = '', ?string &$tag = null, $aad = '', $tag_length = 16)
public static function encrypt($data, $method, $key, $options = 0, $iv = '', &$tag = null, $aad = '', $tag_length = 16)
{
return \openssl_encrypt($data, $method, $key, $options, $iv, $tag, $aad, $tag_length);
}
@@ -49,6 +49,7 @@ class Action extends PlatformAction
$image = new Image(\file_get_contents($path));
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
@@ -204,6 +204,7 @@ class Get extends Action
$image = new Image($data);
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$response
@@ -95,6 +95,7 @@ class Get extends Action
}
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$response
@@ -90,7 +90,7 @@ class Get extends Action
}
}
$rand = (int) \substr((string) $code, -1);
$rand = \substr($code, -1);
$rand = ($rand > \count($themes) - 1) ? $rand % \count($themes) : $rand;
@@ -17,7 +17,7 @@ class Action extends AppwriteAction
return $this->context;
}
public function setHttpPath(string $path): self
public function setHttpPath(string $path): AppwriteAction
{
if (\str_contains($path, '/tablesdb')) {
$this->context = DATABASE_TYPE_TABLESDB;
@@ -28,8 +28,7 @@ class Action extends AppwriteAction
if (\str_contains($path, '/vectorsdb')) {
$this->context = DATABASE_TYPE_VECTORSDB;
}
parent::setHttpPath($path);
return $this;
return parent::setHttpPath($path);
}
/**
@@ -24,7 +24,7 @@ abstract class Action extends DatabasesAction
*/
abstract protected function getResponseModel(): string;
public function setHttpPath(string $path): self
public function setHttpPath(string $path): DatabasesAction
{
if (str_contains($path, '/tablesdb/')) {
$this->context = ROWS;
@@ -47,8 +47,7 @@ abstract class Action extends DatabasesAction
],
];
parent::setHttpPath($path);
return $this;
return parent::setHttpPath($path);
}
protected function getDatabasesOperationReadMetric(): string
@@ -407,6 +406,8 @@ abstract class Action extends DatabasesAction
if (\is_array($related)) {
$document->setAttribute($relationship->getAttribute('key'), \array_values($relations));
} elseif (empty($relations)) {
$document->setAttribute($relationship->getAttribute('key'), null);
}
}
@@ -209,7 +209,7 @@ class Create extends Action
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSDKNamespace() . ' with relationship ' . $this->getStructureContext());
}
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $authorization) {
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $dbForProject, $authorization) {
$allowedPermissions = [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
@@ -122,6 +122,7 @@ class Update extends Action
$dbForDatabases = $getDatabasesDB($database);
// Read permission should not be required for update
/** @var Document $document */
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
if ($transactionId !== null) {
@@ -147,7 +147,7 @@ class XList extends Action
$cacheKeyBase = \sprintf(
'%s-cache-%s:%s:%s:collection:%s:%s:user:%s:%s',
$dbForProject->getCacheName(),
$hostname,
$hostname ?? '',
$dbForProject->getNamespace(),
$dbForProject->getTenant(),
$collectionId,
@@ -99,6 +99,8 @@ class Update extends Action
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
$enabled ??= $collection->getAttribute('enabled', true);
$collection = $dbForProject->updateDocument(
'database_' . $database->getSequence(),
$collectionId,
@@ -103,9 +103,6 @@ class XList extends Action
$os = $detector->getOS();
$client = $detector->getClient();
$device = $detector->getDevice();
$deviceName = \is_array($device) ? ($device['deviceName'] ?? '') : '';
$deviceBrand = \is_array($device) ? ($device['deviceBrand'] ?? '') : '';
$deviceModel = \is_array($device) ? ($device['deviceModel'] ?? '') : '';
$output[$i] = new Document([
'event' => $log['event'],
@@ -124,9 +121,9 @@ class XList extends Action
'clientVersion' => $client['clientVersion'],
'clientEngine' => $client['clientEngine'],
'clientEngineVersion' => $client['clientEngineVersion'],
'deviceName' => $deviceName,
'deviceBrand' => $deviceBrand,
'deviceModel' => $deviceModel,
'deviceName' => $device['deviceName'],
'deviceBrand' => $device['deviceBrand'],
'deviceModel' => $device['deviceModel'],
]);
$record = $geodb->get($log['ip']);
@@ -33,7 +33,7 @@ abstract class Action extends DatabasesAction
return $this->databaseType.'.'.METRIC_DATABASE_ID_OPERATIONS_WRITES;
}
public function setHttpPath(string $path): self
public function setHttpPath(string $path): DatabasesAction
{
switch (true) {
case str_contains($path, '/tablesdb'):
@@ -50,8 +50,7 @@ abstract class Action extends DatabasesAction
$this->databaseType = VECTORSDB;
break;
}
parent::setHttpPath($path);
return $this;
return parent::setHttpPath($path);
}
/**
@@ -239,7 +239,7 @@ class Create extends Action
}
}
$transaction = $authorization->skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $operations) {
$transaction = $authorization->skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) {
$dbForProject->createDocuments('transactionLogs', $staged);
return $dbForProject->increaseDocumentAttribute(
'transactions',
@@ -105,7 +105,8 @@ class Update extends Action
* @throws Exception
* @throws \Throwable
* @throws \Utopia\Database\Exception
* @throws StructureException
* @throws Authorization
* @throws Structure
* @throws \Utopia\Http\Exception
*/
public function action(string $transactionId, bool $commit, bool $rollback, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
@@ -182,7 +183,7 @@ class Update extends Action
$dbForDatabases = $getDatabasesDB($databaseDoc);
try {
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $authorization) {
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $usage, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) {
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
'status' => 'committing',
])));
@@ -97,9 +97,6 @@ class XList extends Action
$os = $detector->getOS();
$client = $detector->getClient();
$device = $detector->getDevice();
$deviceName = \is_array($device) ? ($device['deviceName'] ?? '') : '';
$deviceBrand = \is_array($device) ? ($device['deviceBrand'] ?? '') : '';
$deviceModel = \is_array($device) ? ($device['deviceModel'] ?? '') : '';
$output[$i] = new Document([
'event' => $log['event'],
@@ -118,9 +115,9 @@ class XList extends Action
'clientVersion' => $client['clientVersion'],
'clientEngine' => $client['clientEngine'],
'clientEngineVersion' => $client['clientEngineVersion'],
'deviceName' => $deviceName,
'deviceBrand' => $deviceBrand,
'deviceModel' => $deviceModel,
'deviceName' => $device['deviceName'],
'deviceBrand' => $device['deviceBrand'],
'deviceModel' => $device['deviceModel'],
]);
$record = $geodb->get($log['ip']);
@@ -130,11 +130,9 @@ class Create extends CollectionAction
$indexes[] = new Document($index);
}
try {
// passing null in creates only creates the metadata collection
if (!$dbForDatabases->exists(null, Database::METADATA)) {
try {
$dbForDatabases->create();
} catch (DuplicateException) {
}
$dbForDatabases->create();
}
$dbForDatabases->createCollection(
id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
@@ -88,11 +88,16 @@ class Get extends Action
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
[$path, $device] = match ($type) {
'output' => [$deployment->getAttribute('buildPath', ''), $deviceForBuilds],
'source' => [$deployment->getAttribute('sourcePath', ''), $deviceForFunctions],
default => throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid deployment download type.'),
};
switch ($type) {
case 'output':
$path = $deployment->getAttribute('buildPath', '');
$device = $deviceForBuilds;
break;
case 'source':
$path = $deployment->getAttribute('sourcePath', '');
$device = $deviceForFunctions;
break;
}
if (!$device->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
@@ -213,10 +213,7 @@ class Create extends Base
$current = new Document();
foreach ($sessions as $session) {
if (!$session instanceof Document) {
continue;
}
/** @var Utopia\Database\Document $session */
if ($proofForToken->verify($store->getProperty('secret', ''), $session->getAttribute('secret'))) { // Find most recent active session for user ID and JWT headers
$current = $session;
}
@@ -240,11 +237,11 @@ class Create extends Base
]);
$executionId = ID::unique();
$headers['x-appwrite-execution-id'] = $executionId;
$headers['x-appwrite-execution-id'] = $executionId ?? '';
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey;
$headers['x-appwrite-trigger'] = 'http';
$headers['x-appwrite-user-id'] = $user->getId();
$headers['x-appwrite-user-jwt'] = $jwt;
$headers['x-appwrite-user-id'] = $user->getId() ?? '';
$headers['x-appwrite-user-jwt'] = $jwt ?? '';
$headers['x-appwrite-country-code'] = '';
$headers['x-appwrite-continent-code'] = '';
$headers['x-appwrite-continent-eu'] = 'false';
@@ -353,18 +350,16 @@ class Create extends Base
}
}
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
$this->enqueueDeletes(
$project,
$function->getSequence(),
$executionsRetentionCount,
$queueForDeletes
->setProject($project)
->setResource($function->getSequence())
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
}
);
$response->setStatusCode(Response::STATUS_CODE_ACCEPTED);
$response->dynamic($execution, Response::MODEL_EXECUTION);
return;
return $response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($execution, Response::MODEL_EXECUTION);
}
$durationStart = \microtime(true);
@@ -375,7 +370,7 @@ class Create extends Base
if ($version === 'v2') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_DATA' => $body,
'APPWRITE_FUNCTION_DATA' => $body ?? '',
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
]);
@@ -542,18 +537,32 @@ class Create extends Base
}
}
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
$this->enqueueDeletes(
$project,
$function->getSequence(),
$executionsRetentionCount,
$queueForDeletes
->setProject($project)
->setResource($function->getSequence())
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
}
);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($execution, Response::MODEL_EXECUTION);
}
private function enqueueDeletes(
Document $project,
string $resourceId,
int $executionsRetentionCount,
DeleteEvent $queueForDeletes
): void {
/* cleanup */
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
$queueForDeletes
->setProject($project)
->setResource($resourceId)
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
}
}
}
@@ -424,8 +424,8 @@ class Create extends Base
/** Trigger Realtime Events */
$queueForRealtime
->setSubscribers(['console', $project->getId()])
->from($ruleCreate)
->setSubscribers(['console', $project->getId()])
->trigger();
}
}
@@ -170,6 +170,8 @@ class Update extends Base
$runtime = $function->getAttribute('runtime');
}
$enabled ??= $function->getAttribute('enabled', true);
$repositoryId = $function->getAttribute('repositoryId', '');
$repositoryInternalId = $function->getAttribute('repositoryInternalId', '');
@@ -450,7 +450,7 @@ class Builds extends Action
$providerCommitHash = \trim($stdout);
$deployment->setAttribute('providerCommitHash', $providerCommitHash);
$deployment->setAttribute('providerCommitHash', $providerCommitHash ?? '');
$deployment->setAttribute('providerCommitAuthorUrl', APP_VCS_GITHUB_URL);
$deployment->setAttribute('providerCommitAuthor', APP_VCS_GITHUB_USERNAME);
$deployment->setAttribute('providerCommitMessage', "Create '" . $resource->getAttribute('name', '') . "' function");
@@ -862,7 +862,7 @@ class Builds extends Action
if (\str_contains($logs, '{APPWRITE_DETECTION_SEPARATOR_START}')) {
[$logsBefore, $detectionLogsStart] = \explode('{APPWRITE_DETECTION_SEPARATOR_START}', $logs, 2);
[$detectionLogs, $logsAfter] = \explode('{APPWRITE_DETECTION_SEPARATOR_END}', $detectionLogsStart, 2);
$logs = $logsBefore . $logsAfter;
$logs = ($logsBefore ?? '') . ($logsAfter ?? '');
}
$deployment->setAttribute('buildLogs', $logs);
@@ -1203,8 +1203,6 @@ class Builds extends Action
protected function sendUsage(Document $resource, Document $deployment, Document $project, Context $usage, UsagePublisher $publisherForUsage): void
{
$spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$cpus = (int) ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT);
$memory = (int) ($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT);
switch ($deployment->getAttribute('status')) {
case 'ready':
@@ -1366,8 +1364,6 @@ class Builds extends Action
Realtime $queueForRealtime,
array $platform
): void {
$deployment = new Document();
try {
if ($resource->getAttribute('providerSilentMode', false) === true) {
return;
@@ -1448,7 +1444,7 @@ class Builds extends Action
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$previewUrl = match ($resource->getCollection()) {
'functions' => '',
'sites' => !$rule->isEmpty() ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '',
'sites' => ! empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '',
default => throw new \Exception('Invalid resource type')
};
@@ -83,8 +83,7 @@ class Update extends Action
// If rule is already verified or in certificate generation state, don't queue for verification again
if ($rule->getAttribute('status') === RULE_STATUS_VERIFIED || $rule->getAttribute('status') === RULE_STATUS_CERTIFICATE_GENERATING) {
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
return;
return $response->dynamic($rule, Response::MODEL_PROXY_RULE);
}
try {
@@ -87,11 +87,16 @@ class Get extends Action
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
[$path, $device] = match ($type) {
'output' => [$deployment->getAttribute('buildPath', ''), $deviceForBuilds],
'source' => [$deployment->getAttribute('sourcePath', ''), $deviceForSites],
default => throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid deployment download type.'),
};
switch ($type) {
case 'output':
$path = $deployment->getAttribute('buildPath', '');
$device = $deviceForBuilds;
break;
case 'source':
$path = $deployment->getAttribute('sourcePath', '');
$device = $deviceForSites;
break;
}
if (!$device->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
@@ -172,6 +172,8 @@ class Update extends Base
$framework = $site->getAttribute('framework');
}
$enabled ??= $site->getAttribute('enabled', true);
$repositoryId = $site->getAttribute('repositoryId', '');
$repositoryInternalId = $site->getAttribute('repositoryInternalId', '');
@@ -286,8 +286,6 @@ class Create extends Action
$mimeType = $deviceForFiles->getFileMimeType($path); // Get mime-type before compression and encryption
$fileHash = $deviceForFiles->getFileHash($path); // Get file hash before compression and encryption
$data = '';
$iv = '';
$tag = null;
// Compression
$algorithm = $bucket->getAttribute('compression', Compression::NONE);
if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != Compression::NONE) {
@@ -99,8 +99,12 @@ class Update extends Action
$permissions ??= $bucket->getPermissions();
$maximumFileSize ??= $bucket->getAttribute('maximumFileSize', (int) System::getEnv('_APP_STORAGE_LIMIT', 0));
$allowedFileExtensions ??= $bucket->getAttribute('allowedFileExtensions', []);
$enabled ??= $bucket->getAttribute('enabled', true);
$encryption ??= $bucket->getAttribute('encryption', true);
$antivirus ??= $bucket->getAttribute('antivirus', true);
$compression ??= $bucket->getAttribute('compression', Compression::NONE);
$transformations ??= $bucket->getAttribute('transformations', true);
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
@@ -17,7 +17,6 @@ use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Response;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumberUtil;
use Throwable;
use Utopia\Auth\Proofs\Password;
use Utopia\Auth\Proofs\Token;
use Utopia\Database\Database;
@@ -103,8 +102,6 @@ class Create extends Action
{
$isAppUser = $user->isApp($authorization->getRoles());
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
$invitee = new Document();
$hash = '';
if (empty($url)) {
if (! $isAppUser && ! $isPrivilegedUser) {
@@ -148,6 +145,9 @@ class Create extends Action
}
} elseif (! empty($phone)) {
$invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]);
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 ($invitee->isEmpty()) { // Create new user if no user with same email found
@@ -110,7 +110,10 @@ class Update extends Action
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$providerRepositoryName = $github->getRepositoryName($providerRepositoryId);
$providerRepositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($providerRepositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
@@ -65,7 +65,6 @@ class Get extends Action
}
$state = \json_decode($state, true);
$redirectFailure = $state['failure'] ?? '';
$projectId = $state['projectId'] ?? '';
$project = $dbForPlatform->getDocument('projects', $projectId);
@@ -75,11 +74,10 @@ class Get extends Action
if (!empty($redirectFailure)) {
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
$response
return $response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
return;
}
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
@@ -167,11 +165,10 @@ class Get extends Action
if (!empty($redirectFailure)) {
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
$response
return $response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
return;
}
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
@@ -49,8 +49,6 @@ trait Deployment
) {
$errors = [];
foreach ($repositories as $repository) {
$logBase = 'vcs.github.event.repo.unknown';
try {
$repositoryId = $repository->getId();
$projectId = $repository->getAttribute('projectId');
@@ -109,11 +107,18 @@ trait Deployment
$owner = $github->getOwnerName($providerInstallationId) ?? '';
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId);
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$isAuthorized = !$external;
if (!$isAuthorized && !empty($providerPullRequestId)) {
@@ -286,7 +291,10 @@ trait Deployment
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId);
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
@@ -493,7 +501,7 @@ trait Deployment
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$previewUrl = !$rule->isEmpty() ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
$previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
if (!empty($previewUrl)) {
$comment = new Comment($platform);
@@ -516,7 +524,10 @@ trait Deployment
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId);
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
@@ -83,7 +83,7 @@ class Create extends Action
default => null,
};
$response->json($parsedPayload);
return $response->json($parsedPayload);
}
protected function preprocessEvent(Request $request)
+9 -13
View File
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Tasks;
use Appwrite\ClamAV\Network;
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Config\Config;
use Utopia\Console;
@@ -12,8 +13,6 @@ use Utopia\Domains\Domain;
use Utopia\DSN\DSN;
use Utopia\Http\Http;
use Utopia\Logger\Logger;
use Utopia\Messaging\Adapter\Email as EmailAdapter;
use Utopia\Messaging\Messages\Email as EmailMessage;
use Utopia\Platform\Action;
use Utopia\Pools\Group;
use Utopia\Queue\Broker\Pool as BrokerPool;
@@ -125,7 +124,7 @@ class Doctor extends Action
$providerConfig = System::getEnv('_APP_LOGGING_CONFIG', '');
try {
$loggingProvider = new DSN($providerConfig);
$loggingProvider = new DSN($providerConfig ?? '');
$providerName = $loggingProvider->getScheme();
@@ -213,18 +212,15 @@ class Doctor extends Action
}
try {
/** @var EmailAdapter $smtp */
$smtp = $register->get('smtp');
/* @var PHPMailer $mail */
$mail = $register->get('smtp');
$emailMessage = new EmailMessage(
to: ['demo@example.com'],
subject: 'Test SMTP Connection',
content: 'Hello World',
fromName: \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')),
fromEmail: System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM),
);
$mail->addAddress('demo@example.com', 'Example.com');
$mail->Subject = 'Test SMTP Connection';
$mail->Body = 'Hello World';
$mail->AltBody = 'Hello World';
$smtp->send($emailMessage);
$mail->send();
Console::success('🟢 ' . str_pad("SMTP", 50, '.') . 'connected');
} catch (\Throwable) {
Console::error('🔴 ' . str_pad("SMTP", 47, '.') . 'disconnected');
+1 -1
View File
@@ -227,7 +227,7 @@ class Install extends Action
// Fall back to CLI mode
$enableAssistant = false;
$assistantExistsInOldCompose = false;
if ($existingInstallation) {
if ($existingInstallation && isset($compose)) {
try {
$assistantService = $compose->getService('appwrite-assistant');
$assistantExistsInOldCompose = $assistantService !== null;
-2
View File
@@ -566,8 +566,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$repoBranch = $language['repoBranch'] ?? 'main';
if ($git && !empty($gitUrl)) {
$prUrls = [];
// Generate commit message: use provided message, AI changelog, or fallback
if (! empty($message)) {
$commitMessage = $message;
@@ -60,7 +60,7 @@ class StatsResources extends Action
$interval = (int) System::getEnv('_APP_STATS_RESOURCES_INTERVAL', '3600');
Console::loop(function () use ($queueForStatsResources) {
Console::loop(function () use ($queueForStatsResources, $dbForPlatform) {
$last24Hours = (new \DateTime())->sub(\DateInterval::createFromDateString('24 hours'));
/**
+4 -1
View File
@@ -6,6 +6,7 @@ use Exception;
use Throwable;
use Utopia\Console;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization;
use Utopia\Database\Exception\Structure;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
@@ -50,11 +51,13 @@ class Audits extends Action
/**
* @param Message $message
* @param callable $getProjectDB
* @param Document $project
* @param callable(Document): \Utopia\Audit\Audit $getAudit
* @param callable $getAudit
* @return Commit|NoCommit
* @throws Throwable
* @throws \Utopia\Database\Exception
* @throws Authorization
* @throws Structure
*/
public function action(Message $message, Document $project, callable $getAudit): Commit|NoCommit
+14 -8
View File
@@ -24,6 +24,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;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
@@ -362,6 +363,7 @@ class Deletes extends Action
/**
* @param Document $project
* @param callable $getProjectDB
* @param Document $target
* @return void
* @throws Exception
*/
@@ -436,6 +438,7 @@ class Deletes extends Action
* @param string $resource
* @param string|null $resourceType
* @return void
* @throws Authorization
* @throws Exception
*/
private function deleteCacheByResource(Document $project, callable $getProjectDB, string $resource, ?string $resourceType = null): void
@@ -515,6 +518,7 @@ class Deletes extends Action
}
/**
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param string $hourlyUsageRetentionDatetime
* @return void
@@ -582,6 +586,7 @@ class Deletes extends Action
* @param Database $dbForPlatform
* @param Document $document
* @return void
* @throws Authorization
* @throws DatabaseException
* @throws Conflict
* @throws Restricted
@@ -618,6 +623,7 @@ class Deletes extends Action
* @param Document $document
* @return void
* @throws Exception
* @throws Authorization
* @throws DatabaseException
*/
protected function deleteProject(Database $dbForPlatform, callable $getProjectDB, callable $getDatabasesDB, Device $deviceForFiles, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, Document $document): void
@@ -946,7 +952,7 @@ class Deletes extends Action
// fast path, no need to list anything!
$delete($dbForProject, $resourceInternalId, $resourceType);
} else {
$processResource = function (string $type) use ($dbForProject, $delete) {
$processResource = function (string $type) use ($dbForProject, $delete, $resourceType) {
$this->listByGroup(
collection: $type,
queries: [Query::select(['$id', '$sequence'])],
@@ -1103,7 +1109,7 @@ class Deletes extends Action
Query::equal('resourceInternalId', [$siteInternalId]),
Query::equal('resourceType', ['sites']),
Query::orderAsc()
], $dbForProject, function (Document $document) use ($deviceForSites, $deviceForBuilds, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds, &$deploymentIds) {
], $dbForProject, function (Document $document) use ($project, $certificates, $deviceForSites, $deviceForBuilds, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) {
$deploymentInternalIds[] = $document->getSequence();
$deploymentIds[] = $document->getId();
$this->deleteBuildFiles($deviceForBuilds, $document);
@@ -1166,7 +1172,7 @@ class Deletes extends Action
Query::equal('deploymentResourceInternalId', [$functionInternalId]),
Query::equal('projectInternalId', [$project->getSequence()]),
Query::orderAsc()
], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) {
], $dbForPlatform, function (Document $document) use ($project, $dbForPlatform, $certificates) {
$this->deleteRule($dbForPlatform, $document, $certificates);
});
@@ -1190,7 +1196,7 @@ class Deletes extends Action
Query::equal('resourceInternalId', [$functionInternalId]),
Query::equal('resourceType', ['functions']),
Query::orderAsc()
], $dbForProject, function (Document $document) use ($deviceForFunctions, $deviceForBuilds, &$deploymentInternalIds) {
], $dbForProject, function (Document $document) use ($dbForPlatform, $project, $certificates, $deviceForFunctions, $deviceForBuilds, &$deploymentInternalIds) {
$deploymentInternalIds[] = $document->getSequence();
$this->deleteDeploymentFiles($deviceForFunctions, $document);
$this->deleteBuildFiles($deviceForBuilds, $document);
@@ -1315,7 +1321,7 @@ class Deletes extends Action
/**
* @param Device $device
* @param Document $deployment
* @param Document $build
* @return void
*/
private function deleteBuildFiles(Device $device, Document $deployment): void
@@ -1625,9 +1631,9 @@ class Deletes extends Action
try {
$dbForProject->deleteDocuments('transactions', [
Query::lessThan('expiresAt', DateTime::format(new \DateTime())),
], onNext: function (Document $transaction) use (&$transactionInternalIds) {
], onNext: function (Document $transaction) use ($dbForProject, $project, &$transactionInternalIds) {
$transactionInternalIds[] = $transaction->getSequence();
}, onError: function (Throwable $th) {
}, onError: function (Throwable $th) use ($project) {
// Swallow errors to avoid breaking the cleanup process
});
} catch (Throwable $th) {
@@ -1640,7 +1646,7 @@ class Deletes extends Action
$dbForProject->deleteDocuments('transactionLogs', [
Query::equal('transactionInternalId', $transactionInternalIds),
], onError: function (Throwable $th) {
], onError: function (Throwable $th) use ($project) {
// Swallow errors to avoid breaking the cleanup process
});
}
+6 -9
View File
@@ -33,7 +33,7 @@ class Functions extends Action
}
/**
* @throws \Exception
* @throws Exception
*/
public function __construct()
{
@@ -256,7 +256,7 @@ class Functions extends Action
* @param Document $user
* @param string|null $jwt
* @param string|null $event
* @throws \Exception
* @throws Exception
*/
private function fail(
string $message,
@@ -271,10 +271,10 @@ class Functions extends Action
?string $event = null,
): void {
$executionId = ID::unique();
$headers['x-appwrite-execution-id'] = $executionId;
$headers['x-appwrite-execution-id'] = $executionId ?? '';
$headers['x-appwrite-trigger'] = $trigger;
$headers['x-appwrite-event'] = $event ?? '';
$headers['x-appwrite-user-id'] = $user->getId();
$headers['x-appwrite-user-id'] = $user->getId() ?? '';
$headers['x-appwrite-user-jwt'] = $jwt ?? '';
$headersFiltered = [];
@@ -458,8 +458,8 @@ class Functions extends Action
if ($version === 'v2') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_DATA' => $body,
'APPWRITE_FUNCTION_EVENT_DATA' => $body,
'APPWRITE_FUNCTION_DATA' => $body ?? '',
'APPWRITE_FUNCTION_EVENT_DATA' => $body ?? '',
'APPWRITE_FUNCTION_EVENT' => $headers['x-appwrite-event'] ?? '',
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
@@ -508,9 +508,6 @@ class Functions extends Action
]);
/** Execute function */
$error = null;
$errorCode = 0;
try {
$version = $function->getAttribute('version', 'v2');
$command = $runtime['startCommand'];
+68 -53
View File
@@ -4,13 +4,10 @@ namespace Appwrite\Platform\Workers;
use Appwrite\Template\Template;
use Exception;
use PHPMailer\PHPMailer\PHPMailer;
use Swoole\Runtime;
use Utopia\Database\Document;
use Utopia\Logger\Log;
use Utopia\Messaging\Adapter\Email as EmailAdapter;
use Utopia\Messaging\Adapter\Email\SMTP;
use Utopia\Messaging\Messages\Email as EmailMessage;
use Utopia\Messaging\Messages\Email\Attachment;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Registry\Registry;
@@ -52,9 +49,9 @@ class Mails extends Action
/**
* @param Message $message
* @param Document $project
* @param Registry $register
* @param Log $log
* @throws \PHPMailer\PHPMailer\Exception
* @return void
* @throws Exception
*/
@@ -135,38 +132,36 @@ class Mails extends Action
// render() will return the subject in <p> tags, so use strip_tags() to remove them
$subject = \strip_tags($subjectTemplate->render());
/** @var EmailAdapter $adapter */
$adapter = empty($smtp)
/** @var PHPMailer $mail */
$mail = empty($smtp)
? $register->get('smtp')
: new SMTP(
host: $smtp['host'],
port: (int) $smtp['port'],
username: $smtp['username'] ?? '',
password: $smtp['password'] ?? '',
smtpSecure: $smtp['secure'] ?? '',
smtpAutoTLS: false,
xMailer: 'Appwrite Mailer',
timeout: 10,
keepAlive: true,
timelimit: 30,
);
: $this->getMailer($smtp);
// Resolve from/replyTo using fallback hierarchy: Custom options > SMTP config > Defaults
$defaultFromEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$defaultFromName = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
$mail->clearAddresses();
$mail->clearAllRecipients();
$mail->clearReplyTos();
$mail->clearAttachments();
$mail->clearBCCs();
$mail->clearCCs();
$mail->addAddress($recipient, $name);
$mail->Subject = $subject;
$mail->Body = $body;
$fromEmail = !empty($smtp) ? ($smtp['senderEmail'] ?? $defaultFromEmail) : $defaultFromEmail;
$fromName = !empty($smtp) ? ($smtp['senderName'] ?? $defaultFromName) : $defaultFromName;
$replyTo = $defaultFromEmail;
$replyToName = $defaultFromName;
$mail->AltBody = $body;
$mail->AltBody = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $mail->AltBody);
$mail->AltBody = \strip_tags($mail->AltBody);
$mail->AltBody = \trim($mail->AltBody);
$replyTo = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$replyToName = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
$customMailOptions = $payload['customMailOptions'] ?? [];
if (!empty($customMailOptions['senderEmail'])) {
$fromEmail = $customMailOptions['senderEmail'];
}
if (!empty($customMailOptions['senderName'])) {
$fromName = $customMailOptions['senderName'];
// fallback hierarchy: Custom options > SMTP config > Defaults.
if (!empty($customMailOptions['senderEmail']) || !empty($customMailOptions['senderName'])) {
$fromEmail = $customMailOptions['senderEmail'] ?? $mail->From;
$fromName = $customMailOptions['senderName'] ?? $mail->FromName;
$mail->setFrom($fromEmail, $fromName);
}
if (!empty($customMailOptions['replyToEmail']) || !empty($customMailOptions['replyToName'])) {
@@ -177,32 +172,18 @@ class Mails extends Action
$replyToName = $smtp['senderName'] ?? $replyToName;
}
$attachments = null;
$mail->addReplyTo($replyTo, $replyToName);
if (!empty($attachment['content'] ?? '')) {
$attachments = [
new Attachment(
name: $attachment['filename'] ?? 'unknown.file',
path: '',
type: $attachment['type'] ?? 'plain/text',
content: \base64_decode($attachment['content']),
),
];
$mail->AddStringAttachment(
base64_decode($attachment['content']),
$attachment['filename'] ?? 'unknown.file',
$attachment['encoding'] ?? PHPMailer::ENCODING_BASE64,
$attachment['type'] ?? 'plain/text'
);
}
$emailMessage = new EmailMessage(
to: [['email' => $recipient, 'name' => $name]],
subject: $subject,
content: $body,
fromName: $fromName,
fromEmail: $fromEmail,
replyToName: $replyToName,
replyToEmail: $replyTo,
attachments: $attachments,
html: true,
);
try {
$adapter->send($emailMessage);
$mail->send();
} catch (\Throwable $error) {
if ($type === 'smtp') {
throw new Exception('Error sending mail: ' . $error->getMessage(), 401);
@@ -210,4 +191,38 @@ class Mails extends Action
throw new Exception('Error sending mail: ' . $error->getMessage(), 500);
}
}
/**
* @param array $smtp
* @return PHPMailer
* @throws \PHPMailer\PHPMailer\Exception
*/
protected function getMailer(array $smtp): PHPMailer
{
$mail = new PHPMailer(true);
$mail->isSMTP();
$username = $smtp['username'];
$password = $smtp['password'];
$mail->XMailer = 'Appwrite Mailer';
$mail->Host = $smtp['host'];
$mail->Port = $smtp['port'];
$mail->SMTPAuth = (!empty($username) && !empty($password));
$mail->Username = $username;
$mail->Password = $password;
$mail->SMTPSecure = $smtp['secure'];
$mail->SMTPAutoTLS = false;
$mail->SMTPKeepAlive = true;
$mail->CharSet = 'UTF-8';
$mail->Timeout = 10; /* Connection timeout */
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
$mail->setFrom($smtp['senderEmail'], $smtp['senderName']);
$mail->isHTML();
return $mail;
}
}
+2 -2
View File
@@ -285,7 +285,7 @@ class Messaging extends Action
try {
$response = $adapter->send($data);
$deliveredTotal += (int) $response['deliveredTo'];
$deliveredTotal += $response['deliveredTo'];
foreach ($response['results'] as $result) {
if ($result['status'] === 'failure') {
$deliveryErrors[] = "Failed sending to target {$result['recipient']} with error: {$result['error']}";
@@ -380,7 +380,7 @@ class Messaging extends Action
]));
// Delete any attachments that were downloaded to local storage
if ($providerType === MESSAGE_TYPE_EMAIL) {
if ($provider->getAttribute('type') === MESSAGE_TYPE_EMAIL) {
if ($deviceForFiles->getType() === Storage::DEVICE_LOCAL) {
return;
}
+1 -1
View File
@@ -408,7 +408,6 @@ class Migrations extends Action
$tempAPIKey = $this->generateAPIKey($project);
$transfer = $source = $destination = null;
$aggregatedResources = [];
$host = System::getEnv('_APP_MIGRATION_HOST');
if (empty($host)) {
@@ -445,6 +444,7 @@ class Migrations extends Action
$destination
);
$aggregatedResources = [];
/** Start Transfer */
if (empty($source->getErrors())) {
$migration->setAttribute('stage', 'migrating');
@@ -208,7 +208,7 @@ class StatsResources extends Action
{
$totalFiles = 0;
$totalStorage = 0;
$this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $region, &$totalFiles, &$totalStorage) {
$this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $dbForLogs, $region, &$totalFiles, &$totalStorage) {
try {
$files = $dbForProject->count('bucket_' . $bucket->getSequence());
} catch (Throwable $th) {
+4 -4
View File
@@ -140,7 +140,7 @@ class StatsUsage extends Action
/**
* @param Message $message
* @param callable(Document): Database $getProjectDB
* @param callable(): Database $getProjectDB
* @param callable(): Database $getLogsDB
* @param Registry $register
* @return void
@@ -212,7 +212,7 @@ class StatsUsage extends Action
* @param Document $project
* @param Document $document
* @param array $metrics
* @param callable(Document): Database $getProjectDB
* @param callable(): Database $getProjectDB
* @param string $databaseType Database type from context
* @return void
*/
@@ -394,7 +394,7 @@ class StatsUsage extends Action
/**
* Commit stats to DB
* @param callable(Document): Database $getProjectDB
* @param callable(): Database $getProjectDB
* @return void
*/
public function commitToDb(callable $getProjectDB): void
@@ -459,7 +459,7 @@ class StatsUsage extends Action
/**
* Sort by unique index key reduce locks/deadlocks
*/
usort($projectStats['stats'], function ($a, $b) {
usort($projectStats['stats'], function ($a, $b) use ($sequence) {
// Metric DESC
$cmp = strcmp($b['metric'], $a['metric']);
if ($cmp !== 0) {
+1 -1
View File
@@ -233,7 +233,7 @@ class Webhooks extends Action
$template->setParam('{{webhook}}', $webhook->getAttribute('name'));
$template->setParam('{{project}}', $project->getAttribute('name'));
$template->setParam('{{url}}', $webhook->getAttribute('url'));
$template->setParam('{{error}}', 'The server returned ' . $statusCode . ' status code');
$template->setParam('{{error}}', $curlError ?? 'The server returned ' . $statusCode . ' status code');
$template->setParam('{{path}}', "/console/project-$region-$projectId/settings/webhooks/$webhookId");
$template->setParam('{{attempts}}', $attempts);
-1
View File
@@ -2,7 +2,6 @@
namespace Appwrite\Promises;
/** @phpstan-consistent-constructor */
abstract class Promise
{
protected const STATE_PENDING = 1;
+1 -1
View File
@@ -197,7 +197,7 @@ class Method
public function isHidden(): bool|array
{
return $this->hide;
return $this->hide ?? false;
}
public function isPackaging(): bool
@@ -204,6 +204,9 @@ abstract class Format
*
* Get services value
*
* @param array $services
*
* @return self
*/
public function getServices(): array
{
@@ -79,8 +79,8 @@ class OpenAPI3 extends Format
$output['components']['securitySchemes']['Key']['x-appwrite'] = ['demo' => '<YOUR_API_KEY>'];
}
if (isset($output['components']['securitySchemes']['JWT'])) {
$output['components']['securitySchemes']['JWT']['x-appwrite'] = ['demo' => '<YOUR_JWT>'];
if (isset($output['securityDefinitions']['JWT'])) {
$output['securityDefinitions']['JWT']['x-appwrite'] = ['demo' => '<YOUR_JWT>'];
}
if (isset($output['components']['securitySchemes']['Locale'])) {
@@ -99,7 +99,7 @@ class OpenAPI3 extends Format
$sdk = $route->getLabel('sdk', false);
if ($sdk === false) {
if (empty($sdk)) {
continue;
}
@@ -125,9 +125,7 @@ class OpenAPI3 extends Format
$namespace = $sdk->getNamespace() ?? 'default';
if ($desc === null) {
$desc = '';
}
$desc ??= '';
$descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc;
$temp = [
@@ -165,7 +163,7 @@ class OpenAPI3 extends Format
];
}
if (\is_array($additionalMethods) && \count($additionalMethods) > 0) {
if (!empty($additionalMethods)) {
$temp['x-appwrite']['methods'] = [];
foreach ($additionalMethods as $methodObj) {
/** @var Method $methodObj */
@@ -331,7 +329,7 @@ class OpenAPI3 extends Format
if (($response->getCode() ?? 500) === 204) {
$temp['responses'][(string)$response->getCode() ?? '500']['description'] = 'No content';
unset($temp['responses'][(string)$response->getCode() ?? '500']['content']);
unset($temp['responses'][(string)$response->getCode() ?? '500']['schema']);
}
}
@@ -385,7 +383,7 @@ class OpenAPI3 extends Format
$validator = $validator->getValidator();
}
$class = $validator instanceof Validator
$class = !empty($validator)
? \get_class($validator)
: '';
@@ -433,7 +431,7 @@ class OpenAPI3 extends Format
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
break;
case \Utopia\Database\Validator\Datetime::class:
case \Utopia\Database\Validator\DatetimeValidator::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['format'] = 'datetime';
$node['schema']['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE;
@@ -465,6 +463,7 @@ class OpenAPI3 extends Format
$node['schema']['x-example'] = ($param['example'] ?? '') ?: 'https://example.com';
break;
case \Utopia\Validator\JSON::class:
case \Utopia\Validator\Mock::class:
case \Utopia\Validator\Assoc::class:
$param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['schema']['type'] = 'object';
@@ -564,6 +563,12 @@ class OpenAPI3 extends Format
$node['schema']['x-example'] = $param['example'];
}
break;
case \Utopia\Validator\Length::class:
$node['schema']['type'] = $validator->getType();
if (!empty($param['example'])) {
$node['schema']['x-example'] = $param['example'];
}
break;
case \Utopia\Validator\WhiteList::class:
if ($array) {
$validator = $validator->getValidator();
@@ -96,9 +96,10 @@ class Swagger2 extends Format
$scope = $route->getLabel('scope', '');
/** @var Method $sdk */
$sdk = $route->getLabel('sdk', false);
if ($sdk === false) {
if (empty($sdk)) {
continue;
}
@@ -126,9 +127,7 @@ class Swagger2 extends Format
$sdkPlatforms = array_values(array_unique($sdkPlatforms));
$namespace = $sdk->getNamespace() ?? 'default';
if ($desc === null) {
$desc = '';
}
$desc ??= '';
$descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc;
$temp = [
@@ -172,7 +171,7 @@ class Swagger2 extends Format
$temp['produces'][] = $produces;
}
if (\is_array($additionalMethods) && \count($additionalMethods) > 0) {
if (!empty($additionalMethods)) {
$temp['x-appwrite']['methods'] = [];
foreach ($additionalMethods as $methodObj) {
/** @var Method $methodObj */
@@ -389,7 +388,7 @@ class Swagger2 extends Format
$validator = $validator->getValidator();
}
$class = $validator instanceof Validator
$class = !empty($validator)
? \get_class($validator)
: '';
@@ -437,7 +436,7 @@ class Swagger2 extends Format
$node['type'] = $validator->getType();
$node['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
break;
case \Utopia\Database\Validator\Datetime::class:
case \Utopia\Database\Validator\DatetimeValidator::class:
$node['type'] = $validator->getType();
$node['format'] = 'datetime';
$node['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE;
@@ -480,6 +479,7 @@ class Swagger2 extends Format
}
break;
case \Utopia\Validator\JSON::class:
case \Utopia\Validator\Mock::class:
case \Utopia\Validator\Assoc::class:
$node['type'] = 'object';
$node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
@@ -552,6 +552,12 @@ class Swagger2 extends Format
$node['x-example'] = $param['example'];
}
break;
case \Utopia\Validator\Length::class:
$node['type'] = $validator->getType();
if (!empty($param['example'])) {
$node['x-example'] = $param['example'];
}
break;
case \Utopia\Validator\WhiteList::class:
if ($array) {
$validator = $validator->getValidator();
@@ -86,6 +86,7 @@ class User extends Document
/**
* Check if user is anonymous.
*
* @param Document $this
* @return bool
*/
public function isAnonymous(): bool
@@ -152,6 +153,7 @@ class User extends Document
/**
* Verify session and check that its not expired.
*
* @param array<Document> $sessions
* @param string $secret
*
* @return bool|string
@@ -29,8 +29,8 @@ class DatabasesStringTypesTest extends Scope
protected function setupDatabaseAndCollection(): array
{
$cacheKey = $this->getProject()['$id'] ?? 'default';
if (!empty(self::$setupCache[$cacheKey])) {
return self::$setupCache[$cacheKey];
if (!empty(static::$setupCache[$cacheKey])) {
return static::$setupCache[$cacheKey];
}
$projectId = $this->getProject()['$id'];
@@ -135,12 +135,12 @@ class DatabasesStringTypesTest extends Scope
// Wait for all attributes to be available
$this->waitForAllAttributes($databaseId, $collectionId);
self::$setupCache[$cacheKey] = [
static::$setupCache[$cacheKey] = [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
];
return self::$setupCache[$cacheKey];
return static::$setupCache[$cacheKey];
}
public function testCreateDatabase(): void
@@ -45,12 +45,6 @@ trait DatabasesPermissionsBase
return $recordId ? "{$base}/{$recordId}" : $base;
}
protected function getIndexUrl(string $databaseId, string $containerId, string $indexId = ''): string
{
$base = "{$this->getContainerUrl($databaseId, $containerId)}/indexes";
return $indexId ? "{$base}/{$indexId}" : $base;
}
// User Management Methods
public function createUser(string $id, string $email, string $password = 'test123!'): array
{
@@ -414,7 +414,7 @@ class FunctionsCustomClientTest extends Scope
'offset' => 2
]);
$this->assertEquals(200, $templatesOffset['headers']['status-code']);
$this->addToAssertionCount(1);
$this->addToAssertionCount(1, $templatesOffset['body']['templates']);
$this->assertEquals($templates['body']['templates'][2]['id'], $templatesOffset['body']['templates'][0]['id']);
// List templates with filters
@@ -1003,7 +1003,6 @@ class FunctionsCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id']
];
$id = '';
$largeTag = null;
while (!feof($handle)) {
$curlFile = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'large-fx.tar.gz');
$headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size - 1) . '/' . $size;
@@ -102,7 +102,7 @@ class AbuseTest extends Scope
$maxQueries = System::getEnv('_APP_GRAPHQL_MAX_QUERIES', 10);
$query = [];
for ($i = 0; $i <= ((int) $maxQueries) + 1; $i++) {
for ($i = 0; $i <= $maxQueries + 1; $i++) {
$query[] = ['query' => $this->getQuery(self::LIST_COUNTRIES)];
}
+1 -1
View File
@@ -409,7 +409,7 @@ class MessagingTest extends Scope
$apiKey = $emailDSN->getPassword();
$domain = $emailDSN->getUser();
if (empty($to) || empty($fromName) || empty($fromEmail) || empty($apiKey) || empty($domain) || empty($isEuRegion)) {
if (empty($to) || empty($from) || empty($apiKey) || empty($domain) || empty($isEuRegion)) {
$this->markTestSkipped('Email provider not configured');
}
@@ -229,8 +229,6 @@ class StorageClientTest extends Scope
], $this->getHeaders()), $gqlPayload);
$this->assertEquals(47218, \strlen($file['body']));
return $file;
}
/**
@@ -291,8 +291,6 @@ class StorageServerTest extends Scope
], $this->getHeaders()), $gqlPayload);
$this->assertEquals(47218, \strlen($file['body']));
return $file;
}
/**
@@ -102,7 +102,7 @@ class AbuseTest extends Scope
$maxQueries = System::getEnv('_APP_GRAPHQL_MAX_QUERIES', 10);
$query = [];
for ($i = 0; $i <= ((int) $maxQueries) + 1; $i++) {
for ($i = 0; $i <= $maxQueries + 1; $i++) {
$query[] = ['query' => $this->getQuery(self::LIST_COUNTRIES)];
}
@@ -2241,7 +2241,7 @@ trait MessagingBase
$authKey = $smsDSN->getPassword();
$templateId = $smsDSN->getParam('templateId');
if (empty($to) || empty($senderId) || empty($authKey)) {
if (empty($to) || empty($from) || empty($senderId) || empty($authKey)) {
$this->markTestSkipped('SMS provider not configured');
}
@@ -64,8 +64,8 @@ trait MigrationsBase
*/
protected function setupMigrationDatabase(): array
{
if (!empty(self::$cachedDatabaseData)) {
return self::$cachedDatabaseData;
if (!empty(static::$cachedDatabaseData)) {
return static::$cachedDatabaseData;
}
$response = $this->client->call(Client::METHOD_POST, '/databases', [
@@ -81,11 +81,11 @@ trait MigrationsBase
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
self::$cachedDatabaseData = [
static::$cachedDatabaseData = [
'databaseId' => $response['body']['$id'],
];
return self::$cachedDatabaseData;
return static::$cachedDatabaseData;
}
/**
@@ -94,8 +94,8 @@ trait MigrationsBase
*/
protected function setupMigrationTable(): array
{
if (!empty(self::$cachedTableData)) {
return self::$cachedTableData;
if (!empty(static::$cachedTableData)) {
return static::$cachedTableData;
}
// Ensure database exists first
@@ -141,12 +141,12 @@ trait MigrationsBase
$this->assertEquals('available', $response['body']['status']);
}, 5000, 500);
self::$cachedTableData = [
static::$cachedTableData = [
'databaseId' => $databaseId,
'tableId' => $tableId,
];
return self::$cachedTableData;
return static::$cachedTableData;
}
public function performMigrationSync(array $body): array
@@ -670,7 +670,7 @@ trait MigrationsBase
]);
// Clear the cache since we cleaned up
self::$cachedDatabaseData = [];
static::$cachedDatabaseData = [];
}
public function testAppwriteMigrationDatabasesRow(): void
@@ -757,8 +757,8 @@ trait MigrationsBase
]);
// Clear the caches since we cleaned up
self::$cachedDatabaseData = [];
self::$cachedTableData = [];
static::$cachedDatabaseData = [];
static::$cachedTableData = [];
}
/**
@@ -1331,7 +1331,7 @@ trait MigrationsBase
]
);
$this->assertEventually(function () use ($missingColumn) {
$this->assertEventually(function () use ($missingColumn, $databaseId, $tableId) {
$migrationId = $missingColumn['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@@ -1363,7 +1363,7 @@ trait MigrationsBase
]
);
$this->assertEventually(function () use ($missingColumn) {
$this->assertEventually(function () use ($missingColumn, $databaseId, $tableId) {
$migrationId = $missingColumn['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@@ -1395,7 +1395,7 @@ trait MigrationsBase
]
);
$this->assertEventually(function () use ($irrelevantColumn) {
$this->assertEventually(function () use ($irrelevantColumn, $databaseId, $tableId) {
$migrationId = $irrelevantColumn['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@@ -1422,7 +1422,7 @@ trait MigrationsBase
]
);
$this->assertEventually(function () use ($migration) {
$this->assertEventually(function () use ($migration, $databaseId, $tableId) {
$migrationId = $migration['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@@ -1464,7 +1464,7 @@ trait MigrationsBase
]
);
$this->assertEventually(function () use ($migration) {
$this->assertEventually(function () use ($migration, $databaseId, $tableId) {
$migrationId = $migration['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@@ -4018,7 +4018,7 @@ trait MigrationsBase
]
);
$this->assertEventually(function () use ($migration) {
$this->assertEventually(function () use ($migration, $databaseId, $tableId) {
$migrationId = $migration['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
+1 -3
View File
@@ -97,7 +97,6 @@ trait StorageBase
'x-appwrite-project' => $this->getProject()['$id']
];
$id = '';
$largeFile = null;
while (!feof($handle)) {
$curlFile = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'large-file.mp4');
$headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size - 1) . '/' . $size;
@@ -133,7 +132,7 @@ trait StorageBase
self::$cachedBucketFile[$cacheKey] = [
'bucketId' => $bucketId,
'fileId' => $file['body']['$id'],
'largeFileId' => $largeFile['body']['$id'] ?? '',
'largeFileId' => $largeFile['body']['$id'],
'largeBucketId' => $bucket2['body']['$id'],
'webpFileId' => $webpFile['body']['$id']
];
@@ -262,7 +261,6 @@ trait StorageBase
'x-appwrite-project' => $this->getProject()['$id']
];
$id = '';
$largeFile = null;
while (!feof($handle)) {
$curlFile = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'large-file.mp4');
$headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size - 1) . '/' . $size;
@@ -29,8 +29,8 @@ class DatabasesStringTypesTest extends Scope
protected function setupDatabaseAndTable(): array
{
$cacheKey = $this->getProject()['$id'] ?? 'default';
if (!empty(self::$setupCache[$cacheKey])) {
return self::$setupCache[$cacheKey];
if (!empty(static::$setupCache[$cacheKey])) {
return static::$setupCache[$cacheKey];
}
$projectId = $this->getProject()['$id'];
@@ -131,7 +131,7 @@ class DatabasesStringTypesTest extends Scope
// Cache before waiting so that if waitForAllAttributes times out,
// subsequent calls don't try to re-create the same columns (causing 409)
self::$setupCache[$cacheKey] = [
static::$setupCache[$cacheKey] = [
'databaseId' => $databaseId,
'tableId' => $tableId,
];
@@ -139,7 +139,7 @@ class DatabasesStringTypesTest extends Scope
// Wait for all columns to be available
$this->waitForAllAttributes($databaseId, $tableId);
return self::$setupCache[$cacheKey];
return static::$setupCache[$cacheKey];
}
public function testCreateDatabase(): void
+25 -25
View File
@@ -28,8 +28,8 @@ trait UsersBase
protected function setupUser(): array
{
$projectId = $this->getProject()['$id'];
if (!empty(self::$cachedUser[$projectId])) {
return self::$cachedUser[$projectId];
if (!empty(static::$cachedUser[$projectId])) {
return static::$cachedUser[$projectId];
}
$user = $this->client->call(Client::METHOD_POST, '/users', array_merge([
@@ -52,16 +52,16 @@ trait UsersBase
]);
if (!empty($response['body']['users'])) {
self::$cachedUser[$projectId] = ['userId' => $response['body']['users'][0]['$id']];
return self::$cachedUser[$projectId];
static::$cachedUser[$projectId] = ['userId' => $response['body']['users'][0]['$id']];
return static::$cachedUser[$projectId];
}
}
if ($user['headers']['status-code'] === 201) {
self::$cachedUser[$projectId] = ['userId' => $user['body']['$id']];
static::$cachedUser[$projectId] = ['userId' => $user['body']['$id']];
}
return self::$cachedUser[$projectId];
return static::$cachedUser[$projectId];
}
/**
@@ -90,7 +90,7 @@ trait UsersBase
protected function setupHashedPasswordUsers(): void
{
$projectId = $this->getProject()['$id'];
if (!empty(self::$cachedHashedPasswordUsers[$projectId])) {
if (!empty(static::$cachedHashedPasswordUsers[$projectId])) {
return;
}
@@ -180,7 +180,7 @@ trait UsersBase
'passwordSignerKey' => 'XyEKE9RcTDeLEsL/RjwPDBv/RqDl8fb3gpYEOQaPihbxf1ZAtSOHCjuAAa7Q3oHpCYhXSN9tizHgVOwn6krflQ==',
]);
self::$cachedHashedPasswordUsers[$projectId] = true;
static::$cachedHashedPasswordUsers[$projectId] = true;
}
/**
@@ -189,8 +189,8 @@ trait UsersBase
protected function setupUserTarget(): array
{
$projectId = $this->getProject()['$id'];
if (!empty(self::$cachedUserTarget[$projectId])) {
return self::$cachedUserTarget[$projectId];
if (!empty(static::$cachedUserTarget[$projectId])) {
return static::$cachedUserTarget[$projectId];
}
$data = $this->setupUser();
@@ -233,10 +233,10 @@ trait UsersBase
]);
if ($response['headers']['status-code'] === 201) {
self::$cachedUserTarget[$projectId] = $response['body'];
static::$cachedUserTarget[$projectId] = $response['body'];
}
return self::$cachedUserTarget[$projectId] ?? [];
return static::$cachedUserTarget[$projectId] ?? [];
}
/**
@@ -247,7 +247,7 @@ trait UsersBase
$data = $this->setupUser();
$projectId = $this->getProject()['$id'];
if (self::$userNameUpdated) {
if (static::$userNameUpdated) {
return $data;
}
@@ -258,7 +258,7 @@ trait UsersBase
'name' => 'Updated name',
]);
self::$userNameUpdated = true;
static::$userNameUpdated = true;
return $data;
}
@@ -270,7 +270,7 @@ trait UsersBase
$data = $this->setupUser();
$projectId = $this->getProject()['$id'];
if (self::$userEmailUpdated) {
if (static::$userEmailUpdated) {
return $data;
}
@@ -281,7 +281,7 @@ trait UsersBase
'email' => 'users.service@updated.com',
]);
self::$userEmailUpdated = true;
static::$userEmailUpdated = true;
return $data;
}
@@ -293,7 +293,7 @@ trait UsersBase
$data = $this->setupUser();
$projectId = $this->getProject()['$id'];
if (self::$userNumberUpdated) {
if (static::$userNumberUpdated) {
return $data;
}
@@ -304,7 +304,7 @@ trait UsersBase
'number' => '+910000000000',
]);
self::$userNumberUpdated = true;
static::$userNumberUpdated = true;
return $data;
}
@@ -474,7 +474,7 @@ trait UsersBase
// Cache the user ID for other tests
$projectId = $this->getProject()['$id'];
self::$cachedUser[$projectId] = ['userId' => $body['$id']];
static::$cachedUser[$projectId] = ['userId' => $body['$id']];
}
/**
@@ -1274,7 +1274,7 @@ trait UsersBase
$this->assertEquals($user['body']['name'], 'Updated name');
// Mark name as updated for search tests
self::$userNameUpdated = true;
static::$userNameUpdated = true;
}
public function testUpdateUserNameSearch(): void
@@ -1357,7 +1357,7 @@ trait UsersBase
$this->assertEquals($user['body']['email'], 'users.service@updated.com');
// Mark email as updated for search tests
self::$userEmailUpdated = true;
static::$userEmailUpdated = true;
}
public function testUpdateUserEmailSearch(): void
@@ -1645,7 +1645,7 @@ trait UsersBase
$this->assertEquals($response['body']['type'], $errorType);
// Mark phone as updated for search tests
self::$userNumberUpdated = true;
static::$userNumberUpdated = true;
}
public function testUpdateTwoUsersPhoneToEmpty(): void
@@ -1954,7 +1954,7 @@ trait UsersBase
// Cache for other tests
$projectId = $this->getProject()['$id'];
self::$cachedUserTarget[$projectId] = $response['body'];
static::$cachedUserTarget[$projectId] = $response['body'];
}
public function testUpdateUserTarget(): void
@@ -1973,7 +1973,7 @@ trait UsersBase
// Update cache with new data
$projectId = $this->getProject()['$id'];
self::$cachedUserTarget[$projectId] = $response['body'];
static::$cachedUserTarget[$projectId] = $response['body'];
}
public function testListUserTarget(): void
@@ -2014,7 +2014,7 @@ trait UsersBase
// Clear cached target since it was deleted
$projectId = $this->getProject()['$id'];
unset(self::$cachedUserTarget[$projectId]);
unset(static::$cachedUserTarget[$projectId]);
$response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/targets', array_merge([
'content-type' => 'application/json',
@@ -37,8 +37,8 @@ class VCSConsoleClientTest extends Scope
{
$projectId = $this->getProject()['$id'];
if (!empty(self::$cachedInstallationId[$projectId])) {
return self::$cachedInstallationId[$projectId];
if (!empty(static::$cachedInstallationId[$projectId])) {
return static::$cachedInstallationId[$projectId];
}
$response = $this->client->call(Client::METHOD_GET, '/mock/github/callback', array_merge([
@@ -48,8 +48,8 @@ class VCSConsoleClientTest extends Scope
'projectId' => $projectId,
]);
self::$cachedInstallationId[$projectId] = $response['body']['installationId'];
return self::$cachedInstallationId[$projectId];
static::$cachedInstallationId[$projectId] = $response['body']['installationId'];
return static::$cachedInstallationId[$projectId];
}
/**
@@ -60,8 +60,8 @@ class VCSConsoleClientTest extends Scope
{
$projectId = $this->getProject()['$id'];
if (!empty(self::$cachedFunctionData[$projectId])) {
return self::$cachedFunctionData[$projectId];
if (!empty(static::$cachedFunctionData[$projectId])) {
return static::$cachedFunctionData[$projectId];
}
$installationId = $this->setupInstallation();
@@ -86,12 +86,12 @@ class VCSConsoleClientTest extends Scope
'providerBranch' => 'main',
]);
self::$cachedFunctionData[$projectId] = [
static::$cachedFunctionData[$projectId] = [
'installationId' => $installationId,
'functionId' => $function['body']['$id']
];
return self::$cachedFunctionData[$projectId];
return static::$cachedFunctionData[$projectId];
}
public function testGitHubAuthorize(): void
+3 -3
View File
@@ -25,7 +25,7 @@ class KeyTest extends TestCase
$roleScopes = Config::getParam('roles', [])[User::ROLE_APPS]['scopes'];
$guestRoleScopes = Config::getParam('roles', [])[User::ROLE_GUESTS]['scopes'];
$key = self::generateKey($projectId, $usage, $scopes);
$key = static::generateKey($projectId, $usage, $scopes);
$decoded = Key::decode(
project: new Document(['$id' => $projectId]),
team: new Document(),
@@ -50,7 +50,7 @@ class KeyTest extends TestCase
'previewAuthDisabled' => true,
'deploymentStatusIgnored' => true,
];
$key = self::generateKey($projectId, $usage, $scopes, extra: $extra);
$key = static::generateKey($projectId, $usage, $scopes, extra: $extra);
$decoded = Key::decode(
project: new Document(['$id' => $projectId]),
team: new Document(),
@@ -88,7 +88,7 @@ class KeyTest extends TestCase
$this->assertEquals('UNKNOWN', $decoded->getName());
// Decode expired dynamic key
$expiredKey = self::generateKey($projectId, $usage, $scopes, maxAge: 1, timestamp: time() - 60);
$expiredKey = static::generateKey($projectId, $usage, $scopes, maxAge: 1, timestamp: time() - 60);
\sleep(2);
$decoded = Key::decode(
project: new Document(['$id' => $projectId]),
+2 -1
View File
@@ -5,6 +5,7 @@ namespace Tests\Unit\Event;
use Appwrite\Event\Event;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use Utopia\Queue\Publisher;
require_once __DIR__ . '/../../../app/init.php';
@@ -12,7 +13,7 @@ class EventTest extends TestCase
{
protected ?Event $object = null;
protected string $queue = '';
protected MockPublisher $publisher;
protected Publisher $publisher;
public function setUp(): void
{
@@ -76,6 +76,11 @@ class HeadersTest extends TestCase
];
$this->assertFalse($this->object->isValid($headers));
$headers = [
null => 'value',
];
$this->assertFalse($this->object->isValid($headers));
$headers = [
'X-Header' => null,
];