diff --git a/.gitignore b/.gitignore index cdce28a..b6001a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /.idea/ vendor + +.DS_Store diff --git a/composer.json b/composer.json index fbd9d84..f4d2bb0 100644 --- a/composer.json +++ b/composer.json @@ -14,13 +14,13 @@ } ], "require": { - "php": "^8.0", - "sulu/sulu": "^2.4", - "symfony/config": "^5.0 || ^6.0", - "symfony/dependency-injection": "^5.0 || ^6.0", - "symfony/framework-bundle": "^5.0 || ^6.0", - "symfony/http-foundation": "^5.0 || ^6.0", - "symfony/http-kernel": "^5.0 || ^6.0" + "php": "^8.2", + "sulu/sulu": "^3.0", + "symfony/config": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/http-foundation": "^6.4 || ^7.0", + "symfony/http-kernel": "^6.4 || ^7.0" }, "require-dev": { "dantleech/phpcr-migrations-bundle": "^1.3", diff --git a/docs/images/settings_details.png b/docs/images/settings_details.png new file mode 100644 index 0000000..ba325bc Binary files /dev/null and b/docs/images/settings_details.png differ diff --git a/docs/images/settings_integrations.png b/docs/images/settings_integrations.png new file mode 100644 index 0000000..fd1e24d Binary files /dev/null and b/docs/images/settings_integrations.png differ diff --git a/docs/images/settings_integrations_detail.png b/docs/images/settings_integrations_detail.png new file mode 100644 index 0000000..a435042 Binary files /dev/null and b/docs/images/settings_integrations_detail.png differ diff --git a/readme.md b/readme.md index d337433..fe3114d 100644 --- a/readme.md +++ b/readme.md @@ -15,9 +15,9 @@ It also allows you to manage the consent banner by using the [Tarteaucitron](htt ## Requirements -* PHP >= 8.0 -* Sulu >= 2.5.* -* Symfony >= 5.4 +* PHP >= 8.2 +* Sulu >= 3.0.* +* Symfony >= 6.4 * Composer ## Installation @@ -43,48 +43,158 @@ Enable the bundle by adding it to the list of registered bundles in the `config/ ``` ### Update schema (for dev environnement) + +Sulu uses two kernels, so run the schema update on the admin console: + ```shell script -bin/console do:sch:up --force +bin/adminconsole doctrine:schema:update --force +``` + +### Publish the assets + +The bundle ships the Tarteaucitron library and the consent runtime (`gdpr-consent.js`). +Publish them to your `public/` directory, otherwise the banner cannot load: + +```shell script +bin/adminconsole assets:install --symlink ``` ## Bundle Config -Define the Admin Api Route in `routes_admin.yaml` +Import the bundle's Admin API routes in `routes_admin.yaml` ```yaml -gdpr.setting_api: - type: rest +gdpr_admin_api: + resource: '@GDPRBundle/Resources/config/routing_admin.yaml' prefix: /admin/api - resource: pixel_gdpr.settings_route_controller - name_prefix: gdpr. ``` +### Render the banner + +Call the `gdpr_script()` Twig function in the `` of your layout (e.g. `master.html.twig`). +It renders nothing until you enable "Use cookies management?" in the settings. + +```twig + + {# ... #} + {{ gdpr_script() }} + +``` + +### Permissions + +The bundle registers the `gdpr_settings.settings` security context. Grant your role **View**, +**Add**, **Edit** and **Delete** for it under *Settings → Roles*, otherwise the GDPR settings and +integrations are hidden or read‑only. + +## Upgrading from v1 (single tracker → integrations) + +v2 replaces the fixed provider fields (single Google Analytics code, etc.) with the +**Integrations** list. If you have v1 data, migrate it **before** the schema drops the old columns: + +```shell script +# 1. create the new tables WITHOUT dropping the legacy columns yet +bin/adminconsole doctrine:schema:update --dump-sql # review +# run only the "CREATE TABLE gdpr_integration ..." statements, or use a migration + +# 2. copy the legacy tracking codes into integrations +bin/adminconsole gdpr:integrations:migrate-settings # --locales=de,en + +# 3. now let the schema drop the legacy columns and add foreign keys +bin/adminconsole doctrine:schema:update --force +``` + +On a fresh install (no v1 data) just run `bin/adminconsole doctrine:schema:update --force`. + +Because the Integrations list adds create/delete operations to the existing +`gdpr_settings.settings` security context, grant **Add** and **Delete** for that context to your +role under *Settings → Roles* after upgrading (an existing context does not auto‑grant newly added +permission types). + ## Use The bundle is only composed of the settings, which make the management of the GDPR very easy. To use the GDPR management of the bundle, just check the "Use cookies management?". All the other options should be display. -The **Services** section will take care to manage the different cookies scripts. - The **Parameters** section will help you manage the Tarteaucitron banner, which displays the consent banner. There are plenty of parameters, so don't hesitate to visit the repository of Tarteaucitron. +![GDPR settings](docs/images/settings_details.png) + +The **Integrations** tab is where you add the individual scripts/services that the banner asks +consent for (see below). + +## Integrations + +Each tracker, script or embed you want to gate behind consent is configured as an **integration** +on the **Integrations** tab of the GDPR settings. The list is localized — use the language switcher +to edit the texts shown in the banner per language. + +![Integrations list](docs/images/settings_integrations.png) + +Click **Add** (or a row) to open the full‑page form. The **Type** field decides which other fields +are shown: + +![Integration form](docs/images/settings_integrations_detail.png) + +| Type | What it does | Fields | +|------|--------------|--------| +| **Preconfigured provider** | Wires a known provider into Tarteaucitron for you. | *Provider* (Google Analytics, Google Tag Manager, Google Ads, Bing Ads, Facebook Pixel) + *Tracking ID* | +| **Custom inline script** | Runs the pasted JavaScript when the integration is accepted. | *Inline script* | +| **Custom external JS** | Injects ` - -{% if setting.googleTagManager %} - -{% endif %} - -{% if setting.googleAnalyticsGtagJs %} - -{% endif %} - -{% if setting.pixelFacebook %} - -{% endif %} - -{% if setting.googleAds %} - -{% endif %} - -{% if setting.bingAds %} - -{% endif %} - + \ No newline at end of file + window.gdpr.boot( + { + "privacyUrl": "{{ setting.privacyUrl }}", + "bodyPosition": "{{ setting.bodyPosition }}", + "hashtag": "{{ setting.hashtag }}", + "cookieName": "{{ setting.cookieName }}", + "orientation": "{{ setting.orientation }}", + "groupServices": {{ setting.groupServices ? 'true' : 'false' }}, + "showAlertSmall": {{ setting.showAlertSmall ? 'true' : 'false' }}, + "cookieslist": {{ setting.cookielist ? 'true' : 'false' }}, + "showIcon": {{ setting.showIcon ? 'true' : 'false' }}, + "iconPosition": "{{ setting.iconPosition }}", + "adblocker": {{ setting.adblocker ? 'true' : 'false' }}, + "DenyAllCta": {{ setting.denyAllCta ? 'true' : 'false' }}, + "AcceptAllCta": {{ setting.acceptAllCta ? 'true' : 'false' }}, + "highPrivacy": {{ setting.highPrivacy ? 'true' : 'false' }}, + "handleBrowserDNTRequest": {{ setting.handleBrowserDNTRequest ? 'true' : 'false' }}, + "removeCredit": {{ setting.removeCredit ? 'true' : 'false' }}, + "moreInfoLink": {{ setting.moreInfoLink ? 'true' : 'false' }}, + "useExternalCss": {{ setting.useExternalCss ? 'true' : 'false' }}, + "useExternalJs": {{ setting.useExternalJs ? 'true' : 'false' }}, + "readmoreLink": "{{ setting.readmoreLink }}", + "mandatory": {{ setting.mandatory ? 'true' : 'false' }}, + "mandatoryCta": {{ setting.mandatoryCta ? 'true' : 'false' }} + }, + {{ integrations|json_encode(constant('JSON_UNESCAPED_SLASHES') b-or constant('JSON_UNESCAPED_UNICODE'))|raw }} + ); + diff --git a/src/Twig/SettingsExtension.php b/src/Twig/SettingsExtension.php index e5b9d2e..2c4bb56 100644 --- a/src/Twig/SettingsExtension.php +++ b/src/Twig/SettingsExtension.php @@ -3,23 +3,23 @@ namespace Pixel\GDPRBundle\Twig; use Doctrine\ORM\EntityManagerInterface; +use Pixel\GDPRBundle\Entity\Integration; use Pixel\GDPRBundle\Entity\Setting; +use Symfony\Component\HttpFoundation\RequestStack; use Twig\Environment; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; class SettingsExtension extends AbstractExtension { - private EntityManagerInterface $entityManager; - private Environment $environment; - - public function __construct(EntityManagerInterface $entityManager, Environment $environment) - { - $this->entityManager = $entityManager; - $this->environment = $environment; + public function __construct( + private EntityManagerInterface $entityManager, + private Environment $environment, + private RequestStack $requestStack, + ) { } - public function getFunctions() + public function getFunctions(): array { return [ new TwigFunction("gdpr_settings", [$this, "gdprSettings"]), @@ -31,22 +31,34 @@ public function getFunctions() public function gdprSettings(): Setting { - return $this->entityManager->getRepository(Setting::class)->findOneBy([]); + return $this->entityManager->getRepository(Setting::class)->findOneBy([]) ?? new Setting(); } public function gdprScript(): ?string { $setting = $this->entityManager->getRepository(Setting::class)->findOneBy([]); - if ($setting === null) { + if (null === $setting) { $setting = new Setting(); $setting->setUseCookieHandling(false); } - $useCookieHandling = $setting->getUseCookieHandling(); - if ($useCookieHandling) { - return $this->environment->render("@GDPR/twig/scripts.html.twig", [ - "setting" => $setting, - ]); + if (!$setting->getUseCookieHandling()) { + return null; + } + + $request = $this->requestStack->getCurrentRequest(); + $locale = \explode('_', $request ? $request->getLocale() : 'en')[0]; + + $integrations = []; + foreach ( + $this->entityManager->getRepository(Integration::class)->findBy(['enabled' => true], ['position' => 'ASC']) + as $integration + ) { + $integrations[] = $integration->toFrontendArray($locale); } - return null; + + return $this->environment->render("@GDPR/twig/scripts.html.twig", [ + "setting" => $setting, + "integrations" => $integrations, + ]); } }