Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
16.36% covered (danger)
16.36%
27 / 165
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Legacy
16.36% covered (danger)
16.36%
27 / 165
0.00% covered (danger)
0.00%
0 / 8
1339.35
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 get_url
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
90
 insert_code
50.00% covered (danger)
50.00%
7 / 14
0.00% covered (danger)
0.00%
0 / 1
13.12
 render_ga_code
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 render_gtag_code
54.05% covered (warning)
54.05%
20 / 37
0.00% covered (danger)
0.00%
0 / 1
5.55
 jetpack_wga_classic_anonymize_ip
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 jetpack_wga_classic_track_purchases
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
156
 jetpack_wga_classic_track_add_to_cart
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2/**
3 * Jetpack_Google_Analytics_Legacy hooks and enqueues support for ga.js
4 * https://developers.google.com/analytics/devguides/collection/gajs/
5 *
6 * Copyright 2024 Automattic
7 * Based on code originally Copyright 2006 Aaron D. Campbell (email : wp_plugins@xavisys.com)
8 *
9 * @package automattic/jetpack-google-analytics
10 */
11
12namespace Automattic\Jetpack\Google_Analytics;
13
14/**
15 * Jetpack_Google_Analytics_Legacy hooks and enqueues support for ga.js
16 */
17class Legacy {
18    /**
19     * Jetpack_Google_Analytics_Legacy constructor.
20     */
21    public function __construct() {
22        add_filter( 'jetpack_wga_classic_custom_vars', array( $this, 'jetpack_wga_classic_anonymize_ip' ) );
23        add_filter( 'jetpack_wga_classic_custom_vars', array( $this, 'jetpack_wga_classic_track_purchases' ) );
24        add_action( 'wp_head', array( $this, 'insert_code' ), 999 );
25        add_action( 'wp_footer', array( $this, 'jetpack_wga_classic_track_add_to_cart' ) );
26    }
27
28    /**
29     * Used to generate a tracking URL
30     * Called exclusively by insert_code
31     *
32     * @param array $track - Must have ['data'] and ['code'].
33     * @return string - Tracking URL
34     */
35    private function get_url( $track ) {
36        $site_url = ( is_ssl() ? 'https://' : 'http://' ) . sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ?? '' ) );
37        foreach ( $track as $k => $value ) {
38            if ( strpos( strtolower( $value ), strtolower( $site_url ) ) === 0 ) {
39                $track[ $k ] = substr( $track[ $k ], strlen( $site_url ) );
40            }
41            if ( 'data' === $k ) {
42                $track[ $k ] = preg_replace( '/^https?:\/\/|^\/+/i', '', $track[ $k ] );
43            }
44
45            // This way we don't lose search data.
46            if ( 'data' === $k && 'search' === $track['code'] ) {
47                $track[ $k ] = rawurlencode( $track[ $k ] );
48            } else {
49                $track[ $k ] = preg_replace( '/[^a-z0-9\.\/\+\?=-]+/i', '_', $track[ $k ] );
50            }
51
52            $track[ $k ] = trim( $track[ $k ], '_' );
53        }
54        $char = ( strpos( $track['data'], '?' ) === false ) ? '?' : '&amp;';
55        return str_replace( "'", "\'", "/{$track['code']}/{$track['data']}{$char}referer=" . rawurlencode( isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '' ) );
56    }
57
58    /**
59     * This injects the Google Analytics code into the footer of the page.
60     * Called exclusively by wp_head action
61     */
62    public function insert_code() {
63        $tracking_id = Options::get_tracking_code();
64        if ( empty( $tracking_id ) ) {
65            echo "<!-- Your Google Analytics Plugin is missing the tracking ID -->\r\n";
66            return;
67        }
68
69        // If we're in the admin_area or DNT is honored and enabled, return without inserting code.
70        if (
71            is_admin()
72            || Utils::is_dnt_enabled()
73        ) {
74            return;
75        }
76
77        // @phan-suppress-next-line PhanUndeclaredClassMethod
78        if ( class_exists( 'Jetpack_AMP_Support' ) && \Jetpack_AMP_Support::is_amp_request() ) {
79            // For Reader mode — legacy.
80            add_filter( 'amp_post_template_analytics', array( GA_Manager::class, 'amp_analytics_entries' ), 1000 );
81            // For Standard and Transitional modes.
82            add_filter( 'amp_analytics_entries', array( GA_Manager::class, 'amp_analytics_entries' ), 1000 );
83            return;
84        }
85
86        if ( str_starts_with( $tracking_id, 'G-' ) ) {
87            $this->render_gtag_code( $tracking_id );
88        } else {
89            $this->render_ga_code( $tracking_id );
90        }
91    }
92
93    /**
94     * Renders legacy ga.js code.
95     *
96     * @param string $tracking_id Google Analytics measurement ID.
97     */
98    private function render_ga_code( $tracking_id ) {
99        $custom_vars = array(
100            "_gaq.push(['_setAccount', '{$tracking_id}']);",
101        );
102
103        $track = array();
104        if ( is_404() ) {
105            // This is a 404 and we are supposed to track them.
106            $custom_vars[] = "_gaq.push(['_trackEvent', '404', document.location.href, document.referrer]);";
107        } elseif (
108            is_search()
109            && isset( $_REQUEST['s'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Function renders client-side JS, no site actions.
110        ) {
111            // Set track for searches, if it's a search, and we are supposed to.
112            $track['data'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Function renders client-side JS, no site actions.
113            $track['code'] = 'search';
114        }
115
116        if ( ! empty( $track ) ) {
117            $track['url'] = $this->get_url( $track );
118            // adjust the code that we output, account for both types of tracking.
119            $track['url']  = esc_js( str_replace( '&', '&amp;', $track['url'] ) );
120            $custom_vars[] = "_gaq.push(['_trackPageview','{$track['url']}']);";
121        } else {
122            $custom_vars[] = "_gaq.push(['_trackPageview']);";
123        }
124
125        /**
126         * Allow for additional elements to be added to the classic Google Analytics queue (_gaq) array
127         *
128         * @since jetpack-5.4.0
129         *
130         * @param array $custom_vars Array of classic Google Analytics queue elements
131         */
132        $custom_vars = apply_filters( 'jetpack_wga_classic_custom_vars', $custom_vars );
133
134        // Ref: https://developers.google.com/analytics/devguides/collection/gajs/gaTrackingEcommerce#Example
135        printf(
136            "<!-- Jetpack Google Analytics -->
137            <script type='text/javascript'>
138                var _gaq = _gaq || [];
139                %s
140                (function() {
141                    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
142                    ga.src = ('https:' === document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
143                    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
144                })();
145            </script>
146            <!-- End Jetpack Google Analytics -->\r\n",
147            implode( "\r\n", $custom_vars ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Additional elements added to the classic Google Analytics script.
148        );
149    }
150
151    /**
152     * Renders new gtag code.
153     *
154     * @param string $tracking_id Google Analytics measurement ID.
155     */
156    private function render_gtag_code( $tracking_id ) {
157        /**
158         * Allow for additional elements to be added to the Global Site Tags array.
159         *
160         * @since jetpack-9.2.0
161         *
162         * @param array $universal_commands Array of gtag function calls.
163         */
164        $universal_commands = apply_filters( 'jetpack_gtag_universal_commands', array() );
165        $custom_vars        = array();
166        if ( is_404() ) {
167            $custom_vars[] = array(
168                'event',
169                'exception',
170                array(
171                    'description' => '404',
172                    'fatal'       => false,
173                ),
174            );
175        }
176        // phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript
177        ?>
178        <!-- Jetpack Google Analytics -->
179        <script async src='https://www.googletagmanager.com/gtag/js?id=<?php echo esc_attr( $tracking_id ); ?>'></script>
180        <script>
181            window.dataLayer = window.dataLayer || [];
182            function gtag() { dataLayer.push( arguments ); }
183            gtag( 'js', new Date() );
184            gtag( 'config', <?php echo wp_json_encode( $tracking_id, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?> );
185            <?php
186            foreach ( $universal_commands as $command ) {
187                echo 'gtag( ' . implode(
188                    ', ',
189                    array_map( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped internally
190                        function ( $c ) {
191                            return wp_json_encode( $c, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP );
192                        },
193                        $command
194                    )
195                ) . " );\n";
196            }
197            foreach ( $custom_vars as $var ) {
198                echo 'gtag( ' . implode(
199                    ', ',
200                    array_map( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped internally
201                        function ( $v ) {
202                            return wp_json_encode( $v, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP );
203                        },
204                        $var
205                    )
206                ) . " );\n";
207            }
208            ?>
209        </script>
210        <!-- End Jetpack Google Analytics -->
211        <?php
212        // phpcs:enable
213    }
214
215    /**
216     * Used to filter in the anonymize IP snippet to the custom vars array for classic analytics
217     * Ref https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApi_gat#_gat._anonymizelp
218     *
219     * @param array $custom_vars Custom vars to be filtered.
220     * @return array Possibly updated custom vars.
221     */
222    public function jetpack_wga_classic_anonymize_ip( $custom_vars ) {
223        if ( Options::anonymize_ip_is_enabled() ) {
224            array_push( $custom_vars, "_gaq.push(['_gat._anonymizeIp']);" );
225        }
226
227        return $custom_vars;
228    }
229
230    /**
231     * Used to filter in the order details to the custom vars array for classic analytics
232     *
233     * @phan-suppress PhanUndeclaredClassMethod
234     *
235     * @param array $custom_vars Custom vars to be filtered.
236     * @return array|void Possibly updated custom vars.
237     */
238    public function jetpack_wga_classic_track_purchases( $custom_vars ) {
239        global $wp;
240
241        if ( ! class_exists( 'WooCommerce' ) ) {
242            return $custom_vars;
243        }
244
245        if ( ! Options::has_tracking_code() ) {
246            return;
247        }
248
249        // Ref: https://developers.google.com/analytics/devguides/collection/gajs/gaTrackingEcommerce#Example
250        if ( ! Options::track_purchases_is_enabled() ) {
251            return $custom_vars;
252        }
253
254        // @phan-suppress-next-line PhanUndeclaredConstant
255        $minimum_woocommerce_active = class_exists( 'WooCommerce' ) && version_compare( \WC_VERSION, '3.0', '>=' );
256        // @phan-suppress-next-line PhanUndeclaredFunction
257        if ( $minimum_woocommerce_active && \is_order_received_page() ) {
258            $order_id = $wp->query_vars['order-received'] ?? 0;
259            if ( 0 < $order_id && 1 !== (int) get_post_meta( $order_id, '_ga_tracked', true ) ) {
260                $order = new \WC_Order( $order_id );
261
262                /**
263                 * [ '_add_Trans', '123', 'Site Title', '21.00', '1.00', '5.00', 'Snohomish', 'WA', 'USA' ]
264                 */
265                array_push(
266                    $custom_vars,
267                    sprintf(
268                        '_gaq.push( %s );',
269                        wp_json_encode(
270                            array(
271                                '_addTrans',
272                                (string) $order->get_order_number(),
273                                get_bloginfo( 'name' ),
274                                (string) $order->get_total(),
275                                (string) $order->get_total_tax(),
276                                (string) $order->get_total_shipping(),
277                                (string) $order->get_billing_city(),
278                                (string) $order->get_billing_state(),
279                                (string) $order->get_billing_country(),
280                            ),
281                            JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP
282                        )
283                    )
284                );
285
286                // Order items
287                if ( $order->get_items() ) {
288                    foreach ( $order->get_items() as $item ) {
289                        $product           = $order->get_product_from_item( $item );
290                        $product_sku_or_id = $product->get_sku() ? $product->get_sku() : $product->get_id();
291
292                        array_push(
293                            $custom_vars,
294                            sprintf(
295                                '_gaq.push( %s );',
296                                wp_json_encode(
297                                    array(
298                                        '_addItem',
299                                        (string) $order->get_order_number(),
300                                        (string) $product_sku_or_id,
301                                        $item['name'],
302                                        Utils::get_product_categories_concatenated( $product ),
303                                        (string) $order->get_item_total( $item ),
304                                        (string) $item['qty'],
305                                    ),
306                                    JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP
307                                )
308                            )
309                        );
310                    }
311                } // get_items
312
313                // Mark the order as tracked
314                update_post_meta( $order_id, '_ga_tracked', 1 );
315                array_push( $custom_vars, "_gaq.push(['_trackTrans']);" );
316            } // order not yet tracked
317        } // is order received page
318
319        return $custom_vars;
320    }
321
322    /**
323     * Used to add footer javascript to track user clicking on add-to-cart buttons
324     * on single views (.single_add_to_cart_button) and list views (.add_to_cart_button)
325     */
326    public function jetpack_wga_classic_track_add_to_cart() {
327        if ( ! class_exists( 'WooCommerce' ) ) {
328            return;
329        }
330
331        if ( ! Options::has_tracking_code() ) {
332            return;
333        }
334
335        if ( ! Options::track_add_to_cart_is_enabled() ) {
336            return;
337        }
338
339        // @phan-suppress-next-line PhanUndeclaredFunction
340        if ( \is_product() ) { // product page
341            global $product;
342            $product_sku_or_id = $product->get_sku() ? $product->get_sku() : '#' . $product->get_id();
343            // @phan-suppress-next-line PhanUndeclaredFunction
344            \wc_enqueue_js(
345                "$( '.single_add_to_cart_button' ).click( function() {
346                    _gaq.push(['_trackEvent', 'Products', 'Add to Cart', '#" . esc_js( $product_sku_or_id ) . "']);
347                } );"
348            );
349            // @phan-suppress-next-line PhanUndeclaredFunction
350        } elseif ( \is_woocommerce() ) { // any other page that uses templates (like product lists, archives, etc)
351            // @phan-suppress-next-line PhanUndeclaredFunction
352            \wc_enqueue_js(
353                "$( '.add_to_cart_button:not(.product_type_variable, .product_type_grouped)' ).click( function() {
354                    var label = $( this ).data( 'product_sku' ) ? $( this ).data( 'product_sku' ) : '#' + $( this ).data( 'product_id' );
355                    _gaq.push(['_trackEvent', 'Products', 'Add to Cart', label]);
356                } );"
357            );
358        }
359    }
360}