settings = $settings; } /** * Hook into WordPress. */ public function register() { add_action( 'wp_head', array( $this, 'head_script' ), -1000 ); add_action( 'wp_enqueue_scripts', array( $this, 'assets' ) ); add_action( 'wp_footer', array( $this, 'markup' ), 5 ); add_shortcode( 'goconsent_choices', array( $this, 'choices_shortcode' ) ); } /** * Early inline script: consent state, Google Consent Mode v2 defaults and the browser guard. * Printed before any other script so trackers always see "denied" first. */ public function head_script() { if ( Skip::request() ) { return; } $classifier = new Classifier( $this->settings ); $config = array( 'v' => (string) $this->settings->get( 'version' ), 'gpc' => (bool) $this->settings->get( 'gpc' ), 'ad' => (bool) $this->settings->get( 'analytics_default' ), 'md' => (bool) $this->settings->get( 'media_default' ), 'rg' => (bool) $this->settings->get( 'region_defaults' ), 'nb' => BannerMode::active( $this->settings ), 'guard' => $this->settings->get( 'block' ) ? $classifier->guard_config() : null, 'maps' => MapsGate::enabled() ? MapsGate::needles() : null, ); $js = 'window.GoConsentConfig=' . wp_json_encode( $config ) . ';' . file_get_contents( GOCONSENT_DIR . 'assets/js/consent-head.js' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin file. wp_print_inline_script_tag( $js, array( 'data-goconsent-skip' => '1' ) ); } /** * Front-end assets. */ public function assets() { if ( Skip::request() ) { return; } // The version is also in the file address (gcv), so plugins that rewrite "ver" can't leave browsers on old files. wp_enqueue_style( 'goconsent', GOCONSENT_URL . 'assets/css/consent.css?gcv=' . rawurlencode( GOCONSENT_VERSION ), array(), GOCONSENT_VERSION ); wp_add_inline_style( 'goconsent', ':root{' . self::tokens( (string) $this->settings->get( 'banner_color' ) ) . '}' ); wp_enqueue_script( 'goconsent', GOCONSENT_URL . 'assets/js/consent.js?gcv=' . rawurlencode( GOCONSENT_VERSION ), array(), GOCONSENT_VERSION, array( 'in_footer' => true, 'strategy' => 'defer', ) ); // Stylesheet for the banner's shadow root (sealed off from theme CSS); consent.js adopts it. wp_add_inline_script( 'goconsent', 'window.GoConsentUICSS=' . wp_json_encode( self::ui_css( (string) $this->settings->get( 'banner_color' ) ) ) . ';', 'before' ); wp_localize_script( 'goconsent', 'GoConsentFront', array( 'traffic' => $this->settings->get( 'traffic' ) && ( $this->settings->get( 'traffic_admins' ) || ! current_user_can( 'manage_options' ) ), 'endpoint' => esc_url_raw( rest_url( 'goconsent/v1/hit' ) ), 'consentEndpoint' => esc_url_raw( rest_url( 'goconsent/v1/consent' ) ), 'mapSelectors' => MapsGate::enabled() ? implode( ',', MapsGate::selectors() ) : '', 'mapTriggers' => MapsGate::enabled() ? MapsGate::triggers() : array(), 'i18n' => array( 'mapText' => __( 'This map is provided by Google Maps. Loading it shares your IP address with Google and may set their cookies.', 'goconsent' ), 'mapLoad' => __( 'Load map', 'goconsent' ), 'mapCancel' => __( 'Not now', 'goconsent' ), /* translators: %s: date and time the choice was saved. */ 'saved' => __( 'Saved %s', 'goconsent' ), /* translators: %s: list of allowed categories, e.g. "Analytics, Maps & videos". */ 'allowed' => __( 'Allowed: %s', 'goconsent' ), 'necessary' => __( 'Only necessary cookies', 'goconsent' ), 'copy' => __( 'Copy ID', 'goconsent' ), 'copied' => __( 'Copied', 'goconsent' ), 'cats' => array_map( static function ( $l ) { return $l[1]; }, Cookies::labels() ), ), ) ); } /** * Privacy policy URL. * * @return string */ private function privacy_url() { $id = $this->settings->privacy_page_id(); return $id ? (string) get_permalink( $id ) : ''; } /** * Banner, preferences dialog, cookie policy dialog and optional footer link. */ public function markup() { if ( Skip::request() ) { return; } $privacy = $this->privacy_url(); $position = $this->settings->get( 'banner_position' ); $ad = (bool) $this->settings->get( 'analytics_default' ); $labels = Cookies::labels(); $decl = Cookies::declaration( $this->settings ); /* * The banner and dialogs render inside a Shadow DOM: consent.js attaches a shadow root to * and moves this template into it, so theme and page-builder CSS can't * reach them. Browsers without Shadow DOM get the same markup in the page, styled by * the scoped consent.css. */ echo ''; /** * Whether GoConsent prints its own "Your Privacy Choices" link. * Integrations that place the link in their own footer return false. * * @param bool $show Default from settings. */ if ( apply_filters( 'goconsent_render_footer_link', (bool) $this->settings->get( 'footer_link' ) ) ) { echo ''; } } /** * Floating privacy button (GoConsent shield) that reopens Your Privacy Choices after a choice. * * @param string $side left|right. * @param int $offset Distance from the bottom of the screen, in px. * @return string */ private static function badge( $side, $offset ) { $side = 'right' === $side ? 'right' : 'left'; $offset = max( 12, min( 160, (int) $offset ) ); $label = __( 'Your Privacy Choices', 'goconsent' ); $svg = ''; return ''; } /** * Color tokens from the banner color, adjusted so text always meets WCAG AA (4.5:1): * --gc-on-accent is the button text color (white or near-black, whichever reads better), * --gc-accent-text is the accent darkened as needed for links and outline buttons on white. * * @param string $accent Banner color (hex). * @return string CSS custom properties. */ public static function tokens( $accent ) { $accent = sanitize_hex_color( (string) $accent ); $accent = $accent ? $accent : '#0e7a52'; $rgb = self::rgb( $accent ); $on = self::contrast( $rgb, array( 255, 255, 255 ) ) >= self::contrast( $rgb, array( 17, 17, 17 ) ) ? '#fff' : '#111'; $text = $rgb; for ( $i = 0; $i < 40 && self::contrast( $text, array( 255, 255, 255 ) ) < 4.5; $i++ ) { $text = array_map( static function ( $c ) { return (int) floor( $c * 0.9 ); }, $text ); } return '--gc-accent:' . $accent . ';--gc-on-accent:' . $on . ';--gc-accent-text:' . vsprintf( '#%02x%02x%02x', $text ) . ';'; } /** * Hex color to [r, g, b]. * * @param string $hex #rgb or #rrggbb. * @return int[] */ private static function rgb( $hex ) { $hex = ltrim( $hex, '#' ); if ( 3 === strlen( $hex ) ) { $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; } return array( hexdec( substr( $hex, 0, 2 ) ), hexdec( substr( $hex, 2, 2 ) ), hexdec( substr( $hex, 4, 2 ) ) ); } /** * WCAG contrast ratio between two colors. * * @param int[] $a First color. * @param int[] $b Second color. * @return float */ private static function contrast( array $a, array $b ) { $lum = static function ( array $c ) { $l = array(); foreach ( $c as $v ) { $v = $v / 255; $l[] = $v <= 0.03928 ? $v / 12.92 : pow( ( $v + 0.055 ) / 1.055, 2.4 ); } return 0.2126 * $l[0] + 0.7152 * $l[1] + 0.0722 * $l[2]; }; $x = $lum( $a ); $y = $lum( $b ); return ( max( $x, $y ) + 0.05 ) / ( min( $x, $y ) + 0.05 ); } /** * Stylesheet for the shadow root: consent.css without the page-scope prefix. * * @param string $accent Banner color. * @return string */ private static function ui_css( $accent ) { static $css = null; if ( null === $css ) { $css = (string) file_get_contents( GOCONSENT_DIR . 'assets/css/consent.css' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin file. $css = str_replace( array( 'html.gc-strict:not(#gc-scope) body:not(#gc-scope) ', 'html:not(#gc-scope) body:not(#gc-scope) ', ':root {' ), array( ':host(.gc-strict) ', '', ':host {' ), $css ); $css = (string) preg_replace( '#/\*.*?\*/#s', '', $css ); $css = (string) preg_replace( '/\s+/', ' ', $css ); } // Reset everything the page would pass in (font, color, line height...), then our tokens. return ':host{all:initial;display:block}' . $css . ':host{' . self::tokens( $accent ) . '}'; } /** * [goconsent_choices text="..."] link. * * @param array $atts Attributes. * @return string */ public function choices_shortcode( $atts ) { $atts = shortcode_atts( array( 'text' => __( 'Your Privacy Choices', 'goconsent' ) ), $atts, 'goconsent_choices' ); return Choices::link( $atts['text'] ); } }