summaryrefslogtreecommitdiff
blob: cc9d1de20153965d0d21b2330ffbb736eccaeb4b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
<?php
/**
 * Nosara Tracks for Jetpack
 *
 * @package automattic/jetpack-tracking
 */

namespace Automattic\Jetpack;

/**
 * The Tracking class, used to record events in wpcom
 */
class Tracking {
	/**
	 * The assets version.
	 *
	 * @since 1.13.1
	 *
	 * @var string Assets version.
	 */
	const ASSETS_VERSION = '1.0.0';

	/**
	 * Slug of the product that we are tracking.
	 *
	 * @var string
	 */
	private $product_name;

	/**
	 * Connection manager object.
	 *
	 * @var Object
	 */
	private $connection;

	/**
	 * Creates the Tracking object.
	 *
	 * @param String                                $product_name the slug of the product that we are tracking.
	 * @param Automattic\Jetpack\Connection\Manager $connection   the connection manager object.
	 */
	public function __construct( $product_name = 'jetpack', $connection = null ) {
		$this->product_name = $product_name;
		$this->connection   = $connection;
		if ( is_null( $this->connection ) ) {
			// TODO We should always pass a Connection.
			$this->connection = new Connection\Manager();
		}

		if ( ! did_action( 'jetpack_set_tracks_ajax_hook' ) ) {
			add_action( 'wp_ajax_jetpack_tracks', array( $this, 'ajax_tracks' ) );

			/**
			 * Fires when the Tracking::ajax_tracks() callback has been hooked to the
			 * wp_ajax_jetpack_tracks action. This action is used to ensure that
			 * the callback is hooked only once.
			 *
			 * @since 1.13.11
			 */
			do_action( 'jetpack_set_tracks_ajax_hook' );
		}
	}

	/**
	 * Universal method for for all tracking events triggered via the JavaScript client.
	 *
	 * @access public
	 */
	public function ajax_tracks() {
		// Check for nonce.
		if (
			empty( $_REQUEST['tracksNonce'] )
			|| ! wp_verify_nonce( $_REQUEST['tracksNonce'], 'jp-tracks-ajax-nonce' )
		) {
			wp_send_json_error(
				__( 'You aren’t authorized to do that.', 'jetpack-tracking' ),
				403
			);
		}

		if ( ! isset( $_REQUEST['tracksEventName'] ) || ! isset( $_REQUEST['tracksEventType'] ) ) {
			wp_send_json_error(
				__( 'No valid event name or type.', 'jetpack-tracking' ),
				403
			);
		}

		$tracks_data = array();
		if ( 'click' === $_REQUEST['tracksEventType'] && isset( $_REQUEST['tracksEventProp'] ) ) {
			if ( is_array( $_REQUEST['tracksEventProp'] ) ) {
				$tracks_data = $_REQUEST['tracksEventProp'];
			} else {
				$tracks_data = array( 'clicked' => $_REQUEST['tracksEventProp'] );
			}
		}

		$this->record_user_event( $_REQUEST['tracksEventName'], $tracks_data, null, false );

		wp_send_json_success();
	}

	/**
	 * Register script necessary for tracking.
	 *
	 * @param boolean $enqueue Also enqueue? defaults to false.
	 */
	public static function register_tracks_functions_scripts( $enqueue = false ) {

		// Register jp-tracks as it is a dependency.
		wp_register_script(
			'jp-tracks',
			'//stats.wp.com/w.js',
			array(),
			gmdate( 'YW' ),
			true
		);

		if ( $enqueue ) {
			// Enqueue jp-tracks-functions script.
			wp_enqueue_script(
				'jp-tracks-functions',
				Assets::get_file_url_for_environment( 'js/tracks-callables.js', 'js/tracks-callables.js', __FILE__ ),
				array( 'jp-tracks' ),
				self::ASSETS_VERSION,
				true
			);
		} else {
			// Register jp-tracks-functions script.
			wp_register_script(
				'jp-tracks-functions',
				Assets::get_file_url_for_environment( 'js/tracks-callables.js', 'js/tracks-callables.js', __FILE__ ),
				array( 'jp-tracks' ),
				self::ASSETS_VERSION,
				true
			);
		}

	}

	/**
	 * Enqueue script necessary for tracking.
	 */
	public function enqueue_tracks_scripts() {
		wp_enqueue_script(
			'jptracks',
			Assets::get_file_url_for_environment( 'js/tracks-ajax.js', 'js/tracks-ajax.js', __FILE__ ),
			array(),
			self::ASSETS_VERSION,
			true
		);

		wp_localize_script(
			'jptracks',
			'jpTracksAJAX',
			array(
				'ajaxurl'            => admin_url( 'admin-ajax.php' ),
				'jpTracksAJAX_nonce' => wp_create_nonce( 'jp-tracks-ajax-nonce' ),
			)
		);
	}

	/**
	 * Send an event in Tracks.
	 *
	 * @param string $event_type         Type of the event.
	 * @param array  $data               Data to send with the event.
	 * @param mixed  $user               Username, user_id, or WP_user object.
	 * @param bool   $use_product_prefix Whether to use the object's product name as a prefix to the event type. If
	 *                                   set to false, the prefix will be 'jetpack_'.
	 */
	public function record_user_event( $event_type, $data = array(), $user = null, $use_product_prefix = true ) {
		if ( ! $user ) {
			$user = wp_get_current_user();
		}
		$site_url = get_option( 'siteurl' );

		$data['_via_ua']  = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '';
		$data['_via_ip']  = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '';
		$data['_lg']      = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '';
		$data['blog_url'] = $site_url;
		$data['blog_id']  = \Jetpack_Options::get_option( 'id' );

		// Top level events should not be namespaced.
		if ( '_aliasUser' !== $event_type ) {
			$prefix     = $use_product_prefix ? $this->product_name : 'jetpack';
			$event_type = $prefix . '_' . $event_type;
		}

		$data['jetpack_version'] = defined( 'JETPACK__VERSION' ) ? JETPACK__VERSION : '0';

		return $this->tracks_record_event( $user, $event_type, $data );
	}

	/**
	 * Record an event in Tracks - this is the preferred way to record events from PHP.
	 *
	 * @param mixed  $user                   username, user_id, or WP_user object.
	 * @param string $event_name             The name of the event.
	 * @param array  $properties             Custom properties to send with the event.
	 * @param int    $event_timestamp_millis The time in millis since 1970-01-01 00:00:00 when the event occurred.
	 *
	 * @return bool true for success | \WP_Error if the event pixel could not be fired
	 */
	public function tracks_record_event( $user, $event_name, $properties = array(), $event_timestamp_millis = false ) {

		// We don't want to track user events during unit tests/CI runs.
		if ( $user instanceof \WP_User && 'wptests_capabilities' === $user->cap_key ) {
			return false;
		}
		$terms_of_service = new Terms_Of_Service();
		$status           = new Status();
		// Don't track users who have not agreed to our TOS.
		if ( ! $this->should_enable_tracking( $terms_of_service, $status ) ) {
			return false;
		}

		$event_obj = $this->tracks_build_event_obj( $user, $event_name, $properties, $event_timestamp_millis );

		if ( is_wp_error( $event_obj->error ) ) {
			return $event_obj->error;
		}

		return $event_obj->record();
	}

	/**
	 * Determines whether tracking should be enabled.
	 *
	 * @param Automattic\Jetpack\Terms_Of_Service $terms_of_service A Terms_Of_Service object.
	 * @param Automattic\Jetpack\Status           $status A Status object.
	 *
	 * @return boolean True if tracking should be enabled, else false.
	 */
	public function should_enable_tracking( $terms_of_service, $status ) {
		if ( $status->is_offline_mode() ) {
			return false;
		}

		return $terms_of_service->has_agreed() || $this->connection->is_user_connected();
	}

	/**
	 * Procedurally build a Tracks Event Object.
	 * NOTE: Use this only when the simpler Automattic\Jetpack\Tracking->jetpack_tracks_record_event() function won't work for you.
	 *
	 * @param WP_user $user                   WP_user object.
	 * @param string  $event_name             The name of the event.
	 * @param array   $properties             Custom properties to send with the event.
	 * @param int     $event_timestamp_millis The time in millis since 1970-01-01 00:00:00 when the event occurred.
	 *
	 * @return \Jetpack_Tracks_Event|\WP_Error
	 */
	private function tracks_build_event_obj( $user, $event_name, $properties = array(), $event_timestamp_millis = false ) {
		$identity = $this->tracks_get_identity( $user->ID );

		$properties['user_lang'] = $user->get( 'WPLANG' );

		$blog_details = array(
			'blog_lang' => isset( $properties['blog_lang'] ) ? $properties['blog_lang'] : get_bloginfo( 'language' ),
		);

		$timestamp        = ( false !== $event_timestamp_millis ) ? $event_timestamp_millis : round( microtime( true ) * 1000 );
		$timestamp_string = is_string( $timestamp ) ? $timestamp : number_format( $timestamp, 0, '', '' );

		return new \Jetpack_Tracks_Event(
			array_merge(
				$blog_details,
				(array) $properties,
				$identity,
				array(
					'_en' => $event_name,
					'_ts' => $timestamp_string,
				)
			)
		);
	}

	/**
	 * Get the identity to send to tracks.
	 *
	 * @param int $user_id The user id of the local user.
	 *
	 * @return array $identity
	 */
	public function tracks_get_identity( $user_id ) {

		// Meta is set, and user is still connected.  Use WPCOM ID.
		$wpcom_id = get_user_meta( $user_id, 'jetpack_tracks_wpcom_id', true );
		if ( $wpcom_id && $this->connection->is_user_connected( $user_id ) ) {
			return array(
				'_ut' => 'wpcom:user_id',
				'_ui' => $wpcom_id,
			);
		}

		// User is connected, but no meta is set yet.  Use WPCOM ID and set meta.
		if ( $this->connection->is_user_connected( $user_id ) ) {
			$wpcom_user_data = $this->connection->get_connected_user_data( $user_id );
			update_user_meta( $user_id, 'jetpack_tracks_wpcom_id', $wpcom_user_data['ID'] );

			return array(
				'_ut' => 'wpcom:user_id',
				'_ui' => $wpcom_user_data['ID'],
			);
		}

		// User isn't linked at all.  Fall back to anonymous ID.
		$anon_id = get_user_meta( $user_id, 'jetpack_tracks_anon_id', true );
		if ( ! $anon_id ) {
			$anon_id = \Jetpack_Tracks_Client::get_anon_id();
			add_user_meta( $user_id, 'jetpack_tracks_anon_id', $anon_id, false );
		}

		if ( ! isset( $_COOKIE['tk_ai'] ) && ! headers_sent() ) {
			setcookie( 'tk_ai', $anon_id );
		}

		return array(
			'_ut' => 'anon',
			'_ui' => $anon_id,
		);

	}
}