ay, * deliberately, and the reason is recorded here so the next re-derivation of * that ledger finds an answer rather than re-deciding: * * - json_literal() emits DOUBLE quotes. This block is Google's own container * snippet reproduced byte for byte (single quotes throughout, matching 1.x * and every copy of it Google publishes), and BE-1 keeps byte-exact tests * over it. Migrating would change the emitted snippet for every site to buy * nothing. * - Nothing is being reasoned about here on the basis of what the values * "happen to" contain - which is the filing #110 ruled out. Each of the four * passes an enforced allow-list on the way to this line, and none of those * allow-lists admits a quote: the container ID via ContainerRows::GTM_ID_PATTERN * in header_begin() before this method is called, the domain via * FILTER_VALIDATE_DOMAIN in container_domain(), the path via * ContainerRows::PATH_PATTERN in container_path(), and the data layer name via * ContainerRows::datalayer_name() at the reader. esc_js() is therefore a * provable no-op on all four, and the allow-lists are the actual control. * * If any of those four allow-lists is ever widened to admit a quote, this * block has to move to json_literal() and the byte-exact tests updated with it. */ return ' ' . $this->script_tag->opening_tag() . ' (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({\'gtm.start\': new Date().getTime(),event:\'gtm.js\'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!=\'dataLayer\'?\'&l=\'+l:\'\';j.async=true;j.src= \'//' . esc_js( $this->container_domain( $one_container ) ) . '/' . esc_js( $this->container_path( $one_container ) ) . $_gtm_loader_query . ( '' !== $_gtm_env ? "+'" . $_gtm_env . "'" : '' ) . ';f.parentNode.insertBefore(j,f); })(window,document,\'script\',\'' . esc_js( $datalayer_name ) . '\',\'' . esc_js( $one_gtm_id ) . '\'); '; } /** * Returns a HTML code that includes the noscript/iframe part of the Google Tag Manager container. * Can be used to manually place the snippet next to the opening body tag if the installed template * does not support the wp_body_open hook. * * @return string The HTML code that includes the noscript/iframe part of the GTM container code. */ public function get_tag(): string { $no_console_log = (bool) $this->options->get( GTM4WP_OPTION_NOCONSOLELOG ); $containers = $this->containers(); $excluded_role = $this->excluded_user_role(); $_gtm_tag = ' '; if ( GTM4WP_PLACEMENT_OFF === $this->options->get( GTM4WP_OPTION_GTM_PLACEMENT ) ) { // Placement OFF means the container code (both the head loader and // this noscript iframe) is never auto-emitted; only the data layer // stays active. Mark the code as "written" regardless of the // console-log setting so the iframe block below is always skipped - // otherwise, with console logging disabled, the iframe would still // be output despite the OFF placement. $GLOBALS['gtm4wp_container_code_written'] = true; if ( ! $no_console_log ) { $_gtm_tag .= ' ' . $this->script_tag->opening_tag() . ' console.warn && console.warn("[GTM4WP] Google Tag Manager container code placement set to OFF !!!"); console.warn && console.warn("[GTM4WP] Data layer codes are active but GTM container must be loaded using custom coding !!!"); '; } } elseif ( ! $this->should_output_container() ) { // Kill switch (production-only option / gtm4wp_output_container // filter): suppress the noscript iframe exactly like placement OFF - // mark the code as "written" so the iframe block below is skipped // while the data layer stays active. $GLOBALS['gtm4wp_container_code_written'] = true; if ( ! $no_console_log ) { $_gtm_tag .= $this->container_suppressed_warning(); } } elseif ( '' !== $excluded_role ) { // The user-role exclusion suppresses the
container loader in // header_begin(); this iframe is the same container by another route, // so it has to go with it. It used to be emitted regardless, which // meant an excluded user still loaded the container (and was counted // in the reports) whenever JavaScript was unavailable - the exact // case the noscript iframe exists for. Mark the code as "written" so // the iframe block below is skipped, exactly like placement OFF and // the kill switch above; the data layer stays active. $GLOBALS['gtm4wp_container_code_written'] = true; if ( ! $no_console_log ) { $_gtm_tag .= $this->disabled_role_warning( $excluded_role ); } } if ( ( array() !== $containers ) && ( ! ( $GLOBALS['gtm4wp_container_code_written'] ?? false ) ) ) { foreach ( $containers as $one_container ) { $one_gtm_id = (string) ( $one_container[ ContainerRows::COLUMN_ID ] ?? '' ); // esc_attr() per PART below, at the point of injection (#225). Each part // is already allow-list validated - the id by the pattern one line down, // the domain by container_domain()'s FILTER_VALIDATE_DOMAIN fallback, the // environment values inside container_environment() - so esc_attr() is // the identity function on everything the validators pass today and // exists for the day a pattern is widened. NOT esc_attr()/esc_url() over // the whole src: the environment fragment joins with raw '&' on purpose // (wp_kses encodes it to & at output, the 1.x byte form), and // gtm4wp_get_the_gtm_tag() returns this string as public 1.x API - // pre-encoding it would change those bytes and hand third-party // consumers an already-escaped value to double-escape (RI-4). if ( preg_match( ContainerRows::GTM_ID_PATTERN, $one_gtm_id ) ) { $_gtm_tag .= ' '; } } $_gtm_tag .= ' '; $GLOBALS['gtm4wp_container_code_written'] = true; } return $_gtm_tag; } /** * Outputs a HTML code that includes the noscript/iframe part of the Google Tag Manager container. * * @return void */ public function the_tag(): void { // The iframe's inline style needs `display` and `visibility`. Whether // safecss_filter_attr() strips those depends on the WordPress version: // core added `display` to the safe_style_css default list in 7.0 and // `visibility` in 7.1, so on 6.3-6.9 both are stripped and on 7.0 // `visibility` still is. While the floor is 6.3 this filter is // load-bearing, not belt-and-braces - drop it only in the change that // raises the floor past 7.1, never as a "core allows it now" cleanup. // // Widen the CSS allow-list around THIS wp_kses() call only, then put it back: // safe_style_css is a global WordPress control, so a filter left // registered would relax it for every other wp_kses()/wp_kses_post() call // in the same request - including wp_filter_post_kses on content saved by // users without unfiltered_html, and a REST save is not is_admin(). One // line of our own markup is not a reason to loosen the rule site-wide. add_filter( 'safe_style_css', array( self::class, 'allow_iframe_hiding_styles' ) ); try { // print_markup_block(), not a bare wp_kses(): this block mixes the noscript // iframe (whose src must keep its & entity form) with the console // warning