Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.06% covered (warning)
84.06%
58 / 69
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Main
84.06% covered (warning)
84.06%
58 / 69
57.14% covered (warning)
57.14%
4 / 7
37.41
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%
19 / 19
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
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 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_head', array( __CLASS__, 'hide_smile_css' ) );
73        add_action( 'embed_head', array( __CLASS__, 'hide_smile_css' ) );
74
75        // Map stats caps.
76        add_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_caps' ), 10, 3 );
77
78        XMLRPC_Provider::init();
79
80        /*
81         * REST_Provider only registers its routes on REST init, so defer
82         * constructing it (and autoloading the class) until a REST request is
83         * served. A closure is used because rest_api_init passes the REST server
84         * to callbacks, which would otherwise be read as REST_Provider::init()'s
85         * $new_instance argument.
86         */
87        add_action(
88            'rest_api_init',
89            static function () {
90                REST_Provider::init();
91            },
92            0
93        );
94        Transient_Cleanup::init();
95
96        // Clean up transient cron on module deactivation.
97        add_action( 'jetpack_deactivate_module_stats', array( Transient_Cleanup::class, 'unschedule_cleanup' ) );
98
99        // Set up package version hook.
100        add_filter( 'jetpack_package_versions', __NAMESPACE__ . '\Package_Version::send_package_version_to_tracker' );
101
102        // Register WP Abilities API surface. Gated behind the
103        // `jetpack_wp_abilities_enabled` filter inside Registrar::init(),
104        // which defaults to false â€” so this call is safe to make unconditionally
105        // and still opt-in per-site until the flag is flipped.
106        Stats_Abilities::init();
107    }
108
109    /**
110     * Checks if filter is set and dnt is enabled.
111     *
112     * @return bool
113     */
114    public static function jetpack_is_dnt_enabled() {
115        /**
116         * Filter the option which decides honor DNT or not.
117         *
118         * @module stats
119         * @since-jetpack 6.1.0
120         *
121         * @param bool false Honors DNT for clients who don't want to be tracked. Defaults to false. Set to true to enable.
122         */
123        if ( false === apply_filters( 'jetpack_honor_dnt_header_for_stats', false ) ) {
124            return false;
125        }
126
127        foreach ( $_SERVER as $name => $value ) {
128            if ( 'http_dnt' === strtolower( $name ) && 1 === (int) $value ) {
129                return true;
130            }
131        }
132
133        return false;
134    }
135
136    /**
137     * Maps view_stats cap to read cap as needed.
138     *
139     * @access public
140     * @param mixed $caps Caps.
141     * @param mixed $cap Cap.
142     * @param mixed $user_id User ID.
143     * @return array Possibly mapped capabilities for meta capability.
144     */
145    public static function map_meta_caps( $caps, $cap, $user_id ) {
146        // Map view_stats to exists.
147        if ( 'view_stats' === $cap ) {
148            $user = new WP_User( $user_id );
149            // WordPress 6.9 introduced lazy-loading of some WP_User properties, including `roles`.
150            // It also made said properties protected, so we can't modify keys directly.
151            $user_roles  = $user->roles;
152            $user_role   = array_shift( $user_roles ); // Work with the copy
153            $stats_roles = Options::get_option( 'roles' );
154
155            // Is the users role in the available stats roles?
156            if ( is_array( $stats_roles ) && in_array( $user_role, $stats_roles, true ) ) {
157                $caps = array( 'read' );
158            }
159        }
160
161        return $caps;
162    }
163
164    /**
165     * Stats Template Redirect.
166     *
167     * @access public
168     * @return void
169     */
170    public static function template_redirect() {
171        if ( ! self::should_track() ) {
172            return;
173        }
174
175        add_action( 'wp_enqueue_scripts', array( Tracking_Pixel::class, 'enqueue_stats_script' ), 101 );
176        add_action( 'wp_footer', array( Tracking_Pixel::class, 'add_amp_pixel' ), 101 );
177        add_action( 'web_stories_print_analytics', array( Tracking_Pixel::class, 'add_amp_pixel' ), 101 );
178    }
179
180    /**
181     * CSS to hide the tracking pixel smiley.
182     * It is now hidden for everyone (used to be visible if you had set the hide_smile option).
183     *
184     * @access public
185     * @return void
186     */
187    public static function hide_smile_css() {
188        if ( ! self::should_track() ) {
189            return;
190        }
191        ?>
192    <style>img#wpstats{display:none}</style>
193        <?php
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}