hooks(); } return self::$instance; } /** * Register all necessary hooks for the Privacy module. * * @return void */ public function hooks(): void { add_action( 'adpresso_register_assets', [ $this, 'register_assets' ] ); add_action( 'admin_bar_menu', function( $wp_admin_bar ) { $no_cookies = (bool) Settings::get_setting( 'no_cookies' ); if ( $no_cookies ) { NotificationManager::get()->register( 'adpresso-cookies_disabled', __( 'Cookies are disabled', 'adpresso' ), [ 'location' => 'admin_bar', 'type' => 'warning', ] ); } $method = Settings::get_setting( 'privacy_consent_method' ); NotificationManager::get()->register( 'adpresso-module-privacy-type', /* translators: %s is the selected privacy method */ sprintf( esc_html__( 'Privacy method: %s', 'adpresso' ), $method ), [ 'location' => 'admin_bar', 'context' => 'adpresso-module-privacy', ] ); } ); // wp_footer:5 - after the_content (so block/shortcode-forced JS placements count) and before Runtime::localize_frontend_data() at wp_footer:19. add_action( 'wp_footer', [ $this, 'enqueue_frontend_assets' ], 5 ); add_filter( 'adpresso_data_for_js', [ $this, 'add_config_to_localized_data' ] ); add_filter( 'adpresso_requires_frontend_logic', [ $this, 'force_js_rendering_if_active' ], 10, 3 ); } /** * Registers and enqueues the frontend JavaScript for the Privacy module. * * @return void */ public function enqueue_frontend_assets(): void { if ( ! class_exists( ModuleManager::class ) || ! ModuleManager::is_active( self::MODULE_ID ) ) { return; } if ( is_admin() ) { return; } // privacy-manager.js only does anything when a JS-rendered ad is waiting on consent. if ( ! Runtime::has_js_placements() && ! wp_script_is( 'adpresso-placement-controller', 'enqueued' ) ) { return; } wp_enqueue_script( 'adpresso-privacy' ); } /** * Registers the assets for the Privacy module. * * @param \AdPresso\Core\Assets $assets The Assets service instance. */ public function register_assets( \AdPresso\Core\Assets $assets ): void { if ( ! ModuleManager::is_active( self::MODULE_ID ) ) { return; } $dependencies = [ 'adpresso-core-events', ]; $assets->register_script( 'privacy', 'modules/privacy/assets/js/privacy-manager.js', $dependencies, true ); } /** * Callback for the filter 'adpresso_requires_frontend_logic'. * Forces JS rendering for all placements when the Privacy module is active, * except for specific ad types (Image, Dummy). * * @param bool $needs_js The initial value indicating whether JS rendering is needed. * @param Placement $placement The current Placement object. * @param mixed $item The placement item (Ad or Group). * * @return bool True, if JS rendering should be forced, false otherwise. */ public function force_js_rendering_if_active( bool $needs_js, Placement $placement, mixed $item ): bool { // If JS is already required for other reasons, do not change it. if ( $needs_js ) { return true; } if ( ! class_exists( ModuleManager::class ) || ! ModuleManager::is_active( self::MODULE_ID ) ) { return false; } // The general fallback shows regardless of consent, so SSR may print it. return $item instanceof Entity && $item->is_backend_valid && ConsentPolicy::item_requires_consent( $item, $placement ); } /** * Adds the Privacy module configuration to the localized data * * @param array $data The data array that will be localized for the frontend JS. * * @return array The modified data array with the Privacy configuration added. */ public function add_config_to_localized_data( array $data ): array { if ( ! class_exists( ModuleManager::class ) || ! ModuleManager::is_active( self::MODULE_ID ) ) { return $data; } // Prepare the JavaScript configuration for the Privacy module. $js_config = [ 'consent_method' => Settings::get_setting( 'privacy_consent_method', 'automatic' ), 'tcf_purposes_personalized' => apply_filters( 'adpresso_privacy_tcf_purposes_personalized', [ 1, 3, 4 ] ), 'tcf_purposes_npa' => apply_filters( 'adpresso_privacy_tcf_purposes_npa', [ 1, 2, 7, 9, 10 ] ), // Premium settings: only Pro sends the NPA signal, so Free must treat partial consent as rejected. 'allow_npa_fallback' => Util::is_pro_active() && (bool) Settings::get_setting( 'tcf_personalized_purpose', 1 ), 'tcf_npa_allow_legitimate_interest' => Util::is_pro_active() && (bool) Settings::get_setting( 'npa_allow_legitimate_interest', 0 ), 'cookie_name' => sanitize_text_field( Settings::get_setting( 'cookie_name', 'cookie_consent' ) ), 'cookie_value_accepted' => sanitize_text_field( Settings::get_setting( 'cookie_value', 'accepted' ) ), 'cookie_value_partial' => sanitize_text_field( Settings::get_setting( 'cookie_value_partial', 'partial' ) ), 'rcb_marketing_service_ids' => self::get_cmp_marketing_service_ids( 'rcb_marketing_service_ids' ), 'pixelmate_marketing_service_ids' => self::get_cmp_marketing_service_ids( 'pixelmate_marketing_service_ids' ), ]; $data['privacyConfig'] = $js_config; return $data; } /** * Parses a comma-separated "Marketing Service ID(s)" setting (RCB, Pixelmate, ...) * into a clean list for the frontend. * * @param string $setting_id The settings key holding the comma-separated ID list. * * @return string[] */ private static function get_cmp_marketing_service_ids( string $setting_id ): array { $raw = (string) Settings::get_setting( $setting_id, '' ); if ( $raw === '' ) { return []; } return array_values( array_filter( array_map( static function ( $id ) { return sanitize_text_field( trim( $id ) ); }, explode( ',', $raw ) ) ) ); } /** * * * @return array */ public static function get_tcf_vendor_list_options(): array { return apply_filters( 'adpresso_tcf_vendor_list_options', [ [ 'value' => '0', 'label' => __( 'None / No TCF Vendor', 'adpresso' ) ], [ 'value' => '755', 'label' => 'Google Advertising Products' ], [ 'value' => '42', 'label' => 'Taboola' ], [ 'value' => '10', 'label' => 'Index Exchange' ], [ 'value' => '793', 'label' => 'Amazon Advertising' ], ] ); } /** * Maps a recognizable ad-vendor "keyword" to the TCF vendors. * * @return array Keyword (matched against TCF vendor option labels) => needles. */ public static function get_vendor_detection_patterns(): array { return apply_filters( 'adpresso_tcf_vendor_detection_patterns', [ 'Google' => [ 'googlesyndication.com', 'doubleclick.net', 'googletagservices.com', 'googletagmanager.com', 'adsbygoogle', 'googletag.' ], 'Taboola' => [ 'taboola.com', '_taboola' ], 'Amazon' => [ 'amazon-adsystem.com' ], 'Index Exchange' => [ 'casalemedia.com', 'indexexchange.com' ], ] ); } }