| @@ -87,9 +87,27 @@ | ||
| 87 | 87 | $this->plugin_name = basename($this->plugin_file, '.php'); |
| 88 | 88 | $this->disabled_wp_cron = defined('DISABLE_WP_CRON') && DISABLE_WP_CRON == true; |
| 89 | 89 | $this->enable_self_cron = $this->disabled_wp_cron == true ? true : false; |
| 90 | 90 | |
| 91 | - $this->event_hook = 'put_do_weekly_action'; | |
| 91 | + /** | |
| 92 | + * The cron hook MUST be scoped to this plugin. | |
| 93 | + * | |
| 94 | + * Upstream WP Insights hardcodes the bare `put_do_weekly_action`, and | |
| 95 | + * every plugin shipping a copy of this library hooks its own | |
| 96 | + * do_tracking() onto that one name — on this dev site that was Better | |
| 97 | + * Payment, Essential Addons, EasyJobs and NotificationX, all sharing a | |
| 98 | + * SINGLE scheduled event. `deactivate_this_plugin()` then calls | |
| 99 | + * wp_clear_scheduled_hook() on it, so deactivating Better Payment | |
| 100 | + * silently unscheduled all FOUR plugins' telemetry, and reactivating BP | |
| 101 | + * did not bring the others back (only their own activation hooks | |
| 102 | + * re-schedule it). Observed, not theoretical. | |
| 103 | + * | |
| 104 | + * With a plugin-scoped name we schedule and clear only our own event. | |
| 105 | + * We never schedule, clear, or listen on the shared one again. | |
| 106 | + * | |
| 107 | + * @since 2.3.2 | |
| 108 | + */ | |
| 109 | + $this->event_hook = 'better_payment_put_do_weekly_action'; | |
| 92 | 110 | |
| 93 | 111 | $this->require_optin = isset($args['opt_in']) ? $args['opt_in'] : true; |
| 94 | 112 | $this->include_goodbye_form = isset($args['goodbye_form']) ? $args['goodbye_form'] : true; |
| 95 | 113 | $this->marketing = isset($args['email_marketing']) ? $args['email_marketing'] : true; |
| @@ -117,8 +135,36 @@ | ||
| 117 | 135 | wp_schedule_event(time(), $this->recurrence, $this->event_hook); |
| 118 | 136 | } |
| 119 | 137 | } |
| 120 | 138 | /** |
| 139 | + * Ensure an opted-in site actually has our event scheduled. | |
| 140 | + * | |
| 141 | + * Two ways a consenting site ends up with no schedule, and neither is | |
| 142 | + * self-correcting without this: | |
| 143 | + * | |
| 144 | + * 1. **The rename.** An install that opted in before 2.3.2 has consent | |
| 145 | + * recorded and only the OLD shared event scheduled. Nothing re-runs | |
| 146 | + * `activate_this_plugin()` or the opt-in click on upgrade, so tracking | |
| 147 | + * would simply stop for every existing opted-in user — a silent | |
| 148 | + * regression that looks exactly like "the tracker broke". | |
| 149 | + * 2. **Collateral damage.** Another plugin sharing the old hook clears it | |
| 150 | + * on ITS deactivation. That is the bug being fixed here, seen from the | |
| 151 | + * other side; scoping our hook stops us doing it to others, and this | |
| 152 | + * recovers us when someone does it to us. | |
| 153 | + * | |
| 154 | + * Cheap enough for every admin request: both reads are autoloaded options. | |
| 155 | + * | |
| 156 | + * @since 2.3.2 | |
| 157 | + * @return void | |
| 158 | + */ | |
| 159 | + public function maybe_schedule_tracking() | |
| 160 | + { | |
| 161 | + if ($this->disabled_wp_cron || !$this->is_tracking_allowed()) { | |
| 162 | + return; | |
| 163 | + } | |
| 164 | + $this->schedule_tracking(); | |
| 165 | + } | |
| 166 | + /** | |
| 121 | 167 | * Add the schedule event if the plugin is tracked. |
| 122 | 168 | * |
| 123 | 169 | * @return void |
| 124 | 170 | */ |
| @@ -159,8 +205,21 @@ | ||
| 159 | 205 | delete_option('wpins_deactivation_reason_' . $this->plugin_name); |
| 160 | 206 | delete_option('wpins_deactivation_details_' . $this->plugin_name); |
| 161 | 207 | /** |
| 162 | 208 | * Clear the event schedule. |
| 209 | + * | |
| 210 | + * `$this->event_hook` is plugin-scoped (see the constructor), so this | |
| 211 | + * now clears only our own event. | |
| 212 | + * | |
| 213 | + * The legacy shared `put_do_weekly_action` is deliberately NOT cleaned | |
| 214 | + * up here, even on a site where Better Payment is the one that | |
| 215 | + * scheduled it. There is no way to tell a BP-scheduled event from one | |
| 216 | + * scheduled by Essential Addons / EasyJobs / NotificationX — the cron | |
| 217 | + * entry records no owner — and clearing another plugin's telemetry | |
| 218 | + * schedule is the exact bug this rename fixes. An orphaned event with | |
| 219 | + * no listeners costs a hook dispatch and nothing else. | |
| 220 | + * | |
| 221 | + * @since 2.3.2 | |
| 163 | 222 | */ |
| 164 | 223 | if (!$this->disabled_wp_cron) { |
| 165 | 224 | wp_clear_scheduled_hook($this->event_hook); |
| 166 | 225 | } |
| @@ -173,8 +232,9 @@ | ||
| 173 | 232 | { |
| 174 | 233 | // $this->clicked(); |
| 175 | 234 | add_action('wpdeveloper_notice_clicked_for_' . $this->plugin_name, array($this, 'clicked')); |
| 176 | 235 | add_action($this->event_hook, array($this, 'do_tracking')); |
| 236 | + $this->maybe_schedule_tracking(); | |
| 177 | 237 | // For Test |
| 178 | 238 | // add_action( 'admin_init', array( $this, 'force_tracking' ) ); |
| 179 | 239 | // add_action( 'admin_notices', array( $this, 'notice' ) ); |
| 180 | 240 | add_action('wpdeveloper_optin_notice_for_' . $this->plugin_name, array($this, 'notice')); |
| @@ -387,9 +447,18 @@ | ||
| 387 | 447 | $body['marketing_method'] = $this->marketing; |
| 388 | 448 | $body['server'] = isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : ''; |
| 389 | 449 | |
| 390 | 450 | /** |
| 391 | - * Collect all active and inactive plugins | |
| 451 | + * Collect active plugins. | |
| 452 | + * | |
| 453 | + * The full inventory of *inactive* plugins is deliberately not sent. | |
| 454 | + * It carries little compatibility signal (dormant code does not run) | |
| 455 | + * while being exactly the inventory an attacker would use to match a | |
| 456 | + * site against known vulnerabilities. Only the count is reported, so | |
| 457 | + * the "how much dormant code is installed" signal survives without | |
| 458 | + * shipping the list itself. | |
| 459 | + * | |
| 460 | + * @since 2.3.2 | |
| 392 | 461 | */ |
| 393 | 462 | if (!function_exists('get_plugins')) { |
| 394 | 463 | include ABSPATH . '/wp-admin/includes/plugin.php'; |
| 395 | 464 | } |
| @@ -400,9 +469,9 @@ | ||
| 400 | 469 | unset($plugins[$key]); |
| 401 | 470 | } |
| 402 | 471 | } |
| 403 | 472 | $body['active_plugins'] = $active_plugins; |
| 404 | - $body['inactive_plugins'] = $plugins; | |
| 473 | + $body['inactive_plugins_count'] = count($plugins); | |
| 405 | 474 | |
| 406 | 475 | /** |
| 407 | 476 | * Text Direction. |
| 408 | 477 | */ |
| @@ -435,8 +504,22 @@ | ||
| 435 | 504 | } |
| 436 | 505 | if ($theme->Version) { |
| 437 | 506 | $body['theme_version'] = sanitize_text_field($theme->Version); |
| 438 | 507 | } |
| 508 | + | |
| 509 | + /** | |
| 510 | + * Better Payment's own feature usage — widgets, blocks, campaigns, | |
| 511 | + * subscriptions and feature configuration. Aggregate counts only; see | |
| 512 | + * Usage_Data for what is deliberately excluded (money, customers, | |
| 513 | + * credentials). | |
| 514 | + * | |
| 515 | + * @since 2.3.2 | |
| 516 | + */ | |
| 517 | + $usage = Usage_Data::collect(); | |
| 518 | + if (!empty($usage)) { | |
| 519 | + $body['optional_data'] = $usage; | |
| 520 | + } | |
| 521 | + | |
| 439 | 522 | return $body; |
| 440 | 523 | } |
| 441 | 524 | |
| 442 | 525 | /** |
| @@ -474,15 +557,23 @@ | ||
| 474 | 557 | /** |
| 475 | 558 | * Send Initial Data to API |
| 476 | 559 | */ |
| 477 | 560 | if ($site_id == false && $this->item_id !== false) { |
| 478 | - if (isset($_SERVER['REMOTE_ADDR']) && !empty($_SERVER['REMOTE_ADDR'] && $_SERVER['REMOTE_ADDR'] != '127.0.0.1')) { | |
| 479 | - $country_request = wp_remote_get('http://ip-api.com/json/' . $_SERVER['REMOTE_ADDR'] . '?fields=country'); | |
| 480 | - if (!is_wp_error($country_request) && $country_request['response']['code'] == 200) { | |
| 481 | - $ip_data = json_decode($country_request["body"]); | |
| 482 | - $body['country'] = isset($ip_data->country) ? $ip_data->country : 'NOT SET'; | |
| 483 | - } | |
| 484 | - } | |
| 561 | + /** | |
| 562 | + * Country is intentionally NOT resolved here. | |
| 563 | + * | |
| 564 | + * This used to POST the administrator's IP address to | |
| 565 | + * http://ip-api.com over plain, unencrypted HTTP — a second | |
| 566 | + * third-party recipient of personal data, sent in the clear. | |
| 567 | + * ip-api.com offers no HTTPS on its free tier, so the lookup was | |
| 568 | + * removed outright rather than re-pointed: the receiving endpoint | |
| 569 | + * already sees the request IP and can resolve country server-side | |
| 570 | + * without any extra party or any plaintext hop. | |
| 571 | + * | |
| 572 | + * Do not reintroduce a client-side geo lookup here. | |
| 573 | + * | |
| 574 | + * @since 2.3.2 | |
| 575 | + */ | |
| 485 | 576 | |
| 486 | 577 | $body['plugin_slug'] = $this->plugin_name; |
| 487 | 578 | $body['url'] = $site_url; |
| 488 | 579 | $body['item_id'] = $this->item_id; |
| @@ -560,14 +651,14 @@ | ||
| 560 | 651 | * WP_REMOTE_POST method responsible for send data to the API_URL |
| 561 | 652 | * |
| 562 | 653 | * @param array $data |
| 563 | 654 | * @param array $args |
| 564 | - * @return void | |
| 655 | + * @return array|\WP_Error|null | |
| 565 | 656 | */ |
| 566 | 657 | protected function remote_post($data = array(), $args = array()) |
| 567 | 658 | { |
| 568 | 659 | if (empty($data)) { |
| 569 | - return; | |
| 660 | + return null; | |
| 570 | 661 | } |
| 571 | 662 | |
| 572 | 663 | $args = wp_parse_args($args, array( |
| 573 | 664 | 'method' => 'POST', |
| @@ -734,14 +825,20 @@ | ||
| 734 | 825 | */ |
| 735 | 826 | public function deactivate_reasons_form_submit() |
| 736 | 827 | { |
| 737 | 828 | check_ajax_referer('wpins_deactivation_nonce', 'security'); |
| 829 | + // Better Payment hardening (not upstream): a nonce is not a permission, so only a user | |
| 830 | + // who can deactivate plugins may write these options — and what they write is stored as | |
| 831 | + // plain text, never as whatever structure the request carried. | |
| 832 | + if ( ! current_user_can( 'activate_plugins' ) ) { | |
| 833 | + wp_send_json_error( null, 403 ); | |
| 834 | + } | |
| 738 | 835 | if (isset($_POST['values'])) { |
| 739 | - $values = $_POST['values']; | |
| 836 | + $values = is_scalar( $_POST['values'] ) ? sanitize_text_field( wp_unslash( $_POST['values'] ) ) : ''; | |
| 740 | 837 | update_option('wpins_deactivation_reason_' . $this->plugin_name, $values); |
| 741 | 838 | } |
| 742 | 839 | if (isset($_POST['details'])) { |
| 743 | - $details = sanitize_text_field($_POST['details']); | |
| 840 | + $details = is_scalar( $_POST['details'] ) ? sanitize_text_field( wp_unslash( $_POST['details'] ) ) : ''; | |
| 744 | 841 | update_option('wpins_deactivation_details_' . $this->plugin_name, $details); |
| 745 | 842 | } |
| 746 | 843 | echo 'success'; |
| 747 | 844 | wp_die(); |