diff --git a/ad/manager.php b/ad/manager.php
index ca659df2..1aea3215 100644
--- a/ad/manager.php
+++ b/ad/manager.php
@@ -12,6 +12,21 @@
class manager
{
+ public const CONSENT_CATEGORY = 'marketing';
+
+ /**
+ * Google ad/tag scripts that support Google Consent Mode.
+ *
+ * These should run immediately so Consent Mode can control storage and
+ * personalization instead of blocking the ad tag entirely.
+ */
+ protected const GOOGLE_CONSENT_AWARE_SCRIPT_SOURCE_PATTERNS = array(
+ '~(^|[/.])pagead2\.googlesyndication\.com/pagead/js/adsbygoogle\.js(?:[?#]|$)~i',
+ '~(^|[/.])securepubads\.g\.doubleclick\.net/tag/js/gpt\.js(?:[?#]|$)~i',
+ '~(^|[/.])www\.googletagservices\.com/tag/js/gpt\.js(?:[?#]|$)~i',
+ '~(^|[/.])www\.googletagmanager\.com/(?:gtag/js|gtm\.js)(?:[?#]|$)~i',
+ );
+
/** @var \phpbb\db\driver\driver_interface */
protected $db;
@@ -87,7 +102,7 @@ public function get_ads($ad_locations, $user_groups, $non_content_page = false)
$user_now = $this->user->create_datetime();
$sql_time = $this->user->get_timestamp_from_format('Y-m-d H:i:s', $user_now->format('Y-m-d H:i:s'), new \DateTimeZone('UTC'));
- $sql = 'SELECT al.location_id, a.ad_id, a.ad_code, a.ad_centering
+ $sql = 'SELECT al.location_id, a.ad_id, a.ad_code, a.ad_centering, a.ad_consent
FROM ' . $this->ad_locations_table . ' al
LEFT JOIN ' . $this->ads_table . ' a
ON (al.ad_id = a.ad_id)
@@ -372,6 +387,194 @@ public function load_groups($ad_id)
return $groups;
}
+ /**
+ * Prepare ad code for output, applying consent-manager deferrals when enabled.
+ *
+ * @param string $ad_code Stored advertisement code
+ * @param bool $consent_enabled Whether marketing consent is required
+ * @return string
+ */
+ public function prepare_ad_code($ad_code, $consent_enabled)
+ {
+ $ad_code = htmlspecialchars_decode($ad_code, ENT_COMPAT);
+ $original_ad_code = $ad_code;
+
+ if (!$consent_enabled || $ad_code === '')
+ {
+ return $ad_code;
+ }
+
+ $google_consent_aware_sources = self::get_google_consent_aware_script_sources($ad_code);
+
+ $ad_code = preg_replace_callback('##is', function ($matches) use ($google_consent_aware_sources)
+ {
+ $attributes = $matches[1] ?? '';
+ $content = $matches[2] ?? '';
+
+ if (!$this->should_defer_script_tag($attributes, $content, $google_consent_aware_sources))
+ {
+ return $matches[0];
+ }
+
+ return '';
+ }, $ad_code);
+
+ return $ad_code ?? $original_ad_code;
+ }
+
+ /**
+ * Determine whether a script tag is executable and should be deferred.
+ *
+ * @param string $attributes Script tag attributes
+ * @param string $content Script tag content
+ * @param array $google_consent_aware_sources Known Google loader sources in this ad block
+ * @return bool
+ */
+ protected function should_defer_script_tag($attributes, $content = '', array $google_consent_aware_sources = array())
+ {
+ if (preg_match('/\bdata-consent-category\s*=/i', $attributes))
+ {
+ return false;
+ }
+
+ if (preg_match('/\btype\s*=\s*([\'"])(.*?)\1/i', $attributes, $matches))
+ {
+ $type = strtolower(trim(explode(';', $matches[2])[0]));
+ }
+ else
+ {
+ $type = '';
+ }
+
+ $is_executable = $type === ''
+ || $type === 'text/plain'
+ || $type === 'module'
+ || strpos($type, 'javascript') !== false
+ || strpos($type, 'ecmascript') !== false;
+
+ if (!$is_executable)
+ {
+ return false;
+ }
+
+ return !self::is_google_consent_aware_script($attributes, $content, $google_consent_aware_sources);
+ }
+
+ /**
+ * Determine whether a script should run under Google Consent Mode.
+ *
+ * @param string $attributes Script tag attributes
+ * @param string $content Script tag content
+ * @param array $google_consent_aware_sources Known Google loader sources in this ad block
+ * @return bool
+ */
+ public static function is_google_consent_aware_script($attributes, $content, array $google_consent_aware_sources)
+ {
+ $source = self::extract_script_source($attributes);
+ if ($source !== '')
+ {
+ return isset($google_consent_aware_sources[self::normalize_script_source($source)]);
+ }
+
+ return !empty($google_consent_aware_sources)
+ && preg_match('/\b(?:adsbygoogle|googletag|gtag|dataLayer)\b/', $content);
+ }
+
+ /**
+ * Return known Google Consent Mode-aware loader sources in an ad block.
+ *
+ * @param string $ad_code Advertisement code
+ * @return array
+ */
+ public static function get_google_consent_aware_script_sources($ad_code)
+ {
+ $sources = array();
+
+ if (!preg_match_all('##is', $ad_code, $matches))
+ {
+ return false;
+ }
+
+ $google_consent_aware_sources = \phpbb\ads\ad\manager::get_google_consent_aware_script_sources($ad_code);
+
+ foreach ($matches[1] as $index => $attributes)
+ {
+ $content = $matches[2][$index] ?? '';
+ if (!$this->should_flag_script_tag($attributes))
+ {
+ continue;
+ }
+
+ if (\phpbb\ads\ad\manager::is_google_consent_aware_script($attributes, $content, $google_consent_aware_sources))
+ {
+ continue;
+ }
+
+ if ($this->contains_marketing_host_hint($attributes, $content))
+ {
+ return 'MARKETING_CONSENT_VENDOR_RECOMMENDED';
+ }
+
+ return 'MARKETING_CONSENT_RECOMMENDED';
+ }
+
+ return false;
+ }
+
+ /**
+ * Check for known advertising vendor hints inside script markup or content.
+ *
+ * @param string $attributes Script tag attributes
+ * @param string $content Script tag content
+ * @return bool
+ */
+ protected function contains_marketing_host_hint($attributes, $content)
+ {
+ $haystacks = array($attributes, $content);
+
+ $source = \phpbb\ads\ad\manager::extract_script_source($attributes);
+ if ($source !== '')
+ {
+ $haystacks[] = $source;
+ }
+
+ foreach ($haystacks as $haystack)
+ {
+ foreach (self::MARKETING_HOST_PATTERNS as $pattern)
+ {
+ if (preg_match($pattern, $haystack))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Mirror ads defer logic closely enough to avoid flagging inert script types.
+ *
+ * @param string $attributes Script tag attributes
+ * @return bool
+ */
+ protected function should_flag_script_tag($attributes)
+ {
+ if (preg_match('/\bdata-consent-category\s*=/i', $attributes))
+ {
+ return false;
+ }
+
+ if (!preg_match('/\btype\s*=\s*([\'"])(.*?)\1/i', $attributes, $matches))
+ {
+ return true;
+ }
+
+ $type = strtolower(trim(explode(';', $matches[2])[0]));
+ return $type === ''
+ || $type === 'module'
+ || strpos($type, 'javascript') !== false
+ || strpos($type, 'ecmascript') !== false;
+ }
+}
diff --git a/analyser/test/script_without_async.php b/analyser/test/script_without_async.php
index 2ab7e7b8..3b6828bb 100644
--- a/analyser/test/script_without_async.php
+++ b/analyser/test/script_without_async.php
@@ -20,7 +20,7 @@ class script_without_async implements test_interface
* to load itself asynchronously. Such scripts slow down page rendering
* time and should be made asynchronous.
*/
- public function run($ad_code)
+ public function run($ad_code, array $context = array())
{
if (preg_match_all('/<script(.*)src(.*)>/U', $ad_code, $matches))
{
diff --git a/analyser/test/test_interface.php b/analyser/test/test_interface.php
index 03b235d8..8445d20a 100644
--- a/analyser/test/test_interface.php
+++ b/analyser/test/test_interface.php
@@ -19,7 +19,8 @@ interface test_interface
* Test ad code for potential problems.
*
* @param string $ad_code Advertisement code
+ * @param array $context Optional form context
* @return mixed List of notices and warnings or false when there are none.
*/
- public function run($ad_code);
+ public function run($ad_code, array $context = array());
}
diff --git a/analyser/test/untrusted_connection.php b/analyser/test/untrusted_connection.php
index c13194bc..7b9723fe 100644
--- a/analyser/test/untrusted_connection.php
+++ b/analyser/test/untrusted_connection.php
@@ -32,7 +32,7 @@ public function __construct(\phpbb\request\request $request)
* When board runs on HTTPS and ad tries to load a file from
* HTTP source, browser throws a warning. We should prevent that.
*/
- public function run($ad_code)
+ public function run($ad_code, array $context = array())
{
$is_https = $this->request->server('HTTPS', false);
if ($is_https && preg_match('/http[^s]/', $ad_code))
diff --git a/config/analyser.yml b/config/analyser.yml
index 65ce08c3..82fdafa1 100644
--- a/config/analyser.yml
+++ b/config/analyser.yml
@@ -29,9 +29,22 @@ services:
tags:
- { name: phpbb.ads.analyser.test }
+ phpbb.ads.analyser.test.marketing_consent:
+ class: phpbb\ads\analyser\test\marketing_consent
+ arguments:
+ - '@config'
+ tags:
+ - { name: phpbb.ads.analyser.test }
+
phpbb.ads.analyser.test.untrusted_connection:
class: phpbb\ads\analyser\test\untrusted_connection
arguments:
- '@request'
tags:
- { name: phpbb.ads.analyser.test }
+
+ phpbb.ads.analyser.test.iframe:
+ class: phpbb\ads\analyser\test\iframe
+ tags:
+ - { name: phpbb.ads.analyser.test }
+
diff --git a/controller/admin_controller.php b/controller/admin_controller.php
index 73e3e111..9fcd1161 100644
--- a/controller/admin_controller.php
+++ b/controller/admin_controller.php
@@ -88,7 +88,10 @@ public function __construct(\phpbb\template\template $template, \phpbb\language\
$this->language->add_lang('posting'); // Used by banner_upload() file errors
$this->language->add_lang('acp', 'phpbb/ads');
- $this->template->assign_var('S_PHPBB_ADS', true);
+ $this->template->assign_vars([
+ 'S_PHPBB_ADS' => true,
+ 'S_ADS_CONSENTMANAGER_AVAILABLE' => $this->is_consent_manager_available()
+ ]);
if (!class_exists('auth_admin'))
{
@@ -427,13 +430,13 @@ protected function upload_banner()
/**
* Submit action "analyse_ad_code".
- * Upload banner and append it to the ad code.
+ * Analyse submitted ad code with current form state.
*
* @return void
*/
protected function analyse_ad_code()
{
- $this->analyser->run($this->data['ad_code']);
+ $this->analyser->run($this->data['ad_code'], $this->data);
}
/**
@@ -526,4 +529,15 @@ protected function toggle_permission($user_id)
$this->auth_admin->acl_set('user', 0, $user_id, array('u_phpbb_ads' => (int) $has_ads));
}
}
+
+ /**
+ * Check whether Consent Manager's marketing category is available.
+ *
+ * @return bool
+ */
+ protected function is_consent_manager_available()
+ {
+ return $this->config->offsetExists('consentmanager_marketing_enabled')
+ && (bool) $this->config['consentmanager_marketing_enabled'];
+ }
}
diff --git a/controller/admin_input.php b/controller/admin_input.php
index 77c88c4d..6ae1fe0f 100644
--- a/controller/admin_input.php
+++ b/controller/admin_input.php
@@ -97,6 +97,7 @@ public function get_form_data()
'ad_owner' => $this->request->variable('ad_owner', '', true),
'ad_groups' => $this->request->variable('ad_groups', array(0)),
'ad_centering' => $this->request->variable('ad_centering', true),
+ 'ad_consent' => $this->request->variable('ad_consent', 1),
);
// Validate form key
diff --git a/controller/helper.php b/controller/helper.php
index 81c64f1a..113260fc 100644
--- a/controller/helper.php
+++ b/controller/helper.php
@@ -103,6 +103,7 @@ public function assign_data($data, $errors)
'AD_CLICKS_LIMIT' => $data['ad_clicks_limit'],
'AD_OWNER' => $this->get_username($data['ad_owner']),
'AD_CENTERING' => $data['ad_centering'],
+ 'AD_CONSENT' => $data['ad_consent'] ?? 1,
));
}
diff --git a/event/main_listener.php b/event/main_listener.php
index 30e3f7bf..c8b64c55 100644
--- a/event/main_listener.php
+++ b/event/main_listener.php
@@ -64,6 +64,7 @@ public static function getSubscribedEvents()
'core.adm_page_header_after' => 'disable_xss_protection',
'core.group_add_user_after' => 'destroy_user_group_cache',
'core.group_delete_user_after' => 'destroy_user_group_cache',
+ 'phpbb.consentmanager.collect_registrations' => 'register_ads',
);
}
@@ -133,16 +134,19 @@ public function setup_ads()
{
// check for the existence of 'MESSAGE_TEXT', which signals it's an error page.
$non_content_page = $this->template->retrieve_var('MESSAGE_TEXT') || $this->is_non_content_page();
+ $consent_enabled = (bool) $this->template->retrieve_var('S_CONSENTMANAGER_MARKETING_ENABLED');
$location_ids = $this->location_manager->get_all_location_ids();
$user_groups = $this->manager->load_memberships($this->user->data['user_id']);
$ad_ids = array();
+ $ads = $this->manager->get_ads($location_ids, $user_groups, $non_content_page);
- foreach ($this->manager->get_ads($location_ids, $user_groups, $non_content_page) as $row)
+ foreach ($ads as $row)
{
$ad_ids[] = $row['ad_id'];
+ $ad_consent_enabled = $consent_enabled && (bool) ($row['ad_consent'] ?? true);
$this->template->assign_vars(array(
- 'AD_' . strtoupper($row['location_id']) => htmlspecialchars_decode($row['ad_code'], ENT_COMPAT),
+ 'AD_' . strtoupper($row['location_id']) => $this->manager->prepare_ad_code($row['ad_code'], $ad_consent_enabled),
'AD_' . strtoupper($row['location_id']) . '_ID' => (int) $row['ad_id'],
'AD_' . strtoupper($row['location_id']) . '_CENTER' => (bool) $row['ad_centering'],
));
@@ -302,4 +306,19 @@ public function append_agreement()
$this->template->append_var('AGREEMENT_TEXT', $this->language->lang('PHPBB_ADS_PRIVACY_POLICY', $this->config['sitename']));
}
+
+ /**
+ * Register the advertisement extension with Consent Manager.
+ *
+ * @param \phpbb\event\data|array $event The event object or event data
+ * @return void
+ */
+ public function register_ads($event)
+ {
+ $event['consent_manager']->register('phpbb.ads', array(
+ 'label' => $this->language->lang('PHPBB_ADS_CONSENT_LABEL'),
+ 'category' => \phpbb\ads\ad\manager::CONSENT_CATEGORY,
+ 'description' => $this->language->lang('PHPBB_ADS_CONSENT_DESCRIPTION'),
+ ));
+ }
}
diff --git a/language/en/acp.php b/language/en/acp.php
index 5af42e27..55ceea8d 100644
--- a/language/en/acp.php
+++ b/language/en/acp.php
@@ -56,6 +56,8 @@
'AD_CLICKS' => 'Clicks',
'AD_CLICKS_LIMIT' => 'Clicks Limit',
'AD_CLICKS_LIMIT_EXPLAIN' => 'Set the maximum number of times the advertisement will be clicked, after which the advertisement will no longer be displayed. Set 0 for unlimited clicks.',
+ 'AD_CONSENT' => 'Require marketing consent',
+ 'AD_CONSENT_EXPLAIN' => 'Set to Yes to defer script tags in this advertisement until the visitor grants marketing consent in Privacy Settings. Set to No only for ad code that does not load marketing, tracking, cookies, profiling, or other consent-controlled resources.
Note: This setting has no effect on supported Google AdSense or Google Publisher Tag (GPT) code. Consent Manager automatically manages consent for Google Ads through Google Consent Mode.',
'AD_START_DATE' => 'Start Date',
'AD_START_DATE_EXPLAIN' => 'Set the date when the advertisement can begin displaying (starting at 00:00). The ad must still be manually enabled to appear. If no date is set, the ad can display immediately once enabled.',
'AD_END_DATE' => 'End Date',
@@ -99,8 +101,11 @@
// Analyser tests
'UNSECURE_CONNECTION' => 'Mixed Content
Your board runs on a secure HTTPS connection; however, the advertisement code is attempting to load content from an insecure HTTP connection. This can cause browsers to generate a “Mixed Content” warning to let users know that the page contains insecure resources.',
'SCRIPT_WITHOUT_ASYNC' => 'Non-asynchronous javascript
This advertisement code loads JavaScript code in a non-asynchronous way. This means it will block any other JavaScript from loading until it has completed loading, which can affect page load performance. Use of the async attribute can speed up the page load.',
+ 'MARKETING_CONSENT_RECOMMENDED' => 'Require marketing consent
This advertisement contains executable <script> tags. If this ad loads marketing, tracking, cookies, or other consent-controlled resources, enable Require marketing consent below for this ad so its scripts are deferred until the visitor allows marketing in Privacy Settings.',
+ 'MARKETING_CONSENT_VENDOR_RECOMMENDED' => 'Known ad vendor detected
This advertisement contains executable <script> tags from a known advertising or marketing vendor. Enable Require marketing consent below for this ad so its scripts are deferred until the visitor allows marketing in Privacy Settings.',
'ALERT_USAGE' => 'Usage of alert()
Your code uses the alert() function which is not a good practice and can distract users. Some browsers may also block page load and display additional warnings to the user.',
- 'LOCATION_CHANGE' => 'Redirection
Your code appears it can redirect a user to another page or site. Redirects can sometimes send users to unintended, often malicious, destinations. Please verify the integrity of your advertisement codeadvertisement code’s redirection destination.',
+ 'LOCATION_CHANGE' => 'Redirection
Your code appears it can redirect a user to another page or site. Redirects can sometimes send users to unintended, often malicious, destinations. Please verify the integrity of your advertisement code’s redirection destination.',
+ 'IFRAME_USAGE' => 'Usage of <iframe>
Your code contains HTML-encoded <iframe> tags. Because iframes can introduce third-party tracking or data collection, please review this advertisement snippet to ensure it complies with your user privacy policies.',
// Template location categories
'CAT_TOP_OF_PAGE' => 'Top of page',
diff --git a/language/en/common.php b/language/en/common.php
index ebe45c83..fa60493f 100644
--- a/language/en/common.php
+++ b/language/en/common.php
@@ -26,6 +26,8 @@
],
'ADVERTISEMENT' => 'Advertisement',
'HIDE_AD' => 'Hide advertisement',
+ 'PHPBB_ADS_CONSENT_LABEL' => 'Advertisements',
+ 'PHPBB_ADS_CONSENT_DESCRIPTION' => 'Advertising features that may use cookies or similar technologies to collect data.',
'VISUAL_DEMO' => 'Visual demo for ad locations is active',
'DISABLE_VISUAL_DEMO' => 'Click to disable visual demo',
diff --git a/migrations/v20x/m6_ad_consent_option.php b/migrations/v20x/m6_ad_consent_option.php
new file mode 100644
index 00000000..396ec011
--- /dev/null
+++ b/migrations/v20x/m6_ad_consent_option.php
@@ -0,0 +1,64 @@
+
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ */
+
+namespace phpbb\ads\migrations\v20x;
+
+class m6_ad_consent_option extends \phpbb\db\migration\migration
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function effectively_installed()
+ {
+ return $this->db_tools->sql_column_exists($this->table_prefix . 'ads', 'ad_consent');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public static function depends_on()
+ {
+ return array(
+ '\phpbb\ads\migrations\v20x\m5_add_privacy_setting',
+ );
+ }
+
+ /**
+ * Add the per-ad consent option to ads table.
+ *
+ * @return array Array of table schema
+ */
+ public function update_schema()
+ {
+ return array(
+ 'add_columns' => array(
+ $this->table_prefix . 'ads' => array(
+ 'ad_consent' => array('BOOL', 1),
+ ),
+ ),
+ );
+ }
+
+ /**
+ * Drop the per-ad consent option from ads table.
+ *
+ * @return array Array of table schema
+ */
+ public function revert_schema()
+ {
+ return array(
+ 'drop_columns' => array(
+ $this->table_prefix . 'ads' => array(
+ 'ad_consent',
+ ),
+ ),
+ );
+ }
+}
diff --git a/tests/ad/get_ad_test.php b/tests/ad/get_ad_test.php
index 15e61c4a..46071965 100644
--- a/tests/ad/get_ad_test.php
+++ b/tests/ad/get_ad_test.php
@@ -36,6 +36,7 @@ public function get_ad_data()
'ad_owner' => '2',
'ad_content_only' => '0',
'ad_centering' => '1',
+ 'ad_consent' => '1',
)),
array(0, array()),
);
diff --git a/tests/ad/get_ads_test.php b/tests/ad/get_ads_test.php
index 88fced45..c628c2a1 100644
--- a/tests/ad/get_ads_test.php
+++ b/tests/ad/get_ads_test.php
@@ -21,13 +21,13 @@ public function get_ads_data()
{
return array(
array(array('after_profile'), array(
- array('location_id' => 'after_profile', 'ad_code' => 'Ad Code #1', 'ad_id' => '1', 'ad_centering' => '1'),
+ array('location_id' => 'after_profile', 'ad_code' => 'Ad Code #1', 'ad_id' => '1', 'ad_centering' => '1', 'ad_consent' => '1'),
), false),
array(array('before_profile'), array(
- array('location_id' => 'before_profile', 'ad_code' => 'Ad Code #4', 'ad_id' => '4', 'ad_centering' => '1'),
+ array('location_id' => 'before_profile', 'ad_code' => 'Ad Code #4', 'ad_id' => '4', 'ad_centering' => '1', 'ad_consent' => '1'),
), false),
array(array('below_footer'), array(
- array('location_id' => 'below_footer', 'ad_code' => 'Ad Code #7', 'ad_id' => '7', 'ad_centering' => '1'),
+ array('location_id' => 'below_footer', 'ad_code' => 'Ad Code #7', 'ad_id' => '7', 'ad_centering' => '1', 'ad_consent' => '1'),
), false),
array(array('below_footer'), array(), true),
array(array('foo_bar'), array(), false),
diff --git a/tests/ad/prepare_ad_code_test.php b/tests/ad/prepare_ad_code_test.php
new file mode 100644
index 00000000..f49864c3
--- /dev/null
+++ b/tests/ad/prepare_ad_code_test.php
@@ -0,0 +1,152 @@
+
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ */
+
+namespace phpbb\ads\tests\ad;
+
+class prepare_ad_code_test extends ad_base
+{
+ public function test_consent_category_constant()
+ {
+ self::assertSame('marketing', \phpbb\ads\ad\manager::CONSENT_CATEGORY);
+ }
+
+ public function test_returns_decoded_code_when_consent_disabled()
+ {
+ $raw = htmlspecialchars('', ENT_COMPAT);
+ $result = $this->get_manager()->prepare_ad_code($raw, false);
+ self::assertSame('', $result);
+ self::assertStringNotContainsString('type="text/plain"', $result);
+ }
+
+ public function test_returns_empty_string_unchanged()
+ {
+ self::assertSame('', $this->get_manager()->prepare_ad_code('', true));
+ }
+
+ public function executable_script_type_data()
+ {
+ return [
+ 'normal script' => [
+ '',
+ '',
+ ],
+ 'empty type' => [
+ '',
+ '',
+ ],
+ 'text/plain type' => [
+ '',
+ '',
+ ],
+ 'module type' => [
+ '',
+ '',
+ ],
+ 'javascript type with charset' => [
+ '',
+ '',
+ ],
+ 'ecmascript type' => [
+ '',
+ '',
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider executable_script_type_data
+ */
+ public function test_defers_executable_script_types($input, $expected)
+ {
+ $raw = htmlspecialchars($input, ENT_COMPAT);
+ $result = $this->get_manager()->prepare_ad_code($raw, true);
+ self::assertSame($expected, $result);
+ }
+
+ public function test_preserves_non_executable_script_type()
+ {
+ $raw = htmlspecialchars('', ENT_COMPAT);
+ $result = $this->get_manager()->prepare_ad_code($raw, true);
+ self::assertSame('', $result);
+ }
+
+ public function test_does_not_double_wrap_already_tagged_script()
+ {
+ $script = '';
+ $raw = htmlspecialchars($script, ENT_COMPAT);
+ $result = $this->get_manager()->prepare_ad_code($raw, true);
+ self::assertSame($script, $result);
+ self::assertSame(1, substr_count($result, 'data-consent-category='));
+ }
+
+ public function google_consent_aware_script_data()
+ {
+ return [
+ 'adsense loader' => [
+ '',
+ ],
+ 'gpt loader' => [
+ '',
+ ],
+ 'gtag loader' => [
+ '',
+ ],
+ 'gtm loader' => [
+ '',
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider google_consent_aware_script_data
+ */
+ public function test_does_not_defer_google_consent_aware_loaders($script)
+ {
+ $raw = htmlspecialchars($script, ENT_COMPAT);
+ $result = $this->get_manager()->prepare_ad_code($raw, true);
+ self::assertSame($script, $result);
+ }
+
+ public function test_does_not_defer_adsense_inline_script_when_adsense_loader_is_present()
+ {
+ $script = '';
+ $raw = htmlspecialchars($script, ENT_COMPAT);
+ $result = $this->get_manager()->prepare_ad_code($raw, true);
+ self::assertSame($script, $result);
+ }
+
+ public function test_does_not_defer_gpt_inline_script_when_gpt_loader_is_present()
+ {
+ $script = '';
+ $raw = htmlspecialchars($script, ENT_COMPAT);
+ $result = $this->get_manager()->prepare_ad_code($raw, true);
+ self::assertSame($script, $result);
+ }
+
+ public function test_defers_google_named_inline_script_without_google_loader()
+ {
+ $raw = htmlspecialchars('', ENT_COMPAT);
+ $result = $this->get_manager()->prepare_ad_code($raw, true);
+ self::assertSame('', $result);
+ }
+
+ public function test_google_consent_aware_source_lookup_returns_empty_without_script_tags()
+ {
+ self::assertSame(array(), \phpbb\ads\ad\manager::get_google_consent_aware_script_sources('
No scripts
'));
+ }
+
+ public function test_non_script_html_is_preserved()
+ {
+ $raw = htmlspecialchars('Ad
', ENT_COMPAT);
+ $result = $this->get_manager()->prepare_ad_code($raw, true);
+ self::assertStringContainsString('', $result);
+ self::assertStringNotContainsString('type="text/plain"', $result);
+ }
+}
diff --git a/tests/analyser/analyser_base.php b/tests/analyser/analyser_base.php
index bdde8299..13cfadf8 100644
--- a/tests/analyser/analyser_base.php
+++ b/tests/analyser/analyser_base.php
@@ -24,6 +24,9 @@ class analyser_base extends \phpbb_test_case
/** @var \phpbb\language\language */
protected $lang;
+ /** @var \phpbb\config\config */
+ protected $config;
+
protected static function setup_extensions()
{
return array('phpbb/ads');
@@ -47,13 +50,18 @@ protected function setUp(): void
->disableOriginalConstructor()
->getMock();
$this->lang = new \phpbb\language\language($lang_loader);
+ $this->config = new \phpbb\config\config(array(
+ 'consentmanager_marketing_enabled' => 0,
+ ));
// Tests
$tests = array(
'alert',
'location_href',
'script_without_async',
+ 'marketing_consent',
'untrusted_connection',
+ 'iframe',
);
$analyser_tests = array();
foreach ($tests as $test)
@@ -63,6 +71,10 @@ protected function setUp(): void
{
$analyser_tests['phpbb.ads.analyser.test.' . $test] = new $class($this->request);
}
+ else if ($test === 'marketing_consent')
+ {
+ $analyser_tests['phpbb.ads.analyser.test.' . $test] = new $class($this->config);
+ }
else
{
$analyser_tests['phpbb.ads.analyser.test.' . $test] = new $class();
diff --git a/tests/analyser/run_test.php b/tests/analyser/run_test.php
index 6f67f46d..84a27e64 100644
--- a/tests/analyser/run_test.php
+++ b/tests/analyser/run_test.php
@@ -20,64 +20,64 @@ class run_test extends analyser_base
public function run_data()
{
return array(
- array('<script async>alert()</script>', false, array(
+ 'warns on alert call' => array('<script async>alert()</script>', false, array(), array(
array(
'severity' => 'warning',
'lang_key' => 'ALERT_USAGE',
),
)),
- array('<script async>alert ()</script>', false, array(
+ 'warns on spaced alert call' => array('<script async>alert ()</script>', false, array(), array(
array(
'severity' => 'warning',
'lang_key' => 'ALERT_USAGE',
),
)),
- array('<script async>window.location.href = "new url"</script>', false, array(
+ 'warns on location href assignment' => array('<script async>window.location.href = "new url"</script>', false, array(), array(
array(
'severity' => 'warning',
'lang_key' => 'LOCATION_CHANGE',
),
)),
- array('<script async>window.location.href= "new url"</script>', false, array(
+ 'warns on compact location href assignment' => array('<script async>window.location.href= "new url"</script>', false, array(), array(
array(
'severity' => 'warning',
'lang_key' => 'LOCATION_CHANGE',
),
)),
- array('<script></script>', false, array()),
- array('<script src="script src"></script>', false, array(
+ 'allows empty script without src' => array('<script></script>', false, array(), array()),
+ 'notices script without async' => array('<script src="script src"></script>', false, array(), array(
array(
'severity' => 'notice',
'lang_key' => 'SCRIPT_WITHOUT_ASYNC',
),
)),
- array('<script src="script src"></script><script src="another script src"></script>', false, array(
+ 'notices first of multiple scripts without async' => array('<script src="script src"></script><script src="another script src"></script>', false, array(), array(
array(
'severity' => 'notice',
'lang_key' => 'SCRIPT_WITHOUT_ASYNC',
),
)),
- array('<script async src="script src"></script><script src="another script src"></script>', false, array(
+ 'notices second script without async' => array('<script async src="script src"></script><script src="another script src"></script>', false, array(), array(
array(
'severity' => 'notice',
'lang_key' => 'SCRIPT_WITHOUT_ASYNC',
),
)),
- array('<script src="script src"></script><script async src="another script src"></script>', false, array(
+ 'notices first script without async before async script' => array('<script src="script src"></script><script async src="another script src"></script>', false, array(), array(
array(
'severity' => 'notice',
'lang_key' => 'SCRIPT_WITHOUT_ASYNC',
),
)),
- array('<script async src="http://some.url"></script>', false, array()),
- array('<script async src="https://some.url"></script>', true, array()),
- array('<script async src="http://some.url"></script>', true, array(
+ 'allows http script on http page' => array('<script async src="http://some.url"></script>', false, array(), array()),
+ 'allows https script on https page' => array('<script async src="https://some.url"></script>', true, array(), array()),
+ 'warns on http script on https page' => array('<script async src="http://some.url"></script>', true, array(), array(
array(
'severity' => 'warning',
'lang_key' => 'UNSECURE_CONNECTION',
),
)),
- array('<script src="http://some.url"></script><script>alert("e");window.location.href="new url"</script>', true, array(
+ 'collects multiple analyser warnings' => array('<script src="http://some.url"></script><script>alert("e");window.location.href="new url"</script>', true, array(), array(
array(
'severity' => 'warning',
'lang_key' => 'ALERT_USAGE',
@@ -95,6 +95,72 @@ public function run_data()
'lang_key' => 'UNSECURE_CONNECTION',
),
)),
+ 'notices iframe usage' => array('<iframe src="https://some.url" width="640" height="360" allowfullscreen></iframe>', false, array(), array(
+ array(
+ 'severity' => 'notice',
+ 'lang_key' => 'IFRAME_USAGE',
+ ),
+ )),
+ 'allows consent-aware iframe placeholder' => array('<iframe data-consent-src="https://some.url" width="640" height="360" allowfullscreen></iframe>', false, array(), array()),
+ 'recommends marketing consent for generic ad script' => array('', false, array(
+ 'ad_consent' => 0,
+ 'consentmanager_marketing_enabled' => 1,
+ ), array(
+ array(
+ 'severity' => 'notice',
+ 'lang_key' => 'MARKETING_CONSENT_RECOMMENDED',
+ ),
+ )),
+ 'recommends marketing consent for inline cookie script' => array('', false, array(
+ 'ad_consent' => 0,
+ 'consentmanager_marketing_enabled' => 1,
+ ), array(
+ array(
+ 'severity' => 'notice',
+ 'lang_key' => 'MARKETING_CONSENT_RECOMMENDED',
+ ),
+ )),
+ 'recommends marketing consent for known non-Google vendor script' => array('', false, array(
+ 'ad_consent' => 0,
+ 'consentmanager_marketing_enabled' => 1,
+ ), array(
+ array(
+ 'severity' => 'notice',
+ 'lang_key' => 'MARKETING_CONSENT_VENDOR_RECOMMENDED',
+ ),
+ )),
+ 'allows AdSense loader under Google Consent Mode' => array('', false, array(
+ 'ad_consent' => 0,
+ 'consentmanager_marketing_enabled' => 1,
+ ), array()),
+ 'allows full AdSense snippet under Google Consent Mode' => array('', false, array(
+ 'ad_consent' => 0,
+ 'consentmanager_marketing_enabled' => 1,
+ ), array()),
+ 'allows GPT loader under Google Consent Mode' => array('', false, array(
+ 'ad_consent' => 0,
+ 'consentmanager_marketing_enabled' => 1,
+ ), array()),
+ 'allows non-executable json script' => array('', false, array(
+ 'ad_consent' => 0,
+ 'consentmanager_marketing_enabled' => 1,
+ ), array()),
+ 'allows Google ad iframe because marketing consent analyser only handles scripts' => array('', false, array(
+ 'ad_consent' => 0,
+ 'consentmanager_marketing_enabled' => 1,
+ ), array()),
+ 'allows generic ad script when ad consent is already enabled' => array('', false, array(
+ 'ad_consent' => 1,
+ 'consentmanager_marketing_enabled' => 1,
+ ), array()),
+ 'allows generic ad script when Consent Manager marketing category is disabled' => array('', false, array(
+ 'ad_consent' => 0,
+ 'consentmanager_marketing_enabled' => 0,
+ ), array()),
+ 'allows already consent-tagged script' => array('', false, array(
+ 'ad_consent' => 0,
+ 'consentmanager_marketing_enabled' => 1,
+ ), array()),
);
}
@@ -103,9 +169,10 @@ public function run_data()
*
* @dataProvider run_data
*/
- public function test_run($ad_code, $is_https, $expected)
+ public function test_run($ad_code, $is_https, $context, $expected)
{
$manager = $this->get_manager();
+ $this->config['consentmanager_marketing_enabled'] = $context['consentmanager_marketing_enabled'] ?? 0;
$this->request
->method('server')
@@ -132,6 +199,6 @@ public function test_run($ad_code, $is_https, $expected)
->method('assign_block_vars');
}
- $manager->run($ad_code);
+ $manager->run($ad_code, $context);
}
}
diff --git a/tests/controller/admin_controller_test.php b/tests/controller/admin_controller_test.php
index 1bead390..391f2bc0 100644
--- a/tests/controller/admin_controller_test.php
+++ b/tests/controller/admin_controller_test.php
@@ -479,7 +479,7 @@ public function test_action_add_analyse_ad_code()
$this->analyser->expects(self::once())
->method('run')
- ->with($data['ad_code']);
+ ->with($data['ad_code'], $data);
$this->input->expects(self::once())
->method('get_errors')
diff --git a/tests/controller/admin_input_test.php b/tests/controller/admin_input_test.php
index 39f53d33..d01c10d8 100644
--- a/tests/controller/admin_input_test.php
+++ b/tests/controller/admin_input_test.php
@@ -107,22 +107,22 @@ public function get_input_controller()
public function get_form_data_data()
{
return array(
- array(false, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 5, 0, 0, 0, '', [], false], 0, ['FORM_INVALID']),
- array(true, ['', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 5, 0, 0, 0, '', [], false], 0, ['AD_NAME_REQUIRED']),
- array(true, [str_repeat('a', 256), 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 5, 0, 0, 0, '', [], false], 0, ['AD_NAME_TOO_LONG']),
- array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code with emoji 😀', 0, '', '', '', 5, 0, 0, 0, '', [], false], 0, ['AD_CODE_ILLEGAL_CHARS']),
- array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', 'blah', '', 5, 0, 0, 0, '', [], false], 0, ['AD_START_DATE_INVALID']),
- array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', 'blah', 5, 0, 0, 0, '', [], false], 0, ['AD_END_DATE_INVALID']),
- array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '1970-01-01', '', 5, 0, 0, 0, '', [], false], 0, ['AD_START_DATE_INVALID']),
- array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '1970-01-01', 5, 0, 0, 0, '', [], false], 0, ['AD_END_DATE_INVALID']),
- array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '2060-01-01', '2050-01-01', 5, 0, 0, 0, '', [], false], 0, ['END_DATE_TOO_SOON']),
- array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 0, 0, 0, 0, '', [], false], 0, ['AD_PRIORITY_INVALID']),
- array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 11, 0, 0, 0, '', [], false], 0, ['AD_PRIORITY_INVALID']),
- array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 5, 0, -1, 0, '', [], false], 0, ['AD_VIEWS_LIMIT_INVALID']),
- array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 5, 0, 0, -1, '', [], false], 0, ['AD_CLICKS_LIMIT_INVALID']),
- array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 5, 0, 0, 0, 'adm', [], false], 0, ['AD_OWNER_INVALID']),
- array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 5, 0, 0, 0, 'adm', [], false], 0, ['AD_OWNER_INVALID']),
- array(false, ['', 'Ad Note #1', 'Ad Code #1', 0, '', 'blah', 'blah', 0, 0, -1, -1, 'adm', [], false], 0, [
+ array(false, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 5, 0, 0, 0, '', [], false, 1], 0, ['FORM_INVALID']),
+ array(true, ['', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 5, 0, 0, 0, '', [], false, 1], 0, ['AD_NAME_REQUIRED']),
+ array(true, [str_repeat('a', 256), 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 5, 0, 0, 0, '', [], false, 1], 0, ['AD_NAME_TOO_LONG']),
+ array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code with emoji 😀', 0, '', '', '', 5, 0, 0, 0, '', [], false, 1], 0, ['AD_CODE_ILLEGAL_CHARS']),
+ array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', 'blah', '', 5, 0, 0, 0, '', [], false, 1], 0, ['AD_START_DATE_INVALID']),
+ array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', 'blah', 5, 0, 0, 0, '', [], false, 1], 0, ['AD_END_DATE_INVALID']),
+ array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '1970-01-01', '', 5, 0, 0, 0, '', [], false, 1], 0, ['AD_START_DATE_INVALID']),
+ array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '1970-01-01', 5, 0, 0, 0, '', [], false, 1], 0, ['AD_END_DATE_INVALID']),
+ array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '2060-01-01', '2050-01-01', 5, 0, 0, 0, '', [], false, 1], 0, ['END_DATE_TOO_SOON']),
+ array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 0, 0, 0, 0, '', [], false, 1], 0, ['AD_PRIORITY_INVALID']),
+ array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 11, 0, 0, 0, '', [], false, 1], 0, ['AD_PRIORITY_INVALID']),
+ array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 5, 0, -1, 0, '', [], false, 1], 0, ['AD_VIEWS_LIMIT_INVALID']),
+ array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 5, 0, 0, -1, '', [], false, 1], 0, ['AD_CLICKS_LIMIT_INVALID']),
+ array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 5, 0, 0, 0, 'adm', [], false, 1], 0, ['AD_OWNER_INVALID']),
+ array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', 0, '', '', '', 5, 0, 0, 0, 'adm', [], false, 1], 0, ['AD_OWNER_INVALID']),
+ array(false, ['', 'Ad Note #1', 'Ad Code #1', 0, '', 'blah', 'blah', 0, 0, -1, -1, 'adm', [], false, 1], 0, [
'FORM_INVALID',
'AD_NAME_REQUIRED',
'AD_START_DATE_INVALID',
@@ -132,7 +132,7 @@ public function get_form_data_data()
'AD_CLICKS_LIMIT_INVALID',
'AD_OWNER_INVALID',
]),
- array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', '1', array('above_header', 'above_footer'), '2018-01-01', '2033-01-01', '4', '1', '50', '30', 'admin', ['5'], 0], 2, []),
+ array(true, ['Ad Name #1', 'Ad Note #1', 'Ad Code #1', '1', array('above_header', 'above_footer'), '2018-01-01', '2033-01-01', '4', '1', '50', '30', 'admin', ['5'], 0, 0], 2, []),
);
}
@@ -143,14 +143,14 @@ public function get_form_data_data()
*/
public function test_get_form_data($valid_form, $data, $ad_owner_expected, $errors)
{
- [$ad_name, $ad_note, $ad_code, $ad_enabled, $ad_locations, $ad_start_date, $ad_end_date, $ad_priority, $ad_content_only, $ad_views_limit, $ad_clicks_limit, $ad_owner, $ad_groups, $ad_centering] = $data;
+ [$ad_name, $ad_note, $ad_code, $ad_enabled, $ad_locations, $ad_start_date, $ad_end_date, $ad_priority, $ad_content_only, $ad_views_limit, $ad_clicks_limit, $ad_owner, $ad_groups, $ad_centering, $ad_consent] = $data;
self::$valid_form = $valid_form;
$input_controller = $this->get_input_controller();
- $this->request->expects(self::exactly(14))
+ $this->request->expects(self::exactly(15))
->method('variable')
- ->will(self::onConsecutiveCalls($ad_name, $ad_note, $ad_code, $ad_enabled, $ad_locations, $ad_start_date, $ad_end_date, $ad_priority, $ad_content_only, $ad_views_limit, $ad_clicks_limit, $ad_owner, $ad_groups, $ad_centering));
+ ->will(self::onConsecutiveCalls($ad_name, $ad_note, $ad_code, $ad_enabled, $ad_locations, $ad_start_date, $ad_end_date, $ad_priority, $ad_content_only, $ad_views_limit, $ad_clicks_limit, $ad_owner, $ad_groups, $ad_centering, $ad_consent));
$result = $input_controller->get_form_data();
@@ -176,6 +176,7 @@ public function test_get_form_data($valid_form, $data, $ad_owner_expected, $erro
'ad_owner' => $ad_owner_expected,
'ad_groups' => $ad_groups,
'ad_centering' => $ad_centering,
+ 'ad_consent' => $ad_consent,
), $result);
}
}
diff --git a/tests/controller/helper_test.php b/tests/controller/helper_test.php
index 5d8a1eb7..79f035f6 100644
--- a/tests/controller/helper_test.php
+++ b/tests/controller/helper_test.php
@@ -152,6 +152,7 @@ public function assign_data_data()
'ad_clicks_limit' => 0,
'ad_owner' => 0,
'ad_centering' => false,
+ 'ad_consent' => 1,
'ad_locations' => [],
), '', array('AD_PRIORITY_INVALID'), true, 'AD_PRIORITY_INVALID'),
array(array(
@@ -167,6 +168,7 @@ public function assign_data_data()
'ad_clicks_limit' => 0,
'ad_owner' => 0,
'ad_centering' => 0,
+ 'ad_consent' => 1,
'ad_locations' => [],
), '', array('AD_PRIORITY_INVALID', 'AD_NAME_REQUIRED'), true, 'AD_PRIORITY_INVALID
AD_NAME_REQUIRED'),
array(array(
@@ -182,6 +184,7 @@ public function assign_data_data()
'ad_clicks_limit' => 0,
'ad_owner' => 99,
'ad_centering' => 0,
+ 'ad_consent' => 1,
'ad_locations' => [],
), 'Anonymous', array(), false, ''),
array(array(
@@ -197,6 +200,7 @@ public function assign_data_data()
'ad_clicks_limit' => 0,
'ad_owner' => 99,
'ad_centering' => 0,
+ 'ad_consent' => 1,
'ad_locations' => [],
), 'Anonymous', array(), false, ''),
array(array(
@@ -212,6 +216,7 @@ public function assign_data_data()
'ad_clicks_limit' => 0,
'ad_owner' => 2,
'ad_centering' => 0,
+ 'ad_consent' => 0,
'ad_locations' => [],
), 'admin', array(), false, ''),
);
@@ -252,6 +257,7 @@ public function test_assign_data($data, $owner, $errors, $s_errors, $error_msg)
'AD_CLICKS_LIMIT' => $data['ad_clicks_limit'],
'AD_OWNER' => $owner,
'AD_CENTERING' => $data['ad_centering'],
+ 'AD_CONSENT' => $data['ad_consent'],
));
$helper->assign_data($data, $errors);
diff --git a/tests/event/main_listener_test.php b/tests/event/main_listener_test.php
index 803fe43a..2e9c184f 100644
--- a/tests/event/main_listener_test.php
+++ b/tests/event/main_listener_test.php
@@ -35,6 +35,40 @@ public function test_getSubscribedEvents()
'core.adm_page_header_after',
'core.group_add_user_after',
'core.group_delete_user_after',
+ 'phpbb.consentmanager.collect_registrations',
), array_keys(\phpbb\ads\event\main_listener::getSubscribedEvents()));
}
+
+ public function test_register_ads()
+ {
+ $this->language->add_lang('common', 'phpbb/ads');
+ $listener = $this->get_listener();
+ $consent_manager = new consent_manager_double();
+
+ $listener->register_ads(array(
+ 'consent_manager' => $consent_manager,
+ ));
+
+ self::assertCount(1, $consent_manager->registrations);
+ self::assertSame('phpbb.ads', $consent_manager->registrations[0]['id']);
+ self::assertSame(array(
+ 'label' => $this->language->lang('PHPBB_ADS_CONSENT_LABEL'),
+ 'category' => \phpbb\ads\ad\manager::CONSENT_CATEGORY,
+ 'description' => $this->language->lang('PHPBB_ADS_CONSENT_DESCRIPTION'),
+ ), $consent_manager->registrations[0]['definition']);
+ }
+}
+
+class consent_manager_double
+{
+ /** @var array */
+ public $registrations = array();
+
+ public function register($id, array $definition)
+ {
+ $this->registrations[] = array(
+ 'id' => $id,
+ 'definition' => $definition,
+ );
+ }
}
diff --git a/tests/event/setup_ads_consentmanager_test.php b/tests/event/setup_ads_consentmanager_test.php
new file mode 100644
index 00000000..3d4580fa
--- /dev/null
+++ b/tests/event/setup_ads_consentmanager_test.php
@@ -0,0 +1,250 @@
+
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ */
+
+namespace phpbb\ads\tests\event;
+
+class setup_ads_consentmanager_test extends main_listener_base
+{
+ public function test_setup_ads_defers_ad_markup_when_consentmanager_is_enabled()
+ {
+ $stored_ad_code = htmlspecialchars(
+ 'Ad
',
+ ENT_COMPAT
+ );
+
+ $this->user->data['user_id'] = 1;
+ $this->user->page['page_name'] = 'index.' . $this->php_ext;
+ $this->user->page['page_dir'] = '';
+
+ $this->manager = $this->getMockBuilder('\phpbb\ads\ad\manager')
+ ->disableOriginalConstructor()
+ ->setMethods(array('load_memberships', 'get_ads'))
+ ->getMock();
+ $this->location_manager = $this->getMockBuilder('\phpbb\ads\location\manager')
+ ->disableOriginalConstructor()
+ ->setMethods(array('get_all_location_ids'))
+ ->getMock();
+
+ $this->location_manager->expects(self::once())
+ ->method('get_all_location_ids')
+ ->willReturn(array('above_header'));
+
+ $this->manager->expects(self::once())
+ ->method('load_memberships')
+ ->with(1)
+ ->willReturn(array());
+
+ $this->manager->expects(self::once())
+ ->method('get_ads')
+ ->with(array('above_header'), array(), false)
+ ->willReturn(array(array(
+ 'location_id' => 'above_header',
+ 'ad_id' => 42,
+ 'ad_code' => $stored_ad_code,
+ 'ad_centering' => 0,
+ )));
+
+ $this->template->expects(self::exactly(2))
+ ->method('retrieve_var')
+ ->willReturnCallback(function ($var_name)
+ {
+ return $var_name === 'S_CONSENTMANAGER_MARKETING_ENABLED';
+ });
+
+ $this->template->expects(self::once())
+ ->method('assign_vars')
+ ->with(self::callback(function ($vars)
+ {
+ return $vars['AD_ABOVE_HEADER_ID'] === 42
+ && $vars['AD_ABOVE_HEADER_CENTER'] === false
+ && strpos($vars['AD_ABOVE_HEADER'], 'type="text/plain"') !== false
+ && strpos($vars['AD_ABOVE_HEADER'], 'data-consent-category="marketing"') !== false
+ && strpos($vars['AD_ABOVE_HEADER'], 'src="https://ads.example.com/tag.js"') !== false
+ && strpos($vars['AD_ABOVE_HEADER'], '') !== false
+ && strpos($vars['AD_ABOVE_HEADER'], 'phpbb-ads-consent-placeholder') === false;
+ }));
+
+ $this->get_listener()->setup_ads();
+ }
+
+ public function test_setup_ads_adds_consent_category_to_text_plain_scripts()
+ {
+ $stored_ad_code = htmlspecialchars(
+ '',
+ ENT_COMPAT
+ );
+
+ $this->user->data['user_id'] = 1;
+ $this->user->page['page_name'] = 'index.' . $this->php_ext;
+ $this->user->page['page_dir'] = '';
+
+ $this->manager = $this->getMockBuilder('\phpbb\ads\ad\manager')
+ ->disableOriginalConstructor()
+ ->setMethods(array('load_memberships', 'get_ads'))
+ ->getMock();
+ $this->location_manager = $this->getMockBuilder('\phpbb\ads\location\manager')
+ ->disableOriginalConstructor()
+ ->setMethods(array('get_all_location_ids'))
+ ->getMock();
+
+ $this->location_manager->expects(self::once())
+ ->method('get_all_location_ids')
+ ->willReturn(array('above_header'));
+
+ $this->manager->expects(self::once())
+ ->method('load_memberships')
+ ->with(1)
+ ->willReturn(array());
+
+ $this->manager->expects(self::once())
+ ->method('get_ads')
+ ->with(array('above_header'), array(), false)
+ ->willReturn(array(array(
+ 'location_id' => 'above_header',
+ 'ad_id' => 99,
+ 'ad_code' => $stored_ad_code,
+ 'ad_centering' => 0,
+ )));
+
+ $this->template->expects(self::exactly(2))
+ ->method('retrieve_var')
+ ->willReturnCallback(function ($var_name)
+ {
+ return $var_name === 'S_CONSENTMANAGER_MARKETING_ENABLED';
+ });
+
+ $this->template->expects(self::once())
+ ->method('assign_vars')
+ ->with(self::callback(function ($vars)
+ {
+ return $vars['AD_ABOVE_HEADER_ID'] === 99
+ && strpos($vars['AD_ABOVE_HEADER'], 'type="text/plain"') !== false
+ && strpos($vars['AD_ABOVE_HEADER'], 'data-consent-category="marketing"') !== false
+ && strpos($vars['AD_ABOVE_HEADER'], 'src="https://ads.example.com/legacy.js"') !== false;
+ }));
+
+ $this->get_listener()->setup_ads();
+ }
+
+ public function test_setup_ads_does_not_defer_when_marketing_category_is_disabled()
+ {
+ $stored_ad_code = htmlspecialchars(
+ '',
+ ENT_COMPAT
+ );
+
+ $this->user->data['user_id'] = 1;
+ $this->user->page['page_name'] = 'index.' . $this->php_ext;
+ $this->user->page['page_dir'] = '';
+
+ $this->manager = $this->getMockBuilder('\phpbb\ads\ad\manager')
+ ->disableOriginalConstructor()
+ ->setMethods(array('load_memberships', 'get_ads'))
+ ->getMock();
+ $this->location_manager = $this->getMockBuilder('\phpbb\ads\location\manager')
+ ->disableOriginalConstructor()
+ ->setMethods(array('get_all_location_ids'))
+ ->getMock();
+
+ $this->location_manager->expects(self::once())
+ ->method('get_all_location_ids')
+ ->willReturn(array('above_header'));
+
+ $this->manager->expects(self::once())
+ ->method('load_memberships')
+ ->with(1)
+ ->willReturn(array());
+
+ $this->manager->expects(self::once())
+ ->method('get_ads')
+ ->with(array('above_header'), array(), false)
+ ->willReturn(array(array(
+ 'location_id' => 'above_header',
+ 'ad_id' => 77,
+ 'ad_code' => $stored_ad_code,
+ 'ad_centering' => 0,
+ )));
+
+ $this->template->expects(self::exactly(2))
+ ->method('retrieve_var')
+ ->willReturn(false);
+
+ $this->template->expects(self::once())
+ ->method('assign_vars')
+ ->with(self::callback(function ($vars)
+ {
+ return strpos($vars['AD_ABOVE_HEADER'], 'type="text/plain"') === false
+ && strpos($vars['AD_ABOVE_HEADER'], 'data-consent-category="marketing"') === false
+ && strpos($vars['AD_ABOVE_HEADER'], 'src="https://ads.example.com/tag.js"') !== false;
+ }));
+
+ $this->get_listener()->setup_ads();
+ }
+
+ public function test_setup_ads_does_not_defer_when_ad_consent_is_disabled()
+ {
+ $stored_ad_code = htmlspecialchars(
+ '',
+ ENT_COMPAT
+ );
+
+ $this->user->data['user_id'] = 1;
+ $this->user->page['page_name'] = 'index.' . $this->php_ext;
+ $this->user->page['page_dir'] = '';
+
+ $this->manager = $this->getMockBuilder('\phpbb\ads\ad\manager')
+ ->disableOriginalConstructor()
+ ->setMethods(array('load_memberships', 'get_ads'))
+ ->getMock();
+ $this->location_manager = $this->getMockBuilder('\phpbb\ads\location\manager')
+ ->disableOriginalConstructor()
+ ->setMethods(array('get_all_location_ids'))
+ ->getMock();
+
+ $this->location_manager->expects(self::once())
+ ->method('get_all_location_ids')
+ ->willReturn(array('above_header'));
+
+ $this->manager->expects(self::once())
+ ->method('load_memberships')
+ ->with(1)
+ ->willReturn(array());
+
+ $this->manager->expects(self::once())
+ ->method('get_ads')
+ ->with(array('above_header'), array(), false)
+ ->willReturn(array(array(
+ 'location_id' => 'above_header',
+ 'ad_id' => 78,
+ 'ad_code' => $stored_ad_code,
+ 'ad_centering' => 0,
+ 'ad_consent' => 0,
+ )));
+
+ $this->template->expects(self::exactly(2))
+ ->method('retrieve_var')
+ ->willReturnCallback(function ($var_name)
+ {
+ return $var_name === 'S_CONSENTMANAGER_MARKETING_ENABLED';
+ });
+
+ $this->template->expects(self::once())
+ ->method('assign_vars')
+ ->with(self::callback(function ($vars)
+ {
+ return $vars['AD_ABOVE_HEADER_ID'] === 78
+ && strpos($vars['AD_ABOVE_HEADER'], 'type="text/plain"') === false
+ && strpos($vars['AD_ABOVE_HEADER'], 'data-consent-category="marketing"') === false
+ && strpos($vars['AD_ABOVE_HEADER'], 'src="https://ads.example.com/tag.js"') !== false;
+ }));
+
+ $this->get_listener()->setup_ads();
+ }
+}
diff --git a/tests/functional/functional_base.php b/tests/functional/functional_base.php
index bd5395d1..f7e42a1b 100644
--- a/tests/functional/functional_base.php
+++ b/tests/functional/functional_base.php
@@ -42,7 +42,7 @@ protected function setUp(): void
$this->admin_login();
}
- protected function create_ad($location, $end_date = '', $content_only = false, $centering = true, $start_date = '')
+ protected function create_ad($location, $end_date = '', $content_only = false, $centering = true, $start_date = '', $ad_code = '')
{
// Load Advertisement management ACP page
$crawler = self::request('GET', "adm/index.php?i=-phpbb-ads-acp-main_module&mode=manage&sid={$this->sid}");
@@ -51,11 +51,16 @@ protected function create_ad($location, $end_date = '', $content_only = false, $
$form = $crawler->selectButton($this->lang('ACP_ADS_ADD'))->form();
$crawler = self::submit($form);
+ if ($ad_code === '')
+ {
+ $ad_code = '';
+ }
+
// Create ad
$form_data = array(
'ad_name' => 'Functional test template location ' . $location,
'ad_note' => '',
- 'ad_code' => '',
+ 'ad_code' => $ad_code,
'ad_enabled' => 1,
'ad_locations' => array($location),
'ad_start_date' => $start_date,