settings = $settings; $this->visibility = $visibility; } /** * Register hooks. * * Both the public and the admin enqueue hooks are registered. Whether anything is actually * printed in the admin is the Admin area setting's business, asked once in TagVisibility rather * than decided twice here. * * @return void */ public function boot(): void { add_action( 'wp_enqueue_scripts', array( $this, 'enqueue' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); add_filter( 'script_loader_tag', array( $this, 'make_async' ), 10, 2 ); add_action( 'wp_head', array( $this, 'print_head_code' ), 1 ); add_action( 'admin_head', array( $this, 'print_head_code' ), 1 ); add_action( 'wp_footer', array( $this, 'print_footer_code' ), 20 ); add_action( 'admin_footer', array( $this, 'print_footer_code' ), 20 ); } /** * Enqueue the tracker. * * @return void */ public function enqueue(): void { $settings = $this->settings->load(); if ( ! $this->visibility->allows( $settings ) ) { return; } $measurement_id = $settings->str( 'measurement_id' ); $src = add_query_arg( array( 'id' => rawurlencode( $measurement_id ), 'v' => self::TAG_VERSION, ), $settings->tracker_url() . '/t.js' ); // Version null: the cache key is the v= parameter above, and letting WordPress append its own // ?ver= as well would fragment the CDN cache for no benefit. wp_enqueue_script( self::HANDLE, $src, array(), null, $settings->tag_in_footer() ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion // The command queue has to exist before the async script arrives, or an ap() call made by the // theme in the meantime is a fatal TypeError rather than a queued command. wp_add_inline_script( self::HANDLE, 'window.ap=window.ap||function(){(window.ap.q=window.ap.q||[]).push(arguments)};', 'before' ); $encoded = wp_json_encode( $this->config( $settings ) ); if ( is_string( $encoded ) ) { // init() fires the page view itself and installs the history hook for single-page themes. // Adding a separate page call here is the classic way to double every pageview on the site. wp_add_inline_script( self::HANDLE, 'ap("init",' . $encoded . ');' ); } $custom = $settings->str( 'custom_tag_code' ); if ( '' !== $custom ) { // After the init call, so anything written here can assume ap() is configured. Stored only // by a user with unfiltered_html, which is the same bar WordPress sets for publishing a // script anywhere else on the site. wp_add_inline_script( self::HANDLE, $custom, 'after' ); } } /** * Build the tracker's init configuration. * * @param Settings $settings Current configuration. * @return array */ private function config( Settings $settings ): array { // The browser's events go to the collect host, not to the server-to-server API: that host // answers only /api/* and would turn every page view away. $config = array( 'id' => $settings->str( 'measurement_id' ), 'endpoint' => $settings->endpoint_url(), ); /** * Filters the configuration passed to the tracker's init call. * * @since 1.0.0 * * @param array $config Tracker configuration. * @param Settings $settings Current configuration. */ return (array) apply_filters( 'adpix_analytics_tag_config', $config, $settings ); } /** * Print the site's own markup in the document head. * * @return void */ public function print_head_code(): void { $settings = $this->settings->load(); if ( ! $settings->custom_code_in_footer() ) { $this->print_custom_code( $settings ); } } /** * Print the site's own markup at the end of the document. * * @return void */ public function print_footer_code(): void { $settings = $this->settings->load(); if ( $settings->custom_code_in_footer() ) { $this->print_custom_code( $settings ); } } /** * Output the custom code block verbatim. * * Deliberately unescaped: the setting exists to place markup the site owner wrote, and escaping * it would print the source instead of running it. It can only be saved by a user who holds * unfiltered_html, so the trust decision is made once, at the point of saving, by WordPress's own * rule rather than by a weaker one invented here. * * @param Settings $settings Current configuration. * @return void */ private function print_custom_code( Settings $settings ): void { if ( ! $this->visibility->allows_custom_code( $settings ) ) { return; } echo "\n\n"; echo $settings->str( 'custom_code' ) . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Load the tracker asynchronously. * * Done with a filter rather than the loading strategy argument added in WordPress 6.3, because the * plugin supports back to 5.8 and a script that blocks rendering on older sites is a real cost. * * @param string $tag Script tag HTML. * @param string $handle Script handle. * @return string */ public function make_async( $tag, $handle ): string { if ( self::HANDLE !== $handle || false !== strpos( (string) $tag, ' async' ) ) { return (string) $tag; } return str_replace( ' src=', ' async src=', (string) $tag ); } }