Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
40.86% covered (danger)
40.86%
38 / 93
11.11% covered (danger)
11.11%
1 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
GA_Manager
40.86% covered (danger)
40.86%
38 / 93
11.11% covered (danger)
11.11%
1 / 9
304.07
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 get_instance
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 site_settings_fetch
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 site_settings_update
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
156
 set_status_from_module
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 get_google_analytics_option_name
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 get_google_analytics_settings
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 amp_analytics_entries
96.88% covered (success)
96.88%
31 / 32
0.00% covered (danger)
0.00%
0 / 1
3
 get_amp_tracking_codes
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * The Facade of the package.
4 *
5 * Copyright 2024 Automattic
6 * Based on code originally Copyright 2006 Aaron D. Campbell (email : wp_plugins@xavisys.com)
7 *
8 * @package automattic/jetpack-google-analytics
9 */
10
11namespace Automattic\Jetpack\Google_Analytics;
12
13use Automattic\Jetpack\Modules;
14use WP_Error;
15
16/**
17 * The Facade class of the package.
18 */
19class GA_Manager {
20
21    const PACKAGE_VERSION = '0.3.4';
22
23    /**
24     * Jetpack_Google_Analytics singleton instance.
25     *
26     * @var bool|self
27     */
28    public static $instance = false;
29
30    /**
31     * Property to hold concrete analytics implementation that does the work (universal or legacy).
32     *
33     * @var Universal|Legacy|bool
34     */
35    public static $analytics = false;
36
37    /**
38     * Defaults for the API version >=1.3.
39     *
40     * @var array
41     */
42    private $api_defaults = array(
43        '1.3' => array(
44            'code'                 => '',
45            'anonymize_ip'         => false,
46            'ec_track_purchases'   => false,
47            'ec_track_add_to_cart' => false,
48        ),
49        '1.4' => array(
50            'is_active'                     => false, // This default value will most likely be overwritten by the current status of the GA module.
51            'code'                          => '',
52            'anonymize_ip'                  => false,
53            'honor_dnt'                     => false,
54            'ec_track_purchases'            => false,
55            'ec_track_add_to_cart'          => false,
56            'enh_ec_tracking'               => false,
57            'enh_ec_track_remove_from_cart' => false,
58            'enh_ec_track_prod_impression'  => false,
59            'enh_ec_track_prod_click'       => false,
60            'enh_ec_track_prod_detail_view' => false,
61            'enh_ec_track_checkout_started' => false,
62        ),
63    );
64
65    /**
66     * This is our constructor, which is private to force the use of get_instance()
67     *
68     * @return void
69     */
70    private function __construct() {
71        $settings = $this->get_google_analytics_settings();
72
73        if ( ! empty( $settings['is_active'] ) ) {
74            // At this time, we only leverage universal analytics when enhanced ecommerce is selected and WooCommerce is active.
75            // Otherwise, don't bother emitting the tracking ID or fetching analytics.js
76            if ( class_exists( 'WooCommerce' ) && Options::enhanced_ecommerce_tracking_is_enabled() ) {
77                self::$analytics = new Universal();
78                new AMP_Analytics();
79            } else {
80                self::$analytics = new Legacy();
81            }
82        }
83
84        add_filter( 'site_settings_endpoint_get', array( $this, 'site_settings_fetch' ), 10, 2 );
85        add_filter( 'site_settings_endpoint_update_wga', array( $this, 'site_settings_update' ), 10, 2 );
86        add_filter( 'site_settings_endpoint_update_jetpack_wga', array( $this, 'site_settings_update' ) );
87        add_action( 'jetpack_activate_module_google-analytics', array( $this, 'set_status_from_module' ) );
88        add_action( 'jetpack_deactivate_module_google-analytics', array( $this, 'set_status_from_module' ) );
89    }
90
91    /**
92     * Function to instantiate our class and make it a singleton
93     */
94    public static function get_instance() {
95        if ( ! self::$instance ) {
96            self::$instance = new self();
97        }
98
99        return self::$instance;
100    }
101
102    /**
103     * Includes the GA settings into site settings during a fetch request.
104     *
105     * @phan-suppress PhanUndeclaredTypeParameter,PhanUndeclaredClassInstanceof,PhanUndeclaredClassProperty
106     *
107     * @param array                                  $settings The fetched settings.
108     * @param \WPCOM_JSON_API_Site_Settings_Endpoint $api_handler The API handler object.
109     *
110     * @return array|mixed
111     */
112    public function site_settings_fetch( $settings = array(), $api_handler = null ) {
113        if ( ! is_array( $settings ) || ! $api_handler instanceof \WPCOM_JSON_API_Site_Settings_Endpoint ) {
114            // Safeguard against something that should never happen.
115            return $settings;
116        }
117
118        $settings['wga'] = $this->get_google_analytics_settings();
119
120        if ( array_key_exists( $api_handler->min_version, $this->api_defaults ) ) {
121            $settings['wga'] = wp_parse_args( $settings['wga'], $this->api_defaults[ $api_handler->min_version ] );
122        }
123
124        return $settings;
125    }
126
127    /**
128     * Modifies the GA settings into site settings during an update request.
129     *
130     * @phan-suppress PhanUndeclaredTypeParameter,PhanUndeclaredClassInstanceof,PhanUndeclaredClassProperty
131     *
132     * @param array                                  $value The settings to update.
133     * @param \WPCOM_JSON_API_Site_Settings_Endpoint $api_handler The API handler object.
134     *
135     * @return array|mixed
136     */
137    public function site_settings_update( $value, $api_handler = null ) {
138        if ( ! is_array( $value ) || ! $api_handler instanceof \WPCOM_JSON_API_Site_Settings_Endpoint ) {
139            // This should never happen.
140            return $value;
141        }
142
143        if ( ! isset( $value['code'] ) || ! preg_match( '/^$|^(UA-\d+-\d+)|(G-[A-Z0-9]+)$/i', $value['code'] ) ) {
144            return new WP_Error( 'invalid_code', 'Invalid UA ID' );
145        }
146
147        $option_name = $this->get_google_analytics_option_name();
148
149        $wga         = get_option( $option_name, array() );
150        $wga['code'] = $value['code'];
151
152        if ( ! array_key_exists( 'is_active', $wga ) ) {
153            // The `is_active` flag is missing from the settings, add a default value based on the module status.
154            $wga['is_active'] = ( new Modules() )->is_active( 'google-analytics', false );
155        }
156
157        /**
158         * Allow newer versions of this endpoint to filter in additional fields for Google Analytics
159         *
160         * @since Jetpack 5.4.0
161         * @since 0.2.0
162         *
163         * @param array $wga Associative array of existing Google Analytics settings.
164         * @param array $value Associative array of new Google Analytics settings passed to the endpoint.
165         */
166        $wga = apply_filters( 'site_settings_update_wga', $wga, $value );
167
168        if ( array_key_exists( $api_handler->min_version, $this->api_defaults ) ) {
169            $wga_keys = array_keys( $this->api_defaults[ $api_handler->min_version ] );
170            foreach ( $wga_keys as $wga_key ) {
171                // Skip code since it's already handled.
172                if ( 'code' === $wga_key ) {
173                    continue;
174                }
175
176                // All our new keys are booleans, so let's coerce each key's value
177                // before updating the value in settings
178                if ( array_key_exists( $wga_key, $value ) ) {
179                    $wga[ $wga_key ] = Utils::is_truthy( $value[ $wga_key ] );
180                }
181            }
182        }
183
184        $is_updated = update_option( $option_name, $wga );
185
186        $enabled_or_disabled = $wga['code'] ? 'enabled' : 'disabled';
187
188        /**
189         * Fires for each settings update.
190         *
191         * @since Jetpack 3.6.0
192         * @since 0.2.0
193         *
194         * @param string $action_type Type of settings to track.
195         * @param string $val The settings value.
196         */
197        do_action( 'jetpack_bump_stats_extras', 'google-analytics', $enabled_or_disabled );
198
199        return $is_updated ? $wga : null;
200    }
201
202    /**
203     * Update the `is_active` settings flag depending on the Google Analytics module status.
204     *
205     * @return void
206     */
207    public function set_status_from_module() {
208        $option_name = $this->get_google_analytics_option_name();
209
210        $wga       = get_option( $option_name, array() );
211        $is_active = ( new Modules() )->is_active( 'google-analytics', false );
212
213        if ( ! array_key_exists( 'is_active', $wga ) || $is_active !== $wga['is_active'] ) {
214            $wga['is_active'] = $is_active;
215        }
216
217        update_option( $option_name, $wga );
218    }
219
220    /**
221     * Get the GA settings option name.
222     *
223     * @return string
224     */
225    public function get_google_analytics_option_name() {
226        /**
227         * Filter whether the current site is a Jetpack site.
228         *
229         * @since Jetpack 3.3.0
230         * @since 0.2.0
231         *
232         * @param bool $is_jetpack Is the current site a Jetpack site. Default to false.
233         * @param int $blog_id Blog ID.
234         */
235        $is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
236        return $is_jetpack ? 'jetpack_wga' : 'wga';
237    }
238
239    /**
240     * Get GA settings.
241     *
242     * @return array
243     */
244    public function get_google_analytics_settings() {
245        $settings = get_option( $this->get_google_analytics_option_name() );
246
247        // The `is_active` flag is missing from the settings, add a value based on the module status.
248        if ( is_array( $settings ) && ! array_key_exists( 'is_active', $settings ) ) {
249            $settings['is_active'] = ( new Modules() )->is_active( 'google-analytics', false );
250            update_option( $this->get_google_analytics_option_name(), $settings );
251        }
252
253        return $settings;
254    }
255
256    /**
257     * Add amp-analytics tags.
258     *
259     * @param array $analytics_entries An associative array of the analytics entries.
260     *
261     * @return array
262     */
263    public static function amp_analytics_entries( $analytics_entries ) {
264        if ( ! is_array( $analytics_entries ) ) {
265            $analytics_entries = array();
266        }
267
268        $amp_tracking_codes = static::get_amp_tracking_codes( $analytics_entries );
269        $jetpack_account    = Options::get_tracking_code();
270
271        // Bypass tracking codes already set on AMP plugin.
272        if ( in_array( $jetpack_account, $amp_tracking_codes, true ) ) {
273            return $analytics_entries;
274        }
275
276        $config_data = array(
277            'vars'     => array(
278                'account' => Options::get_tracking_code(),
279            ),
280            'triggers' => array(
281                'trackPageview' => array(
282                    'on'      => 'visible',
283                    'request' => 'pageview',
284                ),
285            ),
286        );
287
288        // Generate a hash string to uniquely identify this entry.
289        $entry_id = substr(
290            md5(
291                'googleanalytics' . wp_json_encode(
292                    $config_data,
293                    0 // phpcs:ignore Jetpack.Functions.JsonEncodeFlags.ZeroFound -- No `json_encode()` flags because we don't want to disrupt the current hash index.
294                )
295            ),
296            0,
297            12
298        );
299
300        $analytics_entries[ $entry_id ] = array(
301            'type'   => 'googleanalytics',
302            'config' => wp_json_encode( $config_data, JSON_UNESCAPED_SLASHES ),
303        );
304
305        return $analytics_entries;
306    }
307
308    /**
309     * Get AMP tracking codes.
310     *
311     * @param array $analytics_entries The codes available for AMP.
312     *
313     * @return array
314     */
315    protected static function get_amp_tracking_codes( $analytics_entries ) {
316        $entries  = array_column( $analytics_entries, 'config' );
317        $accounts = array();
318
319        foreach ( $entries as $entry ) {
320            $entry = json_decode( $entry );
321
322            if ( ! empty( $entry->vars->account ) ) {
323                $accounts[] = $entry->vars->account;
324            }
325        }
326
327        return $accounts;
328    }
329}