From 958a93c1834878a4857c82116a9513dad3dc8fee Mon Sep 17 00:00:00 2001 From: Markus Bruns Date: Wed, 25 Oct 2023 16:38:49 +0200 Subject: [PATCH 01/11] - replaced vendorIds with Sourcepoint Ids - replaced purposeIds with Sourcepoint Ids - updated Interface (int Ids Liveramp vs string Ids Sourcepoint) --- src/StaticConsentData.php | 139 ++++++++++++++++++----------- src/StaticConsentDataInterface.php | 129 ++++++++++++++++++++++++-- 2 files changed, 205 insertions(+), 63 deletions(-) diff --git a/src/StaticConsentData.php b/src/StaticConsentData.php index 9eac05b2..2f645f31 100644 --- a/src/StaticConsentData.php +++ b/src/StaticConsentData.php @@ -26,124 +26,139 @@ public function getAll() { $this->data = [ // Facebook. static::VENDOR_FACEBOOK => [ - 'id' => 10007, + 'id' => '5e716fc09a0b5040d575080f', 'label' => $this->t('Facebook', [], ['context' => 'Cookie consent provider']), // @todo Toggle label should be translatable for Facebook posts. 'toggle_label' => 'Facebook-Posts anzeigen', 'purposes' => [ - 29, + static::PURPOSE_STORE_AND_OR_ACCESS_INFORMATION_ON_A_DEVICE, + static::PURPOSE_FUNCTIONAL, + static::PURPOSE_SOCIAL_MEDIA, ], ], // Google Maps. static::VENDOR_GOOGLE_MAPS => [ - 'id' => 10219, + 'id' => '5eb97b265852312e6a9fbf31', 'label' => $this->t('Google Maps', [], ['context' => 'Cookie consent provider']), // @todo Toggle label should be translatable for Google Maps. 'toggle_label' => 'Google Maps anzeigen', 'purposes' => [ - 26, + static::PURPOSE_STORE_AND_OR_ACCESS_INFORMATION_ON_A_DEVICE, + static::PURPOSE_SOCIAL_MEDIA, ], ], // Instagram. static::VENDOR_INSTAGRAM => [ - 'id' => 10019, + 'id' => '6054c53ca228639c6f285121', 'label' => $this->t('Instagram', [], ['context' => 'Cookie consent provider']), // @todo Toggle label should be translatable for Instagram posts. 'toggle_label' => 'Instagram-Posts anzeigen', 'purposes' => [ - 29, + static::PURPOSE_STORE_AND_OR_ACCESS_INFORMATION_ON_A_DEVICE, + static::PURPOSE_FUNCTIONAL, + static::PURPOSE_SOCIAL_MEDIA, ], ], // Pinterest. static::VENDOR_PINTEREST => [ - 'id' => 10031, + 'id' => '5e839a38b8e05c4e491e738e', 'label' => $this->t('Pinterest', [], ['context' => 'Cookie consent provider']), // @todo Toggle label should be translatable for Pinterest posts. 'toggle_label' => 'Pinterest-Posts anzeigen', 'purposes' => [ - 29, + static::PURPOSE_STORE_AND_OR_ACCESS_INFORMATION_ON_A_DEVICE, + static::PURPOSE_FUNCTIONAL, + static::PURPOSE_SOCIAL_MEDIA, ], ], - // Retyp LLC (OptinMonster). - static::VENDOR_RETYP_LLC => [ - 'id' => 10195, - 'label' => $this->t('Retyp LLC', [], ['context' => 'Cookie consent provider']), - // @todo Toggle label for Retyp LLC / OptinMonster (if needed). - 'toggle_label' => NULL, - 'purposes' => [ - 26, - ], - ], - - // Riddle Technologies AG. - static::VENDOR_RIDDLE => [ - 'id' => 10196, - 'label' => $this->t('Riddle Technologies AG', [], ['context' => 'Cookie consent provider']), - // @todo Toggle label should be translatable for Riddle polls. - 'toggle_label' => 'Riddle-Umfragen anzeigen', - 'purposes' => [ - 26, - 27, - ], - ], +// // Retyp LLC (OptinMonster). +// static::VENDOR_RETYP_LLC => [ +// // @TODO: Missing in Sourcepoint.. +// 'id' => 10195, +// 'label' => $this->t('Retyp LLC', [], ['context' => 'Cookie consent provider']), +// // @todo Toggle label for Retyp LLC / OptinMonster (if needed). +// 'toggle_label' => NULL, +// 'purposes' => [], +// ], +// +// // Riddle Technologies AG. +// static::VENDOR_RIDDLE => [ +// // @TODO: Missing in Sourcepoint.. +// 'id' => 10196, +// 'label' => $this->t('Riddle Technologies AG', [], ['context' => 'Cookie consent provider']), +// // @todo Toggle label should be translatable for Riddle polls. +// 'toggle_label' => 'Riddle-Umfragen anzeigen', +// 'purposes' => [], +// ], // TargetVideo GmbH. static::VENDOR_TARGETVIDEO_GMBH => [ - 'id' => 10200, + 'id' => '5f0838a5b8e05c065164a384', 'label' => $this->t('TargetVideo GmbH', [], ['context' => 'Cookie consent provider']), // @todo Toggle label for TargetVideo GmbH (if needed). 'toggle_label' => NULL, 'purposes' => [ - 28, + static::PURPOSE_MEASURE_CONTENT_PERFORMANCE ], ], - // Twitter. + // Twitter. / X Corp. static::VENDOR_TWITTER => [ - 'id' => 10006, + 'id' => '5e71760b69966540e4554f01', 'label' => $this->t('Twitter', [], ['context' => 'Cookie consent provider']), // @todo Toggle label should be translatable for Twitter posts. 'toggle_label' => 'Twitter-Posts anzeigen', 'purposes' => [ - 29, + static::PURPOSE_STORE_AND_OR_ACCESS_INFORMATION_ON_A_DEVICE, + static::PURPOSE_FUNCTIONAL, + static::PURPOSE_SOCIAL_MEDIA, ], ], // Vimeo. static::VENDOR_VIMEO => [ - 'id' => 10021, + 'id' => '5eac148d4bfee33e7280d13b', 'label' => $this->t('Vimeo', [], ['context' => 'Cookie consent provider']), // @todo Toggle label should be translatable for Vimeo videos. 'toggle_label' => 'Vimeo-Videos anzeigen', 'purposes' => [ - 26, - 29, + static::PURPOSE_STORE_AND_OR_ACCESS_INFORMATION_ON_A_DEVICE, + static::PURPOSE_FUNCTIONAL, + static::PURPOSE_SOCIAL_MEDIA, ], ], // Xandr, Inc. static::VENDOR_XANDR => [ - 'id' => 32, + 'id' => '5e7ced57b8e05c4854221bba', 'label' => $this->t('Xandr, Inc.', [], ['context' => 'Cookie consent provider']), // @todo Toggle label for Xandr, Inc. (if needed). 'toggle_label' => NULL, // @todo Still needs correct purpose IDs for Xandr, Inc. - 'purposes' => [], + 'purposes' => [ + static::PURPOSE_STORE_AND_OR_ACCESS_INFORMATION_ON_A_DEVICE, + static::PURPOSE_USE_LIMITED_DATA_TO_SELECT_ADVERTISING, + static::PURPOSE_CREATE_PROFILES_FOR_PERSONALISED_ADVERTISING, + static::PURPOSE_USE_PROFILES_TO_SELECT_PERSONALISED_ADVERTISING, + static::PURPOSE_MEASURE_ADVERTISING_PERFORMANCE, + static::PURPOSE_DEVELOP_AND_IMPROVE_SERVICES, + ], ], // YouTube. static::VENDOR_YOUTUBE => [ - 'id' => 10020, + 'id' => '5e7ac3fae30e7d1bc1ebf5e8', 'label' => $this->t('YouTube', [], ['context' => 'Cookie consent provider']), // @todo Toggle label should be translatable for YouTube videos. 'toggle_label' => 'YouTube-Videos anzeigen', 'purposes' => [ - 26, - 29, + static::PURPOSE_STORE_AND_OR_ACCESS_INFORMATION_ON_A_DEVICE, + static::PURPOSE_FUNCTIONAL, + static::PURPOSE_SOCIAL_MEDIA, ], ], ]; @@ -158,20 +173,20 @@ public function getAll() { public function getToggleLabel($vendor) { $data = $this->getAll(); - if (is_numeric($vendor)) { + if ($this->isVendorId($vendor)) { $vendor = $this->getVendorName($vendor); } - return isset($data[$vendor]['toggle_label']) ? $data[$vendor]['toggle_label'] : NULL; + return $data[$vendor]['toggle_label'] ?? NULL; } /** * {@inheritdoc} */ - public function getVendorId($vendor) { + public function getVendorId($vendorName) { $data = $this->getAll(); - return isset($data[$vendor]['id']) ? $data[$vendor]['id'] : NULL; + return $data[$vendorName]['id'] ?? NULL; } /** @@ -180,20 +195,20 @@ public function getVendorId($vendor) { public function getVendorLabel($vendor) { $data = $this->getAll(); - if (is_numeric($vendor)) { + if ($this->isVendorId($vendor)) { $vendor = $this->getVendorName($vendor); } - return isset($data[$vendor]['label']) ? $data[$vendor]['label'] : NULL; + return $data[$vendor]['label'] ?? NULL; } /** * {@inheritdoc} */ - public function getVendorName($vendor) { + public function getVendorName($vendorId) { $data = $this->getAll(); - if (($key = array_search($vendor, array_column($data, 'id'))) !== FALSE) { + if (($key = array_search($vendorId, array_column($data, 'id'))) !== FALSE) { return array_keys($data)[$key]; } @@ -203,14 +218,14 @@ public function getVendorName($vendor) { /** * {@inheritdoc} */ - public function getPurposeIds($vendor) { + public function getPurposeIds($vendor): array { $data = $this->getAll(); - if (is_numeric($vendor)) { + if ($this->isVendorId($vendor)) { $vendor = $this->getVendorName($vendor); } - $purposes = isset($data[$vendor]['purposes']) ? $data[$vendor]['purposes'] : []; + $purposes = $data[$vendor]['purposes'] ?? []; // Ensure legitimate interest purpose. $purposes[] = 1; @@ -224,4 +239,20 @@ public function getPurposeIds($vendor) { return $purposes; } + /** + * {@inheritdoc} + */ + public function getAllVendorIds(): array { + $data = $this->getAll(); + return array_column($data, 'id'); + } + + /** + * {@inheritdoc} + */ + public function isVendorId(string $vendor): bool { + $vendorIds = $this->getAllVendorIds(); + return in_array($vendor, $vendorIds); + } + } diff --git a/src/StaticConsentDataInterface.php b/src/StaticConsentDataInterface.php index 96012b87..da0f1f21 100644 --- a/src/StaticConsentDataInterface.php +++ b/src/StaticConsentDataInterface.php @@ -62,6 +62,96 @@ interface StaticConsentDataInterface { */ const VENDOR_YOUTUBE = 'youtube'; + /** + * Purpose Name: Store and/or access information on a device. + */ + const PURPOSE_STORE_AND_OR_ACCESS_INFORMATION_ON_A_DEVICE = '6489861f44cf6406ddda4023'; + + /** + * Purpose Name: Use limited data to select advertising. + */ + const PURPOSE_USE_LIMITED_DATA_TO_SELECT_ADVERTISING = '6489861e44cf6406ddda28e9'; + + /** + * Purpose Name: Create profiles for personalised advertising. + */ + const PURPOSE_CREATE_PROFILES_FOR_PERSONALISED_ADVERTISING = '6489861e44cf6406ddda2c78'; + + /** + * Purpose Name: Use profiles to select personalised advertising. + */ + const PURPOSE_USE_PROFILES_TO_SELECT_PERSONALISED_ADVERTISING = '6489861e44cf6406ddda2fa3'; + + /** + * Purpose Name: Create profiles to personalise content. + */ + const PURPOSE_CREATE_PROFILES_TO_PERSONALISE_CONTENT = '6489861e44cf6406ddda32bb'; + + /** + * Purpose Name: Use profiles to select personalised content. + */ + const PURPOSE_USE_PROFILES_TO_SELECT_PERSONALISED_CONTENT = '6489861e44cf6406ddda33c5'; + + /** + * Purpose Name: Measure advertising performance. + */ + const PURPOSE_MEASURE_ADVERTISING_PERFORMANCE = '6489861e44cf6406ddda34a9'; + + /** + * Purpose Name: Measure content performance. + */ + const PURPOSE_MEASURE_CONTENT_PERFORMANCE = '6489861f44cf6406ddda38dc'; + + /** + * Purpose Name: Understand audiences through statistics or combinations ... + */ + const PURPOSE_UNDERSTAND_AUDIENCES_THROUGH_STATISTICS = '6489861f44cf6406ddda3a8c'; + + /** + * Develop and improve services. + */ + const PURPOSE_DEVELOP_AND_IMPROVE_SERVICES = '6489861f44cf6406ddda3cba'; + + /** + * Use limited data to select content. + */ + const PURPOSE_USE_LIMITED_DATA_TO_SELECT_CONTENT = '652692cc25bbd005067b4994'; + + /** + * Unbedingt erforderliche Cookies. + */ + const PURPOSE_ABSOLUTELY_REQUIRED_COOKIES = '6489861f44cf6406ddda4011'; + + /** + * Funktional. + */ + const PURPOSE_FUNCTIONAL = '6489861f44cf6406ddda4016'; + + /** + * Analytik. + */ + const PURPOSE_ANALYTIC = '6489861f44cf6406ddda401a'; + + /** + * Werbung (Nicht-IAB Anbieter). + */ + const PURPOSE_ADS_NON_IAB = '6489861f44cf6406ddda401d'; + + /** + * Soziale Medien. + */ + const PURPOSE_SOCIAL_MEDIA = '6489861f44cf6406ddda401f'; + + /** + * Direktes Marketing. + */ + const PURPOSE_DIRECT_MARKETING = '6493f84f36160804ecc46e5f'; + + /** + * Datenaustausch. + */ + const PURPOSE_DATA_EXCHANGE = '6493f84f36160804ecc46e64'; + /** * Return all static cookie consent data. * @@ -77,14 +167,23 @@ interface StaticConsentDataInterface { */ public function getAll(); + /** + * Return all vendor IDs from static cookie consent data. + * + * @return string[] + * An indexed array containing all vendor IDs from getAll() function as + * values. + */ + public function getAllVendorIds(): array; + /** * Return required purpose IDs for given vendor. * - * @param string|int $vendor + * @param string $vendor * Either a vendor ID or a vendor name as defined by the * StaticConsentDataInterface::VENDOR_* constants. * - * @return int[] + * @return string[] * An array of purpose IDs on success, otherwise an empty array. * * @see \Drupal\burda_cmp\StaticConsentDataInterface @@ -94,7 +193,7 @@ public function getPurposeIds($vendor); /** * Return toggle button label for given vendor. * - * @param string|int $vendor + * @param string $vendor * Either a vendor ID or a vendor name as defined by the * StaticConsentDataInterface::VENDOR_* constants. * @@ -108,21 +207,21 @@ public function getToggleLabel($vendor); /** * Return vendor ID for given vendor name. * - * @param string $vendor + * @param string $vendorName * A vendor name as defined by the * StaticConsentDataInterface::VENDOR_* constants. * - * @return int|null + * @return string|null * The vendor ID on success, otherwise NULL. * * @see \Drupal\burda_cmp\StaticConsentDataInterface */ - public function getVendorId($vendor); + public function getVendorId($vendorName); /** * Return human-readable label for given vendor. * - * @param string|int $vendor + * @param string $vendor * Either a vendor ID or a vendor name as defined by the * StaticConsentDataInterface::VENDOR_* constants. * @@ -136,7 +235,7 @@ public function getVendorLabel($vendor); /** * Return vendor name for given vendor ID. * - * @param int $vendor + * @param string $vendorId * A vendor ID. * * @return string|null @@ -145,6 +244,18 @@ public function getVendorLabel($vendor); * * @see \Drupal\burda_cmp\StaticConsentDataInterface */ - public function getVendorName($vendor); + public function getVendorName($vendorId); + + /** + * Checks if the param is a vendor ID from the static cookie consent data. + * + * @param string $vendor + * A vendor name or a vendor ID. + * + * @return bool + * TRUE, if the vendor value is found as an ID value in the static cookie + * consent data, otherwise FALSE. + */ + public function isVendorId(string $vendor): bool; } From 55dd6a7443a2b3d3585c7e7d6821e838165d507e Mon Sep 17 00:00:00 2001 From: Markus Bruns Date: Wed, 25 Oct 2023 16:41:03 +0200 Subject: [PATCH 02/11] - added Settings Form Field to add sourcepoint scripts and refactored liveramp with sourcepoint - embedded scripts from SettingsForm --- burda_cmp.module | 44 ++++++++++++++++++++++++++---- config/schema/burda_cmp.schema.yml | 8 +++--- src/Form/SettingsForm.php | 24 +++++++++++++--- 3 files changed, 63 insertions(+), 13 deletions(-) diff --git a/burda_cmp.module b/burda_cmp.module index 320cc05d..49052b68 100644 --- a/burda_cmp.module +++ b/burda_cmp.module @@ -6,6 +6,7 @@ */ use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Render\Markup; /** * Implements hook_module_implements_alter(). @@ -71,24 +72,57 @@ function burda_cmp_library_info_alter(&$libraries, $extension) { function burda_cmp_page_attachments(array &$page) { /** @var \Drupal\Core\Config\ImmutableConfig $config */ $config = \Drupal::config('burda_cmp.settings'); - $liveramp_script_url = $config->get('liveramp_script_url'); + $sourcepoint_script_url = $config->get('sourcepoint_script_url'); + $sourcepoint_shim_script = $config->get('sourcepoint_shim_script'); + $sourcepoint_config_code = $config->get('sourcepoint_config_code'); CacheableMetadata::createFromRenderArray($page) ->addCacheableDependency($config) ->applyTo($page); - // LiveRamp init script. - if ($liveramp_script_url) { + if ($sourcepoint_shim_script) { $page['#attached']['html_head'][] = [ [ '#tag' => 'script', '#attributes' => [ 'type' => 'text/javascript', - 'src' => $liveramp_script_url, + 'data-sourcepoint' => 'shim-script' + ], + '#weight' => -102, + '#value' => Markup::create($sourcepoint_shim_script) + ], + 'burda_cmp_init_shim', + ]; + } + + if ($sourcepoint_config_code) { + $page['#attached']['html_head'][] = [ + [ + '#type' => 'html_tag', + '#tag' => 'script', + '#attributes' => [ + 'type' => 'text/javascript', + 'data-sourcepoint' => 'config-code' + ], + '#weight' => -101, + '#value' => Markup::create($sourcepoint_config_code) + ], + 'burda_cmp_init_config', + ]; + } + + if ($sourcepoint_script_url) { + $page['#attached']['html_head'][] = [ + [ + '#tag' => 'script', + '#attributes' => [ + 'type' => 'text/javascript', + 'src' => $sourcepoint_script_url, + 'data-sourcepoint' => 'script-url' ], '#weight' => -100, ], - 'burda_cmp_init', + 'burda_cmp_init_script', ]; } } diff --git a/config/schema/burda_cmp.schema.yml b/config/schema/burda_cmp.schema.yml index 26e52006..ad392837 100644 --- a/config/schema/burda_cmp.schema.yml +++ b/config/schema/burda_cmp.schema.yml @@ -1,6 +1,6 @@ -block.settings.burda_cmp_liveramp_open_privacy_manager:*: +block.settings.burda_cmp_sourcepoint_open_privacy_manager:*: type: block_settings - label: 'Open privacy manager block (LiveRamp)' + label: 'Open privacy manager block (Sourcepoint)' mapping: link_text: type: label @@ -10,6 +10,6 @@ burda_cmp.settings: type: config_object label: 'Consent management platform settings' mapping: - liveramp_script_url: + sourcepoint_script_url: type: string - label: 'LiveRamp script URL' + label: 'Sourcepoint script URL' diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php index 221f3bad..ba12f4df 100644 --- a/src/Form/SettingsForm.php +++ b/src/Form/SettingsForm.php @@ -32,10 +32,24 @@ public function getFormId() { public function buildForm(array $form, FormStateInterface $form_state) { $config = $this->config('burda_cmp.settings'); - $form['liveramp_script_url'] = [ + $form['sourcepoint_script_url'] = [ '#type' => 'url', - '#title' => $this->t('LiveRamp script URL'), - '#default_value' => $config->get('liveramp_script_url'), + '#title' => $this->t('Sourcepoint script URL'), + '#default_value' => $config->get('sourcepoint_script_url'), + '#required' => TRUE, + ]; + + $form['sourcepoint_shim_script'] = [ + '#type' => 'textarea', + '#title' => $this->t('Sourcepoint Shim URL'), + '#default_value' => $config->get('sourcepoint_shim_script'), + '#required' => TRUE, + ]; + + $form['sourcepoint_config_code'] = [ + '#type' => 'textarea', + '#title' => $this->t('Sourcepoint Config Code'), + '#default_value' => $config->get('sourcepoint_config_code'), '#required' => TRUE, ]; @@ -47,7 +61,9 @@ public function buildForm(array $form, FormStateInterface $form_state) { */ public function submitForm(array &$form, FormStateInterface $form_state) { $this->config('burda_cmp.settings') - ->set('liveramp_script_url', $form_state->getValue('liveramp_script_url')) + ->set('sourcepoint_script_url', $form_state->getValue('sourcepoint_script_url')) + ->set('sourcepoint_shim_script', $form_state->getValue('sourcepoint_shim_script')) + ->set('sourcepoint_config_code', $form_state->getValue('sourcepoint_config_code')) ->save(); parent::submitForm($form, $form_state); From a9ece9dad11365971171a551905bfccab9b7c869 Mon Sep 17 00:00:00 2001 From: Markus Bruns Date: Wed, 25 Oct 2023 16:57:44 +0200 Subject: [PATCH 03/11] - updated PrivacyManagerBlock - added library for sourcepoint to open privacy manager --- burda_cmp.libraries.yml | 8 ++++++ ...da-cmp.sourcepoint.open-privacy-manager.js | 27 +++++++++++++++++++ .../LiveRamp/OpenPrivacyManagerBlock.php | 8 +++--- 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 js/sourcepoint/burda-cmp.sourcepoint.open-privacy-manager.js diff --git a/burda_cmp.libraries.yml b/burda_cmp.libraries.yml index 85d51e1c..e15772da 100644 --- a/burda_cmp.libraries.yml +++ b/burda_cmp.libraries.yml @@ -23,3 +23,11 @@ liveramp.open-privacy-manager: dependencies: - core/drupal - core/jquery + +sourcepoint.open-privacy-manager: + js: + js/sourcepoint/burda-cmp.sourcepoint.open-privacy-manager.js: {} + dependencies: + - core/drupal + - core/jquery + - core/once diff --git a/js/sourcepoint/burda-cmp.sourcepoint.open-privacy-manager.js b/js/sourcepoint/burda-cmp.sourcepoint.open-privacy-manager.js new file mode 100644 index 00000000..7df6cae1 --- /dev/null +++ b/js/sourcepoint/burda-cmp.sourcepoint.open-privacy-manager.js @@ -0,0 +1,27 @@ +/** + * @file + * Consent management platform behaviors. + */ + +(function (Drupal, $, once) { + + 'use strict'; + + Drupal.behaviors.burdaCmpSourcepointOpenPrivacyManager = { + attach: function (context, settings) { + __tcfapi('addEventListener', 2, function (tcData, success) { + if (success && tcData.eventStatus === 'tcloaded') { + $('[data-burda-cmp-open-privacy-manager]', context).once('burdaCmpSourcepointOpenPrivacyManager').each(function () { + $(this).on('click.burdaCmpSourcepointOpenPrivacyManager', function (e) { + e.preventDefault(); + e.stopPropagation(); + // Open privacy manager. + window._sp_.gdpr.loadPrivacyManagerModal(883404); // @TODO PM ID == Correct Privacy Manager Id? + }); + }); + } + }, 'cmpReady'); + }, + }; + +})(Drupal, jQuery, once); diff --git a/src/Plugin/Block/LiveRamp/OpenPrivacyManagerBlock.php b/src/Plugin/Block/LiveRamp/OpenPrivacyManagerBlock.php index 9f70f3ec..19fffc9d 100644 --- a/src/Plugin/Block/LiveRamp/OpenPrivacyManagerBlock.php +++ b/src/Plugin/Block/LiveRamp/OpenPrivacyManagerBlock.php @@ -6,11 +6,11 @@ use Drupal\Core\Form\FormStateInterface; /** - * Provides an 'Open privacy manager' link block for LiveRamp integration. + * Provides an 'Open privacy manager' link block for Sourcepoint integration. * * @Block( - * id = "burda_cmp_liveramp_open_privacy_manager", - * admin_label = @Translation("Open privacy manager (LiveRamp)"), + * id = "burda_cmp_sourcepoint_open_privacy_manager", + * admin_label = @Translation("Open privacy manager (Sourcepoint)"), * ) */ class OpenPrivacyManagerBlock extends BlockBase { @@ -37,7 +37,7 @@ public function build() { ], '#attached' => [ 'library' => [ - 'burda_cmp/liveramp.open-privacy-manager', + 'burda_cmp/sourcepoint.open-privacy-manager', ], ], ], From 4ca53f70305f5868cf632163d9816a2c342fb520 Mon Sep 17 00:00:00 2001 From: Markus Bruns Date: Thu, 26 Oct 2023 10:48:21 +0200 Subject: [PATCH 04/11] Removed vendors: Retyp and Riddle Updated Vendor Id for Twitter --- .../burda_cmp_contribs.module | 5 ----- src/StaticConsentData.php | 22 +------------------ src/StaticConsentDataInterface.php | 10 --------- 3 files changed, 1 insertion(+), 36 deletions(-) diff --git a/modules/burda_cmp_contribs/burda_cmp_contribs.module b/modules/burda_cmp_contribs/burda_cmp_contribs.module index e8891166..d43f05fa 100644 --- a/modules/burda_cmp_contribs/burda_cmp_contribs.module +++ b/modules/burda_cmp_contribs/burda_cmp_contribs.module @@ -48,11 +48,6 @@ function burda_cmp_contribs_library_info_alter(&$libraries, $extension) { */ function _bnp_cmp_contribs_adjust() { $adjust = [ - 'riddle_marketplace' => [ - '*' => [ - 'vendor' => StaticConsentDataInterface::VENDOR_RIDDLE, - ], - ], 'media_entity_pinterest' => [ 'pinterest.widgets' => [ 'vendor' => StaticConsentDataInterface::VENDOR_PINTEREST, diff --git a/src/StaticConsentData.php b/src/StaticConsentData.php index 2f645f31..492dec3c 100644 --- a/src/StaticConsentData.php +++ b/src/StaticConsentData.php @@ -75,26 +75,6 @@ public function getAll() { ], ], -// // Retyp LLC (OptinMonster). -// static::VENDOR_RETYP_LLC => [ -// // @TODO: Missing in Sourcepoint.. -// 'id' => 10195, -// 'label' => $this->t('Retyp LLC', [], ['context' => 'Cookie consent provider']), -// // @todo Toggle label for Retyp LLC / OptinMonster (if needed). -// 'toggle_label' => NULL, -// 'purposes' => [], -// ], -// -// // Riddle Technologies AG. -// static::VENDOR_RIDDLE => [ -// // @TODO: Missing in Sourcepoint.. -// 'id' => 10196, -// 'label' => $this->t('Riddle Technologies AG', [], ['context' => 'Cookie consent provider']), -// // @todo Toggle label should be translatable for Riddle polls. -// 'toggle_label' => 'Riddle-Umfragen anzeigen', -// 'purposes' => [], -// ], - // TargetVideo GmbH. static::VENDOR_TARGETVIDEO_GMBH => [ 'id' => '5f0838a5b8e05c065164a384', @@ -108,7 +88,7 @@ public function getAll() { // Twitter. / X Corp. static::VENDOR_TWITTER => [ - 'id' => '5e71760b69966540e4554f01', + 'id' => '5fab0c31a22863611c5f8764', 'label' => $this->t('Twitter', [], ['context' => 'Cookie consent provider']), // @todo Toggle label should be translatable for Twitter posts. 'toggle_label' => 'Twitter-Posts anzeigen', diff --git a/src/StaticConsentDataInterface.php b/src/StaticConsentDataInterface.php index da0f1f21..cfe7a293 100644 --- a/src/StaticConsentDataInterface.php +++ b/src/StaticConsentDataInterface.php @@ -27,16 +27,6 @@ interface StaticConsentDataInterface { */ const VENDOR_PINTEREST = 'pinterest'; - /** - * Vendor name: Retyp LLC (OptinMonster). - */ - const VENDOR_RETYP_LLC = 'retyp_llc'; - - /** - * Vendor name: Riddle. - */ - const VENDOR_RIDDLE = 'riddle'; - /** * Vendor name: TargetVideo GmbH. */ From 53741bb4216ce3ee368fb0cb366a26c5c4ef11e5 Mon Sep 17 00:00:00 2001 From: Markus Bruns Date: Thu, 26 Oct 2023 10:49:21 +0200 Subject: [PATCH 05/11] updated PM_ID --- js/sourcepoint/burda-cmp.sourcepoint.open-privacy-manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/sourcepoint/burda-cmp.sourcepoint.open-privacy-manager.js b/js/sourcepoint/burda-cmp.sourcepoint.open-privacy-manager.js index 7df6cae1..bad23528 100644 --- a/js/sourcepoint/burda-cmp.sourcepoint.open-privacy-manager.js +++ b/js/sourcepoint/burda-cmp.sourcepoint.open-privacy-manager.js @@ -16,7 +16,7 @@ e.preventDefault(); e.stopPropagation(); // Open privacy manager. - window._sp_.gdpr.loadPrivacyManagerModal(883404); // @TODO PM ID == Correct Privacy Manager Id? + window._sp_.gdpr.loadPrivacyManagerModal(818834); }); }); } From 38c05edfef3f97318476e46dbabd04acf177493f Mon Sep 17 00:00:00 2001 From: Markus Bruns Date: Thu, 26 Oct 2023 11:20:11 +0200 Subject: [PATCH 06/11] removed purpose sorting, we have alphanumeric keys now removed old static legitimate interest purpose Id --- src/StaticConsentData.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/StaticConsentData.php b/src/StaticConsentData.php index 492dec3c..fff266e1 100644 --- a/src/StaticConsentData.php +++ b/src/StaticConsentData.php @@ -207,15 +207,9 @@ public function getPurposeIds($vendor): array { $purposes = $data[$vendor]['purposes'] ?? []; - // Ensure legitimate interest purpose. - $purposes[] = 1; - // Filter duplicates. $purposes = array_unique($purposes); - // Sort purposes. - sort($purposes); - return $purposes; } From 4986671e184289eed3bf7ee47ab6e1898a020f4b Mon Sep 17 00:00:00 2001 From: Markus Bruns Date: Thu, 16 Nov 2023 15:43:59 +0100 Subject: [PATCH 07/11] added conditional content logic to handle embedded content --- burda_cmp.libraries.yml | 9 + ...rda-cmp.sourcepoint.conditional-content.js | 282 ++++++++++++++++++ .../Filter/CmpCompliantIFrameFilter.php | 2 +- .../burda-cmp-conditional-content.html.twig | 2 +- 4 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 js/sourcepoint/burda-cmp.sourcepoint.conditional-content.js diff --git a/burda_cmp.libraries.yml b/burda_cmp.libraries.yml index e15772da..0f081359 100644 --- a/burda_cmp.libraries.yml +++ b/burda_cmp.libraries.yml @@ -24,6 +24,15 @@ liveramp.open-privacy-manager: - core/drupal - core/jquery +sourcepoint.conditional-content: + version: VERSION + js: + js/sourcepoint/burda-cmp.sourcepoint.conditional-content.js: {} + dependencies: + - core/drupal + - core/drupalSettings + - core/jquery + sourcepoint.open-privacy-manager: js: js/sourcepoint/burda-cmp.sourcepoint.open-privacy-manager.js: {} diff --git a/js/sourcepoint/burda-cmp.sourcepoint.conditional-content.js b/js/sourcepoint/burda-cmp.sourcepoint.conditional-content.js new file mode 100644 index 00000000..d2c6234c --- /dev/null +++ b/js/sourcepoint/burda-cmp.sourcepoint.conditional-content.js @@ -0,0 +1,282 @@ +/** + * Conditional behavior for content that is only loaded when consent is given. + */ + +(function (Drupal, $, once, drupalSettings) { + + 'use strict'; + + /** + * Enables the 'Conditional content' functionality on HTML elements. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches the 'Conditional content' behavior. + */ + Drupal.behaviors.burdaCmpSourcepointConditionalContent = { + attach: function attach(context) { + var self = this; + + // Initialize when CMP library is ready. + __tcfapi('addEventListener', 2, function (tcData, success) { + $('[data-burda-cmp-conditional-content]', context).once('burdaCmpSourcepointConditionalContent').each(function () { + var $self = $(this); + self.checkConsent($self); + + // Initialize toggle (if any). + $self.find('[data-burda-cmp-toggle]').on('click.burdaCmpSourcepointConditionalContent', function (e) { + e.preventDefault(); + e.stopPropagation(); + self.toggleConsent($self); + }); + + // Initialize settings button (if any). + $self.find('[data-burda-cmp-show-consent-manager]').on('click.burdaCmpSourcepointConditionalContent', function (e) { + e.preventDefault(); + e.stopPropagation(); + + self.showConsentManager($self); + }); + }); + // In case user initially confirms CMP on this page. + if (success && tcData.eventStatus === 'useractioncomplete') { + $('[data-burda-cmp-conditional-content]').each(function () { + var $self = $(this); + self.checkConsent($self); + }); + } + }); + }, + + /** + * Checks if a consent is given for the conditional content. + * + * @param {jQuery} $element + * A jQuery DOM fragment that represents the conditional content element. + */ + checkConsent: function ($element) { + var self = this; + var $placeholder = self.getWrapperPlaceholder($element); + var $injectedContent = self.getWrapperInjectedContent($element); + + // Prepare consent check data. + var consentData = self.getData($element); + if (consentData.vendorId || consentData.purposeIds) { + // Check consent based on vendor/purpose(s). + __tcfapi('getCustomVendorConsents', 2, function (data, success) { + if (success) { + let consentForVendorWasGiven = false; + let consentForPurposeWasGiven = false; + // Check for Vendor Consent. + if (Array.isArray(data.consentedVendors)) { + data.consentedVendors.forEach((vendor) => { + if (vendor._id === consentData.vendorId) { + consentForVendorWasGiven = true; + } + }); + } + // Check for Purpose Consent. + if (Array.isArray(data.consentedPurposes)) { + consentForPurposeWasGiven = data.consentedPurposes.some(item => consentData.purposeIds.includes(item._id)); + } + // If at least one of them was given, render injected content. + if (consentForVendorWasGiven || consentForPurposeWasGiven) { + if (!self.isContentInjected($element)) { + self.activateContent($element); + } + } else { + self.disableContent($element); + } + } else { + // Show placeholder in case there is no success in getCustomVendorConsents call. + Drupal.detachBehaviors($injectedContent.get(0), drupalSettings); + $injectedContent.html(''); + $placeholder.show(); + $element.attr('aria-expanded', 'false'); + } + }); + } + }, + + /** + * Returns the actual conditional content markup to inject. + * + * @param {jQuery} $element + * A jQuery DOM fragment that represents the conditional content element. + * + * @return {String} + * The HTML markup for conditional content to inject when consent is + * given. + */ + getContent: function getContent($element) { + return $element.attr('data-burda-cmp-conditional-content'); + }, + + /** + * Returns the consent data associated with the conditional content element. + * + * @param {jQuery} $element + * A jQuery DOM fragment that represents the conditional content element. + * + * @return {Object} + * An object containing all related consent data: + * - vendorId: Associated vendor ID. + * - purposeIds: Array of associated purpose ID(s). + */ + getData: function getData($element) { + // Determine required vendor/purpose(s). + var vendorId = this.getVendorId($element); + var purposeIds = this.getPurposeIds($element); + + // Prepare consent check data. + var data = { + vendorId: vendorId, + }; + + if (purposeIds) { + data.purposeIds = purposeIds; + } + + return data; + }, + + /** + * Returns the vendor ID to check consent for. + * + * @param {jQuery} $element + * A jQuery DOM fragment that represents the conditional content element. + * + * @return {String} + * The related vendor ID. + */ + getVendorId: function getVendorId($element) { + return $element.attr('data-burda-cmp-vendor'); + }, + + /** + * Returns the purpose ID(s) to check consent for. + * + * @param {jQuery} $element + * A jQuery DOM fragment that represents the conditional content element. + * + * @return {Array} + * An array containing all related purpose ID(s). + */ + getPurposeIds: function getPurposeIds($element) { + var purposeIds = $element.attr('data-burda-cmp-purposes'); + purposeIds = !purposeIds ? null : purposeIds.split(',').map(function (item) { + return $.trim(item); + }); + return purposeIds; + }, + + /** + * Returns the wrapper element for injected content. + * + * @param {jQuery} $element + * A jQuery DOM fragment that represents the conditional content element. + * + * @return {jQuery} + * A jQuery DOM fragment that represents the wrapper for injected + * conditional content. + */ + getWrapperInjectedContent: function getWrapperInjectedContent($element) { + return $element.find('[data-burda-cmp-injected-content]'); + }, + + /** + * Returns the placeholder wrapper element. + * + * @param {jQuery} $element + * A jQuery DOM fragment that represents the conditional content element. + * + * @return {jQuery} + * A jQuery DOM fragment that represents the placeholder element. + */ + getWrapperPlaceholder: function getWrapperPlaceholder($element) { + return $element.find('[data-burda-cmp-conditional-content-placeholder]'); + }, + + /** + * Returns whether the conditional content has been injected. + * + * @param {jQuery} $element + * A jQuery DOM fragment that represents the conditional content element. + * + * @return {bool} + * Whether the conditional content has been injected. + */ + isContentInjected: function isContentInjected($element) { + var $wrapper = this.getWrapperInjectedContent($element); + + return !$wrapper.is(':empty'); + }, + + /** + * Shows the consent manager. + * + * @param {jQuery} $element + * A jQuery DOM fragment that represents the conditional content element. + */ + showConsentManager: function showConsentManager($element) { + window._sp_.gdpr.loadPrivacyManagerModal(893032); + }, + + /** + * Toggles the consent for the conditional content. + * + * @param {jQuery} $element + * A jQuery DOM fragment that represents the conditional content element. + */ + toggleConsent: function toggleConsent($element) { + var self = this; + // Prepare consent check data. + var consentData = this.getData($element); + if (consentData.vendorId) { + // Rewrite consent data to have vendor ID as array. + consentData.vendorIds = [consentData.vendorId]; + delete consentData.vendorId; + + if (this.isContentInjected($element)) { + // Remove purpose IDs to only reject specific vendor but not all of + // its purposes. + delete consentData.purposeIds; + // @TODO Remove given consent to sourcepoint. At which point can this + // be the case? Because when consent is given, the button will not + // be rendered. + // __tcfapi('reject', null, function () {}, consentData); + } else { + // Send given consent to sourcepoint. Only the vendor. + // See https://sourcepoint-public-api.readme.io/reference/postcustomconsent#command + // __tcfapi('postCustomConsent', 2, callback_fn, [vendorIds], [purposeIds], [legitimateInterestPurposeIds] ) + __tcfapi('postCustomConsent', 2, (data, success) => { + if (success) { + self.activateContent($element); + } + }, consentData.vendorIds, consentData.purposeIds, []); + + } + } + }, + activateContent: function activateContent($element) { + var self = this; + var $placeholder = self.getWrapperPlaceholder($element); + var $injectedContent = self.getWrapperInjectedContent($element); + $injectedContent.append(self.getContent($element)); + $placeholder.hide(); + Drupal.attachBehaviors($injectedContent.get(0), drupalSettings); + $element.attr('aria-expanded', 'true'); + }, + disableContent: function disableContent($element) { + var self = this; + var $placeholder = self.getWrapperPlaceholder($element); + var $injectedContent = self.getWrapperInjectedContent($element); + Drupal.detachBehaviors($injectedContent.get(0), drupalSettings); + $injectedContent.html(''); + $placeholder.show(); + $element.attr('aria-expanded', 'false'); + }, + }; + +})(Drupal, jQuery, once, drupalSettings); diff --git a/src/Plugin/Filter/CmpCompliantIFrameFilter.php b/src/Plugin/Filter/CmpCompliantIFrameFilter.php index c3110d85..87f6d652 100644 --- a/src/Plugin/Filter/CmpCompliantIFrameFilter.php +++ b/src/Plugin/Filter/CmpCompliantIFrameFilter.php @@ -124,7 +124,7 @@ public function process($text, $langcode) { $result->setProcessedText(Html::serialize($dom)) ->addAttachments([ 'library' => [ - 'burda_cmp/liveramp.conditional-content', + 'burda_cmp/sourcepoint.conditional-content', ], ]); } diff --git a/templates/burda-cmp-conditional-content.html.twig b/templates/burda-cmp-conditional-content.html.twig index 76d5feb6..87b318f5 100644 --- a/templates/burda-cmp-conditional-content.html.twig +++ b/templates/burda-cmp-conditional-content.html.twig @@ -1,5 +1,5 @@ {%- if content is not empty and vendor is not empty -%} - {{- attach_library('burda_cmp/liveramp.conditional-content') -}} + {{- attach_library('burda_cmp/sourcepoint.conditional-content') -}}