Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.54% covered (warning)
82.54%
52 / 63
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Main
82.54% covered (warning)
82.54%
52 / 63
57.14% covered (warning)
57.14%
4 / 7
38.80
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%
13 / 13
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        REST_Provider::init();
80        Transient_Cleanup::init();
81
82        // Clean up transient cron on module deactivation.
83        add_action( 'jetpack_deactivate_module_stats', array( Transient_Cleanup::class, 'unschedule_cleanup' ) );
84
85        // Set up package version hook.
86        add_filter( 'jetpack_package_versions', __NAMESPACE__ . '\Package_Version::send_package_version_to_tracker' );
87
88        // Register WP Abilities API surface. Gated behind the
89        // `jetpack_wp_abilities_enabled` filter inside Registrar::init(),
90        // which defaults to false â€” so this call is safe to make unconditionally
91        // and still opt-in per-site until the flag is flipped.
92        Stats_Abilities::init();
93    }
94
95    /**
96     * Checks if filter is set and dnt is enabled.
97     *
98     * @return bool
99     */
100    public static function jetpack_is_dnt_enabled() {
101        /**
102         * Filter the option which decides honor DNT or not.
103         *
104         * @module stats
105         * @since-jetpack 6.1.0
106         *
107         * @param bool false Honors DNT for clients who don't want to be tracked. Defaults to false. Set to true to enable.
108         */
109        if ( false === apply_filters( 'jetpack_honor_dnt_header_for_stats', false ) ) {
110            return false;
111        }
112
113        foreach ( $_SERVER as $name => $value ) {
114            if ( 'http_dnt' === strtolower( $name ) && 1 === (int) $value ) {
115                return true;
116            }
117        }
118
119        return false;
120    }
121
122    /**
123     * Maps view_stats cap to read cap as needed.
124     *
125     * @access public
126     * @param mixed $caps Caps.
127     * @param mixed $cap Cap.
128     * @param mixed $user_id User ID.
129     * @return array Possibly mapped capabilities for meta capability.
130     */
131    public static function map_meta_caps( $caps, $cap, $user_id ) {
132        // Map view_stats to exists.
133        if ( 'view_stats' === $cap ) {
134            $user = new WP_User( $user_id );
135            // WordPress 6.9 introduced lazy-loading of some WP_User properties, including `roles`.
136            // It also made said properties protected, so we can't modify keys directly.
137            $user_roles  = $user->roles;
138            $user_role   = array_shift( $user_roles ); // Work with the copy
139            $stats_roles = Options::get_option( 'roles' );
140
141            // Is the users role in the available stats roles?
142            if ( is_array( $stats_roles ) && in_array( $user_role, $stats_roles, true ) ) {
143                $caps = array( 'read' );
144            }
145        }
146
147        return $caps;
148    }
149
150    /**
151     * Stats Template Redirect.
152     *
153     * @access public
154     * @return void
155     */
156    public static function template_redirect() {
157        if ( ! self::should_track() ) {
158            return;
159        }
160
161        add_action( 'wp_enqueue_scripts', array( Tracking_Pixel::class, 'enqueue_stats_script' ), 101 );
162        add_action( 'wp_footer', array( Tracking_Pixel::class, 'add_amp_pixel' ), 101 );
163        add_action( 'web_stories_print_analytics', array( Tracking_Pixel::class, 'add_amp_pixel' ), 101 );
164    }
165
166    /**
167     * CSS to hide the tracking pixel smiley.
168     * It is now hidden for everyone (used to be visible if you had set the hide_smile option).
169     *
170     * @access public
171     * @return void
172     */
173    public static function hide_smile_css() {
174        if ( ! self::should_track() ) {
175            return;
176        }
177        ?>
178    <style>img#wpstats{display:none}</style>
179        <?php
180    }
181
182    /**
183     * Whether we should add the tracking pixel.
184     *
185     * @return bool
186     */
187    public static function should_track() {
188        global $current_user;
189
190        // Not connected sites should not generate tracking stats.
191        if ( ! ( new Connection_Manager() )->is_connected() ) {
192            return false;
193        }
194
195        // If the stats module is disabled we should not generate tracking stats.
196        if ( ! ( new Modules() )->is_active( 'stats' ) ) {
197            return false;
198        }
199
200        // Do not generate tracking stats for feeds, robots, embeds, previews
201        // or to honour the DNT headers.
202        if (
203            is_feed()
204            || is_robots()
205            || is_embed()
206            || is_trackback()
207            || is_preview()
208            || self::jetpack_is_dnt_enabled()
209        ) {
210            return false;
211        }
212
213        // Sites in Safe Mode should not generate tracking stats.
214        $status = new Status();
215        if ( $status->in_safe_mode() ) {
216            return false;
217        }
218
219        // Should we be counting this user's views?
220        if ( ! empty( $current_user->ID ) ) {
221            $count_roles = Options::get_option( 'count_roles' );
222            if ( ! is_array( $count_roles ) || ! array_intersect( $current_user->roles, $count_roles ) ) {
223                return false;
224            }
225        }
226
227        /**
228         * Allow excluding specific IP addresses from being tracked in Stats.
229         * Note: for this to work well, visitors' IP addresses must:
230         * - be stored and returned properly in IP address headers;
231         * - not be impacted by any caching setup on your site.
232         *
233         * @module stats
234         *
235         * @since-jetpack 10.6
236         *
237         * @param array $excluded_ips An array of IP address strings to exclude from tracking.
238         */
239        $excluded_ips = (array) apply_filters( 'jetpack_stats_excluded_ips', array() );
240
241        // Should we be counting views for this IP address?
242        $current_user_ip = ( new Visitor() )->get_ip( true );
243        if (
244            ! empty( $excluded_ips )
245            && in_array( $current_user_ip, $excluded_ips, true )
246        ) {
247            return false;
248        }
249
250        return true;
251    }
252}