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