*/ private function rules(): array { return (array) apply_filters( 'cumpleconsent_blocker_rules', array( // Publicidad y medición de anuncios. 'googlesyndication.com' => 'publicidad', 'googleadservices.com' => 'publicidad', 'doubleclick.net' => 'publicidad', 'adsbygoogle' => 'publicidad', 'connect.facebook.net' => 'publicidad', 'fbcdn.net' => 'publicidad', 'criteo' => 'publicidad', 'taboola' => 'publicidad', 'outbrain' => 'publicidad', 'bing.com/bat' => 'publicidad', 'clarity.ms' => 'publicidad', 'ads-twitter.com' => 'publicidad', 'analytics.tiktok.com' => 'publicidad', 'snap.licdn.com' => 'publicidad', 'px.ads.linkedin.com' => 'publicidad', 'snaptr' => 'publicidad', 'pintrk' => 'publicidad', // Estadísticas. 'google-analytics.com' => 'analiticas', 'googletagmanager.com' => 'analiticas', 'gtag/js' => 'analiticas', 'static.hotjar.com' => 'analiticas', 'script.hotjar.com' => 'analiticas', 'plausible.io' => 'analiticas', 'umami' => 'analiticas', 'matomo' => 'analiticas', 'statcounter' => 'analiticas', 'histats' => 'analiticas', // Contenido incrustado y redes sociales. 'youtube.com/iframe_api' => 'embeds', 'youtube.com/embed' => 'embeds', 'youtube-nocookie.com' => 'embeds', 'player.vimeo.com' => 'embeds', 'maps.googleapis.com' => 'embeds', 'maps.google.com' => 'embeds', 'platform.twitter.com' => 'embeds', 'platform.instagram.com' => 'embeds', 'connect.instagram.com' => 'embeds', 'snap.licdn.com' => 'embeds', 'open.spotify.com' => 'embeds', 'soundcloud.com' => 'embeds', 'tiktok.com/embed' => 'embeds', 'w.soundcloud.com' => 'embeds', 'calendly.com' => 'embeds', // Herramientas de contacto y soporte que cargan al visitante. 'widget.intercom.io' => 'preferencias', 'js.intercomcdn.com' => 'preferencias', 'client.crisp.chat' => 'preferencias', 'embed.tawk.to' => 'preferencias', 'static.zdassets.com' => 'preferencias', 'js.driftt.com' => 'preferencias', ) ); } /** * Firmas de scripts EN LÍNEA que corresponden a terceros rastreadores. * Se busca la subcadena en el cuerpo del script en línea. * * @return array */ private function inline_rules(): array { return (array) apply_filters( 'cumpleconsent_inline_rules', array( 'googletagmanager.com/gtag/js' => 'analiticas', "gtag('config'" => 'analiticas', 'gtag("config"' => 'analiticas', 'dataLayer.push' => 'analiticas', "createElement('script')" => 'analiticas', 'fbq(' => 'publicidad', 'ttq.load' => 'publicidad', 'snaptr(' => 'publicidad', 'twq(' => 'publicidad', 'pintrk(' => 'publicidad', '_linkedin_partner_id' => 'publicidad', 'clarity(' => 'publicidad', 'hj(' => 'analiticas', 'ym(' => 'analiticas', ) ); } /** * Dominios de iframes incrustados → finalidad. * * @return array */ private function embed_rules(): array { return (array) apply_filters( 'cumpleconsent_embed_rules', array( 'youtube.com' => 'embeds', 'youtube-nocookie.com' => 'embeds', 'youtu.be' => 'embeds', 'player.vimeo.com' => 'embeds', 'maps.google' => 'embeds', 'google.com/maps' => 'embeds', 'openstreetmap.org' => 'embeds', 'spotify.com' => 'embeds', 'soundcloud.com' => 'embeds', 'instagram.com' => 'embeds', 'facebook.com/plugins' => 'embeds', 'twitter.com' => 'embeds', 'x.com' => 'embeds', 'tiktok.com' => 'embeds', 'linkedin.com/embed' => 'embeds', 'pinterest.com' => 'embeds', 'calendly.com' => 'embeds', 'google.com/recaptcha' => 'necesarias', 'recaptcha.net' => 'necesarias', ) ); } /** * Patrones de servicios que entienden Google Consent Mode v2. En modo * equilibrado no se bloquean: cargan sin cookies y se personalizan al aceptar. * * @return string[] */ private function consent_mode_patterns(): array { return (array) apply_filters( 'cumpleconsent_consent_mode_patterns', array( 'googlesyndication.com', 'googleadservices.com', 'doubleclick.net', 'adsbygoogle', 'google-analytics.com', 'googletagmanager.com', 'gtag/js', 'datalayer.push', ) ); } /** Categoría asociada a un texto (URL, handle o cuerpo de script). */ private function match( string $needle, array $rules ): string { foreach ( $rules as $pattern => $purpose ) { if ( '' !== $pattern && false !== stripos( $needle, (string) $pattern ) ) { return (string) $purpose; } } return ''; } /** ¿El patrón corresponde a un servicio gobernado por Consent Mode v2? */ private function is_consent_mode_managed( string $needle ): bool { return '' !== $this->match( $needle, array_fill_keys( $this->consent_mode_patterns(), 'x' ) ); } /** * ¿Hay que neutralizar este recurso? * * @param string $purpose Finalidad detectada. * @param string $needle Texto donde se detectó. * @return bool */ private function should_block( string $purpose, string $needle ): bool { if ( '' === $purpose ) { return false; } if ( self::MODE_BALANCED === self::mode() && $this->is_consent_mode_managed( $needle ) ) { return false; } return true; } /** * Capa 2: scripts encolados por WordPress. * * @param string $tag Etiqueta completa. * @param string $handle Identificador del script. * @param string $src URL. * @return string */ public function rewrite_handle( $tag, $handle, $src ) { if ( is_admin() || ! Settings::is_ready() ) { return $tag; } $needle = (string) $handle . ' ' . (string) $src; $purpose = $this->match( $needle, $this->rules() ); if ( ! $this->should_block( $purpose, $needle ) ) { return $tag; } return self::neutralize_script( (string) $tag, $purpose ); } /** Arranca el buffer del front-end. */ public function maybe_start_buffer(): void { if ( is_admin() || is_feed() || is_robots() || is_trackback() ) { return; } if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return; } if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { return; } if ( is_customize_preview() || is_preview() ) { return; } if ( ! Settings::is_ready() && ! Notice::hash() ) { return; } ob_start( array( $this, 'filter_buffer' ) ); self::$buffer_level = ob_get_level(); } /** * Cierra el búfer que abrió este plugin. * * Un búfer abierto sin cerrar se desalinea con el de otro componente y deja la * salida a medias. Aquí se vacía exactamente hasta el nivel propio, sin tocar * los búferes que hayan abierto el tema u otros plugins. */ public function close_buffer(): void { if ( null === self::$buffer_level ) { return; } while ( ob_get_level() >= self::$buffer_level ) { ob_end_flush(); } self::$buffer_level = null; } /** * Capa 3: reescribe el HTML de salida. * * @param string $html HTML. * @return string */ public function filter_buffer( $html ) { if ( ! is_string( $html ) || strlen( $html ) < 200 ) { return (string) $html; } /** * HTML final que se sirve al visitante, ya reescrito. * * Permite que el aviso se inyecte aquí cuando el tema no imprime el pie: * hay plantillas (páginas de “próximamente”, maquetadores) que no llaman a * wp_footer() y el aviso nunca aparecía. * * @param string $html HTML de la página. */ $html = (string) apply_filters( 'cumpleconsent_served_html', $html ); $has_script = false !== stripos( $html, ']*\btype=["\']text/plain["\'])[^>]*\bsrc\s*=\s*(["\'])([^"\']+)\1[^>]*>(?:\s*)?#i', function ( $m ) { $src = (string) $m[2]; $purpose = $this->match( $src, $this->rules() ); if ( '' === $purpose ) { return $m[0]; } if ( ! $this->should_block( $purpose, $src ) ) { return $m[0]; } return self::neutralize_script( $m[0], $purpose ); }, $html ); // Scripts en línea de terceros (sin src). $html = (string) preg_replace_callback( '#]*\bsrc\s*=)(?![^>]*\btype=["\']text/plain["\'])[^>]*>(.*?)#is', function ( $m ) { $body = (string) $m[1]; if ( strlen( $body ) < 12 ) { return $m[0]; } $purpose = $this->match( $body, $this->inline_rules() ); if ( '' === $purpose || ! $this->should_block( $purpose, $body ) ) { return $m[0]; } return self::neutralize_script( $m[0], $purpose ); }, $html ); } if ( $has_iframe && Settings::get( 'block_iframes', true ) ) { $html = (string) preg_replace_callback( '#]*\bsrc\s*=\s*(["\'])([^"\']+)\1[^>]*>(?:.*?)?#is', function ( $m ) { $src = (string) $m[2]; $purpose = $this->match( $src, $this->embed_rules() ); if ( '' === $purpose || ! $this->should_block( $purpose, $src ) ) { return $m[0]; } return self::embed_placeholder( $m[0], $src, $purpose ); }, $html ); } if ( Settings::get( 'block_embedded', true ) && ( false !== stripos( $html, ']*\b(?:src|data)\s*=\s*(["\'])([^"\']+)\2[^>]*>(?:.*?)?#is', function ( $m ) { $src = (string) $m[3]; $purpose = $this->match( $src, $this->embed_rules() ); if ( '' === $purpose || ! $this->should_block( $purpose, $src ) ) { return $m[0]; } return self::embed_placeholder( $m[0], $src, $purpose ); }, $html ); } return $html; } /** * Convierte una etiqueta