| 1 |
<?php |
| 2 |
/** |
| 3 |
* Usage Tracker functionality to understand what's going on client's sites. |
| 4 |
*/ |
| 5 |
|
| 6 |
if ( ! defined( 'ABSPATH' ) ) { |
| 7 |
exit; // Exit if accessed directly. |
| 8 |
} |
| 9 |
|
| 10 |
/** |
| 11 |
* Merchant_Usage_Tracking class. |
| 12 |
*/ |
| 13 |
class Merchant_Usage_Tracking { |
| 14 |
|
| 15 |
/** |
| 16 |
* The slug that will be used to save the option of Usage Tracker. |
| 17 |
* |
| 18 |
* @since 2.2.0 |
| 19 |
*/ |
| 20 |
const SETTINGS_SLUG = 'usage-tracking-enabled'; |
| 21 |
|
| 22 |
/** |
| 23 |
* Initialize the usage tracking system. |
| 24 |
* |
| 25 |
* @since 2.2.0 |
| 26 |
*/ |
| 27 |
public static function init_system() { |
| 28 |
|
| 29 |
/** |
| 30 |
* Filter whether the Usage Tracking code is allowed to be loaded. |
| 31 |
* |
| 32 |
* @param bool $allowed Whether usage tracking is allowed. |
| 33 |
* |
| 34 |
* @since 2.2.0 |
| 35 |
*/ |
| 36 |
if ( ! apply_filters( 'merchant_usage_tracking_is_allowed', true ) ) { |
| 37 |
return; |
| 38 |
} |
| 39 |
|
| 40 |
$usage_tracking = new self(); |
| 41 |
$usage_tracking->init(); |
| 42 |
|
| 43 |
$send_task = new Merchant_Send_Usage_Task(); |
| 44 |
$send_task->init(); |
| 45 |
} |
| 46 |
|
| 47 |
/** |
| 48 |
* Attach hooks to the WordPress API |
| 49 |
* |
| 50 |
* @since 2.2.0 |
| 51 |
*/ |
| 52 |
public function init() { |
| 53 |
|
| 54 |
// Add settings option. |
| 55 |
add_action( 'merchant_module_settings', array( $this, 'add_settings_option' ) ); |
| 56 |
|
| 57 |
// Deregister the action if option is disabled. |
| 58 |
add_action( 'merchant_options_saved_global-settings', array( $this, 'maybe_cancel_task' ) ); |
| 59 |
|
| 60 |
// Schedule the task if enabled. |
| 61 |
if ( self::is_enabled() ) { |
| 62 |
$this->schedule_task(); |
| 63 |
} |
| 64 |
} |
| 65 |
|
| 66 |
/** |
| 67 |
* Whether Usage Tracking is enabled. |
| 68 |
* |
| 69 |
* @since 2.2.0 |
| 70 |
* |
| 71 |
* @return bool |
| 72 |
*/ |
| 73 |
public static function is_enabled() { |
| 74 |
|
| 75 |
/** |
| 76 |
* Filter whether the Usage Tracking is enabled. |
| 77 |
* |
| 78 |
* @param bool $enabled Whether usage tracking is enabled. |
| 79 |
* |
| 80 |
* @since 2.2.0 |
| 81 |
*/ |
| 82 |
return (bool) apply_filters( |
| 83 |
'merchant_usage_tracking_is_enabled', |
| 84 |
Merchant_Admin_Options::get( 'global-settings', self::SETTINGS_SLUG, false ) |
| 85 |
); |
| 86 |
} |
| 87 |
|
| 88 |
/** |
| 89 |
* Add "Allow Usage Tracking" to Merchant settings. |
| 90 |
* |
| 91 |
* @since 2.2.0 |
| 92 |
*/ |
| 93 |
public function add_settings_option( $settings ) { |
| 94 |
|
| 95 |
if ( |
| 96 |
! isset( $settings['module'] ) || |
| 97 |
$settings['module'] !== 'global-settings' || |
| 98 |
! isset( $settings['title'] ) || |
| 99 |
$settings['title'] !== esc_html__( 'Merchant Analytics', 'merchant' ) |
| 100 |
) { |
| 101 |
return $settings; |
| 102 |
} |
| 103 |
|
| 104 |
/** |
| 105 |
* Filter whether to show the usage tracking setting in the admin. |
| 106 |
* |
| 107 |
* @param bool $show_setting Whether to show the usage tracking setting. |
| 108 |
* |
| 109 |
* @since 2.2.0 |
| 110 |
*/ |
| 111 |
if ( ! apply_filters( 'merchant_usage_tracking_show_setting', true ) ) { |
| 112 |
return $settings; |
| 113 |
} |
| 114 |
|
| 115 |
// Ensure fields array exists. |
| 116 |
if ( ! isset( $settings['fields'] ) || ! is_array( $settings['fields'] ) ) { |
| 117 |
$settings['fields'] = array(); |
| 118 |
} |
| 119 |
|
| 120 |
$settings['fields'] = array_merge( |
| 121 |
array( |
| 122 |
array( |
| 123 |
'id' => self::SETTINGS_SLUG, |
| 124 |
'type' => 'switcher', |
| 125 |
'title' => esc_html__( 'Improve Merchant', 'merchant' ), |
| 126 |
'desc' => esc_html__( 'By allowing us to track usage data, we can better help you, as we will know which WordPress configurations, themes, and plugins we should test. No sensitive data is collected.', 'merchant' ), |
| 127 |
'default' => false, |
| 128 |
), |
| 129 |
), |
| 130 |
$settings['fields'] |
| 131 |
); |
| 132 |
|
| 133 |
return $settings; |
| 134 |
} |
| 135 |
|
| 136 |
/** |
| 137 |
* Schedule the usage tracking task. |
| 138 |
* |
| 139 |
* @since 2.2.0 |
| 140 |
*/ |
| 141 |
public function schedule_task() { |
| 142 |
|
| 143 |
if ( ! function_exists( 'as_schedule_recurring_action' ) ) { |
| 144 |
return; |
| 145 |
} |
| 146 |
|
| 147 |
// Check if already scheduled. |
| 148 |
if ( as_next_scheduled_action( 'merchant_send_usage_data', array(), 'merchant' ) ) { |
| 149 |
return; |
| 150 |
} |
| 151 |
|
| 152 |
// Schedule to run weekly. |
| 153 |
as_schedule_recurring_action( time(), WEEK_IN_SECONDS, 'merchant_send_usage_data', array(), 'merchant', true ); |
| 154 |
} |
| 155 |
|
| 156 |
/** |
| 157 |
* Cancel the usage tracking task if disabled. |
| 158 |
* |
| 159 |
* @since 2.2.0 |
| 160 |
*/ |
| 161 |
public function maybe_cancel_task() { |
| 162 |
|
| 163 |
if ( self::is_enabled() || ! function_exists( 'as_unschedule_action' ) ) { |
| 164 |
return; |
| 165 |
} |
| 166 |
|
| 167 |
as_unschedule_action( 'merchant_send_usage_data', array(), 'merchant' ); |
| 168 |
} |
| 169 |
|
| 170 |
/** |
| 171 |
* Get the User Agent string that will be sent to the API. |
| 172 |
* |
| 173 |
* @since 2.2.0 |
| 174 |
* |
| 175 |
* @return string |
| 176 |
*/ |
| 177 |
public function get_user_agent() { |
| 178 |
|
| 179 |
$version = MERCHANT_VERSION; |
| 180 |
|
| 181 |
if ( defined( 'MERCHANT_PRO_VERSION' ) ) { |
| 182 |
$version .= ' Pro/' . MERCHANT_PRO_VERSION; |
| 183 |
} |
| 184 |
|
| 185 |
return 'aThemes Merchant/' . $version . '; ' . get_bloginfo( 'url' ); |
| 186 |
} |
| 187 |
|
| 188 |
/** |
| 189 |
* Get data for sending to the server. |
| 190 |
* |
| 191 |
* @since 2.2.0 |
| 192 |
* |
| 193 |
* @return array |
| 194 |
*/ |
| 195 |
public function get_data() { |
| 196 |
|
| 197 |
global $wpdb; |
| 198 |
|
| 199 |
$theme_data = wp_get_theme(); |
| 200 |
$is_pro = defined( 'MERCHANT_PRO_VERSION' ); |
| 201 |
|
| 202 |
$data = array( |
| 203 |
// Generic data (environment) - keys without prefix. |
| 204 |
'url' => home_url(), |
| 205 |
'php_version' => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, |
| 206 |
'wp_version' => get_bloginfo( 'version' ), |
| 207 |
'mysql_version' => $wpdb->db_version(), |
| 208 |
'server_version' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '', |
| 209 |
'is_ssl' => (int) is_ssl(), |
| 210 |
'is_multisite' => (int) is_multisite(), |
| 211 |
'is_network_activated' => (int) $this->is_active_for_network(), |
| 212 |
'is_wpcom' => (int) ( defined( 'IS_WPCOM' ) && IS_WPCOM ), |
| 213 |
'is_wpcom_vip' => (int) ( ( defined( 'WPCOM_IS_VIP_ENV' ) && WPCOM_IS_VIP_ENV ) || ( function_exists( 'wpcom_is_vip' ) && wpcom_is_vip() ) ), |
| 214 |
'is_wp_cache' => (int) ( defined( 'WP_CACHE' ) && WP_CACHE ), |
| 215 |
'is_wp_rest_api_enabled' => (int) $this->is_rest_api_enabled(), |
| 216 |
'is_user_logged_in' => (int) is_user_logged_in(), |
| 217 |
'sites_count' => $this->get_sites_total(), |
| 218 |
'active_plugins' => $this->get_active_plugins(), |
| 219 |
'theme_name' => $theme_data->get( 'Name' ), |
| 220 |
'theme_version' => $theme_data->get( 'Version' ), |
| 221 |
'locale' => get_locale(), |
| 222 |
'timezone_offset' => wp_timezone_string(), |
| 223 |
// Merchant-specific data - keys with athemes_merchant_ prefix. |
| 224 |
'athemes_merchant_version' => MERCHANT_VERSION, |
| 225 |
'athemes_merchant_license_key' => $this->get_license_key(), |
| 226 |
'athemes_merchant_license_type' => $this->get_license_type(), |
| 227 |
'athemes_merchant_is_pro' => (int) $is_pro, |
| 228 |
'athemes_merchant_lite_installed_date' => $this->get_installed_date( 'lite' ), |
| 229 |
'athemes_merchant_pro_installed_date' => $is_pro ? $this->get_installed_date( 'pro' ) : '', |
| 230 |
'athemes_merchant_active_modules' => $this->get_active_modules(), |
| 231 |
'athemes_merchant_settings' => $this->get_settings(), |
| 232 |
); |
| 233 |
|
| 234 |
if ( $is_pro ) { |
| 235 |
$data['athemes_merchant_pro_version'] = MERCHANT_PRO_VERSION; |
| 236 |
} |
| 237 |
|
| 238 |
if ( $data['is_multisite'] ) { |
| 239 |
$data['url_primary'] = network_site_url(); |
| 240 |
} |
| 241 |
|
| 242 |
/** |
| 243 |
* Filter the usage tracking data. |
| 244 |
* |
| 245 |
* @param array $data Usage tracking data. |
| 246 |
* |
| 247 |
* @since 2.2.0 |
| 248 |
*/ |
| 249 |
return apply_filters( 'merchant_usage_tracking_data', $data ); |
| 250 |
} |
| 251 |
|
| 252 |
/** |
| 253 |
* Get the installed date for Merchant or Merchant Pro. |
| 254 |
* |
| 255 |
* @since 2.2.0 |
| 256 |
* |
| 257 |
* @param string $type Either 'lite' or 'pro'. |
| 258 |
* |
| 259 |
* @return int Unix timestamp of installation date. |
| 260 |
*/ |
| 261 |
private function get_installed_date( $type = 'lite' ) { |
| 262 |
|
| 263 |
$option_name = 'merchant_installed_date'; |
| 264 |
|
| 265 |
if ( $type === 'pro' ) { |
| 266 |
$option_name = 'merchant_pro_installed_date'; |
| 267 |
} |
| 268 |
|
| 269 |
$installed_date = get_option( $option_name, 0 ); |
| 270 |
|
| 271 |
// If not set, set it now. |
| 272 |
if ( empty( $installed_date ) ) { |
| 273 |
$installed_date = time(); |
| 274 |
// Update the option. |
| 275 |
update_option( $option_name, $installed_date ); |
| 276 |
} |
| 277 |
|
| 278 |
return (int) $installed_date; |
| 279 |
} |
| 280 |
|
| 281 |
/** |
| 282 |
* Get the license key (masked for security) |
| 283 |
* |
| 284 |
* @since 2.2.0 |
| 285 |
|
| 286 |
* @return string |
| 287 |
*/ |
| 288 |
private function get_license_key() { |
| 289 |
|
| 290 |
$license_key = trim( get_option( 'merchant_pro_license_key', '' ) ); |
| 291 |
|
| 292 |
if ( empty( $license_key ) ) { |
| 293 |
return ''; |
| 294 |
} |
| 295 |
|
| 296 |
// Mask the license key for security (show only last 4 characters). |
| 297 |
return sanitize_text_field( $license_key ); |
| 298 |
} |
| 299 |
|
| 300 |
/** |
| 301 |
* Get the license type. |
| 302 |
* |
| 303 |
* @since 2.2.0 |
| 304 |
* |
| 305 |
* @return string |
| 306 |
*/ |
| 307 |
private function get_license_type() { |
| 308 |
|
| 309 |
if ( ! defined( 'MERCHANT_PRO_VERSION' ) ) { |
| 310 |
return 'lite'; |
| 311 |
} |
| 312 |
|
| 313 |
// Get the license item name from option (for future plan names like "agency", "lifetime", etc.) |
| 314 |
$license_item_name = get_option( 'merchant_pro_license_item_name', '' ); |
| 315 |
|
| 316 |
// If option is empty or default, return 'pro', otherwise return the plan name |
| 317 |
if ( empty( $license_item_name ) || $license_item_name === 'Merchant Pro' ) { |
| 318 |
return 'pro'; |
| 319 |
} |
| 320 |
|
| 321 |
return sanitize_text_field( strtolower( $license_item_name ) ); |
| 322 |
} |
| 323 |
|
| 324 |
/** |
| 325 |
* Get all active modules. |
| 326 |
* |
| 327 |
* @since 2.2.0 |
| 328 |
* |
| 329 |
* @return array |
| 330 |
*/ |
| 331 |
private function get_active_modules() { |
| 332 |
|
| 333 |
if ( ! function_exists( 'merchant_get_active_modules' ) ) { |
| 334 |
return array(); |
| 335 |
} |
| 336 |
|
| 337 |
return merchant_get_active_modules(); |
| 338 |
} |
| 339 |
|
| 340 |
/** |
| 341 |
* Get all settings, except those with sensitive data. |
| 342 |
* |
| 343 |
* @since 2.2.0 |
| 344 |
* |
| 345 |
* @return array |
| 346 |
*/ |
| 347 |
private function get_settings() { |
| 348 |
|
| 349 |
// Get global settings (excluding sensitive data). |
| 350 |
$global_settings = Merchant_Admin_Options::get_all( 'global-settings' ); |
| 351 |
|
| 352 |
if ( empty( $global_settings ) || ! is_array( $global_settings ) ) { |
| 353 |
return array(); |
| 354 |
} |
| 355 |
|
| 356 |
return $global_settings; |
| 357 |
} |
| 358 |
|
| 359 |
/** |
| 360 |
* Get the list of active plugins. |
| 361 |
* |
| 362 |
* @since 2.2.0 |
| 363 |
* |
| 364 |
* @return array |
| 365 |
*/ |
| 366 |
private function get_active_plugins() { |
| 367 |
|
| 368 |
if ( ! function_exists( 'get_plugins' ) ) { |
| 369 |
include ABSPATH . '/wp-admin/includes/plugin.php'; |
| 370 |
} |
| 371 |
|
| 372 |
$active = is_multisite() ? |
| 373 |
array_merge( get_option( 'active_plugins', array() ), array_flip( get_site_option( 'active_sitewide_plugins', array() ) ) ) : |
| 374 |
get_option( 'active_plugins', array() ); |
| 375 |
|
| 376 |
$plugins = array_intersect_key( get_plugins(), array_flip( $active ) ); |
| 377 |
|
| 378 |
return array_map( |
| 379 |
static function ( $plugin ) { |
| 380 |
if ( isset( $plugin['Version'] ) ) { |
| 381 |
return $plugin['Version']; |
| 382 |
} |
| 383 |
|
| 384 |
return 'Not Set'; |
| 385 |
}, |
| 386 |
$plugins |
| 387 |
); |
| 388 |
} |
| 389 |
|
| 390 |
/** |
| 391 |
* Test if the REST API is accessible. |
| 392 |
* |
| 393 |
* The REST API might be inaccessible due to various security measures, |
| 394 |
* or it might be completely disabled by a plugin. |
| 395 |
* |
| 396 |
* @since 2.2.0 |
| 397 |
* |
| 398 |
* @return bool |
| 399 |
*/ |
| 400 |
private function is_rest_api_enabled() { |
| 401 |
|
| 402 |
$url = rest_url( 'wp/v2/types/post' ); |
| 403 |
$response = wp_remote_get( |
| 404 |
$url, |
| 405 |
array( |
| 406 |
'timeout' => 10, |
| 407 |
'cookies' => is_user_logged_in() ? wp_unslash( $_COOKIE ) : array(), |
| 408 |
'headers' => array( |
| 409 |
'Cache-Control' => 'no-cache', |
| 410 |
'X-WP-Nonce' => wp_create_nonce( 'wp_rest' ), |
| 411 |
), |
| 412 |
) |
| 413 |
); |
| 414 |
|
| 415 |
// When testing the REST API, an error was encountered, leave early. |
| 416 |
if ( is_wp_error( $response ) ) { |
| 417 |
return false; |
| 418 |
} |
| 419 |
|
| 420 |
// When testing the REST API, an unexpected result was returned, leave early. |
| 421 |
if ( wp_remote_retrieve_response_code( $response ) !== 200 ) { |
| 422 |
return false; |
| 423 |
} |
| 424 |
|
| 425 |
// The REST API did not behave correctly, leave early. |
| 426 |
$body = wp_remote_retrieve_body( $response ); |
| 427 |
|
| 428 |
if ( ! $this->is_json( $body ) ) { |
| 429 |
return false; |
| 430 |
} |
| 431 |
|
| 432 |
// We are all set. Confirm the connection. |
| 433 |
return true; |
| 434 |
} |
| 435 |
|
| 436 |
/** |
| 437 |
* Check if a string is valid JSON. |
| 438 |
* |
| 439 |
* @since 2.2.0 |
| 440 |
* |
| 441 |
* @param string $value The string to check. |
| 442 |
* |
| 443 |
* @return bool |
| 444 |
*/ |
| 445 |
private function is_json( $value ) { |
| 446 |
|
| 447 |
if ( ! is_string( $value ) ) { |
| 448 |
return false; |
| 449 |
} |
| 450 |
|
| 451 |
json_decode( $value, true ); |
| 452 |
|
| 453 |
return json_last_error() === JSON_ERROR_NONE; |
| 454 |
} |
| 455 |
|
| 456 |
/** |
| 457 |
* Determines whether the plugin is active for the entire network. |
| 458 |
* |
| 459 |
* @since 2.2.0 |
| 460 |
* |
| 461 |
* @return bool |
| 462 |
*/ |
| 463 |
private function is_active_for_network() { |
| 464 |
|
| 465 |
// Bail early, in case we are not in multisite. |
| 466 |
if ( ! is_multisite() ) { |
| 467 |
return false; |
| 468 |
} |
| 469 |
|
| 470 |
// Get all active plugins. |
| 471 |
$plugins = get_site_option( 'active_sitewide_plugins' ); |
| 472 |
|
| 473 |
// Bail early, in case the plugin is active for the entire network. |
| 474 |
if ( isset( $plugins[ plugin_basename( MERCHANT_FILE ) ] ) ) { |
| 475 |
return true; |
| 476 |
} |
| 477 |
|
| 478 |
return false; |
| 479 |
} |
| 480 |
|
| 481 |
/** |
| 482 |
* Total number of sites. |
| 483 |
* |
| 484 |
* @since 2.2.0 |
| 485 |
* |
| 486 |
* @return int |
| 487 |
*/ |
| 488 |
private function get_sites_total() { |
| 489 |
return function_exists( 'get_blog_count' ) ? (int) get_blog_count() : 1; |
| 490 |
} |
| 491 |
} |
| 492 |
|
| 493 |
// Initialize usage tracking. |
| 494 |
add_action( 'init', array( 'Merchant_Usage_Tracking', 'init_system' ) ); |
| 495 |
|