diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index b31681fd64..597f02f7d6 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -1528,7 +1528,29 @@ return [ 'default' => false, 'array' => false, 'filters' => [], - ] + ], + [ + '$id' => ID::custom('screenshotLight'), // File ID from 'screenshots' Console bucket + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 32, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('screenshotDark'), // File ID from 'screenshots' Console bucket + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 32, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], ], 'indexes' => [ [ diff --git a/app/config/site-templates.php b/app/config/site-templates.php index fbd4732573..b22b559a15 100644 --- a/app/config/site-templates.php +++ b/app/config/site-templates.php @@ -539,4 +539,49 @@ return [ 'providerVersion' => '0.2.*', 'variables' => [], ], + [ + 'key' => 'nextjs-starter', + 'name' => 'Next.js starter website', + 'useCases' => ['starter'], + 'frameworks' => [ + getFramework('NEXTJS', [ + 'providerRootDirectory' => './nextjs/starter', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'templates-for-sites', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.2.*', + 'variables' => [], + ], + [ + 'key' => 'nuxt-starter', + 'name' => 'Nuxt starter website', + 'useCases' => ['starter'], + 'frameworks' => [ + getFramework('NUXT', [ + 'providerRootDirectory' => './nuxt/starter', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'templates-for-sites', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.2.*', + 'variables' => [], + ], + [ + 'key' => 'sveltekit-starter', + 'name' => 'SvelteKit starter website', + 'useCases' => ['starter'], + 'frameworks' => [ + getFramework('SVELTEKIT', [ + 'providerRootDirectory' => './sveltekit/starter', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'templates-for-sites', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.2.*', + 'variables' => [], + ], ]; diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 921a840c0a..d8394a3b4f 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -236,10 +236,9 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $projectId = $project->getId(); $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); - - $rule = Authorization::skip( + Authorization::skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), diff --git a/app/controllers/general.php b/app/controllers/general.php index 87af29c02f..6d201d4a78 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -4,6 +4,7 @@ require_once __DIR__ . '/../init.php'; use Ahc\Jwt\JWT; use Appwrite\Auth\Auth; +use Appwrite\Auth\Key; use Appwrite\Event\Certificate; use Appwrite\Event\Event; use Appwrite\Event\Func; @@ -54,7 +55,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) +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) { $utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml'); @@ -269,11 +270,11 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw if ($type === 'function') { $jwtExpiry = $resource->getAttribute('timeout', 900); $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); - $apiKey = $jwtObj->encode([ + $jwtKey = $jwtObj->encode([ 'projectId' => $project->getId(), 'scopes' => $resource->getAttribute('scopes', []) ]); - $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey; + $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $jwtKey; $headers['x-appwrite-trigger'] = 'http'; $headers['x-appwrite-user-jwt'] = ''; } @@ -448,18 +449,19 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw requestTimeout: 30 ); - // Branded banner for previews - $transformation = new Transformation(); - $transformation->addAdapter(new Preview()); - $transformation->setInput($executionResponse['body']); - $transformation->setTraits($executionResponse['headers']); - if ($type === 'deployment' && $transformation->transform()) { - $executionResponse['body'] = $transformation->getOutput(); + if (\is_null($apiKey) || $apiKey->isBannerDisabled() === false) { + $transformation = new Transformation(); + $transformation->addAdapter(new Preview()); + $transformation->setInput($executionResponse['body']); + $transformation->setTraits($executionResponse['headers']); + if ($type === 'deployment' && $transformation->transform()) { + $executionResponse['body'] = $transformation->getOutput(); - foreach ($executionResponse['headers'] as $key => $value) { - if (\strtolower($key) === 'content-length') { - $executionResponse['headers'][$key] = \strlen($executionResponse['body']); + foreach ($executionResponse['headers'] as $key => $value) { + if (\strtolower($key) === 'content-length') { + $executionResponse['headers'][$key] = \strlen($executionResponse['body']); + } } } } @@ -585,7 +587,7 @@ App::init() */ App::init() - ->groups(['database', 'functions', 'storage', 'messaging']) + ->groups(['database', 'functions', 'messaging']) ->inject('project') ->inject('request') ->action(function (Document $project, Request $request) { @@ -617,7 +619,8 @@ App::init() ->inject('queueForFunctions') ->inject('isResourceBlocked') ->inject('previewHostname') - ->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) { + ->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) { /* * Appwrite Router */ @@ -625,7 +628,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)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); } } @@ -865,7 +868,8 @@ App::options() ->inject('geodb') ->inject('isResourceBlocked') ->inject('previewHostname') - ->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) { + ->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) { /* * Appwrite Router */ @@ -873,7 +877,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)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); } } @@ -1173,7 +1177,8 @@ App::get('/robots.txt') ->inject('geodb') ->inject('isResourceBlocked') ->inject('previewHostname') - ->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) { + ->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) { $host = $request->getHostname() ?? ''; $mainDomain = System::getEnv('_APP_DOMAIN', ''); @@ -1181,7 +1186,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)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); } } @@ -1203,7 +1208,8 @@ App::get('/humans.txt') ->inject('geodb') ->inject('isResourceBlocked') ->inject('previewHostname') - ->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) { + ->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) { $host = $request->getHostname() ?? ''; $mainDomain = System::getEnv('_APP_DOMAIN', ''); @@ -1211,7 +1217,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)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); } } diff --git a/app/http.php b/app/http.php index b9aa69a7cc..2b1f038777 100644 --- a/app/http.php +++ b/app/http.php @@ -13,6 +13,7 @@ use Swoole\Table; use Utopia\App; use Utopia\Audit\Audit; use Utopia\CLI\Console; +use Utopia\Compression\Compression; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -249,8 +250,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg $audit->setup(); } - if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty() && - !$dbForPlatform->exists($dbForPlatform->getDatabase(), 'bucket_1')) { + if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty()) { Console::info(" └── Creating default bucket..."); $dbForPlatform->createDocument('buckets', new Document([ '$id' => ID::custom('default'), @@ -302,6 +302,59 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg $dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes); } + + if ($dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty()) { + Console::info(" └── Creating screenshots bucket..."); + $dbForPlatform->createDocument('buckets', new Document([ + '$id' => ID::custom('screenshots'), + '$collection' => ID::custom('buckets'), + 'name' => 'Screenshots', + 'maximumFileSize' => 5000000, // ~5MB + 'allowedFileExtensions' => [ 'png' ], + 'enabled' => true, + 'compression' => Compression::GZIP, + 'encryption' => false, + 'antivirus' => false, + 'fileSecurity' => true, + '$permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'search' => 'buckets Screenshots', + ])); + + $bucket = $dbForPlatform->getDocument('buckets', 'screenshots'); + + Console::info(" └── Creating files collection for screenshots bucket..."); + $files = $collections['buckets']['files'] ?? []; + if (empty($files)) { + throw new Exception('Files collection is not configured.'); + } + + $attributes = array_map(fn ($attr) => new Document([ + '$id' => ID::custom($attr['$id']), + 'type' => $attr['type'], + 'size' => $attr['size'], + 'required' => $attr['required'], + 'signed' => $attr['signed'], + 'array' => $attr['array'], + 'filters' => $attr['filters'], + 'default' => $attr['default'] ?? null, + 'format' => $attr['format'] ?? '' + ]), $files['attributes']); + + $indexes = array_map(fn ($index) => new Document([ + '$id' => ID::custom($index['$id']), + 'type' => $index['type'], + 'attributes' => $index['attributes'], + 'lengths' => $index['lengths'], + 'orders' => $index['orders'], + ]), $files['indexes']); + + $dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes); + } }); $pools->reclaim(); diff --git a/app/init.php b/app/init.php index 8609109e90..4e36c91cb1 100644 --- a/app/init.php +++ b/app/init.php @@ -1961,16 +1961,24 @@ App::setResource( fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false ); -App::setResource('previewHostname', function (Request $request) { +App::setResource('previewHostname', function (Request $request, ?Key $apiKey) { + $allowed = false; + if (App::isDevelopment()) { - $host = $request->getQuery('appwrite-hostname') ?? ''; + $allowed = true; + } elseif (!\is_null($apiKey) && $apiKey->getHostnameOverride() === true) { + $allowed = true; + } + + if ($allowed) { + $host = $request->getQuery('appwrite-hostname', $request->getHeader('x-appwrite-hostname', '')); if (!empty($host)) { return $host; } } return ''; -}, ['request']); +}, ['request', 'apiKey']); App::setResource('apiKey', function (Request $request, Document $project): ?Key { $key = $request->getHeader('x-appwrite-key'); diff --git a/composer.lock b/composer.lock index 51535b220d..223a6a7a4e 100644 --- a/composer.lock +++ b/composer.lock @@ -6190,16 +6190,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.0.2", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "51087f87dcce2663e1fed4dfd4e56eccd580297e" + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/51087f87dcce2663e1fed4dfd4e56eccd580297e", - "reference": "51087f87dcce2663e1fed4dfd4e56eccd580297e", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", "shasum": "" }, "require": { @@ -6231,9 +6231,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.2" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" }, - "time": "2025-02-17T20:25:51+00:00" + "time": "2025-02-19T13:28:12+00:00" }, { "name": "phpstan/phpstan", diff --git a/docker-compose.yml b/docker-compose.yml index 63f4514d47..64aa187667 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -435,6 +435,7 @@ services: volumes: - appwrite-functions:/storage/functions:rw - appwrite-builds:/storage/builds:rw + - appwrite-uploads:/storage/uploads:rw - ./app:/usr/src/code/app - ./src:/usr/src/code/src depends_on: @@ -947,6 +948,12 @@ services: environment: - _APP_ASSISTANT_OPENAI_API_KEY + appwrite-browser: + container_name: appwrite-browser + image: appwrite/browser:0.2.0 + networks: + - appwrite + openruntimes-executor: container_name: openruntimes-executor hostname: exc1 diff --git a/src/Appwrite/Auth/Key.php b/src/Appwrite/Auth/Key.php index 1c40b35f54..83f8dd408d 100644 --- a/src/Appwrite/Auth/Key.php +++ b/src/Appwrite/Auth/Key.php @@ -20,6 +20,9 @@ class Key protected string $name, protected bool $expired = false, protected array $disabledMetrics = [], + protected bool $hostnameOverride = false, + protected bool $bannerDisabled = false, + protected bool $projectCheckDisabled = false, ) { } @@ -58,6 +61,23 @@ class Key return $this->disabledMetrics; } + + public function getHostnameOverride(): bool + { + return $this->hostnameOverride; + } + + + public function isBannerDisabled(): bool + { + return $this->bannerDisabled; + } + + public function isProjectCheckDisabled(): bool + { + return $this->projectCheckDisabled; + } + /** * Decode the given secret key into a Key object, containing the project ID, type, role, scopes, and name. * Can be a stored API key or a dynamic key (JWT). @@ -109,9 +129,12 @@ class Key $name = $payload['name'] ?? 'Dynamic Key'; $projectId = $payload['projectId'] ?? ''; $disabledMetrics = $payload['disabledMetrics'] ?? []; + $hostnameOverride = $payload['hostnameOverride'] ?? false; + $bannerDisabled = $payload['bannerDisabled'] ?? false; + $projectCheckDisabled = $payload['projectCheckDisabled'] ?? false; $scopes = \array_merge($payload['scopes'] ?? [], $scopes); - if ($projectId !== $project->getId()) { + if (!$projectCheckDisabled && $projectId !== $project->getId()) { return $guestKey; } @@ -122,7 +145,10 @@ class Key $scopes, $name, $expired, - $disabledMetrics + $disabledMetrics, + $hostnameOverride, + $bannerDisabled, + $projectCheckDisabled ); case API_KEY_STANDARD: $key = $project->find( diff --git a/src/Appwrite/Platform/Modules/Compute/Base.php b/src/Appwrite/Platform/Modules/Compute/Base.php index d31b731407..81e20b240f 100644 --- a/src/Appwrite/Platform/Modules/Compute/Base.php +++ b/src/Appwrite/Platform/Modules/Compute/Base.php @@ -172,14 +172,10 @@ class Base extends Action 'activate' => $activate, ])); - // Preview deployments for sites - $projectId = $project->getId(); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); - - $rule = Authorization::skip( + Authorization::skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 0a4907fdb7..15e5daff40 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -7,6 +7,8 @@ use Appwrite\Event\Event; use Appwrite\Event\Func; use Appwrite\Event\StatsUsage; use Appwrite\Messaging\Adapter\Realtime; +use Appwrite\Permission; +use Appwrite\Role; use Appwrite\Utopia\Response\Model\Deployment; use Appwrite\Vcs\Comment; use Exception; @@ -24,9 +26,11 @@ use Utopia\Database\Exception\Structure; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Fetch\Client as FetchClient; use Utopia\Logger\Log; use Utopia\Platform\Action; use Utopia\Queue\Message; +use Utopia\Storage\Compression\Compression; use Utopia\Storage\Device; use Utopia\Storage\Device\Local; use Utopia\System\System; @@ -56,9 +60,10 @@ class Builds extends Action ->inject('dbForProject') ->inject('deviceForFunctions') ->inject('isResourceBlocked') + ->inject('deviceForFiles') ->inject('log') - ->callback(fn ($message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Log $log) => - $this->action($message, $project, $dbForPlatform, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $isResourceBlocked, $log)); + ->callback(fn ($message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Device $deviceForFiles, Log $log) => + $this->action($message, $project, $dbForPlatform, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $isResourceBlocked, $deviceForFiles, $log)); } /** @@ -71,11 +76,12 @@ class Builds extends Action * @param Cache $cache * @param Database $dbForProject * @param Device $deviceForFunctions + * @param Device $deviceForFiles * @param Log $log * @return void * @throws \Utopia\Database\Exception */ - public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $queueForStatsUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Log $log): void + public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $queueForStatsUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Device $deviceForFiles, Log $log): void { $payload = $message->getPayload() ?? []; @@ -96,7 +102,7 @@ class Builds extends Action case BUILD_TYPE_RETRY: Console::info('Creating build for deployment: ' . $deployment->getId()); $github = new GitHub($cache); - $this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForStatsUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $isResourceBlocked, $log); + $this->buildDeployment($deviceForFunctions, $deviceForFiles, $queueForFunctions, $queueForEvents, $queueForStatsUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $isResourceBlocked, $log); break; default: @@ -106,6 +112,7 @@ class Builds extends Action /** * @param Device $deviceForFunctions + * @param Device $deviceForFiles * @param Func $queueForFunctions * @param Event $queueForEvents * @param StatsUsage $queueForStatsUsage @@ -122,7 +129,7 @@ class Builds extends Action * * @throws Exception */ - protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, StatsUsage $queueForStatsUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $resource, Document $deployment, Document $template, callable $isResourceBlocked, Log $log): void + protected function buildDeployment(Device $deviceForFunctions, Device $deviceForFiles, Func $queueForFunctions, Event $queueForEvents, StatsUsage $queueForStatsUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $resource, Document $deployment, Document $template, callable $isResourceBlocked, Log $log): void { $resourceKey = match($resource->getCollection()) { 'functions' => 'functionId', @@ -713,6 +720,109 @@ class Builds extends Action Console::success("Build id: $buildId created"); + if ($resource->getCollection() === 'sites') { + try { + $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ + Query::equal("projectInternalId", [$project->getInternalId()]), + Query::equal("resourceType", ["deployment"]), + Query::equal("resourceInternalId", [$deployment->getInternalId()]) + ])); + + if ($rule->isEmpty()) { + throw new \Exception("Rule for build not found"); + } + + $client = new FetchClient(); + $client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON); + + $bucket = $dbForPlatform->getDocument('buckets', 'screenshots'); + + $configs = [ + 'screenshotLight' => [ + 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], + 'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=light', + 'theme' => 'light' + ], + 'screenshotDark' => [ + 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], + 'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=dark', + 'theme' => 'dark' + ], + ]; + + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0); + $apiKey = $jwtObj->encode([ + 'hostnameOverride' => true, + 'bannerDisabled' => true, + 'projectCheckDisabled' => true + ]); + + // TODO: @Meldiron if becomes too slow, do concurrently + foreach ($configs as $key => $config) { + $config['headers'] = \array_merge($config['headers'] ?? [], [ + 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey + ]); + + $response = $client->fetch( + url: 'http://appwrite-browser:3000/v1/screenshots', + method: 'POST', + body: $config + ); + + if ($response->getStatusCode() >= 400) { + throw new \Exception("Screenshot failed to generate: " . $response->getBody()); + } + + $screenshot = $response->getBody(); + + $fileId = ID::unique(); + $fileName = $fileId . '.png'; + $path = $deviceForFiles->getPath($fileName); + $path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root + $success = $deviceForFiles->write($path, $screenshot, "image/png"); + + if (!$success) { + throw new \Exception("Screenshot failed to save"); + } + + $teamId = $project->getAttribute('teamId', ''); + $file = new Document([ + '$id' => $fileId, + '$permissions' => [ + Permission::read(Role::team(ID::custom($teamId))), + ], + 'bucketId' => $bucket->getId(), + 'bucketInternalId' => $bucket->getInternalId(), + 'name' => $fileName, + 'path' => $path, + 'signature' => $deviceForFiles->getFileHash($path), + 'mimeType' => $deviceForFiles->getFileMimeType($path), + 'sizeOriginal' => \strlen($screenshot), + 'sizeActual' => $deviceForFiles->getFileSize($path), + 'algorithm' => Compression::GZIP, + 'comment' => '', + 'chunksTotal' => 1, + 'chunksUploaded' => 1, + 'openSSLVersion' => null, + 'openSSLCipher' => null, + 'openSSLTag' => null, + 'openSSLIV' => null, + 'search' => implode(' ', [$fileId, $fileName]), + 'metadata' => ['content_type' => $deviceForFiles->getFileMimeType($path)], + ]); + $file = Authorization::skip(fn () => $dbForPlatform->createDocument('bucket_' . $bucket->getInternalId(), $file)); + + $deployment->setAttribute($key, $fileId); + } + + $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + } catch (\Throwable $th) { + Console::warning("Screenshot failed to generate:"); + Console::warning($th->getMessage()); + Console::warning($th->getTraceAsString()); + } + } + /** Set auto deploy */ if ($deployment->getAttribute('activate') === true) { $resource->setAttribute('deploymentInternalId', $deployment->getInternalId()); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php index 0c5cfc34e4..c76a4c3ffe 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php @@ -10,11 +10,14 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Helpers\ID; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\Storage\Device; +use Utopia\System\System; class Create extends Action { @@ -53,7 +56,9 @@ class Create extends Action ->param('siteId', '', new UID(), 'Site ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') ->inject('response') + ->inject('project') ->inject('dbForProject') + ->inject('dbForPlatform') ->inject('queueForEvents') ->inject('queueForBuilds') ->inject('deviceForSites') @@ -61,7 +66,7 @@ class Create extends Action ->callback([$this, 'action']); } - public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Build $queueForBuilds, Device $deviceForSites, Device $deviceForFunctions) + public function action(string $siteId, string $deploymentId, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents, Build $queueForBuilds, Device $deviceForSites, Device $deviceForFunctions) { $site = $dbForProject->getDocument('sites', $siteId); @@ -97,6 +102,24 @@ class Create extends Action 'search' => implode(' ', [$deploymentId]), ])); + // Preview deployments for sites + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $domain = ID::unique() . "." . $sitesDomain; + $ruleId = md5($domain); + Authorization::skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain, + 'resourceType' => 'deployment', + 'resourceId' => $deploymentId, + 'resourceInternalId' => $deployment->getInternalId(), + 'status' => 'verified', + 'certificateId' => '', + ])) + ); + $queueForBuilds ->setType(BUILD_TYPE_DEPLOYMENT) ->setResource($site) diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php index 38b9e2aeba..fb536a3123 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php @@ -228,14 +228,10 @@ class Create extends Action 'type' => $type ])); - // Preview deployments for sites - $projectId = $project->getId(); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); - - $rule = Authorization::skip( + Authorization::skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), @@ -283,14 +279,10 @@ class Create extends Action 'type' => $type ])); - // Preview deployments for sites - $projectId = $project->getId(); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); - - $rule = Authorization::skip( + Authorization::skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), 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 ec75740759..08be94776d 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -139,18 +139,15 @@ class Create extends Base 'activate' => $activate, ])); - // Preview deployments url - $projectId = $project->getId(); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $previewDomain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; - - $rule = Authorization::skip( + $domain = ID::unique() . "." . $sitesDomain; + $ruleId = md5($domain); + Authorization::skip( fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => \md5($previewDomain), + '$id' => $ruleId, 'projectId' => $project->getId(), 'projectInternalId' => $project->getInternalId(), - 'domain' => $previewDomain, + 'domain' => $domain, 'resourceType' => 'deployment', 'resourceId' => $deploymentId, 'resourceInternalId' => $deployment->getInternalId(), diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 5e894341c7..cfc9a3ff79 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -22,6 +22,7 @@ use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization as ValidatorAuthorization; use Utopia\DSN\DSN; use Utopia\Logger\Log; use Utopia\Platform\Action; @@ -92,13 +93,13 @@ class Deletes extends Action $this->deleteProject($dbForPlatform, $getProjectDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $document); break; case DELETE_TYPE_SITES: - $this->deleteSite($dbForPlatform, $getProjectDB, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $document, $certificates, $project); + $this->deleteSite($dbForPlatform, $getProjectDB, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project); break; case DELETE_TYPE_FUNCTIONS: $this->deleteFunction($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $certificates, $document, $project); break; case DELETE_TYPE_DEPLOYMENTS: - $this->deleteDeployment($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $certificates, $project); + $this->deleteDeployment($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project); break; case DELETE_TYPE_USERS: $this->deleteUser($getProjectDB, $document, $project); @@ -749,7 +750,7 @@ class Deletes extends Action * @return void * @throws Exception */ - private function deleteSite(Database $dbForPlatform, callable $getProjectDB, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, CertificatesAdapter $certificates, Document $project): void + private function deleteSite(Database $dbForPlatform, callable $getProjectDB, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForFiles, Document $document, CertificatesAdapter $certificates, Document $project): void { $dbForProject = $getProjectDB($project); $siteId = $document->getId(); @@ -783,9 +784,10 @@ class Deletes extends Action $deploymentInternalIds = []; $this->deleteByGroup('deployments', [ Query::equal('resourceInternalId', [$siteInternalId]) - ], $dbForProject, function (Document $document) use ($deviceForFunctions, &$deploymentInternalIds) { + ], $dbForProject, function (Document $document) use ($deviceForFunctions, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) { $deploymentInternalIds[] = $document->getInternalId(); $this->deleteDeploymentFiles($deviceForFunctions, $document); + $this->deleteDeploymentScreenshots($deviceForFiles, $dbForPlatform, $document); }); /** @@ -928,6 +930,53 @@ class Deletes extends Action $this->deleteRuntimes($getProjectDB, $document, $project); } + private function deleteDeploymentScreenshots(Device $deviceForFiles, Database $dbForPlatform, Document $deployment): void + { + $screenshotIds = []; + if (!empty($deployment->getAttribute('screenshotLight', ''))) { + $screenshotIds[] = $deployment->getAttribute('screenshotLight', ''); + } + if (!empty($deployment->getAttribute('screenshotDark', ''))) { + $screenshotIds[] = $deployment->getAttribute('screenshotDark', ''); + } + + if (empty($screenshotIds)) { + return; + } + Console::info("Deleting screenshots for deployment " . $deployment->getId()); + + $bucket = ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); + if ($bucket->isEmpty()) { + Console::error('Failed to get bucket for deployment screenshots'); + return; + } + + foreach ($screenshotIds as $id) { + $file = ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('bucket_' . $bucket->getInternalId(), $id)); + + if ($file->isEmpty()) { + Console::error('Failed to get deployment screenshot: ' . $id); + continue; + } + + $path = $file->getAttribute('path', ''); + + try { + if ($deviceForFiles->delete($path, true)) { + Console::success('Deleted deployment screenshot: ' . $path); + } else { + Console::error('Failed to delete deployment screenshot: ' . $path); + } + } catch (\Throwable $th) { + Console::error('Failed to delete deployment screenshot: ' . $path); + Console::error('[Error] Type: ' . get_class($th)); + Console::error('[Error] Message: ' . $th->getMessage()); + Console::error('[Error] File: ' . $th->getFile()); + Console::error('[Error] Line: ' . $th->getLine()); + } + } + } + /** * @param Device $device * @param Document $deployment @@ -999,7 +1048,7 @@ class Deletes extends Action * @return void * @throws Exception */ - private function deleteDeployment(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, CertificatesAdapter $certificates, Document $project): void + private function deleteDeployment(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForFiles, Document $document, CertificatesAdapter $certificates, Document $project): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); @@ -1011,6 +1060,11 @@ class Deletes extends Action */ $this->deleteDeploymentFiles($deviceForFunctions, $document); //TODO: For sites, this should be deviceForSites + /** + * Delete deployment screenshots + */ + $this->deleteDeploymentScreenshots($deviceForFiles, $dbForPlatform, $document); + /** * Delete builds */ diff --git a/src/Appwrite/Utopia/Response/Model/Deployment.php b/src/Appwrite/Utopia/Response/Model/Deployment.php index 0e49c82f82..6c13fd4b9e 100644 --- a/src/Appwrite/Utopia/Response/Model/Deployment.php +++ b/src/Appwrite/Utopia/Response/Model/Deployment.php @@ -76,6 +76,18 @@ class Deployment extends Model 'default' => false, 'example' => true, ]) + ->addRule('screenshotLight', [ + 'type' => self::TYPE_STRING, + 'description' => 'Screenshot with light theme preference file ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('screenshotDark', [ + 'type' => self::TYPE_STRING, + 'description' => 'Screenshot with dark theme preference file ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) ->addRule('status', [ 'type' => self::TYPE_STRING, 'description' => 'The deployment status. Possible values are "processing", "building", "waiting", "ready", and "failed".', diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index ce5df8b746..b7624c24a1 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -51,6 +51,18 @@ trait FunctionsBase $this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); }, 50000, 500); + // Not === so multipart/form-data works fine too + if (($params['activate'] ?? false) == true) { + $this->assertEventually(function () use ($functionId, $deploymentId) { + $function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + $this->assertEquals($deploymentId, $function['body']['deployment'], 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + } + return $deploymentId; } diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index 4fcd34572d..4c11e78d76 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -51,6 +51,18 @@ trait SitesBase $this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); }, 100000, 500); + // Not === so multipart/form-data works fine too + if (($params['activate'] ?? false) == true) { + $this->assertEventually(function () use ($siteId, $deploymentId) { + $site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + $this->assertEquals($deploymentId, $site['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($site['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + } + return $deploymentId; } diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index eefa0ddcbb..1961e40ce5 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -313,10 +313,7 @@ class SitesCustomServerTest extends Scope $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Env variable is Appwrite", $response['body']); @@ -1249,19 +1246,13 @@ class SitesCustomServerTest extends Scope $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Astro Blog", $response['body']); $this->assertStringContainsString("Hello, Astronaut!", $response['body']); - $response = $proxyClient->call(Client::METHOD_GET, '/about', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/about'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Astro Blog", $response['body']); @@ -1300,10 +1291,7 @@ class SitesCustomServerTest extends Scope $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringNotContainsString("This domain is not connected to any Appwrite resource yet", $response['body']); @@ -1350,10 +1338,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals(0, $rules['body']['total']); }, 50000, 500); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(401, $response['headers']['status-code']); $this->assertStringContainsString("This domain is not connected to any Appwrite resource yet", $response['body']); @@ -1416,10 +1401,7 @@ class SitesCustomServerTest extends Scope $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Hello Appwrite", $response['body']); @@ -1430,10 +1412,7 @@ class SitesCustomServerTest extends Scope $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $previewDomain); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Hello Appwrite", $response['body']); @@ -1500,5 +1479,77 @@ class SitesCustomServerTest extends Scope $this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']); } + public function testSiteScreenshot(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Themed site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '', + ]); + + $this->assertNotEmpty($siteId); + + $domain = $this->setupSiteDomain($siteId); + + $deploymentId = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static-themed'), + 'activate' => 'true' + ]); + + $this->assertNotEmpty($deploymentId); + + $domain = $this->getSiteDomain($siteId); + $this->assertNotEmpty($domain); + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Themed website", $response['body']); + $this->assertStringContainsString("@media (prefers-color-scheme: dark)", $response['body']); + + $deployment = $this->getDeployment($siteId, $deploymentId); + + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['screenshotLight']); + $this->assertNotEmpty($deployment['body']['screenshotDark']); + + $screenshotId = $deployment['body']['screenshotLight']; + $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin", array_merge([ + ], $this->getHeaders())); + + $this->assertEquals(200, $file['headers']['status-code']); + $this->assertNotEmpty(200, $file['body']); + $this->assertGreaterThan(1, $file['headers']['content-length']); + $this->assertEquals('image/png', $file['headers']['content-type']); + + $screenshotHash = \md5($file['body']); + $this->assertNotEmpty($screenshotHash); + + $screenshotId = $deployment['body']['screenshotDark']; + $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin", array_merge([ + ], $this->getHeaders())); + + $this->assertEquals(200, $file['headers']['status-code']); + $this->assertNotEmpty(200, $file['body']); + $this->assertGreaterThan(1, $file['headers']['content-length']); + $this->assertEquals('image/png', $file['headers']['content-type']); + + $screenshotDarkHash = \md5($file['body']); + $this->assertNotEmpty($screenshotDarkHash); + + $this->assertNotEquals($screenshotDarkHash, $screenshotHash); + + $this->cleanupSite($siteId); + } + // TODO: Add tests for deletion of resources when site is deleted } diff --git a/tests/resources/sites/static-themed/index.html b/tests/resources/sites/static-themed/index.html new file mode 100644 index 0000000000..955696b473 --- /dev/null +++ b/tests/resources/sites/static-themed/index.html @@ -0,0 +1,23 @@ + + +
+ + +