| 1 |
<?php |
| 2 |
/** |
| 3 |
* Usage_Tracker — anonymous, opt-in plugin usage analytics. |
| 4 |
* |
| 5 |
* Ported from the WP Insights SDK (the same engine WPDeveloper plugins such as |
| 6 |
* EmbedPress ship). Trimmed for xSpeed: no email marketing capture by default. |
| 7 |
* The deactivation "goodbye" survey UI lives in Deactivation_Feedback (shown to |
| 8 |
* every admin); it stores the reason in the canonical WPInsight options and |
| 9 |
* deactivate_this_plugin() transmits it — see that method. |
| 10 |
* |
| 11 |
* PRIVACY CONTRACT (see CLAUDE.md "Hard do-not" + readme.txt): |
| 12 |
* Nothing is collected or sent until the site admin EXPLICITLY opts in via |
| 13 |
* the setup wizard. `require_optin` is always true. Until `opt_in( true )` |
| 14 |
* has run, `is_tracking_allowed()` is false, no cron is scheduled, and |
| 15 |
* `do_tracking()` / `send_data()` short-circuit before any outbound HTTP. |
| 16 |
* |
| 17 |
* @package XSpeed |
| 18 |
* @version 3.0.2 (WP Insights) |
| 19 |
*/ |
| 20 |
|
| 21 |
namespace XSpeed; |
| 22 |
|
| 23 |
defined( 'ABSPATH' ) || exit; |
| 24 |
|
| 25 |
use WP_Error; |
| 26 |
|
| 27 |
if ( ! class_exists( __NAMESPACE__ . '\\Usage_Tracker' ) ) : |
| 28 |
|
| 29 |
class Usage_Tracker { |
| 30 |
|
| 31 |
/** WP Insights SDK version (kept for API compat with send.wpinsight.com). */ |
| 32 |
const WPINS_VERSION = '3.0.2'; |
| 33 |
|
| 34 |
/** Insights ingest endpoint. */ |
| 35 |
const API_URL = 'https://send.wpinsight.com/process-plugin-data'; |
| 36 |
|
| 37 |
/** Daily cron hook (only registered AFTER opt-in). */ |
| 38 |
const EVENT_HOOK = 'xspeed_do_weekly_action'; |
| 39 |
|
| 40 |
private $plugin_file = null; |
| 41 |
private $plugin_name = null; |
| 42 |
|
| 43 |
/** @var string */ |
| 44 |
public $recurrence = 'daily'; |
| 45 |
|
| 46 |
private $disabled_wp_cron; |
| 47 |
private $require_optin; |
| 48 |
private $marketing; |
| 49 |
private $item_id; |
| 50 |
|
| 51 |
/** @var Usage_Tracker|null */ |
| 52 |
private static $instance = null; |
| 53 |
|
| 54 |
/** |
| 55 |
* @param string $plugin_file Main plugin file (XSPEED_FILE). |
| 56 |
* @param array $args opt_in, email_marketing, item_id. |
| 57 |
*/ |
| 58 |
public static function get_instance( $plugin_file, $args = array() ) { |
| 59 |
if ( null === static::$instance ) { |
| 60 |
static::$instance = new static( $plugin_file, $args ); |
| 61 |
} |
| 62 |
return static::$instance; |
| 63 |
} |
| 64 |
|
| 65 |
public function __construct( $plugin_file, $args = array() ) { |
| 66 |
$this->plugin_file = $plugin_file; |
| 67 |
$this->plugin_name = basename( $this->plugin_file, '.php' ); |
| 68 |
$this->disabled_wp_cron = defined( 'DISABLE_WP_CRON' ) && true === DISABLE_WP_CRON; |
| 69 |
|
| 70 |
// require_optin is intentionally forced true — never honor a caller |
| 71 |
// that tries to disable consent gating. |
| 72 |
$this->require_optin = true; |
| 73 |
// Email marketing capture is OFF by default in xSpeed (EmbedPress |
| 74 |
// defaults it on to send a discount coupon; we collect no email |
| 75 |
// unless a caller explicitly turns it on). |
| 76 |
$this->marketing = isset( $args['email_marketing'] ) ? (bool) $args['email_marketing'] : false; |
| 77 |
$this->item_id = ! empty( $args['item_id'] ) ? $args['item_id'] : false; |
| 78 |
|
| 79 |
register_deactivation_hook( $this->plugin_file, array( $this, 'deactivate_this_plugin' ) ); |
| 80 |
} |
| 81 |
|
| 82 |
/** |
| 83 |
* Hook the cron sender. Called once from Plugin::init(). Safe to call |
| 84 |
* unconditionally: the cron event itself is only SCHEDULED after the |
| 85 |
* user opts in, and do_tracking() re-checks consent before sending. |
| 86 |
*/ |
| 87 |
public function init() { |
| 88 |
add_action( self::EVENT_HOOK, array( $this, 'do_tracking' ) ); |
| 89 |
} |
| 90 |
|
| 91 |
/** |
| 92 |
* Public opt-in / opt-out entry point. Called by the onboarding REST |
| 93 |
* handler when the admin flips the wizard's consent toggle. |
| 94 |
* |
| 95 |
* @param bool $allow True = consent granted; false = revoked. |
| 96 |
*/ |
| 97 |
public function opt_in( $allow ) { |
| 98 |
$this->set_is_tracking_allowed( (bool) $allow ); |
| 99 |
if ( $allow ) { |
| 100 |
$this->schedule_tracking(); |
| 101 |
// Fire the first send immediately so the install is registered. |
| 102 |
$this->do_tracking( true ); |
| 103 |
} else { |
| 104 |
if ( ! $this->disabled_wp_cron ) { |
| 105 |
wp_clear_scheduled_hook( self::EVENT_HOOK ); |
| 106 |
} |
| 107 |
} |
| 108 |
} |
| 109 |
|
| 110 |
/** True only after an explicit opt-in. */ |
| 111 |
public function is_opted_in() { |
| 112 |
return $this->is_tracking_allowed(); |
| 113 |
} |
| 114 |
|
| 115 |
/** |
| 116 |
* Schedule the daily send. Only ever called from opt_in( true ). |
| 117 |
*/ |
| 118 |
public function schedule_tracking() { |
| 119 |
if ( $this->disabled_wp_cron ) { |
| 120 |
return; |
| 121 |
} |
| 122 |
if ( ! wp_next_scheduled( self::EVENT_HOOK ) ) { |
| 123 |
wp_schedule_event( time(), $this->recurrence, self::EVENT_HOOK ); |
| 124 |
} |
| 125 |
} |
| 126 |
|
| 127 |
/** |
| 128 |
* On deactivation: report to WPInsight that we went inactive, carrying |
| 129 |
* the deactivation reason the admin submitted on the Plugins screen (if |
| 130 |
* any). Deactivation_Feedback stores that reason in the canonical |
| 131 |
* `wpins_deactivation_reason_<slug>` / `wpins_deactivation_details_<slug>` |
| 132 |
* options; we read + transmit + delete them here. |
| 133 |
* |
| 134 |
* Two send paths: |
| 135 |
* - Usage analytics ON → the full, site-correlated body (get_data()) |
| 136 |
* with the reason appended, via the normal send_data() handshake. |
| 137 |
* This is the canonical WPInsight deactivation record. |
| 138 |
* - Usage analytics OFF → nothing is sent UNLESS the admin explicitly |
| 139 |
* submitted the survey; in that case a minimal, reason-only payload |
| 140 |
* goes out as per-action consent (no diagnostics inventory). |
| 141 |
*/ |
| 142 |
public function deactivate_this_plugin() { |
| 143 |
$reason_key = 'wpins_deactivation_reason_' . $this->plugin_name; |
| 144 |
$details_key = 'wpins_deactivation_details_' . $this->plugin_name; |
| 145 |
$reason = get_option( $reason_key, false ); |
| 146 |
$details = get_option( $details_key, false ); |
| 147 |
|
| 148 |
if ( $this->is_tracking_allowed() ) { |
| 149 |
$body = $this->get_data(); |
| 150 |
$body['status'] = 'Deactivated'; |
| 151 |
$body['deactivated_date'] = time(); |
| 152 |
if ( false !== $reason ) { |
| 153 |
$body['deactivation_reason'] = $reason; |
| 154 |
} |
| 155 |
if ( false !== $details ) { |
| 156 |
$body['deactivation_details'] = $details; |
| 157 |
} |
| 158 |
$this->send_data( $body ); |
| 159 |
|
| 160 |
if ( ! $this->disabled_wp_cron ) { |
| 161 |
wp_clear_scheduled_hook( self::EVENT_HOOK ); |
| 162 |
} |
| 163 |
} elseif ( false !== $reason || false !== $details ) { |
| 164 |
$this->send_deactivation_feedback( $reason, $details ); |
| 165 |
} |
| 166 |
|
| 167 |
// Never let a stored reason linger or double-send on the next cycle. |
| 168 |
delete_option( $reason_key ); |
| 169 |
delete_option( $details_key ); |
| 170 |
} |
| 171 |
|
| 172 |
/** |
| 173 |
* Minimal, reason-only deactivation report for when usage analytics is |
| 174 |
* OFF but the admin submitted the deactivation survey. Sends only plugin |
| 175 |
* identity, WP/PHP version, and the reason/details — never the full |
| 176 |
* diagnostic body get_data() assembles (no plugin inventory, no theme, |
| 177 |
* no xSpeed config). Per-action consent; see the privacy contract at the |
| 178 |
* top of this file and readme.txt "External services". |
| 179 |
* |
| 180 |
* @param string|false $reason Stored deactivation reason label, or false. |
| 181 |
* @param string|false $details Stored free-text detail, or false. |
| 182 |
*/ |
| 183 |
private function send_deactivation_feedback( $reason, $details ) { |
| 184 |
if ( empty( self::API_URL ) ) { |
| 185 |
return; |
| 186 |
} |
| 187 |
$plugin = $this->plugin_data(); |
| 188 |
$body = array( |
| 189 |
'plugin_slug' => sanitize_text_field( $this->plugin_name ), |
| 190 |
'url' => get_bloginfo( 'url' ), |
| 191 |
'status' => 'Deactivated', |
| 192 |
'deactivated_date' => time(), |
| 193 |
'site_version' => get_bloginfo( 'version' ), |
| 194 |
'php_version' => phpversion(), |
| 195 |
'wpins_version' => self::WPINS_VERSION, |
| 196 |
); |
| 197 |
if ( ! empty( $plugin['Name'] ) ) { |
| 198 |
$body['plugin'] = sanitize_text_field( $plugin['Name'] ); |
| 199 |
} |
| 200 |
if ( ! empty( $plugin['Version'] ) ) { |
| 201 |
$body['version'] = sanitize_text_field( $plugin['Version'] ); |
| 202 |
} |
| 203 |
if ( false !== $this->item_id ) { |
| 204 |
$body['item_id'] = $this->item_id; |
| 205 |
} |
| 206 |
if ( false !== $reason ) { |
| 207 |
$body['deactivation_reason'] = sanitize_text_field( $reason ); |
| 208 |
} |
| 209 |
if ( false !== $details ) { |
| 210 |
$body['deactivation_details'] = sanitize_text_field( $details ); |
| 211 |
} |
| 212 |
|
| 213 |
$this->remote_post( $body ); |
| 214 |
} |
| 215 |
|
| 216 |
/** |
| 217 |
* Cron callback. Bails before any HTTP unless tracking is allowed and |
| 218 |
* it's time to send. |
| 219 |
* |
| 220 |
* @param bool $force Skip the once-a-day throttle (used on first opt-in). |
| 221 |
*/ |
| 222 |
public function do_tracking( $force = false ) { |
| 223 |
if ( empty( self::API_URL ) ) { |
| 224 |
return; |
| 225 |
} |
| 226 |
if ( ! $this->is_tracking_allowed() ) { |
| 227 |
return; |
| 228 |
} |
| 229 |
if ( ! $this->is_time_to_track() && ! $force ) { |
| 230 |
return; |
| 231 |
} |
| 232 |
return $this->send_data( $this->get_data() ); |
| 233 |
} |
| 234 |
|
| 235 |
/** Consent gate. */ |
| 236 |
private function is_tracking_allowed() { |
| 237 |
$allow_tracking = get_option( 'wpins_allow_tracking' ); |
| 238 |
return is_array( $allow_tracking ) && isset( $allow_tracking[ $this->plugin_name ] ); |
| 239 |
} |
| 240 |
|
| 241 |
/** Persist the consent flag in the shared WP Insights option. */ |
| 242 |
protected function set_is_tracking_allowed( $is_allowed ) { |
| 243 |
$allow_tracking = get_option( 'wpins_allow_tracking' ); |
| 244 |
if ( ! is_array( $allow_tracking ) ) { |
| 245 |
$allow_tracking = array(); |
| 246 |
} |
| 247 |
if ( $is_allowed ) { |
| 248 |
$allow_tracking[ $this->plugin_name ] = $this->plugin_name; |
| 249 |
} else { |
| 250 |
unset( $allow_tracking[ $this->plugin_name ] ); |
| 251 |
} |
| 252 |
update_option( 'wpins_allow_tracking', $allow_tracking ); |
| 253 |
} |
| 254 |
|
| 255 |
/** Once-a-day throttle. */ |
| 256 |
public function is_time_to_track() { |
| 257 |
$track_times = get_option( 'wpins_last_track_time', array() ); |
| 258 |
if ( ! isset( $track_times[ $this->plugin_name ] ) ) { |
| 259 |
return true; |
| 260 |
} |
| 261 |
return $track_times[ $this->plugin_name ] < strtotime( '-1 day' ); |
| 262 |
} |
| 263 |
|
| 264 |
public function set_track_time() { |
| 265 |
$track_times = get_option( 'wpins_last_track_time', array() ); |
| 266 |
$track_times[ $this->plugin_name ] = time(); |
| 267 |
update_option( 'wpins_last_track_time', $track_times ); |
| 268 |
} |
| 269 |
|
| 270 |
/** |
| 271 |
* Assemble the non-sensitive diagnostic payload. Documented verbatim in |
| 272 |
* readme.txt — keep the two in sync if you add a field here. |
| 273 |
*/ |
| 274 |
public function get_data() { |
| 275 |
$body = array( |
| 276 |
'plugin_slug' => sanitize_text_field( $this->plugin_name ), |
| 277 |
'url' => get_bloginfo( 'url' ), |
| 278 |
'site_name' => get_bloginfo( 'name' ), |
| 279 |
'site_version' => get_bloginfo( 'version' ), |
| 280 |
'site_language' => get_bloginfo( 'language' ), |
| 281 |
'charset' => get_bloginfo( 'charset' ), |
| 282 |
'wpins_version' => self::WPINS_VERSION, |
| 283 |
'php_version' => phpversion(), |
| 284 |
'multisite' => is_multisite(), |
| 285 |
); |
| 286 |
|
| 287 |
if ( $this->marketing ) { |
| 288 |
if ( ! function_exists( 'wp_get_current_user' ) ) { |
| 289 |
include ABSPATH . 'wp-includes/pluggable.php'; |
| 290 |
} |
| 291 |
$email = wp_get_current_user()->user_email; |
| 292 |
if ( is_email( $email ) ) { |
| 293 |
$body['email'] = $email; |
| 294 |
} |
| 295 |
} |
| 296 |
$body['marketing_method'] = $this->marketing; |
| 297 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- server software string, reported as-is to insights. |
| 298 |
$body['server'] = isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : ''; |
| 299 |
|
| 300 |
if ( ! function_exists( 'get_plugins' ) ) { |
| 301 |
include ABSPATH . 'wp-admin/includes/plugin.php'; |
| 302 |
} |
| 303 |
$plugins = array_keys( get_plugins() ); |
| 304 |
$active_plugins = is_network_admin() |
| 305 |
? array_keys( get_site_option( 'active_sitewide_plugins', array() ) ) |
| 306 |
: get_option( 'active_plugins', array() ); |
| 307 |
foreach ( $plugins as $key => $plugin ) { |
| 308 |
if ( in_array( $plugin, $active_plugins, true ) ) { |
| 309 |
unset( $plugins[ $key ] ); |
| 310 |
} |
| 311 |
} |
| 312 |
$body['active_plugins'] = $active_plugins; |
| 313 |
$body['inactive_plugins'] = array_values( $plugins ); |
| 314 |
$body['text_direction'] = is_rtl() ? 'RTL' : 'LTR'; |
| 315 |
|
| 316 |
$plugin = $this->plugin_data(); |
| 317 |
if ( ! empty( $plugin ) ) { |
| 318 |
if ( isset( $plugin['Name'] ) ) { |
| 319 |
$body['plugin'] = sanitize_text_field( $plugin['Name'] ); |
| 320 |
} |
| 321 |
if ( isset( $plugin['Version'] ) ) { |
| 322 |
$body['version'] = sanitize_text_field( $plugin['Version'] ); |
| 323 |
} |
| 324 |
$body['status'] = 'Active'; |
| 325 |
} else { |
| 326 |
$body['status'] = 'NOT FOUND'; |
| 327 |
} |
| 328 |
|
| 329 |
$theme = wp_get_theme(); |
| 330 |
if ( $theme->get( 'Name' ) ) { |
| 331 |
$body['theme'] = sanitize_text_field( $theme->get( 'Name' ) ); |
| 332 |
} |
| 333 |
if ( $theme->get( 'Version' ) ) { |
| 334 |
$body['theme_version'] = sanitize_text_field( $theme->get( 'Version' ) ); |
| 335 |
} |
| 336 |
|
| 337 |
// xSpeed's own configuration — which optimization features are |
| 338 |
// enabled and their settings. Tells us what's actually used and |
| 339 |
// where it breaks. Non-sensitive: these are feature flags + |
| 340 |
// numeric/string knobs, never site content or personal data. |
| 341 |
$config = $this->gather_config(); |
| 342 |
if ( ! empty( $config ) ) { |
| 343 |
$body['xspeed_config'] = $config; |
| 344 |
} |
| 345 |
|
| 346 |
return $body; |
| 347 |
} |
| 348 |
|
| 349 |
/** |
| 350 |
* Collect each registered module's stored settings, keyed by slug. |
| 351 |
* Read through Settings_Manager so we get validated, schema-shaped |
| 352 |
* values (feature toggles + knobs), not raw option blobs. Guarded so |
| 353 |
* the tracker still works if the registry isn't booted yet. |
| 354 |
* |
| 355 |
* @return array<string,array> |
| 356 |
*/ |
| 357 |
private function gather_config() { |
| 358 |
if ( ! class_exists( __NAMESPACE__ . '\\Module_Registry' ) |
| 359 |
|| ! class_exists( __NAMESPACE__ . '\\Settings_Manager' ) ) { |
| 360 |
return array(); |
| 361 |
} |
| 362 |
$config = array(); |
| 363 |
foreach ( Module_Registry::all() as $slug => $module ) { |
| 364 |
$scalars = $this->scalar_settings( Settings_Manager::get( (string) $slug ) ); |
| 365 |
if ( ! empty( $scalars ) ) { |
| 366 |
$config[ (string) $slug ] = $scalars; |
| 367 |
} |
| 368 |
} |
| 369 |
// Legacy fields still in xspeed_options (e.g. cache_enabled). |
| 370 |
if ( class_exists( __NAMESPACE__ . '\\Settings' ) ) { |
| 371 |
$legacy = $this->scalar_settings( Settings::get() ); |
| 372 |
if ( ! empty( $legacy ) ) { |
| 373 |
$config['_options'] = $legacy; |
| 374 |
} |
| 375 |
} |
| 376 |
return $config; |
| 377 |
} |
| 378 |
|
| 379 |
/** |
| 380 |
* Key fragments that mark a credential / PII / identifying field. We do |
| 381 |
* NOT report these at all — not the value, not even whether they're set. |
| 382 |
* The goal is "which features are used", not "is a key configured", so |
| 383 |
* anything secret-shaped is dropped outright. This is the guard that |
| 384 |
* keeps API keys, tokens, passwords, license keys, emails, URLs, and |
| 385 |
* brand assets out of the analytics payload entirely. |
| 386 |
*/ |
| 387 |
const SECRET_KEY_FRAGMENTS = array( |
| 388 |
'key', 'token', 'secret', 'password', 'pass', 'license', 'auth', |
| 389 |
'credential', 'email', 'url', 'endpoint', 'host', 'logo', 'prefix', |
| 390 |
'zone', 'account', 'webhook', 'salt', 'nonce', 'name', 'credit', |
| 391 |
); |
| 392 |
|
| 393 |
/** |
| 394 |
* Reduce a module's settings to just "which features are used + how |
| 395 |
* they're tuned": |
| 396 |
* |
| 397 |
* - bool / int / float on a NON-sensitive key → sent as-is. These are |
| 398 |
* the feature toggles and numeric knobs we actually want. |
| 399 |
* - any key matching SECRET_KEY_FRAGMENTS → dropped entirely. |
| 400 |
* - string values → dropped (free-text can hold secrets/PII, and a |
| 401 |
* string isn't "feature usage" data anyway). |
| 402 |
* - arrays (exclusion / cookie / query lists) → dropped. |
| 403 |
* |
| 404 |
* Net result: a compact map of feature flags + numeric settings, with |
| 405 |
* zero credentials, URLs, names, or other identifying values. |
| 406 |
* |
| 407 |
* @param mixed $settings |
| 408 |
* @return array |
| 409 |
*/ |
| 410 |
private function scalar_settings( $settings ) { |
| 411 |
if ( ! is_array( $settings ) ) { |
| 412 |
return array(); |
| 413 |
} |
| 414 |
$out = array(); |
| 415 |
foreach ( $settings as $key => $value ) { |
| 416 |
// Only booleans and numbers describe "feature usage"; strings |
| 417 |
// and arrays are never feature-usage data, so skip them. |
| 418 |
if ( ! is_bool( $value ) && ! is_int( $value ) && ! is_float( $value ) ) { |
| 419 |
continue; |
| 420 |
} |
| 421 |
$lc = strtolower( (string) $key ); |
| 422 |
$is_secret = false; |
| 423 |
foreach ( self::SECRET_KEY_FRAGMENTS as $frag ) { |
| 424 |
if ( false !== strpos( $lc, $frag ) ) { |
| 425 |
$is_secret = true; |
| 426 |
break; |
| 427 |
} |
| 428 |
} |
| 429 |
if ( $is_secret ) { |
| 430 |
continue; // e.g. a numeric account id — drop it. |
| 431 |
} |
| 432 |
$out[ $key ] = $value; |
| 433 |
} |
| 434 |
return $out; |
| 435 |
} |
| 436 |
|
| 437 |
public function plugin_data() { |
| 438 |
if ( ! function_exists( 'get_plugin_data' ) ) { |
| 439 |
include ABSPATH . 'wp-admin/includes/plugin.php'; |
| 440 |
} |
| 441 |
return get_plugin_data( $this->plugin_file ); |
| 442 |
} |
| 443 |
|
| 444 |
/** |
| 445 |
* Register the site with insights, then send diffs on subsequent runs. |
| 446 |
* Mirrors the WP Insights site-id handshake so the server keeps a stable |
| 447 |
* record per install. |
| 448 |
*/ |
| 449 |
public function send_data( $body ) { |
| 450 |
$site_id_key = "wpins_{$this->plugin_name}_site_id"; |
| 451 |
$site_id = get_option( $site_id_key, false ); |
| 452 |
$site_url = get_bloginfo( 'url' ); |
| 453 |
$original_site_url = get_option( "wpins_{$this->plugin_name}_original_url", false ); |
| 454 |
$diff_data = array(); |
| 455 |
$failed_data = array(); |
| 456 |
|
| 457 |
if ( ( false === $original_site_url || $original_site_url !== $site_url ) |
| 458 |
&& version_compare( $body['wpins_version'], '3.0.1', '>=' ) ) { |
| 459 |
$site_id = false; |
| 460 |
} |
| 461 |
|
| 462 |
if ( false === $site_id && false !== $this->item_id ) { |
| 463 |
$body['plugin_slug'] = $this->plugin_name; |
| 464 |
$body['url'] = $site_url; |
| 465 |
$body['item_id'] = $this->item_id; |
| 466 |
|
| 467 |
$request = $this->remote_post( $body ); |
| 468 |
if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) { |
| 469 |
$retrieved_body = json_decode( wp_remote_retrieve_body( $request ), true ); |
| 470 |
if ( is_array( $retrieved_body ) && isset( $retrieved_body['siteId'] ) ) { |
| 471 |
$site_id = $retrieved_body['siteId']; |
| 472 |
update_option( $site_id_key, $site_id ); |
| 473 |
update_option( "wpins_{$this->plugin_name}_original_url", $site_url ); |
| 474 |
update_option( "wpins_{$this->plugin_name}_{$site_id}", $body ); |
| 475 |
} |
| 476 |
} else { |
| 477 |
$failed_data = $body; |
| 478 |
} |
| 479 |
} |
| 480 |
|
| 481 |
$site_id_data_key = "wpins_{$this->plugin_name}_{$site_id}"; |
| 482 |
$site_id_data_failed_key = "wpins_{$this->plugin_name}_{$site_id}_send_failed"; |
| 483 |
|
| 484 |
if ( false !== $site_id ) { |
| 485 |
$old_sent_data = get_option( $site_id_data_key, array() ); |
| 486 |
$diff_data = $this->diff( $body, $old_sent_data ); |
| 487 |
$failed_data = get_option( $site_id_data_failed_key, array() ); |
| 488 |
if ( ! empty( $failed_data ) && $diff_data !== $failed_data ) { |
| 489 |
$failed_data = array_merge( $failed_data, $diff_data ); |
| 490 |
} |
| 491 |
} |
| 492 |
|
| 493 |
if ( ! empty( $failed_data ) && false !== $site_id ) { |
| 494 |
$failed_data['plugin_slug'] = $this->plugin_name; |
| 495 |
$failed_data['url'] = $site_url; |
| 496 |
$failed_data['site_id'] = $site_id; |
| 497 |
if ( false !== $original_site_url ) { |
| 498 |
$failed_data['original_url'] = $original_site_url; |
| 499 |
} |
| 500 |
$request = $this->remote_post( $failed_data ); |
| 501 |
if ( ! is_wp_error( $request ) ) { |
| 502 |
delete_option( $site_id_data_failed_key ); |
| 503 |
update_option( $site_id_data_key, array_merge( get_option( $site_id_data_key, array() ), $failed_data ) ); |
| 504 |
} |
| 505 |
} |
| 506 |
|
| 507 |
if ( ! empty( $diff_data ) && false !== $site_id && empty( $failed_data ) ) { |
| 508 |
$diff_data['plugin_slug'] = $this->plugin_name; |
| 509 |
$diff_data['url'] = $site_url; |
| 510 |
$diff_data['site_id'] = $site_id; |
| 511 |
if ( false !== $original_site_url ) { |
| 512 |
$diff_data['original_url'] = $original_site_url; |
| 513 |
} |
| 514 |
$request = $this->remote_post( $diff_data ); |
| 515 |
if ( is_wp_error( $request ) ) { |
| 516 |
update_option( $site_id_data_failed_key, $diff_data ); |
| 517 |
} else { |
| 518 |
update_option( $site_id_data_key, array_merge( get_option( $site_id_data_key, array() ), $diff_data ) ); |
| 519 |
} |
| 520 |
} |
| 521 |
|
| 522 |
$this->set_track_time(); |
| 523 |
|
| 524 |
if ( isset( $request ) && is_wp_error( $request ) ) { |
| 525 |
return $request; |
| 526 |
} |
| 527 |
return isset( $request ); |
| 528 |
} |
| 529 |
|
| 530 |
protected function remote_post( $data = array(), $args = array() ) { |
| 531 |
if ( empty( $data ) ) { |
| 532 |
return; |
| 533 |
} |
| 534 |
$args = wp_parse_args( |
| 535 |
$args, |
| 536 |
array( |
| 537 |
'method' => 'POST', |
| 538 |
'timeout' => 30, |
| 539 |
'redirection' => 5, |
| 540 |
'httpversion' => '1.1', |
| 541 |
'blocking' => true, |
| 542 |
'body' => $data, |
| 543 |
'user-agent' => 'XSpeed/' . ( defined( 'XSPEED_VERSION' ) ? XSPEED_VERSION : '1.0' ) . '; ' . get_bloginfo( 'url' ), |
| 544 |
) |
| 545 |
); |
| 546 |
$request = wp_remote_post( esc_url_raw( self::API_URL ), $args ); |
| 547 |
if ( is_wp_error( $request ) |
| 548 |
|| ( isset( $request['response']['code'] ) && 200 !== $request['response']['code'] ) ) { |
| 549 |
return new WP_Error( 500, 'Something went wrong.' ); |
| 550 |
} |
| 551 |
return $request; |
| 552 |
} |
| 553 |
|
| 554 |
protected function diff( $new_data, $old_data ) { |
| 555 |
$data = array(); |
| 556 |
foreach ( (array) $new_data as $key => $value ) { |
| 557 |
if ( isset( $old_data[ $key ] ) && $old_data[ $key ] === $value ) { |
| 558 |
continue; |
| 559 |
} |
| 560 |
$data[ $key ] = $value; |
| 561 |
} |
| 562 |
return $data; |
| 563 |
} |
| 564 |
} |
| 565 |
|
| 566 |
endif; |
| 567 |
|