diff --git a/.env b/.env index a292a8197a..1d46f6cad9 100644 --- a/.env +++ b/.env @@ -23,7 +23,9 @@ _APP_OPENSSL_KEY_V1=your-secret-key _APP_DOMAIN=traefik _APP_DOMAIN_FUNCTIONS=functions.localhost _APP_DOMAIN_SITES=sites.localhost -_APP_DOMAIN_TARGET=test.appwrite.io +_APP_DOMAIN_TARGET_CNAME=test.appwrite.io +_APP_DOMAIN_TARGET_A=127.0.0.1 +_APP_DOMAIN_TARGET_AAAA=::1 _APP_RULES_FORMAT=md5 _APP_REDIS_HOST=redis _APP_REDIS_PORT=6379 @@ -71,7 +73,8 @@ _APP_SMS_PROJECTS_DENY_LIST= _APP_STORAGE_LIMIT=30000000 _APP_STORAGE_PREVIEW_LIMIT=20000000 _APP_COMPUTE_SIZE_LIMIT=30000000 -_APP_COMPUTE_TIMEOUT=900 +_APP_FUNCTIONS_TIMEOUT=900 +_APP_SITES_TIMEOUT=30 _APP_COMPUTE_BUILD_TIMEOUT=900 _APP_COMPUTE_CPUS=8 _APP_COMPUTE_MEMORY=8192 @@ -88,6 +91,7 @@ _APP_MAINTENANCE_RETENTION_CACHE=2592000 _APP_MAINTENANCE_RETENTION_EXECUTION=1209600 _APP_MAINTENANCE_RETENTION_ABUSE=86400 _APP_MAINTENANCE_RETENTION_AUDIT=1209600 +_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE=15778800 _APP_USAGE_AGGREGATION_INTERVAL=30 _APP_STATS_RESOURCES_INTERVAL=3600 _APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000 diff --git a/.gitpod.yml b/.gitpod.yml index 478b62fc8d..78375bb1f0 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -7,6 +7,7 @@ tasks: docker pull composer command: | docker run --rm --interactive --tty \ + --user "$(id -u):$(id -g)" \ --volume $PWD:/app \ composer install \ --ignore-platform-reqs \ @@ -23,11 +24,3 @@ vscode: extensions: - ms-azuretools.vscode-docker - zobo.php-intellisense - -github: - # https://www.gitpod.io/docs/prebuilds#github-specific-configuration - prebuilds: - # enable for pull requests coming from forks (defaults to false) - pullRequestsFromForks: true - # add a check to pull requests (defaults to true) - addCheck: false diff --git a/app/cli.php b/app/cli.php index 2d32be5871..b0e976451b 100644 --- a/app/cli.php +++ b/app/cli.php @@ -9,6 +9,7 @@ use Appwrite\Event\StatsResources; use Appwrite\Event\StatsUsage; use Appwrite\Platform\Appwrite; use Appwrite\Runtimes\Runtimes; +use Executor\Executor; use Utopia\Cache\Adapter\Sharding; use Utopia\Cache\Cache; use Utopia\CLI\CLI; @@ -255,6 +256,8 @@ CLI::setResource('logError', function (Registry $register) { }; }, ['register']); +CLI::setResource('executor', fn () => new Executor(fn (string $projectId, string $deploymentId) => System::getEnv('_APP_EXECUTOR_HOST'))); + $platform = new Appwrite(); $platform->init(Service::TYPE_TASK); diff --git a/app/config/collections/common.php b/app/config/collections/common.php index 866d7b6d5b..f006a01d1e 100644 --- a/app/config/collections/common.php +++ b/app/config/collections/common.php @@ -1038,7 +1038,7 @@ return [ '$id' => ID::custom('providerUid'), 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 2048, + 'size' => 2048, // Decrease to 128 as in index length? 'signed' => true, 'required' => false, 'default' => null, @@ -1107,14 +1107,14 @@ return [ '$id' => ID::custom('_key_userInternalId_provider_providerUid'), 'type' => Database::INDEX_UNIQUE, 'attributes' => ['userInternalId', 'provider', 'providerUid'], - 'lengths' => [11, 128, 128], + 'lengths' => [11, 128, 128], // providerUid is length 2000! 'orders' => [Database::ORDER_ASC, Database::ORDER_ASC], ], [ '$id' => ID::custom('_key_provider_providerUid'), 'type' => Database::INDEX_UNIQUE, 'attributes' => ['provider', 'providerUid'], - 'lengths' => [128, 128], + 'lengths' => [128, 128], // providerUid is length 2000! 'orders' => [Database::ORDER_ASC, Database::ORDER_ASC], ], [ diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 019047436a..9957b930a7 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -25914,7 +25914,7 @@ }, "\/sites\/{siteId}\/deployments\/template": { "post": { - "summary": "Create deployment", + "summary": "Create template deployment", "operationId": "sitesCreateTemplateDeployment", "tags": [ "sites" @@ -40872,11 +40872,21 @@ "description": "Console Variables", "type": "object", "properties": { - "_APP_DOMAIN_TARGET": { + "_APP_DOMAIN_TARGET_CNAME": { "type": "string", "description": "CNAME target for your Appwrite custom domains.", "x-example": "appwrite.io" }, + "_APP_DOMAIN_TARGET_A": { + "type": "string", + "description": "A target for your Appwrite custom domains.", + "x-example": "127.0.0.1" + }, + "_APP_DOMAIN_TARGET_AAAA": { + "type": "string", + "description": "AAAA target for your Appwrite custom domains.", + "x-example": "::1" + }, "_APP_STORAGE_LIMIT": { "type": "integer", "description": "Maximum file size allowed for file upload in bytes.", @@ -40931,7 +40941,9 @@ } }, "required": [ - "_APP_DOMAIN_TARGET", + "_APP_DOMAIN_TARGET_CNAME", + "_APP_DOMAIN_TARGET_A", + "_APP_DOMAIN_TARGET_AAAA", "_APP_STORAGE_LIMIT", "_APP_COMPUTE_SIZE_LIMIT", "_APP_USAGE_STATS", diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index 9903453e60..a8cb0d8eed 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -17792,7 +17792,7 @@ }, "\/sites\/{siteId}\/deployments\/template": { "post": { - "summary": "Create deployment", + "summary": "Create template deployment", "operationId": "sitesCreateTemplateDeployment", "tags": [ "sites" diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 2c1d094b63..09b3f5009f 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -25244,7 +25244,7 @@ "timeout": { "type": "integer", "description": "Maximum request time in seconds.", - "default": 15, + "default": 30, "x-example": 1 }, "installCommand": { @@ -25895,7 +25895,7 @@ "timeout": { "type": "integer", "description": "Maximum request time in seconds.", - "default": 15, + "default": 30, "x-example": 1 }, "installCommand": { @@ -26442,7 +26442,7 @@ }, "\/sites\/{siteId}\/deployments\/template": { "post": { - "summary": "Create deployment", + "summary": "Create template deployment", "operationId": "sitesCreateTemplateDeployment", "consumes": [ "application\/json" @@ -41531,11 +41531,21 @@ "description": "Console Variables", "type": "object", "properties": { - "_APP_DOMAIN_TARGET": { + "_APP_DOMAIN_TARGET_CNAME": { "type": "string", "description": "CNAME target for your Appwrite custom domains.", "x-example": "appwrite.io" }, + "_APP_DOMAIN_TARGET_A": { + "type": "string", + "description": "A target for your Appwrite custom domains.", + "x-example": "127.0.0.1" + }, + "_APP_DOMAIN_TARGET_AAAA": { + "type": "string", + "description": "AAAA target for your Appwrite custom domains.", + "x-example": "::1" + }, "_APP_STORAGE_LIMIT": { "type": "integer", "description": "Maximum file size allowed for file upload in bytes.", @@ -41590,7 +41600,9 @@ } }, "required": [ - "_APP_DOMAIN_TARGET", + "_APP_DOMAIN_TARGET_CNAME", + "_APP_DOMAIN_TARGET_A", + "_APP_DOMAIN_TARGET_AAAA", "_APP_STORAGE_LIMIT", "_APP_COMPUTE_SIZE_LIMIT", "_APP_USAGE_STATS", diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index c89973e2a3..2255731e10 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -17308,7 +17308,7 @@ "timeout": { "type": "integer", "description": "Maximum request time in seconds.", - "default": 15, + "default": 30, "x-example": 1 }, "installCommand": { @@ -17738,7 +17738,7 @@ "timeout": { "type": "integer", "description": "Maximum request time in seconds.", - "default": 15, + "default": 30, "x-example": 1 }, "installCommand": { @@ -18290,7 +18290,7 @@ }, "\/sites\/{siteId}\/deployments\/template": { "post": { - "summary": "Create deployment", + "summary": "Create template deployment", "operationId": "sitesCreateTemplateDeployment", "consumes": [ "application\/json" diff --git a/app/config/templates/site.php b/app/config/templates/site.php index 5aae737843..6989cc026d 100644 --- a/app/config/templates/site.php +++ b/app/config/templates/site.php @@ -116,6 +116,15 @@ const TEMPLATE_FRAMEWORKS = [ 'outputDirectory' => './dist/angular/browser', 'fallbackFile' => 'index.html', ], + 'ANALOG' => [ + 'key' => 'analog', + 'name' => 'Analog', + 'installCommand' => 'npm install', + 'buildCommand' => 'npm run build', + 'buildRuntime' => 'node-22', + 'adapter' => 'ssr', + 'outputDirectory' => './dist/analog', + ], 'VUE' => [ 'key' => 'vue', 'name' => 'Vue.js', @@ -581,6 +590,139 @@ return [ ], ] ], + [ + 'key' => 'starter-for-astro', + 'name' => 'Astro starter', + 'useCases' => [UseCases::STARTER], + 'tagline' => 'Simple Astro application integrated with Appwrite SDK.', + 'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible) + 'screenshotDark' => $url . '/images/sites/templates/starter-for-astro-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/starter-for-astro-light.png', + 'frameworks' => [ + getFramework('ASTRO', [ + 'providerRootDirectory' => './', + 'adapter' => 'static', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'starter-for-astro', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.1.*', + 'variables' => [ + [ + 'name' => 'PUBLIC_APPWRITE_ENDPOINT', + 'description' => 'Endpoint of Appwrite server', + 'value' => '{apiEndpoint}', + 'placeholder' => '{apiEndpoint}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'PUBLIC_APPWRITE_PROJECT_ID', + 'description' => 'Your Appwrite project ID', + 'value' => '{projectId}', + 'placeholder' => '{projectId}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'PUBLIC_APPWRITE_PROJECT_NAME', + 'description' => 'Your Appwrite project name', + 'value' => '{projectName}', + 'placeholder' => '{projectName}', + 'required' => true, + 'type' => 'text' + ], + ] + ], + [ + 'key' => 'starter-for-analog', + 'name' => 'Analog starter', + 'useCases' => [UseCases::STARTER], + 'tagline' => 'Simple Analog application integrated with Appwrite SDK.', + 'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible) + 'screenshotDark' => $url . '/images/sites/templates/starter-for-analog-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/starter-for-analog-light.png', + 'frameworks' => [ + getFramework('ANALOG', [ + 'providerRootDirectory' => './', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'starter-for-analog', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.1.*', + 'variables' => [ + [ + 'name' => 'VITE_APPWRITE_ENDPOINT', + 'description' => 'Endpoint of Appwrite server', + 'value' => '{apiEndpoint}', + 'placeholder' => '{apiEndpoint}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'VITE_APPWRITE_PROJECT_ID', + 'description' => 'Your Appwrite project ID', + 'value' => '{projectId}', + 'placeholder' => '{projectId}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'VITE_APPWRITE_PROJECT_NAME', + 'description' => 'Your Appwrite project name', + 'value' => '{projectName}', + 'placeholder' => '{projectName}', + 'required' => true, + 'type' => 'text' + ], + ] + ], + [ + 'key' => 'starter-for-remix', + 'name' => 'Remix starter', + 'useCases' => [UseCases::STARTER], + 'tagline' => 'Simple Remix application integrated with Appwrite SDK.', + 'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible) + 'screenshotDark' => $url . '/images/sites/templates/starter-for-remix-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/starter-for-remix-light.png', + 'frameworks' => [ + getFramework('REMIX', [ + 'providerRootDirectory' => './', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'starter-for-remix', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.1.*', + 'variables' => [ + [ + 'name' => 'VITE_APPWRITE_ENDPOINT', + 'description' => 'Endpoint of Appwrite server', + 'value' => '{apiEndpoint}', + 'placeholder' => '{apiEndpoint}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'VITE_APPWRITE_PROJECT_ID', + 'description' => 'Your Appwrite project ID', + 'value' => '{projectId}', + 'placeholder' => '{projectId}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'VITE_APPWRITE_PROJECT_NAME', + 'description' => 'Your Appwrite project name', + 'value' => '{projectName}', + 'placeholder' => '{projectName}', + 'required' => true, + 'type' => 'text' + ], + ] + ], [ 'key' => 'starter-for-svelte', 'name' => 'Svelte starter', @@ -966,6 +1108,7 @@ return [ 'providerVersion' => '0.1.*', 'variables' => [] ], + // TODO: Remove astro starter eventually, or add all frameworks's starters [ 'key' => 'astro-starter', 'name' => 'Astro starter', diff --git a/app/config/variables.php b/app/config/variables.php index 98dd9ffec1..d6516f7a54 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -90,13 +90,40 @@ return [ ], [ 'name' => '_APP_DOMAIN_TARGET', - 'description' => 'A DNS A record hostname to serve as a CNAME target for your Appwrite custom domains. You can use the same value as used for the Appwrite \'_APP_DOMAIN\' variable. The default value is \'localhost\'.', + 'description' => 'Deprecated since 1.7.0. A DNS A record hostname to serve as a CNAME target for your Appwrite custom domains. You can use the same value as used for the Appwrite \'_APP_DOMAIN\' variable. The default value is \'localhost\'.', 'introduction' => '', 'default' => 'localhost', 'required' => true, 'question' => 'Enter a DNS A record hostname to serve as a CNAME for your custom domains.' . PHP_EOL . 'You can use the same value as used for the Appwrite hostname.', 'filter' => 'domainTarget' ], + [ + 'name' => '_APP_DOMAIN_TARGET_CNAME', + 'description' => 'A domain that can be used as DNS CNAME record to point to instance of Appwrite server.', + 'introduction' => '', + 'default' => 'localhost', + 'required' => false, + 'question' => '', + 'filter' => '' + ], + [ + 'name' => '_APP_DOMAIN_TARGET_AAAA', + 'description' => 'An IPv6 that can be used as DNS AAAA record to point to instance of Appwrite server.', + 'introduction' => '', + 'default' => '::1', + 'required' => false, + 'question' => '', + 'filter' => '' + ], + [ + 'name' => '_APP_DOMAIN_TARGET_A', + 'description' => 'An IPV4 that can be used as DNS A record to point to instance of Appwrite server.', + 'introduction' => '', + 'default' => '127.0.0.1', + 'required' => false, + 'question' => '', + 'filter' => '' + ], [ 'name' => '_APP_CONSOLE_WHITELIST_ROOT', 'description' => 'This option allows you to disable the creation of new users on the Appwrite console. When enabled only 1 user will be able to use the registration form. New users can be added by inviting them to your project. By default this option is enabled.', @@ -1048,13 +1075,22 @@ return [ ], [ 'name' => '_APP_MAINTENANCE_RETENTION_AUDIT', - 'description' => 'IThe maximum duration (in seconds) upto which to retain audit logs. The default value is 1209600 seconds (14 days).', + 'description' => 'The maximum duration (in seconds) upto which to retain audit logs. The default value is 1209600 seconds (14 days).', 'introduction' => '0.7.0', 'default' => '1209600', 'required' => false, 'question' => '', 'filter' => '' ], + [ + 'name' => '_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', + 'description' => 'The maximum duration (in seconds) upto which to retain console audit logs. The default value is 15778800 seconds (6 months).', + 'introduction' => '1.6.2', + 'default' => '15778800', + 'required' => false, + 'question' => '', + 'filter' => '' + ], [ 'name' => '_APP_MAINTENANCE_RETENTION_ABUSE', 'description' => 'The maximum duration (in seconds) upto which to retain abuse logs. The default value is 86400 seconds (1 day).', diff --git a/app/controllers/api/console.php b/app/controllers/api/console.php index 4cf00faf65..4dd5432d3a 100644 --- a/app/controllers/api/console.php +++ b/app/controllers/api/console.php @@ -8,7 +8,9 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\App; use Utopia\Database\Document; +use Utopia\Domains\Domain; use Utopia\System\System; +use Utopia\Validator\IP; use Utopia\Validator\Text; App::init() @@ -40,10 +42,21 @@ App::get('/v1/console/variables') )) ->inject('response') ->action(function (Response $response) { - $isDomainEnabled = !empty(System::getEnv('_APP_DOMAIN', '')) - && !empty(System::getEnv('_APP_DOMAIN_TARGET', '')) - && System::getEnv('_APP_DOMAIN', '') !== 'localhost' - && System::getEnv('_APP_DOMAIN_TARGET', '') !== 'localhost'; + $validator = new Domain(System::getEnv('_APP_DOMAIN')); + $isDomainValid = !empty(System::getEnv('_APP_DOMAIN', '')) && $validator->isKnown() && !$validator->isTest(); + + $validator = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME')); + $isCNAMEValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')) && $validator->isKnown() && !$validator->isTest(); + + $validator = new IP(IP::V4); + $isAValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_A', '')) && ($validator->isValid(System::getEnv('_APP_DOMAIN_TARGET_A'))); + + $validator = new IP(IP::V6); + $isAAAAValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_AAAA', '')) && $validator->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA')); + + $isDomainEnabled = $isDomainValid && ( + $isAAAAValid || $isAValid || $isCNAMEValid + ); $isVcsEnabled = !empty(System::getEnv('_APP_VCS_GITHUB_APP_NAME', '')) && !empty(System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY', '')) @@ -54,7 +67,9 @@ App::get('/v1/console/variables') $isAssistantEnabled = !empty(System::getEnv('_APP_ASSISTANT_OPENAI_API_KEY', '')); $variables = new Document([ - '_APP_DOMAIN_TARGET' => System::getEnv('_APP_DOMAIN_TARGET'), + '_APP_DOMAIN_TARGET_CNAME' => System::getEnv('_APP_DOMAIN_TARGET_CNAME'), + '_APP_DOMAIN_TARGET_AAAA' => System::getEnv('_APP_DOMAIN_TARGET_AAAA'), + '_APP_DOMAIN_TARGET_A' => System::getEnv('_APP_DOMAIN_TARGET_A'), '_APP_STORAGE_LIMIT' => +System::getEnv('_APP_STORAGE_LIMIT'), '_APP_COMPUTE_SIZE_LIMIT' => +System::getEnv('_APP_COMPUTE_SIZE_LIMIT'), '_APP_USAGE_STATS' => System::getEnv('_APP_USAGE_STATS'), diff --git a/app/controllers/api/proxy.php b/app/controllers/api/proxy.php index e7aa3acb6c..d8e637725a 100644 --- a/app/controllers/api/proxy.php +++ b/app/controllers/api/proxy.php @@ -4,7 +4,7 @@ use Appwrite\Event\Certificate; use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Extend\Exception; -use Appwrite\Network\Validator\CNAME; +use Appwrite\Network\Validator\DNS; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; @@ -21,6 +21,8 @@ use Utopia\Database\Validator\UID; use Utopia\Domains\Domain; use Utopia\Logger\Log; use Utopia\System\System; +use Utopia\Validator\AnyOf; +use Utopia\Validator\IP; use Utopia\Validator\Text; App::get('/v1/proxy/rules') @@ -208,17 +210,27 @@ App::patch('/v1/proxy/rules/:ruleId/verification') throw new Exception(Exception::RULE_NOT_FOUND); } - $target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); + $validators = []; + $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')); + if (!$targetCNAME->isKnown() || $targetCNAME->isTest()) { + $validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME); + } + if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) { + $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A); + } + if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) { + $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); + } - if (!$target->isKnown() || $target->isTest()) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Domain target must be configured as environment variable.'); + if (empty($validators)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.'); } if ($rule->getAttribute('verification') === true) { return $response->dynamic($rule, Response::MODEL_PROXY_RULE); } - $validator = new CNAME($target->get()); // Verify Domain with DNS records + $validator = new AnyOf($validators, AnyOf::TYPE_STRING); $domain = new Domain($rule->getAttribute('domain', '')); $validationStart = \microtime(true); @@ -226,7 +238,14 @@ App::patch('/v1/proxy/rules/:ruleId/verification') $log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart)); $log->addTag('dnsDomain', $domain->get()); - $error = $validator->getLogs(); + $errors = []; + foreach ($validators as $validator) { + if (!empty($validator->getLogs())) { + $errors[] = $validator->getLogs(); + } + } + + $error = \implode("\n", $errors); $log->addExtra('dnsResponse', \is_array($error) ? \json_encode($error) : \strval($error)); throw new Exception(Exception::RULE_VERIFICATION_FAILED); diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index b45c9fd3b9..3e0e366b6b 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -615,7 +615,10 @@ App::post('/v1/teams/:teamId/memberships') $membership = ($isPrivilegedUser || $isAppUser) ? Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)) : $dbForProject->createDocument('memberships', $membership); - Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); + + if ($isPrivilegedUser || $isAppUser) { + Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); + } } elseif ($membership->getAttribute('confirm') === false) { $membership->setAttribute('secret', Auth::hash($secret)); @@ -1031,7 +1034,6 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') ->param('membershipId', '', new UID(), 'Membership ID.') ->param('roles', [], function (Document $project) { if ($project->getId() === 'console') { - ; $roles = array_keys(Config::getParam('roles', [])); array_filter($roles, function ($role) { return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]); @@ -1043,9 +1045,10 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') ->inject('request') ->inject('response') ->inject('user') + ->inject('project') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { + ->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) { $team = $dbForProject->getDocument('teams', $teamId); if ($team->isEmpty()) { @@ -1066,6 +1069,21 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') $isAppUser = Auth::isAppUser(Authorization::getRoles()); $isOwner = Authorization::isRole('team:' . $team->getId() . '/owner'); + if ($project->getId() === 'console') { + // Quick check: fetch up to 2 owners to determine if only one exists + $ownersCount = $dbForProject->count( + collection: 'memberships', + queries: [Query::contains('roles', ['owner'])], + max: 2 + ); + + // Prevent role change if there's only one owner left, + // the requester is that owner, and the new `$roles` no longer include 'owner'! + if ($ownersCount === 1 && $isOwner && !\in_array('owner', $roles)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'There must be at least one owner in the organization.'); + } + } + if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server) throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to modify roles'); } diff --git a/app/controllers/general.php b/app/controllers/general.php index 80097cc471..4ea341e393 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -56,7 +56,7 @@ Config::setParam('domainVerification', false); Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); -function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) +function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) { $host = $request->getHostname() ?? ''; if (!empty($previewHostname)) { @@ -455,7 +455,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } /** Execute function */ - $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); try { $version = match ($type) { 'function' => $resource->getAttribute('version', 'v2'), @@ -724,10 +723,11 @@ App::init() ->inject('queueForEvents') ->inject('queueForCertificates') ->inject('queueForFunctions') + ->inject('executor') ->inject('isResourceBlocked') ->inject('previewHostname') ->inject('apiKey') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) { + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, Executor $executor, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) { /* * Appwrite Router */ @@ -735,7 +735,7 @@ App::init() $mainDomain = System::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain || !empty($previewHostname)) { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); } } @@ -988,12 +988,13 @@ App::options() ->inject('queueForEvents') ->inject('queueForStatsUsage') ->inject('queueForFunctions') + ->inject('executor') ->inject('geodb') ->inject('isResourceBlocked') ->inject('previewHostname') ->inject('project') ->inject('apiKey') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname, Document $project, ?Key $apiKey) { + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, Document $project, ?Key $apiKey) { /* * Appwrite Router */ @@ -1001,7 +1002,7 @@ App::options() $mainDomain = System::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain || !empty($previewHostname)) { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); } } @@ -1118,17 +1119,18 @@ App::error() if (!empty($providerConfig) && $error->getCode() >= 400 && $error->getCode() < 500) { // Register error logger try { - $loggingProvider = new DSN($providerConfig ?? ''); + $loggingProvider = new DSN($providerConfig); $providerName = $loggingProvider->getScheme(); if (!empty($providerName) && $providerName === 'sentry') { $key = $loggingProvider->getPassword(); $projectId = $loggingProvider->getUser() ?? ''; $host = 'https://' . $loggingProvider->getHost(); + $sampleRate = $loggingProvider->getParam('sample', 0.01); $adapter = new Sentry($projectId, $key, $host); $logger = new Logger($adapter); - $logger->setSample(0.01); + $logger->setSample($sampleRate); $publish = true; } else { throw new \Exception('Invalid experimental logging provider'); @@ -1310,11 +1312,12 @@ App::get('/robots.txt') ->inject('queueForEvents') ->inject('queueForStatsUsage') ->inject('queueForFunctions') + ->inject('executor') ->inject('geodb') ->inject('isResourceBlocked') ->inject('previewHostname') ->inject('apiKey') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) { + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) { $host = $request->getHostname() ?? ''; $mainDomain = System::getEnv('_APP_DOMAIN', ''); @@ -1322,7 +1325,7 @@ App::get('/robots.txt') $template = new View(__DIR__ . '/../views/general/robots.phtml'); $response->text($template->render(false)); } else { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); } } @@ -1341,11 +1344,12 @@ App::get('/humans.txt') ->inject('queueForEvents') ->inject('queueForStatsUsage') ->inject('queueForFunctions') + ->inject('executor') ->inject('geodb') ->inject('isResourceBlocked') ->inject('previewHostname') ->inject('apiKey') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) { + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) { $host = $request->getHostname() ?? ''; $mainDomain = System::getEnv('_APP_DOMAIN', ''); @@ -1353,7 +1357,7 @@ App::get('/humans.txt') $template = new View(__DIR__ . '/../views/general/humans.phtml'); $response->text($template->render(false)); } else { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); } } diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 3f87de8fa5..4b780ce66a 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -399,7 +399,8 @@ App::init() ->inject('timelimit') ->inject('mode') ->inject('apiKey') - ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, string $mode, ?Key $apiKey) use ($usageDatabaseListener, $eventDatabaseListener) { + ->inject('plan') + ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, string $mode, ?Key $apiKey, array $plan) use ($usageDatabaseListener, $eventDatabaseListener) { $route = $utopia->getRoute(); @@ -529,6 +530,10 @@ App::init() $useCache = $route->getLabel('cache', false); if ($useCache) { + $route = $utopia->match($request); + $isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview'; + $isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !Auth::isPrivilegedUser(Authorization::getRoles()); + $key = md5($request->getURI() . '*' . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER); $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); $cache = new Cache( @@ -541,7 +546,7 @@ App::init() $parts = explode('/', $cacheLog->getAttribute('resourceType')); $type = $parts[0] ?? null; - if ($type === 'bucket') { + if ($type === 'bucket' && (!$isImageTransformation || !$isDisabled)) { $bucketId = $parts[1] ?? null; $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); @@ -582,8 +587,10 @@ App::init() $response ->addHeader('Cache-Control', sprintf('private, max-age=%d', $timestamp)) ->addHeader('X-Appwrite-Cache', 'hit') - ->setContentType($cacheLog->getAttribute('mimeType')) - ->send($data); + ->setContentType($cacheLog->getAttribute('mimeType')); + if (!$isImageTransformation || !$isDisabled) { + $response->send($data); + } } else { $response ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') diff --git a/app/init/resources.php b/app/init/resources.php index b5bfc3481e..99f90e065f 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -21,6 +21,7 @@ use Appwrite\Extend\Exception; use Appwrite\GraphQL\Schema; use Appwrite\Network\Validator\Origin; use Appwrite\Utopia\Request; +use Executor\Executor; use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis; use Utopia\App; use Utopia\Cache\Adapter\Sharding; @@ -38,6 +39,7 @@ use Utopia\Logger\Log; use Utopia\Pools\Group; use Utopia\Queue\Publisher; use Utopia\Storage\Device; +use Utopia\Storage\Device\AWS; use Utopia\Storage\Device\Backblaze; use Utopia\Storage\Device\DOSpaces; use Utopia\Storage\Device\Linode; @@ -544,7 +546,12 @@ function getDevice(string $root, string $connection = ''): Device switch ($device) { case Storage::DEVICE_S3: - return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl, $url); + if (!empty($url)) { + return new S3($root, $accessKey, $accessSecret, $url, $region, $acl); + } else { + return new AWS($root, $accessKey, $accessSecret, $bucket, $region, $acl); + } + // no break case STORAGE::DEVICE_DO_SPACES: $device = new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl); $device->setHttpVersion(S3::HTTP_VERSION_1_1); @@ -571,7 +578,12 @@ function getDevice(string $root, string $connection = ''): Device $s3Bucket = System::getEnv('_APP_STORAGE_S3_BUCKET', ''); $s3Acl = 'private'; $s3EndpointUrl = System::getEnv('_APP_STORAGE_S3_ENDPOINT', ''); - return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl, $s3EndpointUrl); + if (!empty($s3EndpointUrl)) { + return new S3($root, $s3AccessKey, $s3SecretKey, $s3EndpointUrl, $s3Region, $s3Acl); + } else { + return new AWS($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl); + } + // no break case Storage::DEVICE_DO_SPACES: $doSpacesAccessKey = System::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', ''); $doSpacesSecretKey = System::getEnv('_APP_STORAGE_DO_SPACES_SECRET', ''); @@ -834,3 +846,5 @@ App::setResource('apiKey', function (Request $request, Document $project): ?Key return Key::decode($project, $key); }, ['request', 'project']); + +App::setResource('executor', fn () => new Executor(fn (string $projectId, string $deploymentId) => System::getEnv('_APP_EXECUTOR_HOST'))); diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 523fee911a..bca4222376 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -134,7 +134,8 @@ $image = $this->getParam('image', ''); - _APP_STORAGE_WASABI_REGION - _APP_STORAGE_WASABI_BUCKET - _APP_COMPUTE_SIZE_LIMIT - - _APP_COMPUTE_TIMEOUT + - _APP_FUNCTIONS_TIMEOUT + - _APP_SITES_TIMEOUT - _APP_COMPUTE_BUILD_TIMEOUT - _APP_COMPUTE_CPUS - _APP_COMPUTE_MEMORY @@ -149,6 +150,7 @@ $image = $this->getParam('image', ''); - _APP_MAINTENANCE_RETENTION_CACHE - _APP_MAINTENANCE_RETENTION_ABUSE - _APP_MAINTENANCE_RETENTION_AUDIT + - _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE - _APP_MAINTENANCE_RETENTION_USAGE_HOURLY - _APP_MAINTENANCE_RETENTION_SCHEDULES - _APP_SMS_PROVIDER @@ -341,6 +343,7 @@ $image = $this->getParam('image', ''); - _APP_EXECUTOR_HOST - _APP_MAINTENANCE_RETENTION_ABUSE - _APP_MAINTENANCE_RETENTION_AUDIT + - _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE - _APP_MAINTENANCE_RETENTION_EXECUTION - _APP_SYSTEM_SECURITY_EMAIL_ADDRESS - _APP_EMAIL_CERTIFICATES @@ -404,7 +407,8 @@ $image = $this->getParam('image', ''); - _APP_VCS_GITHUB_APP_NAME - _APP_VCS_GITHUB_PRIVATE_KEY - _APP_VCS_GITHUB_APP_ID - - _APP_COMPUTE_TIMEOUT + - _APP_FUNCTIONS_TIMEOUT + - _APP_SITES_TIMEOUT - _APP_COMPUTE_BUILD_TIMEOUT - _APP_COMPUTE_CPUS - _APP_COMPUTE_MEMORY @@ -495,7 +499,8 @@ $image = $this->getParam('image', ''); - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_COMPUTE_TIMEOUT + - _APP_FUNCTIONS_TIMEOUT + - _APP_SITES_TIMEOUT - _APP_COMPUTE_BUILD_TIMEOUT - _APP_COMPUTE_CPUS - _APP_COMPUTE_MEMORY @@ -652,6 +657,7 @@ $image = $this->getParam('image', ''); - _APP_MAINTENANCE_RETENTION_CACHE - _APP_MAINTENANCE_RETENTION_ABUSE - _APP_MAINTENANCE_RETENTION_AUDIT + - _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE - _APP_MAINTENANCE_RETENTION_USAGE_HOURLY - _APP_MAINTENANCE_RETENTION_SCHEDULES diff --git a/app/worker.php b/app/worker.php index 9b13ff07b0..ca58456308 100644 --- a/app/worker.php +++ b/app/worker.php @@ -16,10 +16,9 @@ use Appwrite\Event\Migration; use Appwrite\Event\Realtime; use Appwrite\Event\StatsUsage; use Appwrite\Event\StatsUsageDump; -/** remove */ -/** /remove */ use Appwrite\Event\Webhook; use Appwrite\Platform\Appwrite; +use Executor\Executor; use Swoole\Runtime; use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis; use Utopia\Cache\Adapter\Sharding; @@ -215,15 +214,18 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache) { }, ['pools', 'cache']); Server::setResource('abuseRetention', function () { - return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); + return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day }); -Server::setResource('auditRetention', function () { - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); -}); +Server::setResource('auditRetention', function (Document $project) { + if ($project->getId() === 'console') { + return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months + } + 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 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); + return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days }); Server::setResource('cache', function (Registry $register) { @@ -416,6 +418,8 @@ Server::setResource('logError', function (Registry $register, Document $project) }; }, ['register', 'project']); +Server::setResource('executor', fn () => new Executor(fn (string $projectId, string $deploymentId) => System::getEnv('_APP_EXECUTOR_HOST'))); + $pools = $register->get('pools'); $platform = new Appwrite(); $args = $platform->getEnv('argv'); diff --git a/composer.json b/composer.json index 8650a8b387..48b5a48fdc 100644 --- a/composer.json +++ b/composer.json @@ -53,11 +53,11 @@ "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", "utopia-php/detector": "0.1.*", - "utopia-php/database": "0.61.*", + "utopia-php/database": "0.64.*", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", - "utopia-php/fetch": "0.3.*", + "utopia-php/fetch": "0.4.*", "utopia-php/image": "0.8.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.6.*", @@ -65,7 +65,7 @@ "utopia-php/migration": "0.8.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", - "utopia-php/pools": "0.7.*", + "utopia-php/pools": "0.8.*", "utopia-php/preloader": "0.2.*", "utopia-php/queue": "0.9.*", "utopia-php/registry": "0.5.*", @@ -74,7 +74,7 @@ "utopia-php/system": "0.9.*", "utopia-php/telemetry": "0.1.*", "utopia-php/vcs": "0.10.*", - "utopia-php/websocket": "0.1.*", + "utopia-php/websocket": "0.3.*", "matomo/device-detector": "6.1.*", "dragonmantank/cron-expression": "3.3.2", "phpmailer/phpmailer": "6.9.1", diff --git a/composer.lock b/composer.lock index a9585532f6..f151523ffc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e4281c45c1b60dade2ec8df58e6926d5", + "content-hash": "cac7679b9486588135dad678d9488f9e", "packages": [ { "name": "adhocore/jwt", @@ -751,66 +751,6 @@ }, "time": "2025-03-26T18:01:50+00:00" }, - { - "name": "jean85/pretty-package-versions", - "version": "2.1.1", - "source": { - "type": "git", - "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", - "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", - "shasum": "" - }, - "require": { - "composer-runtime-api": "^2.1.0", - "php": "^7.4|^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.2", - "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^7.5|^8.5|^9.6", - "rector/rector": "^2.0", - "vimeo/psalm": "^4.3 || ^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Jean85\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Alessandro Lai", - "email": "alessandro.lai85@gmail.com" - } - ], - "description": "A library to get pretty versions strings of installed dependencies", - "keywords": [ - "composer", - "package", - "release", - "versions" - ], - "support": { - "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" - }, - "time": "2025-03-19T14:43:43+00:00" - }, { "name": "league/csv", "version": "9.14.0", @@ -969,75 +909,6 @@ }, "time": "2023-10-02T10:01:54+00:00" }, - { - "name": "mongodb/mongodb", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/mongodb/mongo-php-library.git", - "reference": "b0bbd657f84219212487d01a8ffe93a789e1e488" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mongodb/mongo-php-library/zipball/b0bbd657f84219212487d01a8ffe93a789e1e488", - "reference": "b0bbd657f84219212487d01a8ffe93a789e1e488", - "shasum": "" - }, - "require": { - "ext-hash": "*", - "ext-json": "*", - "ext-mongodb": "^1.11.0", - "jean85/pretty-package-versions": "^1.2 || ^2.0.1", - "php": "^7.1 || ^8.0", - "symfony/polyfill-php80": "^1.19" - }, - "require-dev": { - "doctrine/coding-standard": "^9.0", - "squizlabs/php_codesniffer": "^3.6", - "symfony/phpunit-bridge": "^5.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10.x-dev" - } - }, - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "MongoDB\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Andreas Braun", - "email": "andreas.braun@mongodb.com" - }, - { - "name": "Jeremy Mikola", - "email": "jmikola@gmail.com" - } - ], - "description": "MongoDB driver library", - "homepage": "https://jira.mongodb.org/browse/PHPLIB", - "keywords": [ - "database", - "driver", - "mongodb", - "persistence" - ], - "support": { - "issues": "https://github.com/mongodb/mongo-php-library/issues", - "source": "https://github.com/mongodb/mongo-php-library/tree/1.10.0" - }, - "time": "2021-10-20T22:22:37+00:00" - }, { "name": "mustangostang/spyc", "version": "0.6.3", @@ -1494,16 +1365,16 @@ }, { "name": "open-telemetry/sdk", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "37eec0fe47ddd627911f318f29b6cd48196be0c0" + "reference": "0e7804c176c4b09d95b7985400aa38ce544cb7fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/37eec0fe47ddd627911f318f29b6cd48196be0c0", - "reference": "37eec0fe47ddd627911f318f29b6cd48196be0c0", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/0e7804c176c4b09d95b7985400aa38ce544cb7fc", + "reference": "0e7804c176c4b09d95b7985400aa38ce544cb7fc", "shasum": "" }, "require": { @@ -1580,7 +1451,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-01-29T21:40:28+00:00" + "time": "2025-04-08T09:55:41+00:00" }, { "name": "open-telemetry/sem-conv", @@ -2933,86 +2804,6 @@ ], "time": "2024-09-09T11:45:10+00:00" }, - { - "name": "symfony/polyfill-php80", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, { "name": "symfony/polyfill-php82", "version": "v1.31.0", @@ -3174,16 +2965,16 @@ }, { "name": "tbachert/spi", - "version": "v1.0.2", + "version": "v1.0.3", "source": { "type": "git", "url": "https://github.com/Nevay/spi.git", - "reference": "2ddfaf815dafb45791a61b08170de8d583c16062" + "reference": "506a79c98e1a51522e76ee921ccb6c62d52faf3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nevay/spi/zipball/2ddfaf815dafb45791a61b08170de8d583c16062", - "reference": "2ddfaf815dafb45791a61b08170de8d583c16062", + "url": "https://api.github.com/repos/Nevay/spi/zipball/506a79c98e1a51522e76ee921ccb6c62d52faf3a", + "reference": "506a79c98e1a51522e76ee921ccb6c62d52faf3a", "shasum": "" }, "require": { @@ -3220,9 +3011,9 @@ ], "support": { "issues": "https://github.com/Nevay/spi/issues", - "source": "https://github.com/Nevay/spi/tree/v1.0.2" + "source": "https://github.com/Nevay/spi/tree/v1.0.3" }, - "time": "2024-10-04T16:36:12+00:00" + "time": "2025-04-02T19:38:14+00:00" }, { "name": "thecodingmachine/safe", @@ -3706,16 +3497,16 @@ }, { "name": "utopia-php/database", - "version": "0.61.2", + "version": "0.64.2", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "349fbdf4bc088f7775c7dfb8b80239a617a88436" + "reference": "dc9c4a68c93e8bea2dfaa76d1ba308be539998bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/349fbdf4bc088f7775c7dfb8b80239a617a88436", - "reference": "349fbdf4bc088f7775c7dfb8b80239a617a88436", + "url": "https://api.github.com/repos/utopia-php/database/zipball/dc9c4a68c93e8bea2dfaa76d1ba308be539998bd", + "reference": "dc9c4a68c93e8bea2dfaa76d1ba308be539998bd", "shasum": "" }, "require": { @@ -3724,7 +3515,7 @@ "php": ">=8.1", "utopia-php/cache": "0.12.*", "utopia-php/framework": "0.33.*", - "utopia-php/mongo": "0.3.*" + "utopia-php/pools": "0.8.*" }, "require-dev": { "fakerphp/faker": "1.23.*", @@ -3756,22 +3547,22 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.61.2" + "source": "https://github.com/utopia-php/database/tree/0.64.2" }, - "time": "2025-03-15T11:47:42+00:00" + "time": "2025-04-09T07:53:05+00:00" }, { "name": "utopia-php/detector", - "version": "0.1.2", + "version": "0.1.4", "source": { "type": "git", "url": "https://github.com/utopia-php/detector.git", - "reference": "8617ea205738743ef3363ad95791139e82ae44d5" + "reference": "895a4147463965b5f9cbc083b764b6476f547879" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/detector/zipball/8617ea205738743ef3363ad95791139e82ae44d5", - "reference": "8617ea205738743ef3363ad95791139e82ae44d5", + "url": "https://api.github.com/repos/utopia-php/detector/zipball/895a4147463965b5f9cbc083b764b6476f547879", + "reference": "895a4147463965b5f9cbc083b764b6476f547879", "shasum": "" }, "require": { @@ -3801,9 +3592,9 @@ ], "support": { "issues": "https://github.com/utopia-php/detector/issues", - "source": "https://github.com/utopia-php/detector/tree/0.1.2" + "source": "https://github.com/utopia-php/detector/tree/0.1.4" }, - "time": "2025-03-11T13:01:25+00:00" + "time": "2025-04-09T11:50:45+00:00" }, { "name": "utopia-php/domains", @@ -3914,16 +3705,16 @@ }, { "name": "utopia-php/fetch", - "version": "0.3.1", + "version": "0.4.1", "source": { "type": "git", "url": "https://github.com/utopia-php/fetch.git", - "reference": "524dd50afa8c64670c4fb18f1df4db9b5bb4b3d0" + "reference": "65095dac14037db0c822fb5e209e5bd3187a0303" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/fetch/zipball/524dd50afa8c64670c4fb18f1df4db9b5bb4b3d0", - "reference": "524dd50afa8c64670c4fb18f1df4db9b5bb4b3d0", + "url": "https://api.github.com/repos/utopia-php/fetch/zipball/65095dac14037db0c822fb5e209e5bd3187a0303", + "reference": "65095dac14037db0c822fb5e209e5bd3187a0303", "shasum": "" }, "require": { @@ -3947,9 +3738,9 @@ "description": "A simple library that provides an interface for making HTTP Requests.", "support": { "issues": "https://github.com/utopia-php/fetch/issues", - "source": "https://github.com/utopia-php/fetch/tree/0.3.1" + "source": "https://github.com/utopia-php/fetch/tree/0.4.1" }, - "time": "2025-03-05T18:08:55+00:00" + "time": "2025-04-14T07:34:27+00:00" }, { "name": "utopia-php/framework", @@ -4000,16 +3791,16 @@ }, { "name": "utopia-php/image", - "version": "0.8.0", + "version": "0.8.2", "source": { "type": "git", "url": "https://github.com/utopia-php/image.git", - "reference": "dcae5b1c6deb3ff6865f4e68f012b3709c289bca" + "reference": "6c736965177f9a9e71311e22b80cfa88511768e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/image/zipball/dcae5b1c6deb3ff6865f4e68f012b3709c289bca", - "reference": "dcae5b1c6deb3ff6865f4e68f012b3709c289bca", + "url": "https://api.github.com/repos/utopia-php/image/zipball/6c736965177f9a9e71311e22b80cfa88511768e9", + "reference": "6c736965177f9a9e71311e22b80cfa88511768e9", "shasum": "" }, "require": { @@ -4043,9 +3834,9 @@ ], "support": { "issues": "https://github.com/utopia-php/image/issues", - "source": "https://github.com/utopia-php/image/tree/0.8.0" + "source": "https://github.com/utopia-php/image/tree/0.8.2" }, - "time": "2025-02-20T11:49:03+00:00" + "time": "2025-04-08T11:31:45+00:00" }, { "name": "utopia-php/locale", @@ -4205,16 +3996,16 @@ }, { "name": "utopia-php/migration", - "version": "0.8.4", + "version": "0.8.6", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "845fd04ccf5e0edb03c184b864e0596080a432b8" + "reference": "84163e16edc0b2e64c34ad7b7c4cc5f05d762daf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/845fd04ccf5e0edb03c184b864e0596080a432b8", - "reference": "845fd04ccf5e0edb03c184b864e0596080a432b8", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/84163e16edc0b2e64c34ad7b7c4cc5f05d762daf", + "reference": "84163e16edc0b2e64c34ad7b7c4cc5f05d762daf", "shasum": "" }, "require": { @@ -4255,69 +4046,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.8.4" + "source": "https://github.com/utopia-php/migration/tree/0.8.6" }, - "time": "2025-03-28T02:08:22+00:00" - }, - { - "name": "utopia-php/mongo", - "version": "0.3.1", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/mongo.git", - "reference": "52326a9a43e2d27ff0c15c48ba746dacbe9a7aee" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/mongo/zipball/52326a9a43e2d27ff0c15c48ba746dacbe9a7aee", - "reference": "52326a9a43e2d27ff0c15c48ba746dacbe9a7aee", - "shasum": "" - }, - "require": { - "ext-mongodb": "*", - "mongodb/mongodb": "1.10.0", - "php": ">=8.0" - }, - "require-dev": { - "fakerphp/faker": "^1.14", - "laravel/pint": "1.2.*", - "phpstan/phpstan": "1.8.*", - "phpunit/phpunit": "^9.4", - "swoole/ide-helper": "4.8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\Mongo\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Eldad Fux", - "email": "eldad@appwrite.io" - }, - { - "name": "Wess", - "email": "wess@appwrite.io" - } - ], - "description": "A simple library to manage Mongo database", - "keywords": [ - "database", - "mongo", - "php", - "upf", - "utopia" - ], - "support": { - "issues": "https://github.com/utopia-php/mongo/issues", - "source": "https://github.com/utopia-php/mongo/tree/0.3.1" - }, - "time": "2023-09-01T17:25:28+00:00" + "time": "2025-04-14T08:22:09+00:00" }, { "name": "utopia-php/orchestration", @@ -4421,16 +4152,16 @@ }, { "name": "utopia-php/pools", - "version": "0.7.0", + "version": "0.8.0", "source": { "type": "git", "url": "https://github.com/utopia-php/pools.git", - "reference": "ad64d45afda08ec8b29e2642a8d18075964d40bf" + "reference": "60733929dc328e7ea47e800579c8bbf0d49df5ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/pools/zipball/ad64d45afda08ec8b29e2642a8d18075964d40bf", - "reference": "ad64d45afda08ec8b29e2642a8d18075964d40bf", + "url": "https://api.github.com/repos/utopia-php/pools/zipball/60733929dc328e7ea47e800579c8bbf0d49df5ba", + "reference": "60733929dc328e7ea47e800579c8bbf0d49df5ba", "shasum": "" }, "require": { @@ -4467,9 +4198,9 @@ ], "support": { "issues": "https://github.com/utopia-php/pools/issues", - "source": "https://github.com/utopia-php/pools/tree/0.7.0" + "source": "https://github.com/utopia-php/pools/tree/0.8.0" }, - "time": "2025-03-18T03:55:33+00:00" + "time": "2025-03-19T10:22:03+00:00" }, { "name": "utopia-php/preloader", @@ -4526,23 +4257,23 @@ }, { "name": "utopia-php/queue", - "version": "0.9.0", + "version": "0.9.1", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "077075f1d57afa430f76c35ed3bf4616e0eee8e7" + "reference": "32b6f84c55aae761db5a5ae76cc91ca8dbc8bc32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/077075f1d57afa430f76c35ed3bf4616e0eee8e7", - "reference": "077075f1d57afa430f76c35ed3bf4616e0eee8e7", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/32b6f84c55aae761db5a5ae76cc91ca8dbc8bc32", + "reference": "32b6f84c55aae761db5a5ae76cc91ca8dbc8bc32", "shasum": "" }, "require": { "php": ">=8.3", "php-amqplib/php-amqplib": "^3.7", "utopia-php/cli": "0.15.*", - "utopia-php/fetch": "^0.3.0", + "utopia-php/fetch": "0.4.*", "utopia-php/framework": "0.33.*", "utopia-php/telemetry": "0.1.*" }, @@ -4585,9 +4316,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.9.0" + "source": "https://github.com/utopia-php/queue/tree/0.9.1" }, - "time": "2025-03-13T12:22:41+00:00" + "time": "2025-03-28T19:49:36+00:00" }, { "name": "utopia-php/registry", @@ -4906,27 +4637,28 @@ }, { "name": "utopia-php/websocket", - "version": "0.1.0", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/utopia-php/websocket.git", - "reference": "51fcb86171400d8aa40d76c54593481fd273dab5" + "reference": "629e53640b108eab43c7cc9ab375efade8622d43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/websocket/zipball/51fcb86171400d8aa40d76c54593481fd273dab5", - "reference": "51fcb86171400d8aa40d76c54593481fd273dab5", + "url": "https://api.github.com/repos/utopia-php/websocket/zipball/629e53640b108eab43c7cc9ab375efade8622d43", + "reference": "629e53640b108eab43c7cc9ab375efade8622d43", "shasum": "" }, "require": { "php": ">=8.0" }, "require-dev": { + "laravel/pint": "^1.15", + "phpstan/phpstan": "^1.12", "phpunit/phpunit": "^9.5.5", - "swoole/ide-helper": "4.6.6", + "swoole/ide-helper": "5.1.2", "textalk/websocket": "1.5.2", - "vimeo/psalm": "^4.8.1", - "workerman/workerman": "^4.0" + "workerman/workerman": "4.1.*" }, "type": "library", "autoload": { @@ -4938,16 +4670,6 @@ "license": [ "MIT" ], - "authors": [ - { - "name": "Eldad Fux", - "email": "eldad@appwrite.io" - }, - { - "name": "Torsten Dittmann", - "email": "torsten@appwrite.io" - } - ], "description": "A simple abstraction for WebSocket servers.", "keywords": [ "framework", @@ -4958,9 +4680,9 @@ ], "support": { "issues": "https://github.com/utopia-php/websocket/issues", - "source": "https://github.com/utopia-php/websocket/tree/0.1.0" + "source": "https://github.com/utopia-php/websocket/tree/0.3.0" }, - "time": "2021-12-20T10:50:09+00:00" + "time": "2025-03-28T01:11:13+00:00" }, { "name": "webmozart/assert", @@ -5363,16 +5085,16 @@ }, { "name": "laravel/pint", - "version": "v1.21.2", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "370772e7d9e9da087678a0edf2b11b6960e40558" + "reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/370772e7d9e9da087678a0edf2b11b6960e40558", - "reference": "370772e7d9e9da087678a0edf2b11b6960e40558", + "url": "https://api.github.com/repos/laravel/pint/zipball/7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36", + "reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36", "shasum": "" }, "require": { @@ -5383,9 +5105,9 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.72.0", + "friendsofphp/php-cs-fixer": "^3.75.0", "illuminate/view": "^11.44.2", - "larastan/larastan": "^3.2.0", + "larastan/larastan": "^3.3.1", "laravel-zero/framework": "^11.36.1", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.3", @@ -5425,7 +5147,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-03-14T22:31:42+00:00" + "time": "2025-04-08T22:11:45+00:00" }, { "name": "matthiasmullie/minify", diff --git a/docker-compose.yml b/docker-compose.yml index 276834ea35..bec3d8c88e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -115,7 +115,9 @@ services: - _APP_OPTIONS_COMPUTE_FORCE_HTTPS - _APP_OPENSSL_KEY_V1 - _APP_DOMAIN - - _APP_DOMAIN_TARGET + - _APP_DOMAIN_TARGET_CNAME + - _APP_DOMAIN_TARGET_AAAA + - _APP_DOMAIN_TARGET_A - _APP_DOMAIN_FUNCTIONS - _APP_REDIS_HOST - _APP_REDIS_PORT @@ -160,7 +162,8 @@ services: - _APP_STORAGE_WASABI_REGION - _APP_STORAGE_WASABI_BUCKET - _APP_COMPUTE_SIZE_LIMIT - - _APP_COMPUTE_TIMEOUT + - _APP_FUNCTIONS_TIMEOUT + - _APP_SITES_TIMEOUT - _APP_COMPUTE_BUILD_TIMEOUT - _APP_COMPUTE_CPUS - _APP_COMPUTE_MEMORY @@ -175,6 +178,7 @@ services: - _APP_MAINTENANCE_RETENTION_CACHE - _APP_MAINTENANCE_RETENTION_ABUSE - _APP_MAINTENANCE_RETENTION_AUDIT + - _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE - _APP_MAINTENANCE_RETENTION_USAGE_HOURLY - _APP_MAINTENANCE_RETENTION_SCHEDULES - _APP_SMS_PROVIDER @@ -201,11 +205,13 @@ services: - _APP_DATABASE_SHARED_TABLES_V1 - _APP_DATABASE_SHARED_NAMESPACE - _APP_FUNCTIONS_CREATION_ABUSE_LIMIT + extra_hosts: + - "host.docker.internal:host-gateway" appwrite-console: <<: *x-logging container_name: appwrite-console - image: appwrite/console:5.3.0-sites-rc.38 + image: appwrite/console:5.3.0-sites-rc.40 restart: unless-stopped networks: - appwrite @@ -394,6 +400,8 @@ services: - _APP_DATABASE_SHARED_TABLES - _APP_DATABASE_SHARED_TABLES_V1 - _APP_EMAIL_CERTIFICATES + - _APP_MAINTENANCE_RETENTION_AUDIT + - _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE appwrite-worker-databases: entrypoint: worker-databases @@ -462,7 +470,8 @@ services: - _APP_VCS_GITHUB_APP_NAME - _APP_VCS_GITHUB_PRIVATE_KEY - _APP_VCS_GITHUB_APP_ID - - _APP_COMPUTE_TIMEOUT + - _APP_FUNCTIONS_TIMEOUT + - _APP_SITES_TIMEOUT - _APP_COMPUTE_BUILD_TIMEOUT - _APP_COMPUTE_CPUS - _APP_COMPUTE_MEMORY @@ -494,6 +503,8 @@ services: - _APP_STORAGE_WASABI_BUCKET - _APP_DATABASE_SHARED_TABLES - _APP_DOMAIN_SITES + extra_hosts: + - "host.docker.internal:host-gateway" appwrite-worker-certificates: entrypoint: worker-certificates @@ -515,7 +526,9 @@ services: - _APP_WORKER_PER_CORE - _APP_OPENSSL_KEY_V1 - _APP_DOMAIN - - _APP_DOMAIN_TARGET + - _APP_DOMAIN_TARGET_CNAME + - _APP_DOMAIN_TARGET_AAAA + - _APP_DOMAIN_TARGET_A - _APP_DOMAIN_FUNCTIONS - _APP_EMAIL_CERTIFICATES - _APP_REDIS_HOST @@ -559,7 +572,8 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_COMPUTE_TIMEOUT + - _APP_FUNCTIONS_TIMEOUT + - _APP_SITES_TIMEOUT - _APP_COMPUTE_BUILD_TIMEOUT - _APP_COMPUTE_CPUS - _APP_COMPUTE_MEMORY @@ -680,7 +694,9 @@ services: - _APP_WORKER_PER_CORE - _APP_OPENSSL_KEY_V1 - _APP_DOMAIN - - _APP_DOMAIN_TARGET + - _APP_DOMAIN_TARGET_CNAME + - _APP_DOMAIN_TARGET_AAAA + - _APP_DOMAIN_TARGET_A - _APP_EMAIL_SECURITY - _APP_REDIS_HOST - _APP_REDIS_PORT @@ -712,7 +728,9 @@ services: - _APP_ENV - _APP_WORKER_PER_CORE - _APP_DOMAIN - - _APP_DOMAIN_TARGET + - _APP_DOMAIN_TARGET_CNAME + - _APP_DOMAIN_TARGET_AAAA + - _APP_DOMAIN_TARGET_A - _APP_DOMAIN_FUNCTIONS - _APP_OPENSSL_KEY_V1 - _APP_REDIS_HOST @@ -729,6 +747,7 @@ services: - _APP_MAINTENANCE_RETENTION_CACHE - _APP_MAINTENANCE_RETENTION_ABUSE - _APP_MAINTENANCE_RETENTION_AUDIT + - _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE - _APP_MAINTENANCE_RETENTION_USAGE_HOURLY - _APP_MAINTENANCE_RETENTION_SCHEDULES - _APP_MAINTENANCE_DELAY @@ -1144,4 +1163,4 @@ volumes: appwrite-functions: appwrite-sites: appwrite-builds: - appwrite-config: \ No newline at end of file + appwrite-config: diff --git a/public/images/sites/templates/starter-for-analog-dark.png b/public/images/sites/templates/starter-for-analog-dark.png new file mode 100644 index 0000000000..7529e08dd8 Binary files /dev/null and b/public/images/sites/templates/starter-for-analog-dark.png differ diff --git a/public/images/sites/templates/starter-for-analog-light.png b/public/images/sites/templates/starter-for-analog-light.png new file mode 100644 index 0000000000..7529e08dd8 Binary files /dev/null and b/public/images/sites/templates/starter-for-analog-light.png differ diff --git a/public/images/sites/templates/starter-for-astro-dark.png b/public/images/sites/templates/starter-for-astro-dark.png new file mode 100644 index 0000000000..5380b32b7d Binary files /dev/null and b/public/images/sites/templates/starter-for-astro-dark.png differ diff --git a/public/images/sites/templates/starter-for-astro-light.png b/public/images/sites/templates/starter-for-astro-light.png new file mode 100644 index 0000000000..5380b32b7d Binary files /dev/null and b/public/images/sites/templates/starter-for-astro-light.png differ diff --git a/public/images/sites/templates/starter-for-remix-dark.png b/public/images/sites/templates/starter-for-remix-dark.png new file mode 100644 index 0000000000..76e80dde71 Binary files /dev/null and b/public/images/sites/templates/starter-for-remix-dark.png differ diff --git a/public/images/sites/templates/starter-for-remix-light.png b/public/images/sites/templates/starter-for-remix-light.png new file mode 100644 index 0000000000..76e80dde71 Binary files /dev/null and b/public/images/sites/templates/starter-for-remix-light.png differ diff --git a/src/Appwrite/Migration/Version/V19.php b/src/Appwrite/Migration/Version/V19.php index 18234ebdc4..4415003bfd 100644 --- a/src/Appwrite/Migration/Version/V19.php +++ b/src/Appwrite/Migration/Version/V19.php @@ -10,7 +10,6 @@ use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception; use Utopia\Database\Query; -use Utopia\System\System; class V19 extends Migration { @@ -731,7 +730,7 @@ class V19 extends Migration if (empty($document->getAttribute('scheduleId', null))) { $schedule = $this->consoleDB->createDocument('schedules', new Document([ - 'region' => System::getEnv('_APP_REGION', 'default'), // Todo replace with projects region + 'region' => $project->getAttribute('region'), 'resourceType' => 'function', 'resourceId' => $document->getId(), 'resourceInternalId' => $document->getInternalId(), diff --git a/src/Appwrite/Network/Validator/CNAME.php b/src/Appwrite/Network/Validator/DNS.php similarity index 53% rename from src/Appwrite/Network/Validator/CNAME.php rename to src/Appwrite/Network/Validator/DNS.php index e1ae061c84..9fa78f360a 100644 --- a/src/Appwrite/Network/Validator/CNAME.php +++ b/src/Appwrite/Network/Validator/DNS.php @@ -4,24 +4,22 @@ namespace Appwrite\Network\Validator; use Utopia\Validator; -class CNAME extends Validator +class DNS extends Validator { + public const RECORD_A = 'a'; + public const RECORD_AAAA = 'aaaa'; + public const RECORD_CNAME = 'cname'; + /** * @var mixed */ protected mixed $logs; - /** - * @var string - */ - protected $target; - /** * @param string $target */ - public function __construct($target) + public function __construct(protected $target, protected string $type = self::RECORD_CNAME) { - $this->target = $target; } /** @@ -29,7 +27,7 @@ class CNAME extends Validator */ public function getDescription(): string { - return 'Invalid CNAME record'; + return 'Invalid DNS record'; } /** @@ -41,20 +39,34 @@ class CNAME extends Validator } /** - * Check if CNAME record target value matches selected target + * Check if DNS record value matches specific value * * @param mixed $domain * * @return bool */ - public function isValid($domain): bool + public function isValid($value): bool { - if (!is_string($domain)) { + $typeNative = match ($this->type) { + self::RECORD_A => DNS_A, + self::RECORD_AAAA => DNS_AAAA, + self::RECORD_CNAME => DNS_CNAME, + default => throw new \Exception('Record type not supported.') + }; + + $dnsKey = match ($this->type) { + self::RECORD_A => 'ip', + self::RECORD_AAAA => 'ipv6', + self::RECORD_CNAME => 'target', + default => throw new \Exception('Record type not supported.') + }; + + if (!is_string($value)) { return false; } try { - $records = \dns_get_record($domain, DNS_CNAME); + $records = \dns_get_record($value, $typeNative); $this->logs = $records; } catch (\Throwable $th) { return false; @@ -65,7 +77,7 @@ class CNAME extends Validator } foreach ($records as $record) { - if (isset($record['target']) && $record['target'] === $this->target) { + if (isset($record[$dnsKey]) && $record[$dnsKey] === $this->target) { return true; } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php index 053a10f982..a17fe3a29c 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php @@ -9,7 +9,6 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Executor\Executor; -use Utopia\App; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -58,6 +57,7 @@ class Update extends Action ->inject('dbForProject') ->inject('project') ->inject('queueForEvents') + ->inject('executor') ->callback([$this, 'action']); } @@ -67,7 +67,8 @@ class Update extends Action Response $response, Database $dbForProject, Document $project, - Event $queueForEvents + Event $queueForEvents, + Executor $executor ) { $function = $dbForProject->getDocument('functions', $functionId); @@ -101,7 +102,6 @@ class Update extends Action } try { - $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); $executor->deleteRuntime($project->getId(), $deploymentId . "-build"); } catch (\Throwable $th) { // Don't throw if the deployment doesn't exist diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index 7e89196a5b..b92e5c2784 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -93,6 +93,7 @@ class Create extends Base ->inject('queueForStatsUsage') ->inject('queueForFunctions') ->inject('geodb') + ->inject('executor') ->callback([$this, 'action']); } @@ -113,7 +114,8 @@ class Create extends Base Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, - Reader $geodb + Reader $geodb, + Executor $executor ) { $async = \strval($async) === 'true' || \strval($async) === '1'; @@ -381,7 +383,6 @@ class Create extends Base ]); /** Execute function */ - $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); try { $version = $function->getAttribute('version', 'v2'); $command = $runtime['startCommand']; diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index cf31d3d7d2..9436833dad 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -74,7 +74,7 @@ class Create extends Base ->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true) ->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true) ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) - ->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_COMPUTE_TIMEOUT', 900)), 'Function maximum execution time in seconds.', true) + ->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.', true) ->param('enabled', true, new Boolean(), 'Is function enabled? When set to \'disabled\', users cannot access the function but Server SDKs with and API key can still access the function. No data is lost when this is toggled.', true) ->param('logging', true, new Boolean(), 'When disabled, executions will exclude logs and errors, and will be slightly faster.', true) ->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index 3c876ed91e..3e4399f8db 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -78,7 +78,7 @@ class Update extends Base ->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true) ->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true) ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) - ->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_COMPUTE_TIMEOUT', 900)), 'Maximum execution time in seconds.', true) + ->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Maximum execution time in seconds.', true) ->param('enabled', true, new Boolean(), 'Is function enabled? When set to \'disabled\', users cannot access the function but Server SDKs with and API key can still access the function. No data is lost when this is toggled.', true) ->param('logging', true, new Boolean(), 'When disabled, executions will exclude logs and errors, and will be slightly faster.', true) ->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true) @@ -103,6 +103,7 @@ class Update extends Base ->inject('queueForBuilds') ->inject('dbForPlatform') ->inject('gitHub') + ->inject('executor') ->callback([$this, 'action']); } @@ -132,7 +133,8 @@ class Update extends Base Event $queueForEvents, Build $queueForBuilds, Database $dbForPlatform, - GitHub $github + GitHub $github, + Executor $executor ) { // TODO: If only branch changes, re-deploy $function = $dbForProject->getDocument('functions', $functionId); @@ -234,7 +236,6 @@ class Update extends Base // Enforce Cold Start if spec limits change. if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deploymentId'))) { - $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); try { $executor->deleteRuntime($project->getId(), $function->getAttribute('deploymentId')); } catch (\Throwable $th) { diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index a4951e3525..7e429defca 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -72,6 +72,7 @@ class Builds extends Action ->inject('isResourceBlocked') ->inject('deviceForFiles') ->inject('log') + ->inject('executor') ->callback([$this, 'action']); } @@ -90,6 +91,7 @@ class Builds extends Action * @param Device $deviceForSites * @param Device $deviceForFiles * @param Log $log + * @param Executor $executor * @return void * @throws \Utopia\Database\Exception */ @@ -108,7 +110,8 @@ class Builds extends Action Device $deviceForSites, callable $isResourceBlocked, Device $deviceForFiles, - Log $log + Log $log, + Executor $executor ): void { $payload = $message->getPayload() ?? []; @@ -146,7 +149,8 @@ class Builds extends Action $deployment, $template, $isResourceBlocked, - $log + $log, + $executor ); break; @@ -172,6 +176,7 @@ class Builds extends Action * @param Document $deployment * @param Document $template * @param Log $log + * @param Executor $executor * @return void * @throws \Utopia\Database\Exception * @@ -194,7 +199,8 @@ class Builds extends Action Document $deployment, Document $template, callable $isResourceBlocked, - Log $log + Log $log, + Executor $executor ): void { $resourceKey = match ($resource->getCollection()) { 'functions' => 'functionId', @@ -207,8 +213,6 @@ class Builds extends Action 'functions' => $deviceForFunctions, }; - $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); - $log->addTag($resourceKey, $resource->getId()); $resource = $dbForProject->getDocument($resource->getCollection(), $resource->getId()); @@ -561,6 +565,10 @@ class Builds extends Action // Some runtimes/frameworks can't compile with less memory than this $minMemory = $resource->getCollection() === 'sites' ? 2048 : 1024; + if ($resource->getAttribute('framework', '') === 'analog') { + $minMemory = 4096; + } + $cpus = $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT; $memory = max($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, $minMemory); $timeout = (int) System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT', 900); @@ -800,7 +808,7 @@ class Builds extends Action if ($resource->getCollection() === 'sites') { $date = \date('H:i:s'); - $logs .= "[$date] [appwrite] Screenshot capturing started. \n"; + $logs .= "[$date] [appwrite] Screenshot capturing started. \n"; } $deployment->setAttribute('buildLogs', $logs); @@ -940,7 +948,7 @@ class Builds extends Action $logs = $deployment->getAttribute('buildLogs', ''); $date = \date('H:i:s'); - $logs .= "[$date] [appwrite] Screenshot capturing finished. \n"; + $logs .= "[$date] [appwrite] Screenshot capturing finished. \n"; $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php index 50c56b7ea3..b6349c3aea 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php @@ -5,12 +5,11 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\API; use Appwrite\Event\Certificate; use Appwrite\Event\Event; use Appwrite\Extend\Exception; -use Appwrite\Network\Validator\CNAME; +use Appwrite\Network\Validator\DNS; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; @@ -19,7 +18,9 @@ use Utopia\Domains\Domain; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; +use Utopia\Validator\AnyOf; use Utopia\Validator\Domain as ValidatorDomain; +use Utopia\Validator\IP; class Create extends Action { @@ -95,13 +96,6 @@ class Create extends Action throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); } - // Apex domain prevention due to CNAME limitations - if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) { - if ($domain->get() === $domain->getRegisterable()) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.'); - } - } - // TODO: @christyjacob remove once we migrate the rules in 1.7.x $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(); @@ -110,8 +104,23 @@ class Create extends Action $status = 'verified'; } if ($status === 'created') { - $target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); - $validator = new CNAME($target->get()); + $validators = []; + $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')); + if (!$targetCNAME->isKnown() || $targetCNAME->isTest()) { + $validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME); + } + if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) { + $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A); + } + if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) { + $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); + } + + if (empty($validators)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.'); + } + + $validator = new AnyOf($validators, AnyOf::TYPE_STRING); if ($validator->isValid($domain->get())) { $status = 'verifying'; } diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php index 890eb00dec..a85a4fa063 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php @@ -5,12 +5,11 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Function; use Appwrite\Event\Certificate; use Appwrite\Event\Event; use Appwrite\Extend\Exception; -use Appwrite\Network\Validator\CNAME; +use Appwrite\Network\Validator\DNS; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; @@ -20,7 +19,9 @@ use Utopia\Domains\Domain; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; +use Utopia\Validator\AnyOf; use Utopia\Validator\Domain as ValidatorDomain; +use Utopia\Validator\IP; use Utopia\Validator\Text; class Create extends Action @@ -96,13 +97,6 @@ class Create extends Action throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); } - // Apex domain prevention due to CNAME limitations - if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) { - if ($domain->get() === $domain->getRegisterable()) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.'); - } - } - $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); @@ -118,8 +112,23 @@ class Create extends Action $status = 'verified'; } if ($status === 'created') { - $target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); - $validator = new CNAME($target->get()); + $validators = []; + $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')); + if (!$targetCNAME->isKnown() || $targetCNAME->isTest()) { + $validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME); + } + if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) { + $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A); + } + if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) { + $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); + } + + if (empty($validators)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.'); + } + + $validator = new AnyOf($validators, AnyOf::TYPE_STRING); if ($validator->isValid($domain->get())) { $status = 'verifying'; } diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php index 5e6f69af4b..58271ef08e 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php @@ -5,12 +5,11 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect; use Appwrite\Event\Certificate; use Appwrite\Event\Event; use Appwrite\Extend\Exception; -use Appwrite\Network\Validator\CNAME; +use Appwrite\Network\Validator\DNS; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; @@ -19,7 +18,9 @@ use Utopia\Domains\Domain; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; +use Utopia\Validator\AnyOf; use Utopia\Validator\Domain as ValidatorDomain; +use Utopia\Validator\IP; use Utopia\Validator\URL; use Utopia\Validator\WhiteList; @@ -95,13 +96,6 @@ class Create extends Action throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); } - // Apex domain prevention due to CNAME limitations - if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) { - if ($domain->get() === $domain->getRegisterable()) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.'); - } - } - // TODO: @christyjacob remove once we migrate the rules in 1.7.x $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(); @@ -110,8 +104,23 @@ class Create extends Action $status = 'verified'; } if ($status === 'created') { - $dnsTarget = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); - $validator = new CNAME($dnsTarget->get()); + $validators = []; + $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')); + if (!$targetCNAME->isKnown() || $targetCNAME->isTest()) { + $validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME); + } + if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) { + $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A); + } + if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) { + $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); + } + + if (empty($validators)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.'); + } + + $validator = new AnyOf($validators, AnyOf::TYPE_STRING); if ($validator->isValid($domain->get())) { $status = 'verifying'; } diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php index 4506f10dce..1a0cbe7fdc 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php @@ -5,12 +5,11 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Site; use Appwrite\Event\Certificate; use Appwrite\Event\Event; use Appwrite\Extend\Exception; -use Appwrite\Network\Validator\CNAME; +use Appwrite\Network\Validator\DNS; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; @@ -20,7 +19,9 @@ use Utopia\Domains\Domain; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; +use Utopia\Validator\AnyOf; use Utopia\Validator\Domain as ValidatorDomain; +use Utopia\Validator\IP; use Utopia\Validator\Text; class Create extends Action @@ -96,13 +97,6 @@ class Create extends Action throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); } - // Apex domain prevention due to CNAME limitations - if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) { - if ($domain->get() === $domain->getRegisterable()) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.'); - } - } - $site = $dbForProject->getDocument('sites', $siteId); if ($site->isEmpty()) { throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); @@ -118,8 +112,23 @@ class Create extends Action $status = 'verified'; } if ($status === 'created') { - $target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); - $validator = new CNAME($target->get()); + $validators = []; + $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')); + if (!$targetCNAME->isKnown() || $targetCNAME->isTest()) { + $validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME); + } + if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) { + $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A); + } + if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) { + $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); + } + + if (empty($validators)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.'); + } + + $validator = new AnyOf($validators, AnyOf::TYPE_STRING); if ($validator->isValid($domain->get())) { $status = 'verifying'; } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php index 1970fa898e..6fd2d77f6b 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php @@ -9,7 +9,6 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Executor\Executor; -use Utopia\App; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -56,6 +55,7 @@ class Update extends Action ->inject('dbForProject') ->inject('project') ->inject('queueForEvents') + ->inject('executor') ->callback([$this, 'action']); } @@ -65,7 +65,8 @@ class Update extends Action Response $response, Database $dbForProject, Document $project, - Event $queueForEvents + Event $queueForEvents, + Executor $executor ) { $site = $dbForProject->getDocument('sites', $siteId); @@ -99,7 +100,6 @@ class Update extends Action } try { - $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); $executor->deleteRuntime($project->getId(), $deploymentId . "-build"); } catch (\Throwable $th) { // Don't throw if the deployment doesn't exist diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php index 1a39edea46..c91ec449be 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php @@ -66,7 +66,7 @@ class Create extends Base ->param('framework', '', new WhiteList(\array_keys(Config::getParam('frameworks')), true), 'Sites framework.') ->param('enabled', true, new Boolean(), 'Is site enabled? When set to \'disabled\', users cannot access the site but Server SDKs with and API key can still access the site. No data is lost when this is toggled.', true) ->param('logging', true, new Boolean(), 'When disabled, request logs will exclude logs and errors, and site responses will be slightly faster.', true) - ->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_COMPUTE_TIMEOUT', 900)), 'Maximum request time in seconds.', true) + ->param('timeout', 30, new Range(1, (int) System::getEnv('_APP_SITES_TIMEOUT', 30)), 'Maximum request time in seconds.', true) ->param('installCommand', '', new Text(8192, 0), 'Install Command.', true) ->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true) ->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true) diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php index 416062862a..de220e174c 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php @@ -70,7 +70,7 @@ class Update extends Base ->param('framework', '', new WhiteList(\array_keys(Config::getParam('frameworks')), true), 'Sites framework.') ->param('enabled', true, new Boolean(), 'Is site enabled? When set to \'disabled\', users cannot access the site but Server SDKs with and API key can still access the site. No data is lost when this is toggled.', true) ->param('logging', true, new Boolean(), 'When disabled, request logs will exclude logs and errors, and site responses will be slightly faster.', true) - ->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_COMPUTE_TIMEOUT', 900)), 'Maximum request time in seconds.', true) + ->param('timeout', 30, new Range(1, (int) System::getEnv('_APP_SITES_TIMEOUT', 30)), 'Maximum request time in seconds.', true) ->param('installCommand', '', new Text(8192, 0), 'Install Command.', true) ->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true) ->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true) @@ -96,6 +96,7 @@ class Update extends Base ->inject('queueForBuilds') ->inject('dbForPlatform') ->inject('gitHub') + ->inject('executor') ->callback([$this, 'action']); } @@ -125,7 +126,8 @@ class Update extends Base Event $queueForEvents, Build $queueForBuilds, Database $dbForPlatform, - GitHub $github + GitHub $github, + Executor $executor ) { if (!empty($adapter)) { $configFramework = Config::getParam('frameworks')[$framework] ?? []; @@ -233,7 +235,6 @@ class Update extends Base // Enforce Cold Start if spec limits change. if ($site->getAttribute('specification') !== $specification && !empty($site->getAttribute('deploymentId'))) { - $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); try { $executor->deleteRuntime($project->getId(), $site->getAttribute('deploymentId')); } catch (\Throwable $th) { diff --git a/src/Appwrite/Platform/Tasks/Doctor.php b/src/Appwrite/Platform/Tasks/Doctor.php index a9d0a6220c..329248eae2 100644 --- a/src/Appwrite/Platform/Tasks/Doctor.php +++ b/src/Appwrite/Platform/Tasks/Doctor.php @@ -15,6 +15,7 @@ use Utopia\Registry\Registry; use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; use Utopia\System\System; +use Utopia\Validator\IP; class Doctor extends Action { @@ -43,19 +44,31 @@ class Doctor extends Action Console::log('[Settings]'); $domain = new Domain(System::getEnv('_APP_DOMAIN')); - if (!$domain->isKnown() || $domain->isTest()) { - Console::log('🔴 Hostname has no public suffix (' . $domain->get() . ')'); + Console::log('🔴 Hostname is not valid (' . $domain->get() . ')'); } else { - Console::log('🟢 Hostname has a public suffix (' . $domain->get() . ')'); + Console::log('🟢 Hostname is valid (' . $domain->get() . ')'); } - $domain = new Domain(System::getEnv('_APP_DOMAIN_TARGET')); - + $domain = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME')); if (!$domain->isKnown() || $domain->isTest()) { - Console::log('🔴 CNAME target has no public suffix (' . $domain->get() . ')'); + Console::log('🔴 CNAME record target is not valid (' . $domain->get() . ')'); } else { - Console::log('🟢 CNAME target has a public suffix (' . $domain->get() . ')'); + Console::log('🟢 CNAME record target is valid (' . $domain->get() . ')'); + } + + $ipv4 = new IP(IP::V4); + if (!$ipv4->isValid(System::getEnv('_APP_DOMAIN_TARGET_A'))) { + Console::log('🔴 A record target is not valid (' . System::getEnv('_APP_DOMAIN_TARGET_A') . ')'); + } else { + Console::log('🟢 A record target is valid (' . System::getEnv('_APP_DOMAIN_TARGET_A') . ')'); + } + + $ipv6 = new IP(IP::V6); + if (!$ipv6->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA'))) { + Console::log('🔴 AAAA record target is not valid (' . System::getEnv('_APP_DOMAIN_TARGET_AAAA') . ')'); + } else { + Console::log('🟢 AAAA record target is valid (' . System::getEnv('_APP_DOMAIN_TARGET_AAAA') . ')'); } if (System::getEnv('_APP_OPENSSL_KEY_V1') === 'your-secret-key' || empty(System::getEnv('_APP_OPENSSL_KEY_V1'))) { diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 2c082a7ce4..303e03cb8c 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -103,8 +103,15 @@ abstract class ScheduleBase extends Action $paginationQueries[] = Query::cursorAfter($latestDocument); } + // Temporarly accepting both 'fra' and 'default' + // When all migrated, only use _APP_REGION with 'default' as default value + $regions = [System::getEnv('_APP_REGION', 'default')]; + if (!in_array('default', $regions)) { + $regions[] = 'default'; + } + $results = $dbForPlatform->find('schedules', \array_merge($paginationQueries, [ - Query::equal('region', [System::getEnv('_APP_REGION', 'default')]), + Query::equal('region', $regions), Query::equal('resourceType', [static::getSupportedResource()]), Query::equal('active', [true]), ])); @@ -153,8 +160,15 @@ abstract class ScheduleBase extends Action $paginationQueries[] = Query::cursorAfter($latestDocument); } + // Temporarly accepting both 'fra' and 'default' + // When all migrated, only use _APP_REGION with 'default' as default value + $regions = [System::getEnv('_APP_REGION', 'default')]; + if (!in_array('default', $regions)) { + $regions[] = 'default'; + } + $results = $dbForPlatform->find('schedules', \array_merge($paginationQueries, [ - Query::equal('region', [System::getEnv('_APP_REGION', 'default')]), + Query::equal('region', $regions), Query::equal('resourceType', [static::getSupportedResource()]), Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate), ])); diff --git a/src/Appwrite/Platform/Tasks/Screenshot.php b/src/Appwrite/Platform/Tasks/Screenshot.php index 918ce60e11..79593ab39b 100644 --- a/src/Appwrite/Platform/Tasks/Screenshot.php +++ b/src/Appwrite/Platform/Tasks/Screenshot.php @@ -147,7 +147,7 @@ class Screenshot extends Action 'installCommand' => $framework['installCommand'] ?? '', 'outputDirectory' => $framework['outputDirectory'] ?? '', 'providerRootDirectory' => $framework['providerRootDirectory'], - 'timeout' => 60 + 'timeout' => 30 ]); if ($site['headers']['status-code'] !== 201) { diff --git a/src/Appwrite/Platform/Workers/Audits.php b/src/Appwrite/Platform/Workers/Audits.php index ed5ff8010a..76309145b8 100644 --- a/src/Appwrite/Platform/Workers/Audits.php +++ b/src/Appwrite/Platform/Workers/Audits.php @@ -74,7 +74,11 @@ class Audits extends Action Console::info('Aggregating audit logs'); $event = $payload['event'] ?? ''; - $auditPayload = $payload['payload'] ?? ''; + + $auditPayload = ''; + if ($project->getId() === 'console') { + $auditPayload = $payload['payload'] ?? ''; + } $mode = $payload['mode'] ?? ''; $resource = $payload['resource'] ?? ''; $userAgent = $payload['userAgent'] ?? ''; diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index 712196fc72..15f9645bb0 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -8,7 +8,7 @@ use Appwrite\Event\Func; use Appwrite\Event\Mail; use Appwrite\Event\Realtime; use Appwrite\Event\Webhook; -use Appwrite\Network\Validator\CNAME; +use Appwrite\Network\Validator\DNS; use Appwrite\Template\Template; use Appwrite\Utopia\Response\Model\Rule; use Exception; @@ -28,6 +28,8 @@ use Utopia\Logger\Log; use Utopia\Platform\Action; use Utopia\Queue\Message; use Utopia\System\System; +use Utopia\Validator\AnyOf; +use Utopia\Validator\IP; class Certificates extends Action { @@ -290,22 +292,39 @@ class Certificates extends Action } if (!$isMainDomain) { - // TODO: Would be awesome to also support A/AAAA records here. Maybe dry run? - // Validate if domain target is properly configured - $target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); + $validationStart = \microtime(true); - if (!$target->isKnown() || $target->isTest()) { - throw new Exception('Unreachable CNAME target (' . $target->get() . '), please use a domain with a public suffix.'); + $validators = []; + $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')); + if (!$targetCNAME->isKnown() || $targetCNAME->isTest()) { + $validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME); + } + if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) { + $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A); + } + if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) { + $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA); + } + + // Validate if domain target is properly configured + if (empty($validators)) { + throw new Exception('At least one of domain targets environment variable must be configured.'); } // Verify domain with DNS records - $validationStart = \microtime(true); - $validator = new CNAME($target->get()); + $validator = new AnyOf($validators, AnyOf::TYPE_STRING); if (!$validator->isValid($domain->get())) { $log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart)); $log->addTag('dnsDomain', $domain->get()); - $error = $validator->getLogs(); + $errors = []; + foreach ($validators as $validator) { + if (!empty($validator->getLogs())) { + $errors[] = $validator->getLogs(); + } + } + + $error = \implode("\n", $errors); $log->addExtra('dnsResponse', \is_array($error) ? \json_encode($error) : \strval($error)); throw new Exception('Failed to verify domain DNS records.'); diff --git a/src/Appwrite/Platform/Workers/Databases.php b/src/Appwrite/Platform/Workers/Databases.php index 010973519c..91d4681340 100644 --- a/src/Appwrite/Platform/Workers/Databases.php +++ b/src/Appwrite/Platform/Workers/Databases.php @@ -572,7 +572,8 @@ class Databases extends Action try { $documents = $database->deleteDocuments($collectionId, $queries); } catch (\Throwable $th) { - Console::error('Failed to delete documents for collection ' . $collectionId . ': ' . $th->getMessage()); + $tenant = $database->getSharedTables() ? 'Tenant:'.$database->getTenant() : ''; + Console::error("Failed to delete documents for collection:{$database->getNamespace()}_{$collectionId} {$tenant} :{$th->getMessage()}"); return; } diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index d22cdaee3b..c9e2de3ede 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -32,6 +32,8 @@ use Utopia\System\System; class Deletes extends Action { + protected array $selects = ['$internalId', '$id', '$collection', '$permissions', '$updatedAt']; + public static function getName(): string { return 'deletes'; @@ -55,6 +57,7 @@ class Deletes extends Action ->inject('deviceForBuilds') ->inject('deviceForCache') ->inject('certificates') + ->inject('executor') ->inject('executionRetention') ->inject('auditRetention') ->inject('log') @@ -77,6 +80,7 @@ class Deletes extends Action Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, + Executor $executor, string $executionRetention, string $auditRetention, Log $log @@ -107,10 +111,10 @@ class Deletes extends Action $this->deleteSite($dbForPlatform, $getProjectDB, $deviceForSites, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project); break; case DELETE_TYPE_FUNCTIONS: - $this->deleteFunction($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $certificates, $document, $project); + $this->deleteFunction($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $certificates, $document, $project, $executor); break; case DELETE_TYPE_DEPLOYMENTS: - $this->deleteDeployment($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForSites, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project); + $this->deleteDeployment($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForSites, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project, $executor); break; case DELETE_TYPE_USERS: $this->deleteUser($getProjectDB, $document, $project); @@ -196,10 +200,17 @@ class Deletes extends Action */ private function deleteSchedules(Database $dbForPlatform, callable $getProjectDB, string $datetime): void { + // Temporarly accepting both 'fra' and 'default' + // When all migrated, only use _APP_REGION with 'default' as default value + $regions = [System::getEnv('_APP_REGION', 'default')]; + if (!in_array('default', $regions)) { + $regions[] = 'default'; + } + $this->listByGroup( 'schedules', [ - Query::equal('region', [System::getEnv('_APP_REGION', 'default')]), + Query::equal('region', $regions), Query::lessThanEqual('resourceUpdatedAt', $datetime), Query::equal('active', [false]), ], @@ -264,7 +275,8 @@ class Deletes extends Action $this->deleteByGroup( 'subscribers', [ - Query::equal('topicInternalId', [$topic->getInternalId()]) + Query::equal('topicInternalId', [$topic->getInternalId()]), + Query::orderAsc(), ], $getProjectDB($project) ); @@ -285,7 +297,8 @@ class Deletes extends Action $this->deleteByGroup( 'subscribers', [ - Query::equal('targetInternalId', [$target->getInternalId()]) + Query::equal('targetInternalId', [$target->getInternalId()]), + Query::orderAsc(), ], $dbForProject, function (Document $subscriber) use ($dbForProject, $target) { @@ -322,7 +335,8 @@ class Deletes extends Action $this->deleteByGroup( 'targets', [ - Query::equal('expired', [true]) + Query::equal('expired', [true]), + Query::orderAsc(), ], $getProjectDB($project), function (Document $target) use ($getProjectDB, $project) { @@ -336,7 +350,8 @@ class Deletes extends Action $this->deleteByGroup( 'targets', [ - Query::equal('sessionInternalId', [$session->getInternalId()]) + Query::equal('sessionInternalId', [$session->getInternalId()]), + Query::orderAsc(), ], $getProjectDB($project), function (Document $target) use ($getProjectDB, $project) { @@ -363,14 +378,20 @@ class Deletes extends Action new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId) ); - $query[] = Query::equal('resource', [$resource]); + $queries = [ + Query::equal('resource', [$resource]) + ]; + if (!empty($resourceType)) { - $query[] = Query::equal('resourceType', [$resourceType]); + $queries[] = Query::equal('resourceType', [$resourceType]); } + $queries[] = Query::select($this->selects); + $queries[] = Query::orderAsc(); + $this->deleteByGroup( 'cache', - $query, + $queries, $dbForProject, function (Document $document) use ($cache, $projectId) { $path = APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId . DIRECTORY_SEPARATOR . $document->getId(); @@ -401,15 +422,16 @@ class Deletes extends Action new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId) ); - $query = [ + $queries = [ + Query::select([...$this->selects, 'accessedAt']), Query::lessThan('accessedAt', $datetime), Query::orderDesc('accessedAt'), - Query::orderDesc('$internalId'), + Query::orderDesc(), ]; $this->deleteByGroup( 'cache', - $query, + $queries, $dbForProject, function (Document $document) use ($cache, $projectId) { $path = APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId . DIRECTORY_SEPARATOR . $document->getId(); @@ -435,12 +457,15 @@ class Deletes extends Action /** @var Database $dbForProject*/ $dbForProject = $getProjectDB($project); + $selects = [...$this->selects, 'time']; + // Delete Usage stats from projectDB $this->deleteByGroup('stats', [ + Query::select($selects), + Query::equal('period', ['1h']), Query::lessThan('time', $hourlyUsageRetentionDatetime), Query::orderDesc('time'), - Query::orderDesc('$internalId'), - Query::equal('period', ['1h']), + Query::orderDesc(), ], $dbForProject); if ($project->getId() !== 'console') { @@ -449,10 +474,11 @@ class Deletes extends Action // Delete Usage stats from logsDB $this->deleteByGroup('stats', [ + Query::select($selects), + Query::equal('period', ['1h']), Query::lessThan('time', $hourlyUsageRetentionDatetime), Query::orderDesc('time'), - Query::orderDesc('$internalId'), - Query::equal('period', ['1h']), + Query::orderDesc(), ], $dbForLogs); } } @@ -473,7 +499,8 @@ class Deletes extends Action $this->deleteByGroup( 'memberships', [ - Query::equal('teamInternalId', [$teamInternalId]) + Query::equal('teamInternalId', [$teamInternalId]), + Query::orderAsc() ], $dbForProject, function (Document $membership) use ($dbForProject) { @@ -564,7 +591,13 @@ class Deletes extends Action if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) { $dbForProject->deleteCollection($collection->getId()); } else { - $this->deleteByGroup($collection->getId(), [], database: $dbForProject); + $this->deleteByGroup( + $collection->getId(), + [ + Query::orderAsc() + ], + database: $dbForProject + ); } } catch (Throwable $e) { Console::error('Error deleting '.$collection->getId().' '.$e->getMessage()); @@ -584,58 +617,78 @@ class Deletes extends Action // Delete Platforms $this->deleteByGroup('platforms', [ - Query::equal('projectInternalId', [$projectInternalId]) + Query::equal('projectInternalId', [$projectInternalId]), + Query::orderAsc() ], $dbForPlatform); // Delete project and function rules $this->deleteByGroup('rules', [ - Query::equal('projectInternalId', [$projectInternalId]) + Query::equal('projectInternalId', [$projectInternalId]), + Query::orderAsc() ], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) { $this->deleteRule($dbForPlatform, $document, $certificates); }); // Delete Keys $this->deleteByGroup('keys', [ - Query::equal('projectInternalId', [$projectInternalId]) + Query::equal('projectInternalId', [$projectInternalId]), + Query::orderAsc() ], $dbForPlatform); // Delete Webhooks $this->deleteByGroup('webhooks', [ - Query::equal('projectInternalId', [$projectInternalId]) + Query::equal('projectInternalId', [$projectInternalId]), + Query::orderAsc() ], $dbForPlatform); // Delete VCS Installations $this->deleteByGroup('installations', [ - Query::equal('projectInternalId', [$projectInternalId]) + Query::equal('projectInternalId', [$projectInternalId]), + Query::orderAsc() ], $dbForPlatform); // Delete VCS Repositories $this->deleteByGroup('repositories', [ Query::equal('projectInternalId', [$projectInternalId]), + Query::orderAsc() ], $dbForPlatform); // Delete VCS comments $this->deleteByGroup('vcsComments', [ Query::equal('projectInternalId', [$projectInternalId]), + Query::orderAsc() ], $dbForPlatform); - // Delete Schedules (No projectInternalId in this collection) + // Delete Schedules $this->deleteByGroup('schedules', [ Query::equal('projectId', [$projectId]), + Query::orderAsc() ], $dbForPlatform); // Delete metadata table if ($projectTables) { $dbForProject->deleteCollection(Database::METADATA); } elseif ($sharedTablesV1) { - $this->deleteByGroup(Database::METADATA, [], $dbForProject); + $this->deleteByGroup( + Database::METADATA, + [ + Query::orderAsc() + ], + $dbForProject + ); } elseif ($sharedTablesV2) { $queries = \array_map( fn ($id) => Query::notEqual('$id', $id), $projectCollectionIds ); - $this->deleteByGroup(Database::METADATA, $queries, $dbForProject); + $queries[] = Query::orderAsc(); + + $this->deleteByGroup( + Database::METADATA, + $queries, + $dbForProject + ); } // Delete all storage directories @@ -661,14 +714,16 @@ class Deletes extends Action // Delete all sessions of this user from the sessions table and update the sessions field of the user record $this->deleteByGroup('sessions', [ - Query::equal('userInternalId', [$userInternalId]) + Query::equal('userInternalId', [$userInternalId]), + Query::orderAsc() ], $dbForProject); $dbForProject->purgeCachedDocument('users', $userId); // Delete Memberships and decrement team membership counts $this->deleteByGroup('memberships', [ - Query::equal('userInternalId', [$userInternalId]) + Query::equal('userInternalId', [$userInternalId]), + Query::orderAsc() ], $dbForProject, function (Document $document) use ($dbForProject) { if ($document->getAttribute('confirm')) { // Count only confirmed members $teamId = $document->getAttribute('teamId'); @@ -681,19 +736,22 @@ class Deletes extends Action // Delete tokens $this->deleteByGroup('tokens', [ - Query::equal('userInternalId', [$userInternalId]) + Query::equal('userInternalId', [$userInternalId]), + Query::orderAsc() ], $dbForProject); // Delete identities $this->deleteByGroup('identities', [ - Query::equal('userInternalId', [$userInternalId]) + Query::equal('userInternalId', [$userInternalId]), + Query::orderAsc() ], $dbForProject); // Delete targets $this->deleteByGroup( 'targets', [ - Query::equal('userInternalId', [$userInternalId]) + Query::equal('userInternalId', [$userInternalId]), + Query::orderAsc() ], $dbForProject, function (Document $target) use ($getProjectDB, $project) { @@ -715,9 +773,10 @@ class Deletes extends Action // Delete Executions $this->deleteByGroup('executions', [ + Query::select([...$this->selects, '$createdAt']), Query::lessThan('$createdAt', $datetime), Query::orderDesc('$createdAt'), - Query::orderDesc('$internalId'), + Query::orderDesc(), ], $dbForProject); } @@ -735,9 +794,10 @@ class Deletes extends Action // Delete Sessions $this->deleteByGroup('sessions', [ + Query::select([...$this->selects, '$createdAt']), Query::lessThan('$createdAt', $expired), Query::orderDesc('$createdAt'), - Query::orderDesc('$internalId'), + Query::orderDesc(), ], $dbForProject); } @@ -753,14 +813,14 @@ class Deletes extends Action $this->deleteByGroup('realtime', [ Query::lessThan('timestamp', $datetime), Query::orderDesc('timestamp'), - Query::orderDesc('$internalId'), + Query::orderAsc(), ], $dbForPlatform); } /** * @param Database $dbForPlatform * @param callable $getProjectDB - * @param string $datetime + * @param string $auditRetention * @return void * @throws Exception */ @@ -771,9 +831,10 @@ class Deletes extends Action try { $this->deleteByGroup(Audit::COLLECTION, [ + Query::select([...$this->selects, 'time']), Query::lessThan('time', $auditRetention), Query::orderDesc('time'), - Query::orderDesc('$internalId'), + Query::orderAsc(), ], $dbForProject); } catch (DatabaseException $e) { Console::error('Failed to delete audit logs for project ' . $projectId . ': ' . $e->getMessage()); @@ -833,18 +894,6 @@ class Deletes extends Action $this->deleteDeploymentScreenshots($deviceForFiles, $dbForPlatform, $document); }); - /** - * Delete builds - */ - Console::info("Deleting builds for site " . $siteId); - foreach ($deploymentInternalIds as $deploymentInternalId) { - $this->deleteByGroup('builds', [ - Query::equal('deploymentInternalId', [$deploymentInternalId]) - ], $dbForProject, function (Document $document) use ($deviceForBuilds) { - $this->deleteBuildFiles($deviceForBuilds, $document); - }); - } - /** * Delete VCS Repositories and VCS Comments */ @@ -869,10 +918,11 @@ class Deletes extends Action * @param Device $deviceForBuilds * @param Document $document function document * @param Document $project + * @param Executor $executor * @return void * @throws Exception */ - private function deleteFunction(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, CertificatesAdapter $certificates, Document $document, Document $project): void + private function deleteFunction(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, CertificatesAdapter $certificates, Document $document, Document $project, Executor $executor): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); @@ -887,7 +937,8 @@ class Deletes extends Action Query::equal('type', ['deployment']), Query::equal('deploymentResourceType', ['function']), Query::equal('deploymentResourceInternalId', [$functionInternalId]), - Query::equal('projectInternalId', [$project->getInternalId()]) + Query::equal('projectInternalId', [$project->getInternalId()]), + Query::orderAsc() ], $dbForPlatform, function (Document $document) use ($project, $dbForPlatform, $certificates) { $this->deleteRule($dbForPlatform, $document, $certificates); }); @@ -897,8 +948,9 @@ class Deletes extends Action */ Console::info("Deleting variables for function " . $functionId); $this->deleteByGroup('variables', [ + Query::equal('resourceInternalId', [$functionInternalId]), Query::equal('resourceType', ['function']), - Query::equal('resourceInternalId', [$functionInternalId]) + Query::orderAsc() ], $dbForProject); /** @@ -908,7 +960,8 @@ class Deletes extends Action $deploymentInternalIds = []; $this->deleteByGroup('deployments', [ - Query::equal('resourceInternalId', [$functionInternalId]) + Query::equal('resourceInternalId', [$functionInternalId]), + Query::orderAsc() ], $dbForProject, function (Document $document) use ($dbForPlatform, $project, $certificates, $deviceForFunctions, $deviceForBuilds, &$deploymentInternalIds) { $deploymentInternalIds[] = $document->getInternalId(); $this->deleteDeploymentFiles($deviceForFunctions, $document); @@ -920,7 +973,9 @@ class Deletes extends Action */ Console::info("Deleting executions for function " . $functionId); $this->deleteByGroup('executions', [ - Query::equal('functionInternalId', [$functionInternalId]) + Query::select($this->selects), + Query::equal('functionInternalId', [$functionInternalId]), + Query::orderAsc() ], $dbForProject); /** @@ -931,12 +986,15 @@ class Deletes extends Action Query::equal('projectInternalId', [$project->getInternalId()]), Query::equal('resourceInternalId', [$functionInternalId]), Query::equal('resourceType', ['function']), + Query::orderAsc() ], $dbForPlatform, function (Document $document) use ($dbForPlatform) { $providerRepositoryId = $document->getAttribute('providerRepositoryId', ''); $projectInternalId = $document->getAttribute('projectInternalId', ''); + $this->deleteByGroup('vcsComments', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::equal('projectInternalId', [$projectInternalId]), + Query::orderAsc() ], $dbForPlatform); }); @@ -944,7 +1002,7 @@ class Deletes extends Action * Request executor to delete all deployment containers */ Console::info("Requesting executor to delete all deployment containers for function " . $functionId); - $this->deleteRuntimes($getProjectDB, $document, $project); + $this->deleteRuntimes($getProjectDB, $document, $project, $executor); } private function deleteDeploymentScreenshots(Device $deviceForFiles, Database $dbForPlatform, Document $deployment): void @@ -1063,10 +1121,11 @@ class Deletes extends Action * @param Device $deviceForBuilds * @param Document $document * @param Document $project + * @param Executor $executor * @return void * @throws Exception */ - private function deleteDeployment(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForSites, Device $deviceForBuilds, Device $deviceForFiles, Document $document, CertificatesAdapter $certificates, Document $project): void + private function deleteDeployment(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForSites, Device $deviceForBuilds, Device $deviceForFiles, Document $document, CertificatesAdapter $certificates, Document $project, Executor $executor): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); @@ -1109,7 +1168,7 @@ class Deletes extends Action * Request executor to delete all deployment containers */ Console::info("Requesting executor to delete deployment container for deployment " . $deploymentId); - $this->deleteRuntimes($getProjectDB, $document, $project); + $this->deleteRuntimes($getProjectDB, $document, $project, $executor); } /** @@ -1128,10 +1187,15 @@ class Deletes extends Action ): void { $start = \microtime(true); + /** + * deleteDocuments uses a cursor, we need to add a unique order by field or use default + */ + try { $documents = $database->deleteDocuments($collection, $queries); } catch (Throwable $th) { - Console::error('Failed to delete documents for collection ' . $collection . ': ' . $th->getMessage()); + $tenant = $database->getSharedTables() ? 'Tenant:'.$database->getTenant() : ''; + Console::error("Failed to delete documents for collection:{$database->getNamespace()}_{$collection} {$tenant} :{$th->getMessage()}"); return; } @@ -1260,13 +1324,12 @@ class Deletes extends Action * @param callable $getProjectDB * @param ?Document $function * @param Document $project + * @param Executor $executor * @return void * @throws Exception */ - private function deleteRuntimes(callable $getProjectDB, ?Document $function, Document $project): void + private function deleteRuntimes(callable $getProjectDB, ?Document $function, Document $project, Executor $executor): void { - $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); - $deleteByFunction = function (Document $function) use ($getProjectDB, $project, $executor) { $this->listByGroup( 'deployments', diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index 99dbea43b2..e81a180c42 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -51,6 +51,7 @@ class Functions extends Action ->inject('queueForEvents') ->inject('queueForStatsUsage') ->inject('log') + ->inject('executor') ->inject('isResourceBlocked') ->callback([$this, 'action']); } @@ -65,6 +66,7 @@ class Functions extends Action Event $queueForEvents, StatsUsage $queueForStatsUsage, Log $log, + Executor $executor, callable $isResourceBlocked ): void { $payload = $message->getPayload() ?? []; @@ -156,6 +158,7 @@ class Functions extends Action queueForEvents: $queueForEvents, project: $project, function: $function, + executor: $executor, trigger: 'event', path: '/', method: 'POST', @@ -198,6 +201,7 @@ class Functions extends Action queueForEvents: $queueForEvents, project: $project, function: $function, + executor: $executor, trigger: 'http', path: $path, method: $method, @@ -222,6 +226,7 @@ class Functions extends Action queueForEvents: $queueForEvents, project: $project, function: $function, + executor: $executor, trigger: 'schedule', path: $path, method: $method, @@ -308,6 +313,7 @@ class Functions extends Action * @param Event $queueForEvents * @param Document $project * @param Document $function + * @param Executor $executor * @param string $trigger * @param string $path * @param string $method @@ -334,6 +340,7 @@ class Functions extends Action Event $queueForEvents, Document $project, Document $function, + Executor $executor, string $trigger, string $path, string $method, @@ -512,7 +519,6 @@ class Functions extends Action try { $version = $function->getAttribute('version', 'v2'); $command = $runtime['startCommand']; - $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); $command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"'; $executionResponse = $executor->createExecution( projectId: $project->getId(), diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Base.php b/src/Appwrite/Utopia/Database/Validator/Queries/Base.php index b8cff64214..e8eafba5a0 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Base.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Base.php @@ -71,12 +71,18 @@ class Base extends Queries 'array' => false, ]); + $internalId = new Document([ + 'key' => '$internalId', + 'type' => Database::VAR_STRING, + 'array' => false, + ]); + $validators = [ new Limit(), new Offset(), new Cursor(), new Filter($attributes, APP_DATABASE_QUERY_MAX_VALUES), - new Order($attributes), + new Order([...$attributes, $internalId]), ]; parent::__construct($validators); diff --git a/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php b/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php index 57f287fc78..97dae2efcd 100644 --- a/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php +++ b/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php @@ -10,12 +10,24 @@ class ConsoleVariables extends Model public function __construct() { $this - ->addRule('_APP_DOMAIN_TARGET', [ - 'type' => self::TYPE_STRING, - 'description' => 'CNAME target for your Appwrite custom domains.', - 'default' => '', - 'example' => 'appwrite.io', - ]) + ->addRule('_APP_DOMAIN_TARGET_CNAME', [ + 'type' => self::TYPE_STRING, + 'description' => 'CNAME target for your Appwrite custom domains.', + 'default' => '', + 'example' => 'appwrite.io', + ]) + ->addRule('_APP_DOMAIN_TARGET_A', [ + 'type' => self::TYPE_STRING, + 'description' => 'A target for your Appwrite custom domains.', + 'default' => '', + 'example' => '127.0.0.1', + ]) + ->addRule('_APP_DOMAIN_TARGET_AAAA', [ + 'type' => self::TYPE_STRING, + 'description' => 'AAAA target for your Appwrite custom domains.', + 'default' => '', + 'example' => '::1', + ]) ->addRule('_APP_STORAGE_LIMIT', [ 'type' => self::TYPE_INTEGER, 'description' => 'Maximum file size allowed for file upload in bytes.', diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index c0596fd272..c381ceeae3 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -21,17 +21,19 @@ class Executor private bool $selfSigned = false; - private string $endpoint; + /** + * @var callable(string, string): string $endpoint + */ + private $endpointSelector; protected array $headers; - public function __construct(string $endpoint) + /** + * @param callable(string, string): string $endpointSelector + */ + public function __construct(callable $endpointSelector) { - if (!filter_var($endpoint, FILTER_VALIDATE_URL)) { - throw new Exception('Unsupported endpoint'); - } - - $this->endpoint = $endpoint; + $this->endpointSelector = $endpointSelector; $this->headers = [ 'content-type' => 'application/json', 'authorization' => 'Bearer ' . System::getEnv('_APP_EXECUTOR_SECRET', ''), @@ -94,7 +96,8 @@ class Executor 'outputDirectory' => $outputDirectory ]; - $response = $this->call(self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout); + $endpoint = $this->selectEndpoint($projectId, $deploymentId); + $response = $this->call($endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout); $status = $response['headers']['status-code']; if ($status >= 400) { @@ -124,7 +127,8 @@ class Executor 'timeout' => $timeout ]; - $this->call(self::METHOD_GET, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout, $callback); + $endpoint = $this->selectEndpoint($projectId, $deploymentId); + $this->call($endpoint, self::METHOD_GET, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout, $callback); } /** @@ -140,7 +144,8 @@ class Executor $runtimeId = "$projectId-$deploymentId" . $suffix; $route = "/runtimes/$runtimeId"; - $response = $this->call(self::METHOD_DELETE, $route, [ + $endpoint = $this->selectEndpoint($projectId, $deploymentId); + $response = $this->call($endpoint, self::METHOD_DELETE, $route, [ 'x-opr-addressing-method' => 'broadcast' ], [], true, 30); @@ -233,7 +238,8 @@ class Executor $requestTimeout = $timeout + 15; } - $response = $this->call(self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId, 'content-type' => 'multipart/form-data', 'accept' => 'multipart/form-data' ], $params, true, $requestTimeout); + $endpoint = $this->selectEndpoint($projectId, $deploymentId); + $response = $this->call($endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId, 'content-type' => 'multipart/form-data', 'accept' => 'multipart/form-data' ], $params, true, $requestTimeout); $status = $response['headers']['status-code']; if ($status >= 400) { @@ -241,7 +247,11 @@ class Executor throw new \Exception($message, $status); } - $response['body']['headers'] = \json_decode($response['body']['headers'] ?? '{}', true); + $headers = $response['body']['headers'] ?? []; + if (is_string($headers)) { + $headers = \json_decode($headers, true); + } + $response['body']['headers'] = $headers; $response['body']['statusCode'] = \intval($response['body']['statusCode'] ?? 500); $response['body']['duration'] = \floatval($response['body']['duration'] ?? 0); $response['body']['startTime'] = \floatval($response['body']['startTime'] ?? \microtime(true)); @@ -263,7 +273,8 @@ class Executor 'timeout' => $timeout ]; - $response = $this->call(self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout); + $endpoint = $this->selectEndpoint($projectId, $deploymentId); + $response = $this->call($endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout); $status = $response['headers']['status-code']; if ($status >= 400) { @@ -287,10 +298,10 @@ class Executor * @return array|string * @throws Exception */ - public function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true, int $timeout = 15, callable $callback = null) + private function call(string $endpoint, string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true, int $timeout = 15, callable $callback = null) { $headers = array_merge($this->headers, $headers); - $ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : '')); + $ch = curl_init($endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : '')); $responseHeaders = []; $responseStatus = -1; $responseType = ''; @@ -453,4 +464,9 @@ class Executor return $output; } + + private function selectEndpoint(string $projectId, string $deploymentId): string + { + return call_user_func($this->endpointSelector, $projectId, $deploymentId); + } } diff --git a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php index a24ec148bf..6059cb2000 100644 --- a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php +++ b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php @@ -24,11 +24,12 @@ class ConsoleConsoleClientTest extends Scope ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertCount(11, $response['body']); - $this->assertIsString($response['body']['_APP_DOMAIN_TARGET']); + $this->assertCount(13, $response['body']); + $this->assertIsString($response['body']['_APP_DOMAIN_TARGET_CNAME']); + $this->assertIsString($response['body']['_APP_DOMAIN_TARGET_A']); + $this->assertIsString($response['body']['_APP_DOMAIN_TARGET_AAAA']); $this->assertIsInt($response['body']['_APP_STORAGE_LIMIT']); $this->assertIsInt($response['body']['_APP_COMPUTE_SIZE_LIMIT']); - $this->assertIsString($response['body']['_APP_DOMAIN_TARGET']); $this->assertIsBool($response['body']['_APP_DOMAIN_ENABLED']); $this->assertIsBool($response['body']['_APP_VCS_ENABLED']); $this->assertIsBool($response['body']['_APP_ASSISTANT_ENABLED']); diff --git a/tests/e2e/Services/Proxy/ProxyCustomServerTest.php b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php index 6312840095..0ac2d9c186 100644 --- a/tests/e2e/Services/Proxy/ProxyCustomServerTest.php +++ b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php @@ -53,8 +53,10 @@ class ProxyCustomServerTest extends Scope public function testCreateRuleApex(): void { - $rule = $this->createAPIRule('myapp.com'); - $this->assertEquals(400, $rule['headers']['status-code']); + $domain = \uniqid() . '.com'; + $rule = $this->createAPIRule($domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); } public function testCreateRuleVcs(): void @@ -337,7 +339,7 @@ class ProxyCustomServerTest extends Scope $this->cleanupRule($rule['body']['$id']); // Create + update - $domain = \uniqid() . '-cname-api.custom.localhost'; + $domain = \uniqid() . '-cname-api.custom.com'; $rule = $this->createAPIRule($domain); $this->assertEquals(201, $rule['headers']['status-code']); diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 43993c7b13..db61767ffa 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -9,6 +9,7 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; +use Utopia\CLI\Console; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; @@ -2611,7 +2612,102 @@ class SitesCustomServerTest extends Scope ]); $this->assertEquals(400, $response['headers']['status-code']); $this->assertStringContainsString("Deployment build failed", $response['body']); + + $this->cleanupSite($siteId); + } + + public function testEmptySiteSource(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Empty source site', + 'framework' => 'other', + 'buildRuntime' => 'node-22', + 'outputDirectory' => './', + ]); + $this->assertNotEmpty($siteId); + + // Prepare empty site folder + // We cant use .gitkeep, because that would make deployment non-empty + $stdout = ''; + $stderr = ''; + $folderPath = realpath(__DIR__ . '/../../../resources/sites') . '/empty'; + Console::execute("mkdir -p $folderPath", '', $stdout, $stderr); + + $deployment = $this->createDeployment($siteId, [ + 'code' => $this->packageSite('empty'), + 'activate' => true + ]); + $this->assertEquals(202, $deployment['headers']['status-code']); + + $deploymentId = $deployment['body']['$id']; + $this->assertNotEmpty($deploymentId); + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $deployment = $this->getDeployment($siteId, $deploymentId); + $this->assertEquals('failed', $deployment['body']['status'], 'Deployment status does not match: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); + $this->assertStringContainsString('Error:', $deployment['body']['buildLogs'], 'Deployment logs do not match: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); $this->cleanupSite($siteId); } + + public function testOutputDirectoryEmpty(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Empty output directory', + 'framework' => 'other', + 'buildRuntime' => 'node-22', + 'outputDirectory' => './empty-directory', + 'buildCommand' => 'mkdir -p ./empty-directory' + ]); + $this->assertNotEmpty($siteId); + + $deployment = $this->createDeployment($siteId, [ + 'code' => $this->packageSite('static'), + 'activate' => true + ]); + $this->assertEquals(202, $deployment['headers']['status-code']); + + $deploymentId = $deployment['body']['$id']; + $this->assertNotEmpty($deploymentId); + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $deployment = $this->getDeployment($siteId, $deploymentId); + $this->assertEquals('failed', $deployment['body']['status'], 'Deployment status does not match: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); + $this->assertStringContainsString('Error:', $deployment['body']['buildLogs'], 'Deployment logs do not match: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + + $this->cleanupSite($siteId); + } + + public function testOutputDirectoryMissing(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Missing output directory', + 'framework' => 'other', + 'buildRuntime' => 'node-22', + 'outputDirectory' => './non-existing-directory', + ]); + $this->assertNotEmpty($siteId); + + $deployment = $this->createDeployment($siteId, [ + 'code' => $this->packageSite('static'), + 'activate' => true + ]); + $this->assertEquals(202, $deployment['headers']['status-code']); + + $deploymentId = $deployment['body']['$id']; + $this->assertNotEmpty($deploymentId); + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $deployment = $this->getDeployment($siteId, $deploymentId); + $this->assertEquals('failed', $deployment['body']['status'], 'Deployment status does not match: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); + $this->assertStringContainsString('Error:', $deployment['body']['buildLogs'], 'Deployment logs do not match: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + + $this->cleanupSite($siteId); + } } diff --git a/tests/e2e/Services/Teams/TeamsBase.php b/tests/e2e/Services/Teams/TeamsBase.php index 2328e4cdbf..80ac1621ee 100644 --- a/tests/e2e/Services/Teams/TeamsBase.php +++ b/tests/e2e/Services/Teams/TeamsBase.php @@ -37,6 +37,37 @@ trait TeamsBase $teamUid = $response1['body']['$id']; $teamName = $response1['body']['name']; + /** + * Test: Attempt to downgrade the only OWNER in an organization (should fail) + */ + if ($this->getProject()['$id'] === 'console') { + // Step 1: Fetch all team memberships — only one exists at this point + $response = $this->client->call(Client::METHOD_GET, '/teams/' . $teamUid . '/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::limit(1)->toString(), + ], + ]); + + // Step 2: Extract the membership ID of the only member (also the only OWNER) + $membershipID = $response['body']['memberships'][0]['$id']; + + // Step 3: Attempt to downgrade the member's role to 'developer' + $response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamUid . '/memberships/' . $membershipID, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'roles' => ['developer'] + ]); + + // Step 4: Assert failure — cannot remove the only OWNER from a team + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals('general_argument_invalid', $response['body']['type']); + $this->assertEquals('There must be at least one owner in the organization.', $response['body']['message']); + } + $teamId = ID::unique(); $response2 = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ 'content-type' => 'application/json', diff --git a/tests/resources/docker/docker-compose.yml b/tests/resources/docker/docker-compose.yml index 67bb9a1aef..1551a2c30c 100644 --- a/tests/resources/docker/docker-compose.yml +++ b/tests/resources/docker/docker-compose.yml @@ -82,7 +82,8 @@ services: - _APP_USAGE_STATS - _APP_STORAGE_ANTIVIRUS=disabled - _APP_STORAGE_LIMIT - - _APP_COMPUTE_TIMEOUT + - _APP_FUNCTIONS_TIMEOUT + - _APP_SITES_TIMEOUT - _APP_COMPUTE_CPUS - _APP_COMPUTE_MEMORY - _APP_EXECUTOR_HOST @@ -238,7 +239,8 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_COMPUTE_TIMEOUT + - _APP_FUNCTIONS_TIMEOUT + - _APP_SITES_TIMEOUT - _APP_COMPUTE_CPUS - _APP_COMPUTE_MEMORY - _APP_EXECUTOR_HOST diff --git a/tests/unit/Network/Validators/CNAMETest.php b/tests/unit/Network/Validators/CNAMETest.php deleted file mode 100644 index cbb07f19b5..0000000000 --- a/tests/unit/Network/Validators/CNAMETest.php +++ /dev/null @@ -1,29 +0,0 @@ -object = new CNAME('appwrite.io'); - } - - public function tearDown(): void - { - } - - public function testValues(): void - { - $this->assertEquals($this->object->isValid(''), false); - $this->assertEquals($this->object->isValid(null), false); - $this->assertEquals($this->object->isValid(false), false); - $this->assertEquals($this->object->isValid('cname-unit-test.appwrite.org'), true); - $this->assertEquals($this->object->isValid('test1.appwrite.org'), false); - } -} diff --git a/tests/unit/Network/Validators/DNSTest.php b/tests/unit/Network/Validators/DNSTest.php new file mode 100644 index 0000000000..5e8652381a --- /dev/null +++ b/tests/unit/Network/Validators/DNSTest.php @@ -0,0 +1,50 @@ +assertEquals($validator->isValid(''), false); + $this->assertEquals($validator->isValid(null), false); + $this->assertEquals($validator->isValid(false), false); + $this->assertEquals($validator->isValid('cname-unit-test.appwrite.org'), true); + $this->assertEquals($validator->isValid('test1.appwrite.org'), false); + } + + public function testA(): void + { + // IPv4 for documentation purposes + $validator = new DNS('203.0.113.1', DNS::RECORD_A); + $this->assertEquals($validator->isValid(''), false); + $this->assertEquals($validator->isValid(null), false); + $this->assertEquals($validator->isValid(false), false); + $this->assertEquals($validator->isValid('a-unit-test.appwrite.org'), true); + $this->assertEquals($validator->isValid('test1.appwrite.org'), false); + } + + public function testAAAA(): void + { + // IPv6 for documentation purposes + $validator = new DNS('2001:db8::1', DNS::RECORD_AAAA); + $this->assertEquals($validator->isValid(''), false); + $this->assertEquals($validator->isValid(null), false); + $this->assertEquals($validator->isValid(false), false); + $this->assertEquals($validator->isValid('aaaa-unit-test.appwrite.org'), true); + $this->assertEquals($validator->isValid('test1.appwrite.org'), false); + } +}