From 150c9033cc8a1a8174a5fb032b94be2d2a8e0015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 30 Jan 2026 16:20:46 +0100 Subject: [PATCH 1/8] Support arrays in domains env variables --- .env | 2 +- app/config/platform.php | 9 ++- app/controllers/general.php | 63 +++++++++------ app/http.php | 26 ++++++- .../Modules/Console/Http/Resources/Get.php | 35 +++++---- .../Platform/Modules/Proxy/Action.php | 77 +++++++++++++++---- .../Modules/Proxy/Http/Rules/API/Create.php | 8 +- .../Proxy/Http/Rules/Function/Create.php | 8 +- .../Proxy/Http/Rules/Redirect/Create.php | 8 +- .../Modules/Proxy/Http/Rules/Site/Create.php | 8 +- .../Projects/ProjectsCustomServerTest.php | 4 +- .../Services/Proxy/ProxyCustomServerTest.php | 6 +- tests/e2e/Services/Sites/SitesBase.php | 3 +- .../Services/Sites/SitesCustomServerTest.php | 3 +- 14 files changed, 173 insertions(+), 87 deletions(-) diff --git a/.env b/.env index ad973f24f9..29861820c2 100644 --- a/.env +++ b/.env @@ -26,7 +26,7 @@ _APP_DNS=172.16.238.100 # CoreDNS _APP_DOMAIN=appwrite.test _APP_CONSOLE_DOMAIN=localhost _APP_DOMAIN_FUNCTIONS=functions.localhost -_APP_DOMAIN_SITES=sites.localhost +_APP_DOMAIN_SITES=sites.localhost,branded.localhost _APP_DOMAIN_TARGET_CNAME=cname.localhost _APP_DOMAIN_TARGET_A=203.0.0.1 _APP_DOMAIN_TARGET_AAAA=::1 diff --git a/app/config/platform.php b/app/config/platform.php index 913390ae5e..19a7a4986a 100644 --- a/app/config/platform.php +++ b/app/config/platform.php @@ -2,6 +2,13 @@ use Utopia\System\System; +// For now, take first domain as primary (for previews) +// Later-on this can become platform-specific with new env var (appwrite=this,imagine=that) +$sitePreviewDomain = System::getEnv('_APP_DOMAIN_SITES', ''); +if (\str_contains($sitePreviewDomain, ',')) { + $sitePreviewDomain = explode(',', $sitePreviewDomain)[0]; +} + /** * Platform configuration */ @@ -23,5 +30,5 @@ return [ 'privacyUrl' => APP_EMAIL_PRIVACY_URL, 'websiteUrl' => 'https://' . APP_DOMAIN, 'emailSenderName' => APP_EMAIL_PLATFORM_NAME, - 'sitePreviewDomain' => System::getEnv('_APP_DOMAIN_SITES', ''), + 'sitePreviewDomain' => $sitePreviewDomain, ]; diff --git a/app/controllers/general.php b/app/controllers/general.php index 40bcdf4ea3..09bb1a5ff8 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -85,22 +85,32 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $platformHostnames = $platform['hostnames'] ?? []; if ($rule->isEmpty()) { - $appDomainFunctionsFallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''); - $appDomainFunctions = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - $appDomainSites = System::getEnv('_APP_DOMAIN_SITES', ''); - if (!empty($appDomainFunctionsFallback) && \str_ends_with($host, $appDomainFunctionsFallback)) { - $appDomainFunctions = $appDomainFunctionsFallback; + $denyDomains = []; + $denyEnvVars = [ + System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''), + System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), + System::getEnv('_APP_DOMAIN_SITES', ''), + ]; + foreach ($denyEnvVars as $denyEnvVar) { + foreach (\explode(',', $denyEnvVar) as $denyDomain) { + if (empty($denyDomain)) { + continue; + } + $denyDomains[] = $denyDomain; + } } - if ($host === $appDomainFunctions || $host === $appDomainSites) { - throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.', view: $errorView); - } + foreach ($denyDomains as $denyDomain) { + if ($host === $denyDomain) { + throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.', view: $errorView); + } - if (\str_ends_with($host, $appDomainFunctions) || \str_ends_with($host, $appDomainSites)) { - $exception = new AppwriteException(AppwriteException::RULE_NOT_FOUND, 'This domain is not connected to any Appwrite resources. Visit domains tab under function/site settings to configure it.', view: $errorView); + if (\str_ends_with($host, $denyDomain)) { + $exception = new AppwriteException(AppwriteException::RULE_NOT_FOUND, 'This domain is not connected to any Appwrite resources. Visit domains tab under function/site settings to configure it.', view: $errorView); - $exception->addCTA('Start with this domain', $url . '/console'); - throw $exception; + $exception->addCTA('Start with this domain', $url . '/console'); + throw $exception; + } } if (!in_array($host, $platformHostnames)) { @@ -1094,19 +1104,28 @@ App::init() // 5. Create new rule $owner = ''; - $fallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''); - $funcDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - $siteDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - if (!empty($fallback) && \str_ends_with($domain->get(), $fallback)) { - $funcDomain = $fallback; + // Mark owner as Appwrite if its appwirte-owned domain + $appwriteDomains = []; + $appwriteDomainEnvs = [ + System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''), + System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), + System::getEnv('_APP_DOMAIN_SITES', ''), + ]; + foreach ($appwriteDomainEnvs as $appwriteDomainEnv) { + foreach (\explode(',', $appwriteDomainEnv) as $appwriteDomain) { + if (empty($appwriteDomain)) { + continue; + } + $appwriteDomains[] = $appwriteDomain; + } } - if ( - (!empty($funcDomain) && \str_ends_with($domain->get(), $funcDomain)) || - (!empty($siteDomain) && \str_ends_with($domain->get(), $siteDomain)) - ) { - $owner = 'Appwrite'; + foreach ($appwriteDomains as $appwriteDomain) { + if (\str_ends_with($domain->get(), $appwriteDomain)) { + $owner = 'Appwrite'; + break; + } } $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); diff --git a/app/http.php b/app/http.php index 5d08c53eee..9fc0ea0950 100644 --- a/app/http.php +++ b/app/http.php @@ -591,9 +591,33 @@ $http->on(Constant::EVENT_TASK, function () use ($register, $domains) { $sum = count($results); foreach ($results as $document) { $domain = $document->getAttribute('domain'); - if (str_ends_with($domain, System::getEnv('_APP_DOMAIN_FUNCTIONS')) || str_ends_with($domain, System::getEnv('_APP_DOMAIN_SITES'))) { + + $denyDomains = []; + $denyEnvVars = [ + System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''), + System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), + System::getEnv('_APP_DOMAIN_SITES', ''), + ]; + foreach ($denyEnvVars as $denyEnvVar) { + foreach (\explode(',', $denyEnvVar) as $denyDomain) { + if (empty($denyDomain)) { + continue; + } + $denyDomains[] = $denyDomain; + } + } + + $isDenyDomain = false; + foreach ($denyDomains as $denyDomain) { + if (str_ends_with($domain, $denyDomain)) { + $isDenyDomain = true; + } + } + + if ($isDenyDomain) { continue; } + $domains->set(md5($domain), ['value' => 1]); } $latestDocument = !empty(array_key_last($results)) ? $results[array_key_last($results)] : null; diff --git a/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php b/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php index 1468bf71ac..ae2317e880 100644 --- a/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php +++ b/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php @@ -74,36 +74,41 @@ class Get extends Action ) { $domains = $platform['hostnames'] ?? []; if ($type === 'rules') { - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - + $deniedDomains = [...$domains]; $restrictions = []; - if (!empty($sitesDomain)) { + + $sitesDomains = System::getEnv('_APP_DOMAIN_SITES', ''); + foreach (\explode(',', $sitesDomains) as $sitesDomain) { + if (empty($sitesDomain)) { + continue; + } + + $deniedDomains[] = $sitesDomain; + // Ensure site domains are exactly 1 subdomain, and dont start with reserved prefix $domainLevel = \count(\explode('.', $sitesDomain)); $restrictions[] = DomainValidator::createRestriction($sitesDomain, $domainLevel + 1, ['commit-', 'branch-']); } - if (!empty($functionsDomain)) { + + $functionsDomains = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + foreach (\explode(',', $functionsDomains) as $functionsDomain) { + if (empty($sitesDomain)) { + continue; + } + + $deniedDomains[] = $functionsDomain; + // Ensure function domains are exactly 1 subdomain $domainLevel = \count(\explode('.', $functionsDomain)); $restrictions[] = DomainValidator::createRestriction($functionsDomain, $domainLevel + 1); } + $validator = new DomainValidator($restrictions); if (!$validator->isValid($value)) { throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.'); } - $deniedDomains = [...$domains]; - - if (!empty($sitesDomain)) { - $deniedDomains[] = $sitesDomain; - } - - if (!empty($functionsDomain)) { - $deniedDomains[] = $functionsDomain; - } - $denyListDomains = System::getEnv('_APP_CUSTOM_DOMAIN_DENY_LIST', ''); $denyListDomains = \array_map('trim', explode(',', $denyListDomains)); foreach ($denyListDomains as $denyListDomain) { diff --git a/src/Appwrite/Platform/Modules/Proxy/Action.php b/src/Appwrite/Platform/Modules/Proxy/Action.php index c3fa535a5c..913d9e6a8d 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Action.php +++ b/src/Appwrite/Platform/Modules/Proxy/Action.php @@ -31,26 +31,42 @@ class Action extends PlatformAction protected function validateDomainRestrictions(string $domain, array $platform): void { $domains = $platform['hostnames'] ?? []; - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + $deniedDomains = [...$domains]; $restrictions = []; - if (!empty($sitesDomain)) { + + $sitesDomains = System::getEnv('_APP_DOMAIN_SITES', ''); + foreach (\explode(',', $sitesDomains) as $sitesDomain) { + if (empty($sitesDomain)) { + continue; + } + + $deniedDomains[] = $sitesDomain; + + // Ensure site domains are exactly 1 subdomain, and dont start with reserved prefix $domainLevel = \count(\explode('.', $sitesDomain)); $restrictions[] = ValidatorDomain::createRestriction($sitesDomain, $domainLevel + 1, ['commit-', 'branch-']); } - if (!empty($functionsDomain)) { + + $functionsDomains = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + foreach (\explode(',', $functionsDomains) as $functionsDomain) { + if (empty($sitesDomain)) { + continue; + } + + $deniedDomains[] = $functionsDomain; + + // Ensure function domains are exactly 1 subdomain $domainLevel = \count(\explode('.', $functionsDomain)); $restrictions[] = ValidatorDomain::createRestriction($functionsDomain, $domainLevel + 1); } + $validator = new ValidatorDomain($restrictions); if (!$validator->isValid($domain)) { throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.'); } - $deniedDomains = [...$domains]; - if (!empty($sitesDomain)) { $deniedDomains[] = $sitesDomain; } @@ -117,31 +133,40 @@ class Action extends PlatformAction } } - $targetCNAME = null; + $targetCNAMEs = []; $ruleType = $rule->getAttribute('type', ''); $resourceType = $rule->getAttribute('deploymentResourceType', ''); // Ensures different target based on rule's type, as configured by env variables if ($resourceType === 'function') { // For example: fra.appwrite.run - $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_FUNCTIONS', '')); + foreach (\explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) as $targetCNAME) { + $targetCNAMEs[] = new Domain($targetCNAME); + } } elseif ($resourceType === 'site') { // For example: appwrite.network - $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_SITES', '')); + foreach (\explode(',', System::getEnv('_APP_DOMAIN_SITES', '')) as $targetCNAME) { + $targetCNAMEs[] = new Domain($targetCNAME); + } } elseif ($ruleType === 'api') { // For example: fra.cloud.appwrite.io - $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')); + $targetCNAMEs[] = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')); } elseif ($ruleType === 'redirect') { // Shouldn't be needed, because redirect should always have resourceTyp too, but just in case we default to sites // For example: appwrite.network - $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_SITES', '')); + $targetCNAMEs[] = new Domain(System::getEnv('_APP_DOMAIN_SITES', '')); } $validators = []; $mainValidator = null; // Validator to use for error description - if (!is_null($targetCNAME)) { - $validator = new $dnsValidatorClass($targetCNAME->get(), Record::TYPE_CNAME, $dnsServers); + if (\count($targetCNAMEs) > 0) { + $cnameValidators = []; + foreach ($targetCNAMEs as $targetCNAME) { + $cnameValidators[] = new $dnsValidatorClass($targetCNAME->get(), Record::TYPE_CNAME, $dnsServers); + } + + $validator = new AnyOf($cnameValidators); $validators[] = $validator; if (\is_null($mainValidator)) { @@ -185,4 +210,30 @@ class Action extends PlatformAction throw new Exception(Exception::RULE_VERIFICATION_FAILED, $mainValidator->getDescription()); } } + + protected function isAppwriteOwned(string $domain): bool + { + $appwriteDomains = []; + $appwriteDomainEnvs = [ + System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''), + System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), + System::getEnv('_APP_DOMAIN_SITES', ''), + ]; + foreach ($appwriteDomainEnvs as $appwriteDomainEnv) { + foreach (\explode(',', $appwriteDomainEnv) as $appwriteDomain) { + if (empty($appwriteDomain)) { + continue; + } + $appwriteDomains[] = $appwriteDomain; + } + } + + foreach ($appwriteDomains as $appwriteDomain) { + if (\str_ends_with($domain, $appwriteDomain)) { + return true; + } + } + + return false; + } } 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 95ea8dd8cf..86b780bde0 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php @@ -74,18 +74,12 @@ class Create extends Action { $this->validateDomainRestrictions($domain, $platform); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - // TODO: (@Meldiron) Remove after 1.7.x migration $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique(); $status = RULE_STATUS_CREATED; $owner = ''; - if ( - ($functionsDomain != '' && \str_ends_with($domain, $functionsDomain)) || - ($sitesDomain != '' && \str_ends_with($domain, $sitesDomain)) - ) { + if ($this->isAppwriteOwned($domain)) { $status = RULE_STATUS_VERIFIED; $owner = 'Appwrite'; } 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 ea0fb69050..5837d80630 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php @@ -79,9 +79,6 @@ class Create extends Action { $this->validateDomainRestrictions($domain, $platform); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); @@ -94,10 +91,7 @@ class Create extends Action $status = RULE_STATUS_CREATED; $owner = ''; - if ( - ($functionsDomain != '' && \str_ends_with($domain, $functionsDomain)) || - ($sitesDomain != '' && \str_ends_with($domain, $sitesDomain)) - ) { + if ($this->isAppwriteOwned($domain)) { $status = RULE_STATUS_VERIFIED; $owner = 'Appwrite'; } 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 f21374b49a..e1dd3de108 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php @@ -82,9 +82,6 @@ class Create extends Action { $this->validateDomainRestrictions($domain, $platform); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - $collection = match ($resourceType) { 'site' => 'sites', 'function' => 'functions' @@ -99,10 +96,7 @@ class Create extends Action $status = RULE_STATUS_CREATED; $owner = ''; - if ( - ($functionsDomain != '' && \str_ends_with($domain, $functionsDomain)) || - ($sitesDomain != '' && \str_ends_with($domain, $sitesDomain)) - ) { + if ($this->isAppwriteOwned($domain)) { $status = RULE_STATUS_VERIFIED; $owner = 'Appwrite'; } 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 26bd453eb3..20829c1b91 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php @@ -79,9 +79,6 @@ class Create extends Action { $this->validateDomainRestrictions($domain, $platform); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - $site = $dbForProject->getDocument('sites', $siteId); if ($site->isEmpty()) { throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); @@ -94,10 +91,7 @@ class Create extends Action $status = RULE_STATUS_CREATED; $owner = ''; - if ( - ($functionsDomain != '' && \str_ends_with($domain, $functionsDomain)) || - ($sitesDomain != '' && \str_ends_with($domain, $sitesDomain)) - ) { + if ($this->isAppwriteOwned($domain)) { $status = RULE_STATUS_VERIFIED; $owner = 'Appwrite'; } diff --git a/tests/e2e/Services/Projects/ProjectsCustomServerTest.php b/tests/e2e/Services/Projects/ProjectsCustomServerTest.php index b2cf57ddc4..313a4d53be 100644 --- a/tests/e2e/Services/Projects/ProjectsCustomServerTest.php +++ b/tests/e2e/Services/Projects/ProjectsCustomServerTest.php @@ -50,7 +50,7 @@ class ProjectsCustomServerTest extends Scope $this->assertEquals(204, $response['headers']['status-code']); - $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + $functionsDomain = \explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))[0]; $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [ 'domain' => $functionsDomain, @@ -59,7 +59,7 @@ class ProjectsCustomServerTest extends Scope $this->assertEquals(400, $response['headers']['status-code']); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0]; $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [ 'domain' => $sitesDomain, diff --git a/tests/e2e/Services/Proxy/ProxyCustomServerTest.php b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php index 1830adbae3..0fe333b0c0 100644 --- a/tests/e2e/Services/Proxy/ProxyCustomServerTest.php +++ b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php @@ -112,7 +112,8 @@ class ProxyCustomServerTest extends Scope $this->assertEquals(201, $rule['headers']['status-code']); $this->cleanupRule($rule['body']['$id']); - $domain = \uniqid() . '-vcs.' . System::getEnv('_APP_DOMAIN_SITES', ''); + $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0]; + $domain = \uniqid() . '-vcs.' . $sitesDomain; $rule = $this->createSiteRule('commit-' . $domain, $siteId); $this->assertEquals(400, $rule['headers']['status-code']); @@ -393,7 +394,8 @@ class ProxyCustomServerTest extends Scope $this->cleanupRule($rule['body']['$id']); // Create site appwrite-network domain - $domain = \uniqid() . '-cname-api.' . System::getEnv('_APP_DOMAIN_SITES'); + $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0]; + $domain = \uniqid() . '-cname-api.' . $sitesDomain; $rule = $this->createAPIRule($domain); $this->assertEquals(201, $rule['headers']['status-code']); diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index 7eb5d9699c..e7af09d1d9 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -368,12 +368,13 @@ trait SitesBase protected function setupSiteDomain(string $siteId, string $subdomain = ''): string { + $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0]; $subdomain = $subdomain ? $subdomain : ID::unique(); $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'domain' => $subdomain . '.' . System::getEnv('_APP_DOMAIN_SITES', ''), + 'domain' => $subdomain . '.' . $sitesDomain, 'siteId' => $siteId, ]); diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index ff4d8dd5e1..27294a9c3b 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1810,11 +1810,12 @@ class SitesCustomServerTest extends Scope $siteId2 = $site2['body']['$id']; + $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0]; $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'domain' => $subdomain . '.' . System::getEnv('_APP_DOMAIN_SITES', ''), + 'domain' => $subdomain . '.' . $sitesDomain, 'siteId' => $siteId2, ]); From 817475a2514e2bb80068169355a55404126c5edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 30 Jan 2026 16:24:41 +0100 Subject: [PATCH 2/8] Add tests --- .env | 2 +- .../e2e/Services/Proxy/ProxyCustomServerTest.php | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.env b/.env index 29861820c2..a2b6e218d2 100644 --- a/.env +++ b/.env @@ -26,7 +26,7 @@ _APP_DNS=172.16.238.100 # CoreDNS _APP_DOMAIN=appwrite.test _APP_CONSOLE_DOMAIN=localhost _APP_DOMAIN_FUNCTIONS=functions.localhost -_APP_DOMAIN_SITES=sites.localhost,branded.localhost +_APP_DOMAIN_SITES=sites.localhost,rebranded.localhost _APP_DOMAIN_TARGET_CNAME=cname.localhost _APP_DOMAIN_TARGET_A=203.0.0.1 _APP_DOMAIN_TARGET_AAAA=::1 diff --git a/tests/e2e/Services/Proxy/ProxyCustomServerTest.php b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php index 0fe333b0c0..56b3ac92f4 100644 --- a/tests/e2e/Services/Proxy/ProxyCustomServerTest.php +++ b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php @@ -293,10 +293,26 @@ class ProxyCustomServerTest extends Scope $ruleId = $this->setupSiteRule($domain, $siteId); $this->assertNotEmpty($ruleId); + $rule = $this->getRule($ruleId); + $this->assertSame(200, $rule['headers']['status-code']); + $this->assertSame('created', $rule['body']['status']); $response = $proxyClient->call(Client::METHOD_GET, '/contact'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString('Contact page', $response['body']); + + // Wildcard domains automatically get verified status + $domains = [ + \uniqid() . '.sites.localhost', + \uniqid() . '.rebranded.localhost', + ]; + foreach ($domains as $domain) { + $wildcardRuleId = $this->setupSiteRule($domain, $siteId); + $this->assertNotEmpty($wildcardRuleId); + $rule = $this->getRule($wildcardRuleId); + $this->assertSame(200, $rule['headers']['status-code']); + $this->assertSame('verified', $rule['body']['status']); + } $rules = $this->listRules([ 'queries' => [ From 4807dcf52936a77d361d907f83c0248835295f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 30 Jan 2026 16:48:40 +0100 Subject: [PATCH 3/8] Array support for all functions domain usecases --- app/config/platform.php | 13 +++++++++---- app/http.php | 9 +++++++-- .../Platform/Modules/Console/Http/Variables/Get.php | 7 ++++--- .../Modules/Functions/Http/Functions/Create.php | 7 ++++--- tests/e2e/General/UsageTest.php | 3 ++- tests/e2e/Services/Functions/FunctionsBase.php | 3 ++- tests/e2e/Services/Proxy/ProxyCustomServerTest.php | 5 +++-- 7 files changed, 31 insertions(+), 16 deletions(-) diff --git a/app/config/platform.php b/app/config/platform.php index 19a7a4986a..4030949ce6 100644 --- a/app/config/platform.php +++ b/app/config/platform.php @@ -4,9 +4,13 @@ use Utopia\System\System; // For now, take first domain as primary (for previews) // Later-on this can become platform-specific with new env var (appwrite=this,imagine=that) -$sitePreviewDomain = System::getEnv('_APP_DOMAIN_SITES', ''); -if (\str_contains($sitePreviewDomain, ',')) { - $sitePreviewDomain = explode(',', $sitePreviewDomain)[0]; +$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); +if (\str_contains($sitesDomain, ',')) { + $sitesDomain = explode(',', $sitesDomain)[0]; +} +$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); +if (\str_contains($functionsDomain, ',')) { + $functionsDomain = explode(',', $functionsDomain)[0]; } /** @@ -30,5 +34,6 @@ return [ 'privacyUrl' => APP_EMAIL_PRIVACY_URL, 'websiteUrl' => 'https://' . APP_DOMAIN, 'emailSenderName' => APP_EMAIL_PLATFORM_NAME, - 'sitePreviewDomain' => $sitePreviewDomain, + 'sitesDomain' => $sitesDomain, + 'functionsDomain' => $functionsDomain, ]; diff --git a/app/http.php b/app/http.php index 9fc0ea0950..d03bbc0355 100644 --- a/app/http.php +++ b/app/http.php @@ -100,11 +100,16 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int $risky = false; if (str_starts_with($request, 'POST') && str_contains($request, '/executions')) { $risky = true; - } elseif (str_ends_with($domain, System::getEnv('_APP_DOMAIN_FUNCTIONS'))) { - $risky = true; } elseif ($domains->get(md5($domain), 'value') === 1) { // executions request coming from custom domain $risky = true; + } else { + foreach (\explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS')) as $riskyDomain) { + if (str_ends_with($domain, $riskyDomain)) { + $risky = true; + break; + } + } } if ($risky) { diff --git a/src/Appwrite/Platform/Modules/Console/Http/Variables/Get.php b/src/Appwrite/Platform/Modules/Console/Http/Variables/Get.php index 03d692954f..af83a3efd3 100644 --- a/src/Appwrite/Platform/Modules/Console/Http/Variables/Get.php +++ b/src/Appwrite/Platform/Modules/Console/Http/Variables/Get.php @@ -46,10 +46,11 @@ class Get extends Action contentType: ContentType::JSON )) ->inject('response') + ->inject('platform') ->callback($this->action(...)); } - public function action(Response $response) + public function action(Response $response, array $platform) { $validator = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME')); $isCNAMEValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')) && $validator->isKnown() && !$validator->isTest(); @@ -82,8 +83,8 @@ class Get extends Action '_APP_VCS_ENABLED' => $isVcsEnabled, '_APP_DOMAIN_ENABLED' => $isDomainEnabled, '_APP_ASSISTANT_ENABLED' => $isAssistantEnabled, - '_APP_DOMAIN_SITES' => System::getEnv('_APP_DOMAIN_SITES'), - '_APP_DOMAIN_FUNCTIONS' => System::getEnv('_APP_DOMAIN_FUNCTIONS'), + '_APP_DOMAIN_SITES' => $platform['sitesDomain'], + '_APP_DOMAIN_FUNCTIONS' => $platform['functionsDomain'], '_APP_OPTIONS_FORCE_HTTPS' => System::getEnv('_APP_OPTIONS_FORCE_HTTPS'), '_APP_DOMAINS_NAMESERVERS' => System::getEnv('_APP_DOMAINS_NAMESERVERS'), ]); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index 4bf072d115..581cfb5716 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -116,6 +116,7 @@ class Create extends Base ->inject('request') ->inject('gitHub') ->inject('authorization') + ->inject('platform') ->callback($this->action(...)); } @@ -154,7 +155,8 @@ class Create extends Base Database $dbForPlatform, Request $request, GitHub $github, - Authorization $authorization + Authorization $authorization, + array $platform ) { // Temporary abuse check @@ -321,7 +323,6 @@ class Create extends Base template: $template, github: $github, activate: true, - authorization: $authorization, reference: $providerBranch, referenceType: 'branch' ); @@ -366,7 +367,7 @@ class Create extends Base ->setTemplate($template); } - $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + $functionsDomain = $platform['functionsDomain']; if (!empty($functionsDomain)) { $routeSubdomain = ID::unique(); $domain = "{$routeSubdomain}.{$functionsDomain}"; diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index dc49d27aea..e30197cf13 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -1283,6 +1283,7 @@ class UsageTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); + $functionsDomain = \explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))[0]; $rule = $this->client->call( Client::METHOD_POST, '/proxy/rules/function', @@ -1291,7 +1292,7 @@ class UsageTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'domain' => 'test-' . ID::unique() . '.' . System::getEnv('_APP_DOMAIN_FUNCTIONS'), + 'domain' => 'test-' . ID::unique() . '.' . $functionsDomain, 'functionId' => $functionId, ], ); diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 7403b23a73..4994561c9a 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -316,12 +316,13 @@ trait FunctionsBase protected function setupFunctionDomain(string $functionId, string $subdomain = ''): string { + $functionsDomain = \explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))[0]; $subdomain = $subdomain ? $subdomain : ID::unique(); $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'domain' => $subdomain . '.' . System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), + 'domain' => $subdomain . '.' . $functionsDomain, 'functionId' => $functionId, ]); diff --git a/tests/e2e/Services/Proxy/ProxyCustomServerTest.php b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php index 56b3ac92f4..279b2a7077 100644 --- a/tests/e2e/Services/Proxy/ProxyCustomServerTest.php +++ b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php @@ -300,7 +300,7 @@ class ProxyCustomServerTest extends Scope $response = $proxyClient->call(Client::METHOD_GET, '/contact'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString('Contact page', $response['body']); - + // Wildcard domains automatically get verified status $domains = [ \uniqid() . '.sites.localhost', @@ -401,7 +401,8 @@ class ProxyCustomServerTest extends Scope public function testUpdateRule(): void { // Create function appwrite-network domain - $domain = \uniqid() . '-cname-api.' . System::getEnv('_APP_DOMAIN_FUNCTIONS'); + $functionsDomain = \explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))[0]; + $domain = \uniqid() . '-cname-api.' . $functionsDomain; $rule = $this->createAPIRule($domain); $this->assertEquals(201, $rule['headers']['status-code']); From fea4994ef050c078efedb48635667acf1dffcb00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 30 Jan 2026 21:30:00 +0100 Subject: [PATCH 4/8] ai review fixes --- app/controllers/api/vcs.php | 2 +- app/controllers/general.php | 2 +- app/http.php | 3 +++ src/Appwrite/Platform/Modules/Compute/Base.php | 2 +- .../Modules/Console/Http/Resources/Get.php | 2 +- .../Modules/Functions/Workers/Builds.php | 2 +- src/Appwrite/Platform/Modules/Proxy/Action.php | 16 +++++++--------- .../Modules/Sites/Http/Deployments/Create.php | 4 ++-- .../Sites/Http/Deployments/Duplicate/Create.php | 2 +- .../Sites/Http/Deployments/Template/Create.php | 2 +- .../e2e/Services/Proxy/ProxyCustomServerTest.php | 1 + 11 files changed, 20 insertions(+), 18 deletions(-) diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 3a67068f1c..046f10f715 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -341,7 +341,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $projectId = $project->getId(); // Deployment preview - $sitesDomain = $platform['sitePreviewDomain']; + $sitesDomain = $platform['sitesDomain']; $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); $previewRuleId = $ruleId; diff --git a/app/controllers/general.php b/app/controllers/general.php index 09bb1a5ff8..3ab59eb870 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1105,7 +1105,7 @@ App::init() // 5. Create new rule $owner = ''; - // Mark owner as Appwrite if its appwirte-owned domain + // Mark owner as Appwrite if its appwrite-owned domain $appwriteDomains = []; $appwriteDomainEnvs = [ System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''), diff --git a/app/http.php b/app/http.php index d03bbc0355..e23880e943 100644 --- a/app/http.php +++ b/app/http.php @@ -105,6 +105,9 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int $risky = true; } else { foreach (\explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS')) as $riskyDomain) { + if (empty($riskyDomain)) { + continue; + } if (str_ends_with($domain, $riskyDomain)) { $risky = true; break; diff --git a/src/Appwrite/Platform/Modules/Compute/Base.php b/src/Appwrite/Platform/Modules/Compute/Base.php index 7b844200cb..6f604bae1b 100644 --- a/src/Appwrite/Platform/Modules/Compute/Base.php +++ b/src/Appwrite/Platform/Modules/Compute/Base.php @@ -235,7 +235,7 @@ class Base extends Action ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); $dbForProject->updateDocument('sites', $site->getId(), $site); - $sitesDomain = $platform['sitePreviewDomain']; + $sitesDomain = $platform['sitesDomain']; $domain = ID::unique() . "." . $sitesDomain; // TODO: (@Meldiron) Remove after 1.7.x migration diff --git a/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php b/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php index ae2317e880..717ef6c788 100644 --- a/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php +++ b/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php @@ -92,7 +92,7 @@ class Get extends Action $functionsDomains = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); foreach (\explode(',', $functionsDomains) as $functionsDomain) { - if (empty($sitesDomain)) { + if (empty($functionsDomain)) { continue; } diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 7473d0eb1f..1c70a1b4ef 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -1037,7 +1037,7 @@ class Builds extends Action // VCS branch $branchName = $deployment->getAttribute('providerBranch'); if (!empty($branchName)) { - $sitesDomain = $platform['sitePreviewDomain']; + $sitesDomain = $platform['sitesDomain']; $branchPrefix = substr($branchName, 0, 16); if (strlen($branchName) > 16) { $remainingChars = substr($branchName, 16); diff --git a/src/Appwrite/Platform/Modules/Proxy/Action.php b/src/Appwrite/Platform/Modules/Proxy/Action.php index 913d9e6a8d..52f40aec00 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Action.php +++ b/src/Appwrite/Platform/Modules/Proxy/Action.php @@ -50,7 +50,7 @@ class Action extends PlatformAction $functionsDomains = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); foreach (\explode(',', $functionsDomains) as $functionsDomain) { - if (empty($sitesDomain)) { + if (empty($functionsDomains)) { continue; } @@ -67,14 +67,6 @@ class Action extends PlatformAction throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.'); } - if (!empty($sitesDomain)) { - $deniedDomains[] = $sitesDomain; - } - - if (!empty($functionsDomain)) { - $deniedDomains[] = $functionsDomain; - } - $denyListDomains = System::getEnv('_APP_CUSTOM_DOMAIN_DENY_LIST', ''); $denyListDomains = \array_map('trim', explode(',', $denyListDomains)); foreach ($denyListDomains as $denyListDomain) { @@ -141,11 +133,17 @@ class Action extends PlatformAction if ($resourceType === 'function') { // For example: fra.appwrite.run foreach (\explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) as $targetCNAME) { + if (empty($targetCNAME)) { + continue; + } $targetCNAMEs[] = new Domain($targetCNAME); } } elseif ($resourceType === 'site') { // For example: appwrite.network foreach (\explode(',', System::getEnv('_APP_DOMAIN_SITES', '')) as $targetCNAME) { + if (empty($targetCNAME)) { + continue; + } $targetCNAMEs[] = new Domain($targetCNAME); } } elseif ($ruleType === 'api') { diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php index c2420aa223..6d6b599ed7 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php @@ -274,7 +274,7 @@ class Create extends Action ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); $dbForProject->updateDocument('sites', $site->getId(), $site); - $sitesDomain = $platform['sitePreviewDomain']; + $sitesDomain = $platform['sitesDomain']; $domain = ID::unique() . "." . $sitesDomain; // TODO: (@Meldiron) Remove after 1.7.x migration @@ -344,7 +344,7 @@ class Create extends Action ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); $dbForProject->updateDocument('sites', $site->getId(), $site); - $sitesDomain = $platform['sitePreviewDomain']; + $sitesDomain = $platform['sitesDomain']; $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); $authorization->skip( diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php index bb0c007850..1d60c6776c 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php @@ -145,7 +145,7 @@ class Create extends Action $dbForProject->updateDocument('sites', $site->getId(), $site); // Preview deployments for sites - $sitesDomain = $platform['sitePreviewDomain']; + $sitesDomain = $platform['sitesDomain']; $domain = ID::unique() . "." . $sitesDomain; // TODO: (@Meldiron) Remove after 1.7.x migration diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php index 2e079a057b..e36bed94bc 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -189,7 +189,7 @@ class Create extends Base ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); $dbForProject->updateDocument('sites', $site->getId(), $site); - $sitesDomain = $platform['sitePreviewDomain']; + $sitesDomain = $platform['sitesDomain']; $domain = ID::unique() . "." . $sitesDomain; // TODO: (@Meldiron) Remove after 1.7.x migration diff --git a/tests/e2e/Services/Proxy/ProxyCustomServerTest.php b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php index 279b2a7077..be0b89b404 100644 --- a/tests/e2e/Services/Proxy/ProxyCustomServerTest.php +++ b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php @@ -312,6 +312,7 @@ class ProxyCustomServerTest extends Scope $rule = $this->getRule($wildcardRuleId); $this->assertSame(200, $rule['headers']['status-code']); $this->assertSame('verified', $rule['body']['status']); + $this->cleanupRule($wildcardRuleId); } $rules = $this->listRules([ From ce1bb3fd2947ec5477a97311f1c4d3916d8e52bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 30 Jan 2026 22:23:36 +0100 Subject: [PATCH 5/8] ai review fixes --- src/Appwrite/Platform/Modules/Proxy/Action.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Proxy/Action.php b/src/Appwrite/Platform/Modules/Proxy/Action.php index 52f40aec00..62d6efbfa3 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Action.php +++ b/src/Appwrite/Platform/Modules/Proxy/Action.php @@ -152,7 +152,12 @@ class Action extends PlatformAction } elseif ($ruleType === 'redirect') { // Shouldn't be needed, because redirect should always have resourceTyp too, but just in case we default to sites // For example: appwrite.network - $targetCNAMEs[] = new Domain(System::getEnv('_APP_DOMAIN_SITES', '')); + foreach (\explode(',', System::getEnv('_APP_DOMAIN_SITES', '')) as $targetCNAME) { + if (empty($targetCNAME)) { + continue; + } + $targetCNAMEs[] = new Domain($targetCNAME); + } } $validators = []; From f648d82c8c49f7bd92e6df0a4630648043256f64 Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Tue, 3 Feb 2026 01:18:34 +0530 Subject: [PATCH 6/8] Remove cleanup-stale-executions from Interval task (#11224) --- src/Appwrite/Platform/Tasks/Interval.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/Interval.php b/src/Appwrite/Platform/Tasks/Interval.php index b83cbb8185..6d431705ab 100644 --- a/src/Appwrite/Platform/Tasks/Interval.php +++ b/src/Appwrite/Platform/Tasks/Interval.php @@ -84,13 +84,6 @@ class Interval extends Action $this->verifyDomain($dbForPlatform, $queueForCertificates); }, 'interval' => $intervalDomainVerification * 1000, - ], - [ - 'name' => 'cleanupStaleExecutions', - 'callback' => function (Database $dbForPlatform, callable $getProjectDB, Certificate $queueForCertificates) { - $this->cleanupStaleExecutions($dbForPlatform, $getProjectDB); - }, - 'interval' => $intervalCleanupStaleExecutions * 1000, ] ]; } From f15e9eb3563b63e3f3abfbfeac3e2d044fc9ae68 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 3 Feb 2026 09:03:03 +0530 Subject: [PATCH 7/8] Add 7-day job TTL for functions queue Set jobTTL to 7 days for FUNCTIONS_QUEUE_NAME using a match expression for easy extensibility to other queues in the future. --- src/Appwrite/Event/Event.php | 28 +++++++++++++++++++++++++++- src/Appwrite/Event/Func.php | 3 ++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index 9805ab7830..6db001398f 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -23,6 +23,7 @@ class Event public const FUNCTIONS_QUEUE_NAME = 'v1-functions'; public const FUNCTIONS_CLASS_NAME = 'FunctionsV1'; + public const FUNCTIONS_QUEUE_TTL = 60 * 60 * 24 * 7; // 7 days public const STATS_RESOURCES_QUEUE_NAME = 'v1-stats-resources'; public const STATS_RESOURCES_CLASS_NAME = 'StatsResourcesV1'; @@ -65,6 +66,8 @@ class Event /** @var bool Non-critical events will not throw an exception when enqueuing of the event fails. */ protected bool $critical = true; + protected int $ttl = 0; + /** * @param Publisher $publisher * @return void @@ -114,6 +117,29 @@ class Event return $this->queue; } + /** + * Set TTL (time-to-live) for jobs in this queue. + * + * @param int $ttl TTL in seconds + * @return static + */ + public function setTTL(int $ttl): static + { + $this->ttl = $ttl; + + return $this; + } + + /** + * Get TTL (time-to-live) for jobs in this queue. + * + * @return int + */ + public function getTTL(): int + { + return $this->ttl; + } + /** * Set event name used for this event. * @param string $event @@ -369,7 +395,7 @@ class Event } /** The getter is required since events like Databases need to override the queue name depending on the project */ - $queue = new Queue($this->getQueue()); + $queue = new Queue($this->getQueue(), 'utopia-queue', $this->getTTL()); // Merge the base payload with any trimmed values $payload = array_merge($this->preparePayload(), $this->trimPayload()); diff --git a/src/Appwrite/Event/Func.php b/src/Appwrite/Event/Func.php index 8d8c51b540..2f7f8e3c5c 100644 --- a/src/Appwrite/Event/Func.php +++ b/src/Appwrite/Event/Func.php @@ -27,7 +27,8 @@ class Func extends Event $this ->setQueue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME)) - ->setClass(System::getEnv('_APP_FUNCTIONS_CLASS_NAME', Event::FUNCTIONS_CLASS_NAME)); + ->setClass(System::getEnv('_APP_FUNCTIONS_CLASS_NAME', Event::FUNCTIONS_CLASS_NAME)) + ->setTTL(Event::FUNCTIONS_QUEUE_TTL); } /** From 6c4394947fbbe8623cf84f485bedad0656e6a697 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 3 Feb 2026 09:49:37 +0530 Subject: [PATCH 8/8] Fix Safe PHP deprecation warnings --- app/cli.php | 1 + src/Appwrite/Auth/OAuth2/Exception.php | 2 +- src/Appwrite/Extend/Exception.php | 6 +++--- src/Appwrite/Messaging/Adapter/Realtime.php | 2 +- src/Appwrite/Migration/Migration.php | 8 ++++---- src/Appwrite/Platform/Action.php | 2 +- src/Appwrite/Platform/Modules/Compute/Base.php | 2 +- .../Databases/Collections/Attributes/Action.php | 2 +- .../Modules/Databases/Workers/Databases.php | 2 +- src/Appwrite/Platform/Workers/Deletes.php | 5 +++-- src/Appwrite/Platform/Workers/Functions.php | 14 +++++++------- src/Appwrite/Utopia/Database/Documents/User.php | 2 +- src/Appwrite/Utopia/Fetch/BodyMultipart.php | 2 +- 13 files changed, 26 insertions(+), 24 deletions(-) diff --git a/app/cli.php b/app/cli.php index af67f4e0e6..b683af961d 100644 --- a/app/cli.php +++ b/app/cli.php @@ -44,6 +44,7 @@ Config::setParam('runtimes', (new Runtimes('v5'))->getAll(supported: false)); // require controllers after overwriting runtimes require_once __DIR__ . '/controllers/general.php'; +global $register; CLI::setResource('register', fn () => $register); CLI::setResource('cache', function ($pools) { diff --git a/src/Appwrite/Auth/OAuth2/Exception.php b/src/Appwrite/Auth/OAuth2/Exception.php index df5054ae9a..f908eb9954 100644 --- a/src/Appwrite/Auth/OAuth2/Exception.php +++ b/src/Appwrite/Auth/OAuth2/Exception.php @@ -10,7 +10,7 @@ class Exception extends AppwriteException protected string $error = ''; protected string $errorDescription = ''; - public function __construct(string $response = '', int $code = 0, \Throwable $previous = null) + public function __construct(string $response = '', int $code = 0, ?\Throwable $previous = null) { $this->response = $response; $this->message = $response; diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index df123323ca..2bc7021b31 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -392,9 +392,9 @@ class Exception extends \Exception public function __construct( string $type = Exception::GENERAL_UNKNOWN, - string $message = null, - int|string $code = null, - \Throwable $previous = null, + ?string $message = null, + int|string|null $code = null, + ?\Throwable $previous = null, ?string $view = null, array $params = [] ) { diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index 9e03a7aaf7..ae198e0042 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -297,7 +297,7 @@ class Realtime extends MessagingAdapter * @return array * @throws \Exception */ - public static function fromPayload(string $event, Document $payload, Document $project = null, Document $database = null, Document $collection = null, Document $bucket = null): array + public static function fromPayload(string $event, Document $payload, ?Document $project = null, ?Document $database = null, ?Document $collection = null, ?Document $bucket = null): array { $channels = []; $roles = []; diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index ea51225ba6..749c64d08c 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -211,7 +211,7 @@ abstract class Migration * @return void * @throws \Throwable */ - protected function createCollection(string $id, string $name = null): void + protected function createCollection(string $id, ?string $name = null): void { $name ??= $id; @@ -262,7 +262,7 @@ abstract class Migration Database $database, string $collectionId, array $attributeIds, - string $from = null + ?string $from = null ): void { $from ??= $collectionId; @@ -327,7 +327,7 @@ abstract class Migration Database $database, string $collectionId, string $attributeId, - string $from = null + ?string $from = null ): void { $from ??= $collectionId; @@ -385,7 +385,7 @@ abstract class Migration * @throws Duplicate * @throws Limit */ - public function createIndexFromCollection(Database $database, string $collectionId, string $indexId, string $from = null): void + public function createIndexFromCollection(Database $database, string $collectionId, string $indexId, ?string $from = null): void { $from ??= $collectionId; diff --git a/src/Appwrite/Platform/Action.php b/src/Appwrite/Platform/Action.php index ba93225a77..b9f8a8186a 100644 --- a/src/Appwrite/Platform/Action.php +++ b/src/Appwrite/Platform/Action.php @@ -44,7 +44,7 @@ class Action extends UtopiaAction * * @return void */ - protected function foreachDocument(Database $database, string $collection, array $queries = [], callable $callback = null, int $limit = 1000, bool $concurrent = false): void + protected function foreachDocument(Database $database, string $collection, array $queries = [], ?callable $callback = null, int $limit = 1000, bool $concurrent = false): void { $results = []; $sum = $limit; diff --git a/src/Appwrite/Platform/Modules/Compute/Base.php b/src/Appwrite/Platform/Modules/Compute/Base.php index 6f604bae1b..efbf95d74f 100644 --- a/src/Appwrite/Platform/Modules/Compute/Base.php +++ b/src/Appwrite/Platform/Modules/Compute/Base.php @@ -144,7 +144,7 @@ class Base extends Action return $deployment; } - public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForPlatform, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, Authorization $authorization, string $referenceType = 'branch', string $reference = '', array $platform): Document + public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForPlatform, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, Authorization $authorization, array $platform, string $referenceType = 'branch', string $reference = ''): Document { $deploymentId = ID::unique(); $providerInstallationId = $installation->getAttribute('providerInstallationId', ''); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php index d9df7f4a24..3709fb2814 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php @@ -488,7 +488,7 @@ abstract class Action extends UtopiaAction return $attribute; } - protected function updateAttribute(string $databaseId, string $collectionId, string $key, Database $dbForProject, Event $queueForEvents, Authorization $authorization, string $type, int $size = null, string $filter = null, string|bool|int|float|array $default = null, bool $required = null, int|float|null $min = null, int|float|null $max = null, array $elements = null, array $options = [], string $newKey = null): Document + protected function updateAttribute(string $databaseId, string $collectionId, string $key, Database $dbForProject, Event $queueForEvents, Authorization $authorization, string $type, ?int $size = null, ?string $filter = null, string|bool|int|float|array|null $default = null, ?bool $required = null, int|float|null $min = null, int|float|null $max = null, ?array $elements = null, array $options = [], ?string $newKey = null): Document { $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); diff --git a/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php index 9a98d77d2d..c7cf7c8695 100644 --- a/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php +++ b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php @@ -578,7 +578,7 @@ class Databases extends Action * @return void * @throws Exception */ - protected function deleteByGroup(string $collectionId, array $queries, Database $database, callable $callback = null): void + protected function deleteByGroup(string $collectionId, array $queries, Database $database, ?callable $callback = null): void { $start = \microtime(true); diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index b53871f7b4..dc1775e8b0 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -23,6 +23,7 @@ use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; use Utopia\DSN\DSN; use Utopia\Logger\Log; use Utopia\Platform\Action; @@ -335,7 +336,7 @@ class Deletes extends Action * @throws Authorization * @throws Exception */ - private function deleteCacheByResource(Document $project, callable $getProjectDB, string $resource, string $resourceType = null): void + private function deleteCacheByResource(Document $project, callable $getProjectDB, string $resource, ?string $resourceType = null): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); @@ -1316,7 +1317,7 @@ class Deletes extends Action * @return void * @throws Exception */ - protected function listByGroup(string $collection, array $queries, Database $database, callable $callback = null): void + protected function listByGroup(string $collection, array $queries, Database $database, ?callable $callback = null): void { $count = 0; $limit = 1000; diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index e053efa021..a0f4b65763 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -272,8 +272,8 @@ class Functions extends Action string $path, string $method, Document $user, - string $jwt = null, - string $event = null, + ?string $jwt = null, + ?string $event = null, ): void { $executionId = ID::unique(); $headers['x-appwrite-execution-id'] = $executionId ?? ''; @@ -357,12 +357,12 @@ class Functions extends Action string $method, array $headers, array $platform, - string $data = null, + ?string $data = null, ?Document $user = null, - string $jwt = null, - string $event = null, - string $eventData = null, - string $executionId = null, + ?string $jwt = null, + ?string $event = null, + ?string $eventData = null, + ?string $executionId = null, ): void { $user ??= new Document(); $functionId = $function->getId(); diff --git a/src/Appwrite/Utopia/Database/Documents/User.php b/src/Appwrite/Utopia/Database/Documents/User.php index cbd22aaee5..9d9c846ae0 100644 --- a/src/Appwrite/Utopia/Database/Documents/User.php +++ b/src/Appwrite/Utopia/Database/Documents/User.php @@ -131,7 +131,7 @@ class User extends Document return false; } - public function tokenVerify(int $type = null, string $secret, Proof $proofForToken): false|Document + public function tokenVerify(?int $type, string $secret, Proof $proofForToken): false|Document { $tokens = $this->getAttribute('tokens', []); foreach ($tokens as $token) { diff --git a/src/Appwrite/Utopia/Fetch/BodyMultipart.php b/src/Appwrite/Utopia/Fetch/BodyMultipart.php index c697b4e5c0..ee482a7d9e 100644 --- a/src/Appwrite/Utopia/Fetch/BodyMultipart.php +++ b/src/Appwrite/Utopia/Fetch/BodyMultipart.php @@ -10,7 +10,7 @@ class BodyMultipart private array $parts = []; private string $boundary = ""; - public function __construct(string $boundary = null) + public function __construct(?string $boundary = null) { if (is_null($boundary)) { $this->boundary = self::generateBoundary();