Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
14.55% covered (danger)
14.55%
8 / 55
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Tracks_Client
14.55% covered (danger)
14.55%
8 / 55
16.67% covered (danger)
16.67%
1 / 6
517.24
0.00% covered (danger)
0.00%
0 / 1
 record_event
33.33% covered (danger)
33.33%
4 / 12
0.00% covered (danger)
0.00%
0 / 1
21.52
 record_pixel
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 get_user_agent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 build_pixel_url
n/a
0 / 0
n/a
0 / 0
1
 validate_and_sanitize
n/a
0 / 0
n/a
0 / 0
2
 build_timestamp
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_anon_id
23.08% covered (danger)
23.08%
3 / 13
0.00% covered (danger)
0.00%
0 / 1
55.52
 get_connected_user_tracks_identity
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Legacy Jetpack Tracks Client
4 *
5 * @package automattic/jetpack-tracking
6 */
7
8use Automattic\Jetpack\Connection\Manager;
9
10/**
11 * Jetpack_Tracks_Client
12 *
13 * Send Tracks events on behalf of a user
14 *
15 * Example Usage:
16```php
17    require( dirname(__FILE__).'path/to/tracks/class-jetpack-tracks-client.php' );
18
19    $result = Jetpack_Tracks_Client::record_event( array(
20        '_en'        => $event_name,       // required
21        '_ui'        => $user_id,          // required unless _ul is provided
22        '_ul'        => $user_login,       // required unless _ui is provided
23
24        // Optional, but recommended
25        '_ts'        => $ts_in_ms,         // Default: now
26        '_via_ip'    => $client_ip,        // we use it for geo, etc.
27
28        // Possibly useful to set some context for the event
29        '_via_ua'    => $client_user_agent,
30        '_via_url'   => $client_url,
31        '_via_ref'   => $client_referrer,
32
33        // For user-targeted tests
34        'abtest_name'        => $abtest_name,
35        'abtest_variation'   => $abtest_variation,
36
37        // Your application-specific properties
38        'custom_property'    => $some_value,
39    ) );
40
41    if ( is_wp_error( $result ) ) {
42        // Handle the error in your app
43    }
44```
45 */
46class Jetpack_Tracks_Client {
47    const PIXEL           = 'https://pixel.wp.com/t.gif';
48    const BROWSER_TYPE    = 'php-agent';
49    const USER_AGENT_SLUG = 'tracks-client';
50    const VERSION         = '0.3';
51
52    /**
53     * Stores the Terms of Service Object Reference.
54     *
55     * @var null
56     */
57    private static $terms_of_service = null;
58
59    /**
60     * Record an event.
61     *
62     * @param  mixed $event Event object to send to Tracks. An array will be cast to object. Required.
63     *                      Properties are included directly in the pixel query string after light validation.
64     * @return mixed         True on success, WP_Error on failure
65     */
66    public static function record_event( $event ) {
67        if ( ! self::$terms_of_service ) {
68            self::$terms_of_service = new \Automattic\Jetpack\Terms_Of_Service();
69        }
70
71        // Don't track users who have opted out or not agreed to our TOS, or are not running an active Jetpack.
72        if ( ! self::$terms_of_service->has_agreed() || ! empty( $_COOKIE['tk_opt-out'] ) ) {
73            return false;
74        }
75
76        if ( ! $event instanceof Jetpack_Tracks_Event ) {
77            $event = new Jetpack_Tracks_Event( $event );
78        }
79        if ( is_wp_error( $event ) ) {
80            return $event;
81        }
82
83        $pixel = $event->build_pixel_url();
84
85        if ( ! $pixel ) {
86            return new WP_Error( 'invalid_pixel', 'cannot generate tracks pixel for given input', 400 );
87        }
88
89        return self::record_pixel( $pixel );
90    }
91
92    /**
93     * Synchronously request the pixel.
94     *
95     * @param string $pixel The wp.com tracking pixel.
96     * @return array|bool|WP_Error True if successful. wp_remote_get response or WP_Error if not.
97     */
98    public static function record_pixel( $pixel ) {
99        // Add the Request Timestamp and URL terminator just before the HTTP request.
100        $pixel .= '&_rt=' . self::build_timestamp() . '&_=_';
101
102        $response = wp_remote_get(
103            $pixel,
104            array(
105                'blocking'    => true, // The default, but being explicit here :).
106                'timeout'     => 1,
107                'redirection' => 2,
108                'httpversion' => '1.1',
109                'user-agent'  => self::get_user_agent(),
110            )
111        );
112
113        if ( is_wp_error( $response ) ) {
114            return $response;
115        }
116
117        $code = isset( $response['response']['code'] ) ? $response['response']['code'] : 0;
118
119        if ( 200 !== $code ) {
120            return new WP_Error( 'request_failed', 'Tracks pixel request failed', $code );
121        }
122
123        return true;
124    }
125
126    /**
127     * Get the user agent.
128     *
129     * @return string The user agent.
130     */
131    public static function get_user_agent() {
132        return self::USER_AGENT_SLUG . '-v' . self::VERSION;
133    }
134
135    /**
136     * Build an event and return its tracking URL
137     *
138     * @deprecated          Call the `build_pixel_url` method on a Jetpack_Tracks_Event object instead.
139     * @param  array $event Event keys and values.
140     * @return string       URL of a tracking pixel.
141     */
142    public static function build_pixel_url( $event ) {
143        $_event = new Jetpack_Tracks_Event( $event );
144        return $_event->build_pixel_url();
145    }
146
147    /**
148     * Validate input for a tracks event.
149     *
150     * @deprecated          Instantiate a Jetpack_Tracks_Event object instead
151     * @param  array $event Event keys and values.
152     * @return mixed        Validated keys and values or WP_Error on failure
153     */
154    private static function validate_and_sanitize( $event ) {
155        $_event = new Jetpack_Tracks_Event( $event );
156        if ( is_wp_error( $_event ) ) {
157            return $_event;
158        }
159        return get_object_vars( $_event );
160    }
161
162    /**
163     * Builds a timestamp.
164     *
165     * Milliseconds since 1970-01-01.
166     *
167     * @return string
168     */
169    public static function build_timestamp() {
170        $ts = round( microtime( true ) * 1000 );
171        return number_format( $ts, 0, '', '' );
172    }
173
174    /**
175     * Grabs the user's anon id from cookies, or generates and sets a new one
176     *
177     * @return string An anon id for the user
178     */
179    public static function get_anon_id() {
180        static $anon_id = null;
181
182        if ( ! isset( $anon_id ) ) {
183
184            // Did the browser send us a cookie?
185            if ( isset( $_COOKIE['tk_ai'] ) && preg_match( '#^[a-z]+:[A-Za-z0-9+/=]{24}$#', $_COOKIE['tk_ai'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
186                $anon_id = $_COOKIE['tk_ai']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
187            } else {
188
189                $binary = '';
190
191                // Generate a new anonId and try to save it in the browser's cookies.
192                // Note that base64-encoding an 18 character string generates a 24-character anon id.
193                for ( $i = 0; $i < 18; ++$i ) {
194                    $binary .= chr( wp_rand( 0, 255 ) );
195                }
196
197                $anon_id = 'jetpack:' . base64_encode( $binary ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
198
199                if ( ! headers_sent()
200                    && ! ( defined( 'REST_REQUEST' ) && REST_REQUEST )
201                    && ! ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
202                ) {
203                    setcookie( 'tk_ai', $anon_id, 0, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), false ); // phpcs:ignore Jetpack.Functions.SetCookie -- This is a random value and should be fine.
204                }
205            }
206        }
207
208        return $anon_id;
209    }
210
211    /**
212     * Gets the WordPress.com user's Tracks identity, if connected.
213     *
214     * @return array|bool
215     */
216    public static function get_connected_user_tracks_identity() {
217        $user_data = ( new Manager() )->get_connected_user_data();
218        if ( ! $user_data ) {
219            return false;
220        }
221
222        return array(
223            'blogid'      => Jetpack_Options::get_option( 'id', 0 ),
224            'email'       => $user_data['email'],
225            'userid'      => $user_data['ID'],
226            'username'    => $user_data['login'],
227            'user_locale' => $user_data['user_locale'],
228        );
229    }
230}