PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.4.1
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.4.1
2.4.1 2.4.0 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 All 68 releases
← All changes | includes/Classes/Plugin_Usage_Tracker.php +111 -14 2.2.0 → 2.4.1 View file →
@@ -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();