get_id() ?? '?'; RuntimeDebugger::add_server_error_log_entry( "Privacy: Item #{$item->get_id()} in Placement #{$placement_id} requires consent, forcing JS." ); RuntimeDebugger::note_js_forcing_reason( 'privacy', __( 'Privacy module active (forces JS for consent check, except Image/Dummy ads and ads with "Ignore Privacy Settings" enabled)', 'adpresso' ) ); } elseif ( $item instanceof Ad && (bool) $item->get_meta( 'ignore_privacy_settings', 0 ) ) { RuntimeDebugger::add_server_error_log_entry( "Privacy: Ad #{$item->get_id()} has 'ignore_privacy_settings' enabled. Allowing SSR." ); } return $requires_consent; } /** * Adds the `requires_consent` flag to an item's JS config, so placement-controller.js doesn't re-derive the rule. * Config-only decision, so it is safe in full-page-cached HTML. No-op while the Privacy module is off. * * @param array $config The item's frontend config. * @param Entity $item The Ad or Group the config belongs to. * @param Placement|null $placement Passed to the filter as context. * * @return array */ public static function add_js_flag( array $config, Entity $item, ?Placement $placement = null ): array { if ( ! apply_filters( 'adpresso_privacy_module_active', ModuleManager::is_active( PrivacyService::MODULE_ID ) ) ) { return $config; } $config['requires_consent'] = self::requires_consent( $item, $placement ); // Each group child too: privacy-pro.js decides per ad in the NPA (partial) state. if ( $item instanceof Group && ! empty( $config['ads'] ) && is_array( $config['ads'] ) && method_exists( $item, 'get_valid_ads' ) ) { $child_ads = $item->get_valid_ads(); foreach ( $config['ads'] as $ad_id => $ad_config ) { if ( is_array( $ad_config ) && ( $child_ads[ $ad_id ] ?? null ) instanceof Ad ) { $config['ads'][ $ad_id ]['requires_consent'] = self::requires_consent( $child_ads[ $ad_id ], $placement ); } } } return $config; } /** * The general fallback is the last resort and shows regardless of consent. * The placement's custom code stays withheld without consent (placement-controller.js). * * @param array $config The fallback's frontend config. * * @return array */ public static function add_general_fallback_js_flag( array $config ): array { if ( ! apply_filters( 'adpresso_privacy_module_active', ModuleManager::is_active( PrivacyService::MODULE_ID ) ) ) { return $config; } $config['requires_consent'] = false; $config['is_general_fallback'] = true; // Group children too: privacy-pro.js checks each ad (partial consent, TCF vendor, CMP service). if ( ! empty( $config['ads'] ) && is_array( $config['ads'] ) ) { foreach ( $config['ads'] as $ad_id => $ad_config ) { if ( is_array( $ad_config ) ) { $config['ads'][ $ad_id ]['requires_consent'] = false; $config['ads'][ $ad_id ]['is_general_fallback'] = true; } } } return $config; } /** * The decision itself, without debug side effects. * * @param Entity $item * @param Placement|null $placement Its custom placement code is printed with the item. * * @return bool */ private static function requires_consent( Entity $item, ?Placement $placement = null ): bool { if ( ( $item instanceof Ad || $item instanceof Group ) && $placement && self::has_output_custom_code( $placement, 'custom_placement_code' ) ) { $requires_consent = true; } elseif ( $item instanceof Ad ) { $requires_consent = ! self::ad_is_privacy_safe( $item ); } elseif ( $item instanceof Group ) { $requires_consent = self::group_requires_consent( $item ); } else { $requires_consent = false; } return (bool) apply_filters( 'adpresso_item_requires_consent', $requires_consent, $item, $placement ); } /** * Whether a single Ad can be rendered SSR without consent. * * @param Ad $ad * * @return bool */ private static function ad_is_privacy_safe( Ad $ad ): bool { if ( (bool) $ad->get_meta( 'ignore_privacy_settings', 0 ) ) { return true; } // A type-based exemption (Image/Dummy) only covers that type's own first-party output. custom_ad_code can carry arbitrary third-party markup on any ad type. if ( self::has_output_custom_code( $ad, 'custom_ad_code' ) ) { return false; } $allowed_privacy_types = apply_filters( 'adpresso_privacy_exceptional_ad_types', [ 'image', 'dummy' ] ); return in_array( $ad->get_type(), $allowed_privacy_types, true ); } /** * Whether custom code is actually output: same Pro gate as Util::get_custom_code(). * * @param Entity $entity * @param string $meta_key * * @return bool */ private static function has_output_custom_code( Entity $entity, string $meta_key ): bool { return Util::is_pro_active() && (string) $entity->get_meta( $meta_key, '' ) !== ''; } /** * @param Group $group * * @return bool */ private static function group_requires_consent( Group $group ): bool { if ( ! method_exists( $group, 'get_valid_ads' ) ) { return true; } foreach ( $group->get_valid_ads() as $ad_in_group ) { if ( ! $ad_in_group instanceof Ad || ! self::ad_is_privacy_safe( $ad_in_group ) ) { return true; } } return false; } }