diff --git a/assets/js/components/dashboard.js b/assets/js/components/dashboard.js index b658e4c2..da5a4137 100644 --- a/assets/js/components/dashboard.js +++ b/assets/js/components/dashboard.js @@ -97,10 +97,7 @@ module.exports = () => ({ continue; } - if ( - null === project.currentEnvironment && null === environment - || (null !== project.currentEnvironment && null !== environment && project.currentEnvironment.code === environment.code) - ) { + if (null !== project.currentEnvironment && project.currentEnvironment.code === environment.code) { continue; } @@ -162,8 +159,12 @@ module.exports = () => ({ query.push(`endDate=${encodeURIComponent(end.format('YYYY-MM-DD'))}`); query.push(`projects[]=${encodeURIComponent(project.code)}`); - if (null !== project.currentEnvironment) { - query.push(`environment=${encodeURIComponent(project.currentEnvironment.code)}`); + if (project.environments.length) { + if (null !== project.currentEnvironment && null !== project.currentEnvironment.code) { + query.push(`environment=${encodeURIComponent(project.currentEnvironment.code)}`); + } + } else { + query.push('environment=*'); } const promise = fetch(this.request.endpoint + '?' + query.join('&'), { @@ -248,6 +249,10 @@ module.exports = () => ({ throw new Error('Invalid project data, the required keys are "code", "name" and "color".'); } + if (project.environments.length) { + project.currentEnvironment = project.environments[0]; + } + this.projects.push(project); }, diff --git a/src/Api/Internal/Controller/StatisticsController.php b/src/Api/Internal/Controller/StatisticsController.php index aab0f6f5..f2cc59aa 100644 --- a/src/Api/Internal/Controller/StatisticsController.php +++ b/src/Api/Internal/Controller/StatisticsController.php @@ -110,7 +110,7 @@ private function buildData(array $projectIdsByCodes, string $locale, DateTimeImm } $period = Period::create($startDate, $endDate); - $environmentImpl = '//default//' === $environment ? new DefaultEnvironment() : (is_string($environment) ? new NamedEnvironment($environment) : null); + $environmentImpl = empty($environment) ? new DefaultEnvironment() : ('*' !== $environment ? new NamedEnvironment($environment) : null); foreach ($projectIdsByCodes as $code => $projectId) { $consentStatistics = $this->projectStatisticsCalculator->calculateConsentStatistics( diff --git a/src/Api/V1/Controller/CookiesController.php b/src/Api/V1/Controller/CookiesController.php index 2d79691a..1e5d5ba5 100644 --- a/src/Api/V1/Controller/CookiesController.php +++ b/src/Api/V1/Controller/CookiesController.php @@ -15,6 +15,7 @@ use App\Application\Cookie\TemplateRendererInterface; use App\Application\GlobalSettings\EnabledEnvironmentsResolver; use App\Application\GlobalSettings\GlobalSettingsInterface; +use App\Domain\GlobalSettings\ValueObject\Environment; use App\Domain\Project\ValueObject\Environments; use App\Domain\Project\ValueObject\ProjectId; use App\Domain\Shared\ValueObject\Locale; @@ -40,12 +41,17 @@ public function __construct( private readonly EtagStoreInterface $etagStore, ) {} - public static function getTemplateUrl(string $projectCode, ?string $locale = null): string + public static function getTemplateUrl(string $projectCode, ?string $locale = null, ?string $environment = null): string { + $query = http_build_query([ + 'locale' => $locale, + 'environment' => $environment, + ]); + return sprintf( '/api/v1/cookies/%s/template%s', $projectCode, - null !== $locale ? '?locale=' . $locale : '', + '' !== $query ? ('?' . $query) : '', ); } @@ -104,12 +110,13 @@ public function getJson(ApiRequest $request, ApiResponse $response): ApiResponse $projectCode = $request->getParameter('project'); $requestEntity = $request->getEntity(); assert($requestEntity instanceof CookiesRequestBody); + $environment = empty($requestEntity->environment) ? null : $requestEntity->environment; $etagKey = sprintf( '%s/%s/%s/[%s]/json', $projectCode, $requestEntity->locale ?? '_', - $requestEntity->environment ?? '__default__', + $environment ?? '#default', implode(',', (array) $requestEntity->category), ); @@ -130,7 +137,7 @@ public function getJson(ApiRequest $request, ApiResponse $response): ApiResponse ]); } - $errorResponse = $this->tryCreateErrorResponseOnInvalidEnvironment($project->environments, $requestEntity, $response); + $errorResponse = $this->tryCreateErrorResponseOnInvalidEnvironment($project->environments, $environment, $response); if (null !== $errorResponse) { return $errorResponse; @@ -142,7 +149,13 @@ public function getJson(ApiRequest $request, ApiResponse $response): ApiResponse $responseBody = json_encode([ 'status' => 'success', - 'data' => $this->getCookiesData($project->id, $locale, $project->locales->defaultLocale(), $requestEntity->category, $requestEntity->environment), + 'data' => $this->getCookiesData( + projectId: $project->id, + locale: $locale, + defaultLocale: $project->locales->defaultLocale(), + environments: $this->createEnvironmentsForQuery($environment, $project->environments), + categories: $requestEntity->category, + ), ], JSON_THROW_ON_ERROR); $response = $response->withStatus(ApiResponse::S200_OK) @@ -169,12 +182,13 @@ public function getTemplate(ApiRequest $request, ApiResponse $response): ApiResp $projectCode = $request->getParameter('project'); $requestEntity = $request->getEntity(); assert($requestEntity instanceof CookiesRequestBody); + $environment = empty($requestEntity->environment) ? null : $requestEntity->environment; $etagKey = sprintf( '%s/%s/%s/[%s]/html', $projectCode, $requestEntity->locale ?? '_', - $requestEntity->environment ?? '__default__', + $environment ?? '#default', implode(',', (array) $requestEntity->category), ); @@ -195,7 +209,7 @@ public function getTemplate(ApiRequest $request, ApiResponse $response): ApiResp ]); } - $errorResponse = $this->tryCreateErrorResponseOnInvalidEnvironment($projectTemplate->environments, $requestEntity, $response); + $errorResponse = $this->tryCreateErrorResponseOnInvalidEnvironment($projectTemplate->environments, $environment, $response); if (null !== $errorResponse) { return $errorResponse; @@ -205,14 +219,20 @@ public function getTemplate(ApiRequest $request, ApiResponse $response): ApiResp ? Locale::fromValue($requestEntity->locale) : $projectTemplate->projectLocalesConfig->defaultLocale(); - $data = $this->getCookiesData($projectTemplate->projectId, $locale, $projectTemplate->projectLocalesConfig->defaultLocale(), $requestEntity->category, $requestEntity->environment); + $data = $this->getCookiesData( + projectId: $projectTemplate->projectId, + locale: $locale, + defaultLocale: $projectTemplate->projectLocalesConfig->defaultLocale(), + environments: $this->createEnvironmentsForQuery($environment, $projectTemplate->environments), + categories: $requestEntity->category, + ); $data = json_encode($data, JSON_THROW_ON_ERROR); $data = json_decode($data, false, 512, JSON_THROW_ON_ERROR); $template = Template::create( $projectTemplate->projectId->toString(), $projectTemplate->template->value(), - TemplateArguments::create($data->providers, $data->cookies, $requestEntity->environment), + TemplateArguments::create($data->providers, $data->cookies, $environment), ); $responseBody = $this->templateRenderer->render($template); @@ -257,24 +277,25 @@ private function applyCacheHeaders(string $etagKey, string $responseBody, ApiRes /** * @param mixed $categories */ - private function getCookiesData(ProjectId $projectId, Locale $locale, ?Locale $defaultLocale, string|array|null $categories = null, ?string $environment = null): array + private function getCookiesData(ProjectId $projectId, Locale $locale, ?Locale $defaultLocale, array $environments, string|array|null $categories = null): array { $data = [ 'providers' => [], 'cookies' => [], ]; - $query = FindCookiesForApiQuery::create($projectId->toString(), null !== $defaultLocale && $defaultLocale->equals($locale) ? null : $locale->value()) - ->withBatchSize(100); + $query = FindCookiesForApiQuery::create( + projectId: $projectId->toString(), + environments: $environments, + locale: null !== $defaultLocale && $defaultLocale->equals($locale) ? null : $locale->value(), + ); + + $query = $query->withBatchSize(100); if (null !== $categories) { $query = $query->withCategoryCodes((array) $categories); } - if (null !== $environment) { - $query = $query->withEnvironment($environment); - } - foreach ($this->queryBus->dispatch($query) as $batch) { assert($batch instanceof Batch); @@ -308,9 +329,29 @@ private function getCookiesData(ProjectId $projectId, Locale $locale, ?Locale $d return $data; } - private function tryCreateErrorResponseOnInvalidEnvironment(Environments $projectEnvironments, CookiesRequestBody $body, ApiResponse $response): ?ApiResponse + /** + * @return array + */ + private function createEnvironmentsForQuery(?string $environment, Environments $projectEnvironments): array + { + if ('*' !== $environment) { + return [$environment]; + } + + $environments = array_map( + static fn (Environment $environment): string => $environment->code, + EnabledEnvironmentsResolver::resolveProjectEnvironments( + globalSettingsEnvironments: $this->globalSettings->environments(), + projectEnvironments: $projectEnvironments, + ), + ); + + return [null, ...$environments]; + } + + private function tryCreateErrorResponseOnInvalidEnvironment(Environments $projectEnvironments, ?string $environment, ApiResponse $response): ?ApiResponse { - if (null === $body->environment || '' === $body->environment) { + if (null === $environment || '*' === $environment) { return null; } @@ -319,8 +360,8 @@ private function tryCreateErrorResponseOnInvalidEnvironment(Environments $projec projectEnvironments: $projectEnvironments, ); - foreach ($environments as $environment) { - if ($environment->code === $body->environment) { + foreach ($environments as $env) { + if ($env->code === $environment) { return null; } } @@ -332,7 +373,7 @@ private function tryCreateErrorResponseOnInvalidEnvironment(Environments $projec 'code' => ApiResponse::S400_BAD_REQUEST, 'error' => sprintf( 'Project does not have the "%s" environment.', - $body->environment, + $environment, ), ], ]); diff --git a/src/Domain/GlobalSettings/ValueObject/Environment.php b/src/Domain/GlobalSettings/ValueObject/Environment.php index ec1e62aa..2ba30a96 100644 --- a/src/Domain/GlobalSettings/ValueObject/Environment.php +++ b/src/Domain/GlobalSettings/ValueObject/Environment.php @@ -42,6 +42,10 @@ public static function fromNative(mixed $native): self } } + if (!preg_match('/^[a-z0-9_\-\.]+$/', $native['code'])) { + throw UnableToCreateEnvironmentFromNativeValue::invalidNativeValueType($key, 'a string that matches the pattern "^[a-z0-9_\-\.]+$".'); + } + return new Environment( code: $native['code'], name: $native['name'], diff --git a/src/Infrastructure/Cookie/Doctrine/ReadModel/FindCookiesForApiQueryHandler.php b/src/Infrastructure/Cookie/Doctrine/ReadModel/FindCookiesForApiQueryHandler.php index 88830a96..39c9e89f 100644 --- a/src/Infrastructure/Cookie/Doctrine/ReadModel/FindCookiesForApiQueryHandler.php +++ b/src/Infrastructure/Cookie/Doctrine/ReadModel/FindCookiesForApiQueryHandler.php @@ -13,6 +13,7 @@ use App\ReadModel\Cookie\FindCookiesForApiQuery; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query\Expr\Join; +use Doctrine\ORM\Query\Expr\Orx; use Generator; use SixtyEightPublishers\ArchitectureBundle\Infrastructure\Doctrine\ReadModel\BatchGeneratorFactory; use SixtyEightPublishers\ArchitectureBundle\ReadModel\Query\QueryHandlerInterface; @@ -37,11 +38,20 @@ public function __invoke(FindCookiesForApiQuery $query): Generator ->leftJoin(ProjectHasCookieProvider::class, 'phc', Join::WITH, 'phc.cookieProviderId = cp.id AND phc.project = p') ->where('c.deletedAt IS NULL') ->andWhere('c.active = true') - ->andWhere('(c.allEnvironments = true OR JSONB_CONTAINS(c.environments, :environment) = true)') ->andWhere('(phc.id IS NOT NULL OR cp.id = p.cookieProviderId)') ->orderBy('c.createdAt', 'ASC') - ->setParameter('projectId', $query->projectId()) - ->setParameter('environment', json_encode([$query->environment()])); + ->setParameter('projectId', $query->projectId()); + + $environmentsConditions = [ + 'c.allEnvironments = true', + ]; + + foreach ($query->environments() as $index => $environment) { + $environmentsConditions[] = "JSONB_CONTAINS(c.environments, :environment_$index) = true"; + $qb->setParameter('environment_' . $index, json_encode([$environment])); + } + + $qb->andWhere(new Orx($environmentsConditions)); $qb->leftJoin('c.translations', 'ct_default', Join::WITH, 'ct_default.locale = p.locales.defaultLocale') ->leftJoin('cat.translations', 'catt_default', Join::WITH, 'catt_default.locale = p.locales.defaultLocale') diff --git a/src/ReadModel/Cookie/FindCookiesForApiQuery.php b/src/ReadModel/Cookie/FindCookiesForApiQuery.php index a222da1e..7b9e41f6 100644 --- a/src/ReadModel/Cookie/FindCookiesForApiQuery.php +++ b/src/ReadModel/Cookie/FindCookiesForApiQuery.php @@ -11,10 +11,14 @@ */ final class FindCookiesForApiQuery extends AbstractBatchedQuery { - public static function create(string $projectId, ?string $locale = null): self + /** + * @param non-empty-list $environments + */ + public static function create(string $projectId, array $environments, ?string $locale = null): self { return self::fromParameters([ 'project_id' => $projectId, + 'environments' => $environments, 'locale' => $locale, ]); } @@ -27,11 +31,6 @@ public function withCategoryCodes(array $categoryCodes): self return $this->withParam('category_codes', $categoryCodes); } - public function withEnvironment(string $environment): self - { - return $this->withParam('environment', $environment); - } - public function projectId(): string { return $this->getParam('project_id'); @@ -50,8 +49,11 @@ public function categoryCodes(): ?array return $this->getParam('category_codes'); } - public function environment(): ?string + /** + * @return non-empty-list + */ + public function environments(): array { - return $this->getParam('environment'); + return $this->getParam('environments'); } } diff --git a/src/Web/AdminModule/ApplicationModule/Control/EnvironmentsForm/EnvironmentsFormControl.php b/src/Web/AdminModule/ApplicationModule/Control/EnvironmentsForm/EnvironmentsFormControl.php index 2c851400..bfa3d5ca 100644 --- a/src/Web/AdminModule/ApplicationModule/Control/EnvironmentsForm/EnvironmentsFormControl.php +++ b/src/Web/AdminModule/ApplicationModule/Control/EnvironmentsForm/EnvironmentsFormControl.php @@ -50,6 +50,7 @@ protected function createComponentForm(): Form $container->addText('code') ->setHtmlAttribute('placeholder', 'code.placeholder') ->setRequired('code.required') + ->addRule(Form::Pattern, 'code.rule.pattern', '[a-z0-9_\-\.]+') ->addRule(UniqueMultiplierValuesValidator::Validator, 'code.rule.values_are_not_unique'); $container->addText('name') diff --git a/src/Web/AdminModule/CookieModule/Control/CookieForm/templates/form.imports.latte b/src/Web/AdminModule/CookieModule/Control/CookieForm/templates/form.imports.latte index 3008ef2a..9b14b575 100644 --- a/src/Web/AdminModule/CookieModule/Control/CookieForm/templates/form.imports.latte +++ b/src/Web/AdminModule/CookieModule/Control/CookieForm/templates/form.imports.latte @@ -40,7 +40,7 @@
{if '' === $k}