PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.1
16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 All 504 releases
jetpack / jetpack_vendor / automattic / jetpack-connection / src / class-tracking.php

class-tracking.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.1, at jetpack_vendor/automattic/jetpack-connection/src/class-tracking.php

333 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Nosara Tracks for Jetpack
4 *
5 * @package automattic/jetpack-connection
6 */
7
8 namespace Automattic\Jetpack;
9
10 use Automattic\Jetpack\IP\Utils as IP_Utils;
11
12 /**
13 * The Tracking class, used to record events in wpcom
14 */
15 class Tracking {
16 /**
17 * The assets version.
18 *
19 * @since 1.13.1
20 * @deprecated since 1.40.1
21 *
22 * @var string Assets version.
23 */
24 const ASSETS_VERSION = '1.0.0';
25
26 /**
27 * Slug of the product that we are tracking.
28 *
29 * @var string
30 */
31 private $product_name;
32
33 /**
34 * Connection manager object.
35 *
36 * @var Object
37 */
38 private $connection;
39
40 /**
41 * Creates the Tracking object.
42 *
43 * @param string $product_name the slug of the product that we are tracking.
44 * @param \Automattic\Jetpack\Connection\Manager $connection the connection manager object.
45 */
46 public function __construct( $product_name = 'jetpack', $connection = null ) {
47 $this->product_name = $product_name;
48 $this->connection = $connection;
49 if ( $this->connection === null ) {
50 // TODO We should always pass a Connection.
51 $this->connection = new Connection\Manager();
52 }
53
54 if ( ! did_action( 'jetpack_set_tracks_ajax_hook' ) ) {
55 add_action( 'wp_ajax_jetpack_tracks', array( $this, 'ajax_tracks' ) );
56
57 /**
58 * Fires when the Tracking::ajax_tracks() callback has been hooked to the
59 * wp_ajax_jetpack_tracks action. This action is used to ensure that
60 * the callback is hooked only once.
61 *
62 * @since 1.13.11
63 */
64 do_action( 'jetpack_set_tracks_ajax_hook' );
65 }
66 }
67
68 /**
69 * Universal method for for all tracking events triggered via the JavaScript client.
70 *
71 * @access public
72 */
73 public function ajax_tracks() {
74 // Check for nonce.
75 if (
76 empty( $_REQUEST['tracksNonce'] )
77 || ! wp_verify_nonce( $_REQUEST['tracksNonce'], 'jp-tracks-ajax-nonce' ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WP core doesn't pre-sanitize nonces either.
78 ) {
79 wp_send_json_error(
80 __( 'You aren’t authorized to do that.', 'jetpack-connection' ),
81 403,
82 JSON_UNESCAPED_SLASHES
83 );
84 }
85
86 if ( ! isset( $_REQUEST['tracksEventName'] ) || ! isset( $_REQUEST['tracksEventType'] ) ) {
87 wp_send_json_error(
88 __( 'No valid event name or type.', 'jetpack-connection' ),
89 403,
90 JSON_UNESCAPED_SLASHES
91 );
92 exit; // @phan-suppress-current-line PhanPluginUnreachableCode -- @todo Remove when WP 7.1 is the minimum version.
93 }
94
95 $tracks_data = array();
96 if ( 'click' === $_REQUEST['tracksEventType'] && isset( $_REQUEST['tracksEventProp'] ) ) {
97 if ( is_array( $_REQUEST['tracksEventProp'] ) ) {
98 // map_deep() rather than array_map(): the request is client-supplied and may nest,
99 // and sanitize_text_field() returns an empty string when handed an array.
100 $tracks_data = map_deep( wp_unslash( $_REQUEST['tracksEventProp'] ), 'sanitize_text_field' );
101 } else {
102 $tracks_data = array( 'clicked' => sanitize_text_field( wp_unslash( $_REQUEST['tracksEventProp'] ) ) );
103 }
104 }
105
106 // Tracks only accepts lowercase alphanumerics and underscores in an event name
107 // (see Jetpack_Tracks_Event::EVENT_NAME_REGEX), which is what sanitize_key() permits.
108 $this->record_user_event( sanitize_key( wp_unslash( $_REQUEST['tracksEventName'] ) ), $tracks_data, null, false );
109
110 wp_send_json_success( null, null, JSON_UNESCAPED_SLASHES );
111 }
112
113 /**
114 * Register script necessary for tracking.
115 *
116 * @param boolean $enqueue Also enqueue? defaults to false.
117 */
118 public static function register_tracks_functions_scripts( $enqueue = false ) {
119
120 // Register jp-tracks as it is a dependency.
121 wp_register_script(
122 'jp-tracks',
123 '//stats.wp.com/w.js',
124 array(),
125 gmdate( 'YW' ),
126 true
127 );
128
129 Assets::register_script(
130 'jp-tracks-functions',
131 '../dist/tracks-callables.js',
132 __FILE__,
133 array(
134 'dependencies' => array( 'jp-tracks' ),
135 'enqueue' => $enqueue,
136 'in_footer' => true,
137 )
138 );
139 }
140
141 /**
142 * Enqueue script necessary for tracking.
143 */
144 public function enqueue_tracks_scripts() {
145 Assets::register_script(
146 'jptracks',
147 '../dist/tracks-ajax.js',
148 __FILE__,
149 array(
150 'dependencies' => array( 'jquery' ),
151 'enqueue' => true,
152 'in_footer' => true,
153 )
154 );
155
156 wp_localize_script(
157 'jptracks',
158 'jpTracksAJAX',
159 array(
160 'ajaxurl' => admin_url( 'admin-ajax.php' ),
161 'jpTracksAJAX_nonce' => wp_create_nonce( 'jp-tracks-ajax-nonce' ),
162 )
163 );
164 }
165
166 /**
167 * Send an event in Tracks.
168 *
169 * @param string $event_type Type of the event.
170 * @param array $data Data to send with the event.
171 * @param mixed $user Username, user_id, or WP_User object.
172 * @param bool $use_product_prefix Whether to use the object's product name as a prefix to the event type. If
173 * set to false, the prefix will be 'jetpack_'.
174 */
175 public function record_user_event( $event_type, $data = array(), $user = null, $use_product_prefix = true ) {
176 if ( ! $user ) {
177 $user = wp_get_current_user();
178 }
179 $site_url = get_option( 'siteurl' );
180
181 $data['_via_ua'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
182 $data['_via_ip'] = isset( $_SERVER['REMOTE_ADDR'] ) ? (string) IP_Utils::clean_ip( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- clean_ip() validates the address.
183 $data['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '';
184 $data['blog_url'] = $site_url;
185 $data['blog_id'] = \Jetpack_Options::get_option( 'id' );
186
187 // Top level events should not be namespaced.
188 if ( '_aliasUser' !== $event_type ) {
189 $prefix = $use_product_prefix ? $this->product_name : 'jetpack';
190 $event_type = $prefix . '_' . $event_type;
191 }
192
193 $data['jetpack_version'] = defined( 'JETPACK__VERSION' ) ? JETPACK__VERSION : '0';
194
195 return $this->tracks_record_event( $user, $event_type, $data );
196 }
197
198 /**
199 * Record an event in Tracks - this is the preferred way to record events from PHP.
200 *
201 * @param mixed $user username, user_id, or WP_User object.
202 * @param string $event_name The name of the event.
203 * @param array $properties Custom properties to send with the event.
204 * @param int $event_timestamp_millis The time in millis since 1970-01-01 00:00:00 when the event occurred.
205 *
206 * @return bool true for success | \WP_Error if the event pixel could not be fired
207 */
208 public function tracks_record_event( $user, $event_name, $properties = array(), $event_timestamp_millis = false ) {
209
210 // We don't want to track user events during unit tests/CI runs.
211 if ( $user instanceof \WP_User && 'wptests_capabilities' === $user->cap_key ) {
212 return false;
213 }
214 $terms_of_service = new Terms_Of_Service();
215 $status = new Status();
216 // Don't track users who have not agreed to our TOS.
217 if ( ! $this->should_enable_tracking( $terms_of_service, $status ) ) {
218 return false;
219 }
220
221 $event_obj = $this->tracks_build_event_obj( $user, $event_name, $properties, $event_timestamp_millis );
222
223 if ( is_wp_error( $event_obj->error ) ) {
224 return $event_obj->error;
225 }
226
227 return $event_obj->record();
228 }
229
230 /**
231 * Determines whether tracking should be enabled.
232 *
233 * @param \Automattic\Jetpack\Terms_Of_Service $terms_of_service A Terms_Of_Service object.
234 * @param \Automattic\Jetpack\Status $status A Status object.
235 *
236 * @return boolean True if tracking should be enabled, else false.
237 */
238 public function should_enable_tracking( $terms_of_service, $status ) {
239 if ( $status->is_offline_mode() ) {
240 return false;
241 }
242
243 return $terms_of_service->has_agreed() || $this->connection->is_user_connected();
244 }
245
246 /**
247 * Procedurally build a Tracks Event Object.
248 * NOTE: Use this only when the simpler Automattic\Jetpack\Tracking->jetpack_tracks_record_event() function won't work for you.
249 *
250 * @param \WP_User $user WP_User object.
251 * @param string $event_name The name of the event.
252 * @param array $properties Custom properties to send with the event.
253 * @param int $event_timestamp_millis The time in millis since 1970-01-01 00:00:00 when the event occurred.
254 *
255 * @return \Jetpack_Tracks_Event|\WP_Error
256 */
257 private function tracks_build_event_obj( $user, $event_name, $properties = array(), $event_timestamp_millis = false ) {
258 $identity = $this->tracks_get_identity( $user->ID );
259
260 $properties['user_lang'] = $user->get( 'WPLANG' );
261
262 $blog_details = array(
263 'blog_lang' => $properties['blog_lang'] ?? get_bloginfo( 'language' ),
264 'blog_id' => \Jetpack_Options::get_option( 'id' ),
265 );
266
267 $timestamp = ( false !== $event_timestamp_millis ) ? $event_timestamp_millis : round( microtime( true ) * 1000 );
268 $timestamp_string = is_string( $timestamp ) ? $timestamp : number_format( $timestamp, 0, '', '' );
269
270 return new \Jetpack_Tracks_Event(
271 array_merge(
272 $blog_details,
273 (array) $properties,
274 $identity,
275 array(
276 '_en' => $event_name,
277 '_ts' => $timestamp_string,
278 )
279 )
280 );
281 }
282
283 /**
284 * Get the identity to send to tracks.
285 *
286 * @param int $user_id The user id of the local user.
287 *
288 * @return array $identity
289 */
290 public function tracks_get_identity( $user_id ) {
291
292 // Meta is set, and user is still connected. Use WPCOM ID.
293 $wpcom_id = get_user_meta( $user_id, 'jetpack_tracks_wpcom_id', true );
294 if ( $wpcom_id && is_string( $wpcom_id ) && $this->connection->is_user_connected( $user_id ) ) {
295 return array(
296 '_ut' => 'wpcom:user_id',
297 '_ui' => $wpcom_id,
298 );
299 }
300
301 // User is connected, but no meta is set yet. Use WPCOM ID and set meta.
302 if ( $this->connection->is_user_connected( $user_id ) ) {
303 $wpcom_user_data = $this->connection->get_connected_user_data( $user_id );
304 $wpcom_id = $wpcom_user_data['ID'] ?? null;
305
306 if ( is_string( $wpcom_id ) ) {
307 update_user_meta( $user_id, 'jetpack_tracks_wpcom_id', $wpcom_id );
308
309 return array(
310 '_ut' => 'wpcom:user_id',
311 '_ui' => $wpcom_id,
312 );
313 }
314 }
315
316 // User isn't linked at all. Fall back to anonymous ID.
317 $anon_id = get_user_meta( $user_id, 'jetpack_tracks_anon_id', true );
318 if ( ! $anon_id ) {
319 $anon_id = \Jetpack_Tracks_Client::get_anon_id();
320 add_user_meta( $user_id, 'jetpack_tracks_anon_id', $anon_id, false );
321 }
322
323 if ( ! isset( $_COOKIE['tk_ai'] ) && ! headers_sent() ) {
324 setcookie( 'tk_ai', $anon_id, 0, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), false ); // phpcs:ignore Jetpack.Functions.SetCookie -- This is a random string and should be fine.
325 }
326
327 return array(
328 '_ut' => 'anon',
329 '_ui' => $anon_id,
330 );
331 }
332 }
333