Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.86% covered (warning)
89.86%
62 / 69
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Main
89.86% covered (warning)
89.86%
62 / 69
71.43% covered (warning)
71.43%
5 / 7
34.14
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 __construct
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
3
 jetpack_is_dnt_enabled
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 map_meta_caps
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 template_redirect
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 hide_smile_css
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 should_track
75.00% covered (warning)
75.00%
18 / 24
0.00% covered (danger)
0.00%
0 / 1
18.52
1<?php
2/**
3 * Stats Main
4 *
5 * @package automattic/jetpack-stats
6 */
7
8namespace Automattic\Jetpack\Stats;
9
10use Automattic\Jetpack\Connection\Manager as Connection_Manager;
11use Automattic\Jetpack\Constants;
12use Automattic\Jetpack\Modules;
13use Automattic\Jetpack\Stats\Abilities\Stats_Abilities;
14use Automattic\Jetpack\Status;
15use Automattic\Jetpack\Status\Visitor;
16use WP_User;
17
18/**
19 * Stats Main class.
20 *
21 * Entrypoint for Stats.
22 *
23 * @since 0.1.0
24 */
25class Main {
26    /**
27     * Stats version.
28     * Mostly needed for backwards compatibility.
29     */
30    const STATS_VERSION = '9';
31
32    /**
33     * Singleton Main instance.
34     *
35     * @var Main
36     **/
37    private static $instance = null;
38
39    /**
40     * Initializer.
41     * Used to configure the stats package, eg when called via the Config package.
42     *
43     * @return object
44     */
45    public static function init() {
46        if ( null === self::$instance ) {
47            self::$instance = new Main();
48        }
49
50        return self::$instance;
51    }
52
53    /**
54     * Class constructor.
55     *
56     * @return void
57     */
58    private function __construct() {
59        /**
60         * This avoids conflicts when running Stats package with older versions of the Jetpack plugin.
61         *
62         * On JP version 11.5-a.2 the hooks below were removed from the Jetpack plugin and it is safe
63         * to register them in the Stats package.
64         */
65        $jp_plugin_version = Constants::get_constant( 'JETPACK__VERSION' );
66        if ( $jp_plugin_version && version_compare( $jp_plugin_version, '11.5-a.2', '<' ) ) {
67            return;
68        }
69        // Generate the tracking code after wp() has queried for posts.
70        add_action( 'template_redirect', array( __CLASS__, 'template_redirect' ), 1 );
71
72        add_action( 'wp_enqueue_scripts', array( __CLASS__, 'hide_smile_css' ) );
73
74        // Map stats caps.
75        add_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_caps' ), 10, 3 );
76
77        XMLRPC_Provider::init();
78
79        /*
80         * REST_Provider only registers its routes on REST init, so defer
81         * constructing it (and autoloading the class) until a REST request is
82         * served. A closure is used because rest_api_init passes the REST server
83         * to callbacks, which would otherwise be read as REST_Provider::init()'s
84         * $new_instance argument.
85         */
86        add_action(
87            'rest_api_init',
88            static function () {
89                REST_Provider::init();
90            },
91            0
92        );
93        Transient_Cleanup::init();
94
95        // Clean up transient cron on module deactivation.
96        add_action( 'jetpack_deactivate_module_stats', array( Transient_Cleanup::class, 'unschedule_cleanup' ) );
97
98        // Set up package version hook.
99        add_filter( 'jetpack_package_versions', __NAMESPACE__ . '\Package_Version::send_package_version_to_tracker' );
100
101        // Register WP Abilities API surface. Gated behind the
102        // `jetpack_wp_abilities_enabled` filter inside Registrar::init(),
103        // which defaults to false â€” so this call is safe to make unconditionally
104        // and still opt-in per-site until the flag is flipped.
105        Stats_Abilities::init();
106    }
107
108    /**
109     * Checks if filter is set and dnt is enabled.
110     *
111     * @return bool
112     */
113    public static function jetpack_is_dnt_enabled() {
114        /**
115         * Filter the option which decides honor DNT or not.
116         *
117         * @module stats
118         * @since-jetpack 6.1.0
119         *
120         * @param bool false Honors DNT for clients who don't want to be tracked. Defaults to false. Set to true to enable.
121         */
122        if ( false === apply_filters( 'jetpack_honor_dnt_header_for_stats', false ) ) {
123            return false;
124        }
125
126        foreach ( $_SERVER as $name => $value ) {
127            if ( 'http_dnt' === strtolower( $name ) && 1 === (int) $value ) {
128                return true;
129            }
130        }
131
132        return false;
133    }
134
135    /**
136     * Maps view_stats cap to read cap as needed.
137     *
138     * @access public
139     * @param mixed $caps Caps.
140     * @param mixed $cap Cap.
141     * @param mixed $user_id User ID.
142     * @return array Possibly mapped capabilities for meta capability.
143     */
144    public static function map_meta_caps( $caps, $cap, $user_id ) {
145        // Map view_stats to exists.
146        if ( 'view_stats' === $cap ) {
147            $user = new WP_User( $user_id );
148            // WordPress 6.9 introduced lazy-loading of some WP_User properties, including `roles`.
149            // It also made said properties protected, so we can't modify keys directly.
150            $user_roles  = $user->roles;
151            $user_role   = array_shift( $user_roles ); // Work with the copy
152            $stats_roles = Options::get_option( 'roles' );
153
154            // Is the users role in the available stats roles?
155            if ( is_array( $stats_roles ) && in_array( $user_role, $stats_roles, true ) ) {
156                $caps = array( 'read' );
157            }
158        }
159
160        return $caps;
161    }
162
163    /**
164     * Stats Template Redirect.
165     *
166     * @access public
167     * @return void
168     */
169    public static function template_redirect() {
170        if ( ! self::should_track() ) {
171            return;
172        }
173
174        add_action( 'wp_enqueue_scripts', array( Tracking_Pixel::class, 'enqueue_stats_script' ), 101 );
175        add_action( 'wp_footer', array( Tracking_Pixel::class, 'add_amp_pixel' ), 101 );
176        add_action( 'web_stories_print_analytics', array( Tracking_Pixel::class, 'add_amp_pixel' ), 101 );
177    }
178
179    /**
180     * CSS to hide the tracking pixel smiley.
181     * It is now hidden for everyone (used to be visible if you had set the hide_smile option).
182     *
183     * @access public
184     * @return void
185     */
186    public static function hide_smile_css() {
187        if ( ! self::should_track() ) {
188            return;
189        }
190
191        wp_register_style( 'jetpack-stats', false, array(), Package_Version::PACKAGE_VERSION );
192        wp_enqueue_style( 'jetpack-stats' );
193        wp_add_inline_style( 'jetpack-stats', 'img#wpstats{display:none}' );
194    }
195
196    /**
197     * Whether we should add the tracking pixel.
198     *
199     * @return bool
200     */
201    public static function should_track() {
202        global $current_user;
203
204        // Not connected sites should not generate tracking stats.
205        if ( ! ( new Connection_Manager() )->is_connected() ) {
206            return false;
207        }
208
209        // If the stats module is disabled we should not generate tracking stats.
210        if ( ! ( new Modules() )->is_active( 'stats' ) ) {
211            return false;
212        }
213
214        // Do not generate tracking stats for feeds, robots, embeds, previews
215        // or to honour the DNT headers.
216        if (
217            is_feed()
218            || is_robots()
219            || is_embed()
220            || is_trackback()
221            || is_preview()
222            || self::jetpack_is_dnt_enabled()
223        ) {
224            return false;
225        }
226
227        // Sites in Safe Mode should not generate tracking stats.
228        $status = new Status();
229        if ( $status->in_safe_mode() ) {
230            return false;
231        }
232
233        // Should we be counting this user's views?
234        if ( ! empty( $current_user->ID ) ) {
235            $count_roles = Options::get_option( 'count_roles' );
236            if ( ! is_array( $count_roles ) || ! array_intersect( $current_user->roles, $count_roles ) ) {
237                return false;
238            }
239        }
240
241        /**
242         * Allow excluding specific IP addresses from being tracked in Stats.
243         * Note: for this to work well, visitors' IP addresses must:
244         * - be stored and returned properly in IP address headers;
245         * - not be impacted by any caching setup on your site.
246         *
247         * @module stats
248         *
249         * @since-jetpack 10.6
250         *
251         * @param array $excluded_ips An array of IP address strings to exclude from tracking.
252         */
253        $excluded_ips = (array) apply_filters( 'jetpack_stats_excluded_ips', array() );
254
255        // Should we be counting views for this IP address?
256        $current_user_ip = ( new Visitor() )->get_ip( true );
257        if (
258            ! empty( $excluded_ips )
259            && in_array( $current_user_ip, $excluded_ips, true )
260        ) {
261            return false;
262        }
263
264        return true;
265    }
266}