PluginProbe ʕ •ᴥ•ʔ
WP Job Manager / 2.4.6
WP Job Manager v2.4.6
2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.1.0 1.1.1 1.1.2 1.1.3 1.10.0 1.11.0 1.11.1 1.12.0 1.12.1 1.13.0 1.14.0 1.15.0 1.16.0 1.16.1 1.17.0 1.18.0 1.19.0 1.2.0 1.20.0 1.20.1 1.21.0 1.21.1 1.21.2 1.21.3 1.21.4 1.22.0 1.22.1 1.22.2 1.22.3 1.23.0 1.23.1 1.23.10 1.23.11 1.23.12 1.23.13 1.23.2 1.23.3 1.23.4 1.23.5 1.23.6 1.23.7 1.23.8 1.23.9 1.24.0 1.24.0.1 1.25.0 1.25.0.1 1.25.1 1.25.1.1 1.25.2 1.25.2.1 1.25.3 1.25.3.1 1.26.0 1.26.0.1 1.26.1 1.26.1.1 1.26.2 1.26.2.1 1.27.0 1.27.0.1 1.28.0 1.28.0.1 1.29.0 1.29.0.1 1.29.1 1.29.1.1 1.29.2 1.29.2.1 1.29.3 1.29.3.1 1.3.0 1.3.1 1.30.0 1.30.0.1 1.30.1 1.30.1.1 1.30.2 1.30.2.1 1.31.0 1.31.0.1 1.31.1 1.31.1.1 1.31.2 1.31.3 1.32.0 1.32.1 1.32.2 1.32.3 1.33.0 1.33.1 1.33.2 1.33.3 1.33.4 1.33.5 1.34.0 1.34.1 1.34.2 1.34.3 1.34.4 1.34.5 1.35.0 1.35.1 1.35.2 1.35.3 1.36.0 1.36.1 1.36.2 1.37.0 1.38.0 1.38.1 1.39.0 1.4.0 1.40.0 1.40.1 1.40.2 1.41.0 1.42.0 1.5.0 1.5.1 1.5.2 1.6.0 1.7.0 1.7.1 1.7.3 1.8.0 1.8.1 1.8.2 1.9.0 1.9.1 1.9.2 1.9.3 2.0.0 2.1.0 2.1.1 2.2.0 2.2.1 2.2.2 2.3.0 2.4.0 2.4.1
wp-job-manager / lib / usage-tracking / class-wp-job-manager-usage-tracking-base.php
wp-job-manager / lib / usage-tracking Last commit date
class-wp-job-manager-usage-tracking-base.php 2 months ago
class-wp-job-manager-usage-tracking-base.php
584 lines
1 <?php
2 /**
3 * Reusable Usage Tracking library. For sending plugin usage data and events to
4 * Tracks.
5 *
6 * @package wp-job-manager
7 **/
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * Usage Tracking class. Please update the prefix to something unique to your
15 * plugin.
16 */
17 abstract class WP_Job_Manager_Usage_Tracking_Base {
18 const PLUGIN_PREFIX = 'plugin_';
19
20 const DISPLAY_ONCE_OPTION = 'job_manager_display_usage_tracking_once';
21
22 /*
23 * Instance variables.
24 */
25
26 /**
27 * The name of the option for hiding the Usage Tracking opt-in dialog.
28 *
29 * @var string
30 **/
31 protected $hide_tracking_opt_in_option_name;
32
33 /**
34 * The name of the cron job action for regularly logging usage data.
35 *
36 * @var string
37 **/
38 private $job_name;
39
40 /**
41 * Callback function for the usage tracking job.
42 *
43 * @var array
44 **/
45 private $callback;
46
47
48 /*
49 * Class variables.
50 */
51
52 /**
53 * Subclass instances.
54 *
55 * @var array
56 **/
57 private static $instances = [];
58
59 /**
60 * Gets the singleton instance of this class. Subclasses should implement
61 * this as follows:
62 *
63 * ```
64 * public static function get_instance() {
65 * return self::get_instance_for_subclass( get_class() );
66 * }
67 * ```
68 *
69 * This function cannot be abstract (because it is static) but it *must* be
70 * implemented by subclasses.
71 *
72 * @throws Exception When get_instance is not implemented.
73 */
74 public static function get_instance() {
75 throw new Exception( 'Usage Tracking subclasses must implement get_instance. See class-wp-job-manager-usage-tracking-base.php' );
76 }
77
78
79 /*
80 * Abstract methods.
81 */
82
83
84 /**
85 * Get prefix for actions and strings. Should be unique to this plugin.
86 *
87 * @return string The prefix string.
88 **/
89 abstract protected function get_prefix();
90
91 /**
92 * Determine whether usage tracking is enabled.
93 *
94 * @return bool true if usage tracking is enabled, false otherwise.
95 **/
96 abstract protected function get_tracking_enabled();
97
98 /**
99 * Set whether usage tracking is enabled.
100 *
101 * @param bool $enable true if usage tracking should be enabled, false if
102 * it should be disabled.
103 **/
104 abstract protected function set_tracking_enabled( $enable );
105
106 /**
107 * Determine whether current user can manage the tracking options.
108 *
109 * @return bool true if the current user is allowed to manage the tracking.
110 * options, false otherwise.
111 **/
112 abstract protected function current_user_can_manage_tracking();
113
114 /**
115 * Get the text to display in the opt-in dialog for users to enable
116 * tracking. This text should include a link to a page indicating what data
117 * is being tracked.
118 *
119 * @return string the text to display in the opt-in dialog.
120 **/
121 abstract protected function opt_in_dialog_text();
122
123 /**
124 * Gets the base data returned with system information.
125 *
126 * @return array
127 */
128 protected function get_base_system_data() {
129 return [];
130 }
131
132 /*
133 * Initialization.
134 */
135
136 /**
137 * Subclasses may override this to add plugin-specific initialization code.
138 * However, this constructor must be called by the subclass in order to
139 * properly initialize the Usage Tracking system.
140 *
141 * This class is meant to be a singleton, and assumes that the subclass is
142 * implemented as such. If multiple instances are instantiated, the results
143 * are undefined.
144 **/
145 protected function __construct() {
146 // Init instance vars.
147 $this->hide_tracking_opt_in_option_name = $this->get_prefix() . '_usage_tracking_opt_in_hide';
148 $this->job_name = $this->get_prefix() . '_usage_tracking_send_usage_data';
149
150 // Set up the opt-in dialog.
151 add_action( 'wpjm_admin_notices', [ $this, 'maybe_display_tracking_opt_in' ] );
152 add_action( 'admin_action_' . $this->get_prefix() . '_tracking_opt_in', [ $this, 'handle_tracking_opt_in' ] );
153 add_action( 'wp_job_manager_notice_dismissed', [ $this, 'handle_tracking_opt_out' ], 10, 2 );
154
155 // Set up schedule and action needed for cron job.
156 add_filter( 'cron_schedules', [ $this, 'add_usage_tracking_two_week_schedule' ] );
157 add_action( $this->job_name, [ $this, 'send_usage_data' ] );
158 }
159
160 /**
161 * Create (if necessary) and return the singleton instance for the given
162 * subclass.
163 *
164 * @param string $subclass the name of the subclass.
165 *
166 * @return object Instance of $subclass.
167 */
168 protected static function get_instance_for_subclass( $subclass ) {
169 if ( ! isset( self::$instances[ $subclass ] ) ) {
170 self::$instances[ $subclass ] = new $subclass();
171 }
172
173 return self::$instances[ $subclass ];
174 }
175
176
177 /*
178 * Public methods.
179 */
180
181 /**
182 * Set the Usage Data Callback. This callback should return an array of
183 * data to be logged periodically to Tracks.
184 *
185 * @param callable $callback the callback returning the usage data to be logged.
186 **/
187 public function set_callback( $callback ) {
188 $this->callback = $callback;
189 }
190
191 /**
192 * Send an event to Tracks if tracking is enabled.
193 *
194 * @param string $event The event name. The prefix string will be
195 * automatically prepended to this, so please supply this string without a
196 * prefix.
197 * @param array $properties Event Properties.
198 * @param null|int $event_timestamp When the event occurred.
199 *
200 * @return null|WP_Error
201 **/
202 public function send_event( $event, $properties = [], $event_timestamp = null ) {
203
204 // Only continue if tracking is enabled.
205 if ( ! $this->is_tracking_enabled() ) {
206 return false;
207 }
208
209 $pixel = 'http://pixel.wp.com/t.gif';
210 $event_name = $this->get_event_prefix() . '_' . $event;
211 $user = wp_get_current_user();
212
213 if ( null === $event_timestamp ) {
214 $event_timestamp = time();
215 }
216
217 $properties['admin_email'] = get_option( 'admin_email' );
218 $properties['_ut'] = $this->get_event_prefix() . ':site_url';
219 // Use site URL as the userid to enable usage tracking at the site level.
220 // Note that we would likely want to use site URL + user ID for userid if we were.
221 // to ever add event tracking at the user level.
222 $properties['_ui'] = site_url();
223 $properties['_ul'] = $user->user_login;
224 $properties['_en'] = $event_name;
225 $properties['_ts'] = $event_timestamp . '000';
226 $properties['_rt'] = round( microtime( true ) * 1000 ); // log time.
227 $p = [];
228
229 foreach ( $properties as $key => $value ) {
230 $p[] = rawurlencode( $key ) . '=' . rawurlencode( $value );
231 }
232
233 $pixel .= '?' . implode( '&', $p ) . '&_=_'; // EOF marker.
234 $response = wp_remote_get(
235 $pixel,
236 [
237 'blocking' => true,
238 'timeout' => 1,
239 'redirection' => 2,
240 'httpversion' => '1.1',
241 'user-agent' => $this->get_event_prefix() . '_usage_tracking',
242 ]
243 );
244
245 if ( is_wp_error( $response ) ) {
246 return $response;
247 }
248
249 $code = isset( $response['response']['code'] ) ? $response['response']['code'] : 0;
250
251 if ( 200 !== $code ) {
252 return new WP_Error( 'request_failed', 'HTTP Request failed', $code );
253 }
254
255 return true;
256 }
257
258 /**
259 * Set up a regular cron job to send usage data. The job will only send
260 * the data if tracking is enabled, so it is safe to call this function,
261 * and schedule the job, before the user opts into tracking.
262 **/
263 public function schedule_tracking_task() {
264 if ( ! wp_next_scheduled( $this->job_name ) ) {
265 wp_schedule_event( time(), $this->get_prefix() . '_usage_tracking_two_weeks', $this->job_name );
266 }
267 }
268
269 /**
270 * Unschedule the job scheduled by schedule_tracking_task if any is
271 * scheduled. This should be called on plugin deactivation.
272 **/
273 public function unschedule_tracking_task() {
274 if ( wp_next_scheduled( $this->job_name ) ) {
275 wp_clear_scheduled_hook( $this->job_name );
276 }
277 }
278
279 /**
280 * Check if tracking is enabled.
281 *
282 * @return bool true if tracking is enabled, false otherwise.
283 **/
284 public function is_tracking_enabled() {
285 // Defer to the plugin-specific function.
286 return $this->get_tracking_enabled();
287 }
288
289 /**
290 * Call the usage data callback and send the usage data to Tracks. Only
291 * sends data if tracking is enabled.
292 **/
293 public function send_usage_data() {
294 if ( ! self::is_tracking_enabled() || ! is_callable( $this->callback ) ) {
295 return;
296 }
297
298 $usage_data = call_user_func( $this->callback );
299
300 if ( ! is_array( $usage_data ) ) {
301 return;
302 }
303
304 self::send_event( 'system_log', $this->get_system_data() );
305 self::send_event( 'stats_log', $usage_data );
306 }
307
308
309 /**
310 * Internal methods.
311 */
312
313 /**
314 * Get the prefix for the event-related values. By default, this is the
315 * same prefix used everywhere else, but plugins may override this if
316 * needed.
317 */
318 protected function get_event_prefix() {
319 return $this->get_prefix();
320 }
321
322 /**
323 * Add two week schedule to use for cron job. Should not be called
324 * externally.
325 *
326 * @param array $schedules the existing cron schedules.
327 *
328 * @return array of $schedules.
329 **/
330 public function add_usage_tracking_two_week_schedule( $schedules ) {
331 $schedules[ $this->get_prefix() . '_usage_tracking_two_weeks' ] = [
332 'interval' => 15 * DAY_IN_SECONDS,
333 'display' => esc_html__( 'Every Two Weeks', 'wp-job-manager' ),
334 ];
335
336 return $schedules;
337 }
338
339 /**
340 * Collect system data to track.
341 *
342 * @return array
343 */
344 public function get_system_data() {
345 global $wp_version;
346
347 /**
348 * Current active theme.
349 *
350 * @var WP_Theme $theme
351 */
352 $theme = wp_get_theme();
353
354 $system_data = $this->get_base_system_data();
355 $system_data['wp_version'] = $wp_version;
356 $system_data['php_version'] = PHP_VERSION;
357 $system_data['locale'] = get_locale();
358 $system_data['multisite'] = is_multisite() ? 1 : 0;
359 $system_data['active_theme'] = $theme['Name'];
360 $system_data['active_theme_version'] = $theme['Version'];
361
362 $plugin_data = $this->get_plugin_data();
363 foreach ( $plugin_data as $plugin_name => $plugin_version ) {
364 $plugin_friendly_name = preg_replace( '/[^a-z0-9]/', '_', $plugin_name );
365 $plugin_key = self::PLUGIN_PREFIX . $plugin_friendly_name;
366 $system_data[ $plugin_key ] = $plugin_version;
367 }
368
369 return $system_data;
370 }
371
372 /**
373 * Gets a list of activated plugins.
374 *
375 * @return array List of plugins. Index is friendly name, value is version.
376 */
377 protected function get_plugin_data() {
378 $plugins = [];
379 foreach ( $this->get_plugins() as $plugin_basename => $plugin ) {
380 $plugin_name = $this->get_plugin_name( $plugin_basename );
381 $plugins[ $plugin_name ] = $plugin['Version'];
382 }
383
384 return $plugins;
385 }
386
387 /**
388 * Partial wrapper for for `get_plugins()` function. Filters out non-active plugins.
389 *
390 * @return array Key is the plugin file path and the value is an array of the plugin data.
391 */
392 protected function get_plugins() {
393 if ( ! function_exists( 'get_plugins' ) ) {
394 include_once ABSPATH . 'wp-admin/includes/plugin.php';
395 }
396 $plugins = get_plugins();
397 foreach ( $plugins as $plugin_basename => $plugin_data ) {
398 if ( ! is_plugin_active( $plugin_basename ) ) {
399 unset( $plugins[ $plugin_basename ] );
400 }
401 }
402
403 return $plugins;
404 }
405
406 /**
407 * Returns a friendly slug for a plugin.
408 *
409 * @param string $basename Plugin basename.
410 *
411 * @return string
412 */
413 private function get_plugin_name( $basename ) {
414 $basename = strtolower( $basename );
415 if ( false === strpos( $basename, '/' ) ) {
416 return basename( $basename, '.php' );
417 }
418
419 return dirname( $basename );
420 }
421
422 /**
423 * Hide the opt-in for enabling usage tracking.
424 **/
425 protected function hide_tracking_opt_in() {
426 update_option( $this->hide_tracking_opt_in_option_name, true );
427 }
428
429 /**
430 * Determine whether the opt-in for enabling usage tracking is hidden.
431 *
432 * @return bool true if the opt-in is hidden, false otherwise.
433 **/
434 protected function is_opt_in_hidden() {
435 $delayed_notice_timestamp = (int) get_option( self::DISPLAY_ONCE_OPTION );
436
437 // The delay has passed, hide the notice if the user refused.
438 if ( 0 === $delayed_notice_timestamp ) {
439 return (bool) get_option( $this->hide_tracking_opt_in_option_name );
440 }
441
442 // When the delay passes, display the tracking notice regardless if the user refused to enable usage tracking in the past.
443 if ( $delayed_notice_timestamp < time() ) {
444 update_option( self::DISPLAY_ONCE_OPTION, 0 );
445 update_option( $this->hide_tracking_opt_in_option_name, false );
446
447 return false;
448 }
449
450 // The delay hasn't passed, hide the notice.
451 return true;
452 }
453
454 /**
455 * Allowed html tags, used by wp_kses, for the translated opt-in dialog
456 * text.
457 *
458 * @return array the html tags.
459 **/
460 protected function opt_in_dialog_text_allowed_html() {
461 return [
462 'a' => [
463 'href' => [],
464 'title' => [],
465 'target' => [],
466 ],
467 'p' => [],
468 'br' => [],
469 'em' => [],
470 'strong' => [],
471 ];
472 }
473
474 /**
475 * If needed, display opt-in dialog to enable tracking. Should not be
476 * called externally.
477 *
478 * @param array $notices Current notices.
479 *
480 * @access private
481 **/
482 public function maybe_display_tracking_opt_in( $notices ) {
483 $opt_in_hidden = $this->is_opt_in_hidden();
484 $user_tracking_enabled = $this->is_tracking_enabled();
485 $can_manage_tracking = $this->current_user_can_manage_tracking();
486
487 if ( ! $user_tracking_enabled && ! $opt_in_hidden && $can_manage_tracking ) {
488
489 $action = $this->get_prefix() . '_tracking_opt_in';
490
491 $notices['usage_tracking_opt_in'] = [
492 'level' => 'info',
493 'dismissible' => true,
494 'heading' => __( 'Improve your experience', 'wp-job-manager' ),
495 'message' => wp_kses( $this->opt_in_dialog_text(), $this->opt_in_dialog_text_allowed_html() ),
496 'actions' => [
497 [
498 'label' => __( 'Enable Usage Tracking', 'wp-job-manager' ),
499 'url' => add_query_arg(
500 [
501 'action' => $action,
502 '_wpnonce' => wp_create_nonce( $action ),
503 ],
504 admin_url( 'admin.php' )
505 ),
506 ],
507 ],
508 ];
509
510 }
511
512 return $notices;
513 }
514
515 /**
516 * Handle ajax request from the opt-in dialog. Should not be called
517 * externally.
518 *
519 * @access private
520 **/
521 public function handle_tracking_opt_in() {
522 check_admin_referer( $this->get_prefix() . '_tracking_opt_in' );
523
524 if ( ! $this->current_user_can_manage_tracking() ) {
525 wp_die( '', '', 403 );
526 }
527
528 $this->set_tracking_enabled( true );
529 $this->hide_tracking_opt_in();
530 $this->send_usage_data();
531
532 wp_safe_redirect(
533 add_query_arg(
534 [
535 'action' => false,
536 '_wpnonce' => false,
537 ],
538 admin_url( 'edit.php?post_type=job_listing' )
539 )
540 );
541
542 }
543
544 /**
545 * Disable usage tracking when the notice is dismissed.
546 *
547 * @param array $notice Notice data.
548 * @param string $notice_id Notice ID.
549 *
550 * @access private
551 */
552 public function handle_tracking_opt_out( $notice, $notice_id ) {
553
554 if ( 'usage_tracking_opt_in' !== $notice_id ) {
555 return;
556 }
557
558 $this->set_tracking_enabled( false );
559 $this->hide_tracking_opt_in();
560 }
561
562 /**
563 * Ensure that jQuery has been enqueued since the opt-in dialog JS depends
564 * on it. Should not be called externally.
565 *
566 * @deprecated since 2.0.0
567 **/
568 public function enqueue_script_deps() {
569 _deprecated_function( __METHOD__, '2.0.0' );
570 }
571
572 /**
573 * Output the JS code to handle the opt-in dialog. Should not be called
574 * externally.
575 *
576 * @deprecated since 2.0.0
577 **/
578 public function output_opt_in_js() {
579 _deprecated_function( __METHOD__, '2.0.0' );
580 ?>
581 <?php
582 }
583 }
584