PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.4.0
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.4.0
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
better-payment / includes / Classes / Plugin_Usage_Tracker.php

Plugin_Usage_Tracker.php in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More 2.4.0, at includes/Classes/Plugin_Usage_Tracker.php

1,045 lines 43.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Better_Payment\Lite\Classes;
4
5 use Better_Payment\Lite\Traits\Helper;
6
7 /**
8 * Exit if accessed directly
9 */
10 if (!defined('ABSPATH')) {
11 exit;
12 }
13
14 /**
15 * Plugin_Usage_Tracker
16 * This class is responsible for data sending to insights.
17 * @version 0.0.2
18 */
19
20 /**
21 * Main SDK for Plugin_Usage_Tracker.
22 */
23 class Plugin_Usage_Tracker
24 {
25 use Helper;
26
27 /**
28 * WP Insights Version
29 */
30 const WPINS_VERSION = '3.0.3';
31 /**
32 * API URL
33 */
34 const API_URL = 'https://send.wpinsight.com/process-plugin-data';
35 /**
36 * Installed Plugin File
37 *
38 * @var string
39 */
40 private $plugin_file = null;
41 /**
42 * Installed Plugin Name
43 *
44 * @var string
45 */
46 private $plugin_name = null;
47 /**
48 * How often the event should subsequently
49 * @var string
50 */
51 public $recurrence = 'daily';
52 private $event_hook = null;
53 /**
54 * Instace of Plugin_Usage_Tracker
55 * @var Plugin_Usage_Tracker
56 */
57 private static $_instance = null;
58
59 private $disabled_wp_cron;
60 private $enable_self_cron;
61 private $require_optin;
62 private $include_goodbye_form;
63 private $marketing;
64 private $options;
65 private $item_id;
66 private $notice_options;
67
68 /**
69 * Get Instance of Plugin_Usage_Tracker
70 * @return Plugin_Usage_Tracker
71 */
72 public static function get_instance($plugin_file, $args = [])
73 {
74 if (is_null(static::$_instance)) {
75 static::$_instance = new static($plugin_file, $args);
76 }
77 return static::$_instance;
78 }
79 /**
80 * Automatically Invoked when initialized.
81 *
82 * @param array $args
83 */
84 public function __construct($plugin_file, $args = [])
85 {
86 $this->plugin_file = $plugin_file;
87 $this->plugin_name = basename($this->plugin_file, '.php');
88 $this->disabled_wp_cron = defined('DISABLE_WP_CRON') && DISABLE_WP_CRON == true;
89 $this->enable_self_cron = $this->disabled_wp_cron == true ? true : false;
90
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';
110
111 $this->require_optin = isset($args['opt_in']) ? $args['opt_in'] : true;
112 $this->include_goodbye_form = isset($args['goodbye_form']) ? $args['goodbye_form'] : true;
113 $this->marketing = isset($args['email_marketing']) ? $args['email_marketing'] : true;
114 $this->options = isset($args['options']) ? $args['options'] : [];
115 $this->item_id = isset($args['item_id']) ? $args['item_id'] : false;
116 /**
117 * Activation Hook
118 */
119 register_activation_hook($this->plugin_file, array($this, 'activate_this_plugin'));
120 /**
121 * Deactivation Hook
122 */
123 register_deactivation_hook($this->plugin_file, array($this, 'deactivate_this_plugin'));
124 }
125 /**
126 * When user agreed to opt-in tracking schedule is enabled.
127 * @since 3.0.0
128 */
129 public function schedule_tracking()
130 {
131 if ($this->disabled_wp_cron) {
132 return;
133 }
134 if (!wp_next_scheduled($this->event_hook)) {
135 wp_schedule_event(time(), $this->recurrence, $this->event_hook);
136 }
137 }
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 /**
167 * Add the schedule event if the plugin is tracked.
168 *
169 * @return void
170 */
171 public function activate_this_plugin()
172 {
173 $allow_tracking = $this->is_tracking_allowed();
174 if (!$allow_tracking) {
175 return;
176 }
177 $this->schedule_tracking();
178 }
179 /**
180 * Remove the schedule event when plugin is deactivated and send the deactivated reason to inishghts if user submitted.
181 * @since 3.0.0
182 */
183 public function deactivate_this_plugin()
184 {
185 /**
186 * Check tracking is allowed or not.
187 */
188 $allow_tracking = $this->is_tracking_allowed();
189 if (!$allow_tracking) {
190 return;
191 }
192 $body = $this->get_data();
193 $body['status'] = 'Deactivated';
194 $body['deactivated_date'] = time();
195
196 // Check deactivation reason and add for insights data.
197 if (false !== get_option('wpins_deactivation_reason_' . $this->plugin_name)) {
198 $body['deactivation_reason'] = get_option('wpins_deactivation_reason_' . $this->plugin_name);
199 }
200 if (false !== get_option('wpins_deactivation_details_' . $this->plugin_name)) {
201 $body['deactivation_details'] = get_option('wpins_deactivation_details_' . $this->plugin_name);
202 }
203
204 $this->send_data($body);
205 delete_option('wpins_deactivation_reason_' . $this->plugin_name);
206 delete_option('wpins_deactivation_details_' . $this->plugin_name);
207 /**
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
222 */
223 if (!$this->disabled_wp_cron) {
224 wp_clear_scheduled_hook($this->event_hook);
225 }
226 }
227 /**
228 * Initial Method to Hook Everything.
229 * @return void
230 */
231 public function init()
232 {
233 // $this->clicked();
234 add_action('wpdeveloper_notice_clicked_for_' . $this->plugin_name, array($this, 'clicked'));
235 add_action($this->event_hook, array($this, 'do_tracking'));
236 $this->maybe_schedule_tracking();
237 // For Test
238 // add_action( 'admin_init', array( $this, 'force_tracking' ) );
239 // add_action( 'admin_notices', array( $this, 'notice' ) );
240 add_action('wpdeveloper_optin_notice_for_' . $this->plugin_name, array($this, 'notice'));
241 /**
242 * Deactivation Reason Form and Submit Data to Insights.
243 */
244 add_filter('plugin_action_links_' . plugin_basename($this->plugin_file), array($this, 'deactivate_action_links'));
245 add_action('admin_footer-plugins.php', array($this, 'deactivate_reasons_form'));
246 add_action('wp_ajax_deactivation_form_' . esc_attr($this->plugin_name), array($this, 'deactivate_reasons_form_submit'));
247 }
248 /**
249 * For Redirecting Current Page without Arguments!
250 *
251 * @return void
252 */
253 private function redirect_to()
254 {
255 $request_uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
256 $query_string = parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY);
257 parse_str($query_string, $current_url);
258
259 $unset_array = array('dismiss', 'plugin', '_wpnonce', 'later', 'plugin_action', 'marketing_optin');
260
261 foreach ($unset_array as $value) {
262 if (isset($current_url[$value])) {
263 unset($current_url[$value]);
264 }
265 }
266
267 $current_url = http_build_query($current_url);
268 $redirect_url = $request_uri . '?' . $current_url;
269 return $redirect_url;
270 }
271 /**
272 * This method forcing the do_tracking method to execute instant.
273 * @return void
274 */
275 public function force_tracking()
276 {
277 $this->do_tracking(true);
278 }
279 /**
280 * This method is responsible for all the magic from the front of the plugin.
281 * @since 3.0.0
282 * @param $force Force tracking if it's not the correct time to track/
283 */
284 public function do_tracking($force = false)
285 {
286 /**
287 * Check URL is set or not.
288 */
289 if (empty(self::API_URL)) {
290 return;
291 }
292 /**
293 * Check is tracking allowed or not.
294 */
295 if (!$this->is_tracking_allowed()) {
296 return;
297 }
298 /**
299 * Check is this the correct time to track or not.
300 * or Force to track.
301 */
302 if (!$this->is_time_to_track() && !$force) {
303 return;
304 }
305 /**
306 * Get All Data.
307 */
308 $body = $this->get_data();
309 /**
310 * Send all data.
311 */
312 return $this->send_data($body);
313 }
314 /**
315 * Is tracking allowed?
316 * @since 1.0.0
317 */
318 private function is_tracking_allowed()
319 {
320 // First, check if the user has changed their mind and opted out of tracking
321 if ($this->has_user_opted_out()) {
322 $this->set_is_tracking_allowed(false, $this->plugin_name);
323 return false;
324 }
325 // The wpins_allow_tracking option is an array of plugins that are being tracked
326 $allow_tracking = get_option('wpins_allow_tracking');
327 // If this plugin is in the array, then tracking is allowed
328 if (isset($allow_tracking[$this->plugin_name])) {
329 return true;
330 }
331 return false;
332 }
333 /**
334 * Set a flag in DB If tracking is allowed.
335 *
336 * @since 3.0.0
337 * @param $is_allowed Boolean true if is allowed.
338 */
339 public function set_is_tracking_allowed($is_allowed, $plugin = null)
340 {
341 if (empty($plugin)) {
342 $plugin = $this->plugin_name;
343 }
344 /**
345 * Get All Tracked Plugin List using this Tracker.
346 */
347 $allow_tracking = get_option('wpins_allow_tracking');
348 /**
349 * Check user is opted out for tracking or not.
350 */
351 if ($this->has_user_opted_out()) {
352 if (isset($allow_tracking[$plugin])) {
353 unset($allow_tracking[$plugin]);
354 }
355 } else if ($is_allowed || !$this->require_optin) {
356 /**
357 * If user has agreed to allow tracking
358 */
359 if (empty($allow_tracking) || !is_array($allow_tracking)) {
360 $allow_tracking = array($plugin => $plugin);
361 } else {
362 $allow_tracking[$plugin] = $plugin;
363 }
364 } else {
365 if (isset($allow_tracking[$plugin])) {
366 unset($allow_tracking[$plugin]);
367 }
368 }
369 update_option('wpins_allow_tracking', $allow_tracking);
370 }
371
372 /**
373 * Check the user has opted out or not.
374 *
375 * @since 3.0.0
376 * @return Boolean
377 */
378 protected function has_user_opted_out()
379 {
380 if (!empty($this->options)) {
381 foreach ($this->options as $option_name) {
382 $options = get_option($option_name);
383 if (!empty($options['wpins_opt_out'])) {
384 return true;
385 }
386 }
387 }
388 return false;
389 }
390 /**
391 * Check if it's time to track
392 *
393 * @since 3.0.0
394 */
395 public function is_time_to_track()
396 {
397 $track_times = get_option('wpins_last_track_time', array());
398 return !isset($track_times[$this->plugin_name]) ? true : ((isset($track_times[$this->plugin_name]) && $track_times[$this->plugin_name]) < strtotime('-1 day') ? true : false);
399 }
400 /**
401 * Set tracking time.
402 *
403 * @since 3.0.0
404 */
405 public function set_track_time()
406 {
407 $track_times = get_option('wpins_last_track_time', array());
408 $track_times[$this->plugin_name] = time();
409 update_option('wpins_last_track_time', $track_times);
410 }
411 /**
412 * This method is responsible for collecting all data.
413 *
414 * @since 3.0.0
415 */
416 public function get_data()
417 {
418 $body = array(
419 'plugin_slug' => sanitize_text_field($this->plugin_name),
420 'url' => get_bloginfo('url'),
421 'site_name' => get_bloginfo('name'),
422 'site_version' => get_bloginfo('version'),
423 'site_language' => get_bloginfo('language'),
424 'charset' => get_bloginfo('charset'),
425 'wpins_version' => self::WPINS_VERSION,
426 'php_version' => phpversion(),
427 'multisite' => is_multisite(),
428 'file_location' => __FILE__
429 );
430
431 // Collect the email if the correct option has been set
432 if ($this->marketing) {
433 if (!function_exists('wp_get_current_user')) {
434 include ABSPATH . 'wp-includes/pluggable.php';
435 }
436 $current_user = wp_get_current_user();
437 $email = $current_user->user_email;
438 if (is_email($email)) {
439 $body['email'] = $email;
440 } else {
441 $email = get_option('admin_email');
442 if (is_email($email)) {
443 $body['email'] = $email;
444 }
445 }
446 }
447 $body['marketing_method'] = $this->marketing;
448 $body['server'] = isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : '';
449
450 /**
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
461 */
462 if (!function_exists('get_plugins')) {
463 include ABSPATH . '/wp-admin/includes/plugin.php';
464 }
465 $plugins = array_keys(get_plugins());
466 $active_plugins = is_network_admin() ? array_keys(get_site_option('active_sitewide_plugins', array())) : get_option('active_plugins', array());
467 foreach ($plugins as $key => $plugin) {
468 if (in_array($plugin, $active_plugins)) {
469 unset($plugins[$key]);
470 }
471 }
472 $body['active_plugins'] = $active_plugins;
473 $body['inactive_plugins_count'] = count($plugins);
474
475 /**
476 * Text Direction.
477 */
478 $body['text_direction'] = (function_exists('is_rtl') ? (is_rtl() ? 'RTL' : 'LTR') : 'NOT SET');
479 /**
480 * Get Our Plugin Data.
481 * @since 3.0.0
482 */
483 $plugin = $this->plugin_data();
484 if (empty($plugin)) {
485 $body['message'] .= __('We can\'t detect any plugin information. This is most probably because you have not included the code in the plugin main file.', 'better-payment');
486 $body['status'] = 'NOT FOUND';
487 } else {
488 if (isset($plugin['Name'])) {
489 $body['plugin'] = sanitize_text_field($plugin['Name']);
490 }
491 if (isset($plugin['Version'])) {
492 $body['version'] = sanitize_text_field($plugin['Version']);
493 }
494 $body['status'] = 'Active';
495 }
496
497 /**
498 * Get active theme name and version
499 * @since 3.0.0
500 */
501 $theme = wp_get_theme();
502 if ($theme->Name) {
503 $body['theme'] = sanitize_text_field($theme->Name);
504 }
505 if ($theme->Version) {
506 $body['theme_version'] = sanitize_text_field($theme->Version);
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
522 return $body;
523 }
524
525 /**
526 * Collect plugin data,
527 * Retrieve current plugin information
528 *
529 * @since 3.0.0
530 */
531 public function plugin_data()
532 {
533 if (!function_exists('get_plugin_data')) {
534 include ABSPATH . '/wp-admin/includes/plugin.php';
535 }
536 $plugin = get_plugin_data($this->plugin_file);
537 return $plugin;
538 }
539 /**
540 * Send the data to insights.
541 * @since 3.0.0
542 */
543 public function send_data($body)
544 {
545 /**
546 * Get SITE ID
547 */
548 $site_id_key = "wpins_{$this->plugin_name}_site_id";
549 $site_id = get_option($site_id_key, false);
550 $failed_data = [];
551 $site_url = get_bloginfo('url');
552 $original_site_url = get_option("wpins_{$this->plugin_name}_original_url", false);
553
554 if (($original_site_url === false || $original_site_url != $site_url) && version_compare($body['wpins_version'], '3.0.1', '>=')) {
555 $site_id = false;
556 }
557 /**
558 * Send Initial Data to API
559 */
560 if ($site_id == false && $this->item_id !== false) {
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 */
576
577 $body['plugin_slug'] = $this->plugin_name;
578 $body['url'] = $site_url;
579 $body['item_id'] = $this->item_id;
580
581 $request = $this->remote_post($body);
582 if (!is_wp_error($request) && $request['response']['code'] == 200) {
583 $retrieved_body = json_decode(wp_remote_retrieve_body($request), true);
584 if (is_array($retrieved_body) && isset($retrieved_body['siteId'])) {
585 update_option($site_id_key, $retrieved_body['siteId']);
586 update_option("wpins_{$this->plugin_name}_original_url", $site_url);
587 update_option("wpins_{$this->plugin_name}_{$retrieved_body['siteId']}", $body);
588 }
589 } else {
590 $failed_data = $body;
591 }
592 }
593
594 $site_id_data_key = "wpins_{$this->plugin_name}_{$site_id}";
595 $site_id_data_failed_key = "wpins_{$this->plugin_name}_{$site_id}_send_failed";
596
597 if ($site_id != false) {
598 $old_sent_data = get_option($site_id_data_key, []);
599 $diff_data = $this->diff($body, $old_sent_data);
600 $failed_data = get_option($site_id_data_failed_key, []);
601 if (!empty($failed_data) && $diff_data != $failed_data) {
602 $failed_data = array_merge($failed_data, $diff_data);
603 }
604 }
605
606 if (!empty($failed_data) && $site_id != false) {
607 $failed_data['plugin_slug'] = $this->plugin_name;
608 $failed_data['url'] = $site_url;
609 $failed_data['site_id'] = $site_id;
610 if ($original_site_url != false) {
611 $failed_data['original_url'] = $original_site_url;
612 }
613
614 $request = $this->remote_post($failed_data);
615 if (!is_wp_error($request)) {
616 delete_option($site_id_data_failed_key);
617 $replaced_data = array_merge($old_sent_data, $failed_data);
618 update_option($site_id_data_key, $replaced_data);
619 }
620 }
621
622 if (!empty($diff_data) && $site_id != false && empty($failed_data)) {
623 $diff_data['plugin_slug'] = $this->plugin_name;
624 $diff_data['url'] = $site_url;
625 $diff_data['site_id'] = $site_id;
626 if ($original_site_url != false) {
627 $diff_data['original_url'] = $original_site_url;
628 }
629
630 $request = $this->remote_post($diff_data);
631 if (is_wp_error($request)) {
632 update_option($site_id_data_failed_key, $diff_data);
633 } else {
634 $replaced_data = array_merge($old_sent_data, $diff_data);
635 update_option($site_id_data_key, $replaced_data);
636 }
637 }
638
639 $this->set_track_time();
640
641 if (isset($request) && is_wp_error($request)) {
642 return $request;
643 }
644
645 if (isset($request)) {
646 return true;
647 }
648 return false;
649 }
650 /**
651 * WP_REMOTE_POST method responsible for send data to the API_URL
652 *
653 * @param array $data
654 * @param array $args
655 * @return array|\WP_Error|null
656 */
657 protected function remote_post($data = array(), $args = array())
658 {
659 if (empty($data)) {
660 return null;
661 }
662
663 $args = wp_parse_args($args, array(
664 'method' => 'POST',
665 'timeout' => 30,
666 'redirection' => 5,
667 'httpversion' => '1.1',
668 'blocking' => true,
669 'body' => $data,
670 'user-agent' => 'PUT/1.0.0; ' . get_bloginfo('url')
671 ));
672 $request = wp_remote_post(esc_url(self::API_URL), $args);
673 if (is_wp_error($request) || (isset($request['response'], $request['response']['code']) && $request['response']['code'] != 200)) {
674 return new \WP_Error(500, 'Something went wrong.');
675 }
676 return $request;
677 }
678 /**
679 * Difference between old and new data
680 *
681 * @param array $new_data
682 * @param array $old_data
683 * @return void
684 */
685 protected function diff($new_data, $old_data)
686 {
687 $data = [];
688 if (!empty($new_data)) {
689 foreach ($new_data as $key => $value) {
690 if (isset($old_data[$key])) {
691 if ($old_data[$key] == $value) {
692 continue;
693 }
694 }
695 $data[$key] = $value;
696 }
697 }
698 return $data;
699 }
700 /**
701 * Display the admin notice to users to allow them to opt in
702 *
703 * @since 3.0.0
704 */
705 public function notice()
706 {
707 /**
708 * Return if notice is not set.
709 */
710 if (!isset($this->notice_options['notice'])) {
711 return;
712 }
713 /**
714 * Check is allowed or blocked for notice.
715 */
716 $block_notice = get_option('wpins_block_notice');
717 if (isset($block_notice[$this->plugin_name])) {
718 return;
719 }
720 if (!current_user_can('manage_options')) {
721 return;
722 }
723
724 $url_yes = add_query_arg([
725 'plugin' => $this->plugin_name,
726 'plugin_action' => 'yes',
727 ]);
728 $url_no = add_query_arg(array(
729 'plugin' => $this->plugin_name,
730 'plugin_action' => 'no'
731 ));
732
733 $url_yes = wp_nonce_url( $url_yes, '_wpnonce_optin_' . $this->plugin_name );
734 $url_no = wp_nonce_url( $url_no, '_wpnonce_optin_' . $this->plugin_name );
735
736 // Decide on notice text
737 $notice_text = $this->notice_options['notice'] . ' <a href="#" class="wpinsights-' . $this->plugin_name . '-collect">' . $this->notice_options['consent_button_text'] . '</a>';
738 $extra_notice_text = $this->notice_options['extra_notice'];
739
740 $output = '';
741 $output .= '<div class="notice notice-info updated put-dismiss-notice">';
742 $output .= '<p>' . $notice_text . '</p>';
743 $output .= '<div class="wpinsights-data" style="display: none;">';
744 $output .= '<p>' . $extra_notice_text . '</p>';
745 $output .= '</div>';
746 $output .= '<p>';
747 $output .= '<a href="' . esc_url($url_yes) . '" class="button-primary">' . $this->notice_options['yes'] . '</a>&nbsp;';
748 $output .= '<a href="' . esc_url($url_no) . '" class="button-secondary">' . $this->notice_options['no'] . '</a>';
749 $output .= '</p>';
750 $output .= "<script type='text/javascript'>jQuery('.wpinsights-" . $this->plugin_name . "-collect').on('click', function(e) {e.preventDefault();jQuery('.wpinsights-data').slideToggle('fast');});</script>";
751 $output .= '</div>';
752
753 printf( '%1$s', $output );
754 }
755 /**
756 * Set all notice options to customized notice.
757 *
758 * @since 3.0.0
759 * @param array $options
760 * @return void
761 */
762 public function set_notice_options($options = [])
763 {
764 $default_options = [
765 'consent_button_text' => 'What we collect.',
766 'yes' => 'Sure, I\'d like to help',
767 'no' => 'No Thanks.',
768 ];
769 $options = wp_parse_args($options, $default_options);
770 $this->notice_options = $options;
771 }
772 /**
773 * Responsible for track the click from Notice.
774 * @return void
775 */
776 public function clicked()
777 {
778 if ( isset( $_GET['_wpnonce'] ) && isset( $_GET['plugin'] ) && trim( $_GET['plugin'] ) === $this->plugin_name && isset( $_GET['plugin_action'] ) ) {
779 if ( ! wp_verify_nonce( $_GET['_wpnonce'], '_wpnonce_optin_' . $this->plugin_name ) ) {
780 return;
781 }
782
783 if( isset( $_GET['tab'] ) && $_GET['tab'] === 'plugin-information' ) {
784 return;
785 }
786 $plugin = sanitize_text_field( $_GET['plugin'] );
787 $action = sanitize_text_field( $_GET['plugin_action'] );
788 if( $action == 'yes' ) {
789 $this->schedule_tracking();
790 $this->set_is_tracking_allowed( true, $plugin );
791 if( $this->do_tracking( true ) ) {
792 $this->update_block_notice( $plugin );
793 }
794 /**
795 * Redirect User To the Current URL, but without set query arguments.
796 */
797 wp_safe_redirect( $this->redirect_to() );
798 } else {
799 $this->set_is_tracking_allowed( false, $plugin );
800 $this->update_block_notice( $plugin );
801 }
802 }
803 }
804 /**
805 * Set if we should block the opt-in notice for this plugin
806 *
807 * @since 3.0.0
808 */
809 public function update_block_notice($plugin = null)
810 {
811 if (empty($plugin)) {
812 $plugin = $this->plugin_name;
813 }
814 $block_notice = get_option('wpins_block_notice');
815 if (empty($block_notice) || !is_array($block_notice)) {
816 $block_notice = array($plugin => $plugin);
817 } else {
818 $block_notice[$plugin] = $plugin;
819 }
820 update_option('wpins_block_notice', $block_notice);
821 }
822 /**
823 * AJAX callback when the deactivated form is submitted.
824 * @since 3.0.0
825 */
826 public function deactivate_reasons_form_submit()
827 {
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 }
835 if (isset($_POST['values'])) {
836 $values = is_scalar( $_POST['values'] ) ? sanitize_text_field( wp_unslash( $_POST['values'] ) ) : '';
837 update_option('wpins_deactivation_reason_' . $this->plugin_name, $values);
838 }
839 if (isset($_POST['details'])) {
840 $details = is_scalar( $_POST['details'] ) ? sanitize_text_field( wp_unslash( $_POST['details'] ) ) : '';
841 update_option('wpins_deactivation_details_' . $this->plugin_name, $details);
842 }
843 echo 'success';
844 wp_die();
845 }
846 /**
847 * Filter the deactivation link to allow us to present a form when the user deactivates the plugin
848 * @since 3.0.0
849 */
850 public function deactivate_action_links($links)
851 {
852 /**
853 * Check is tracking allowed or not.
854 */
855 if (!$this->is_tracking_allowed()) {
856 return $links;
857 }
858 if (isset($links['deactivate']) && $this->include_goodbye_form) {
859 $deactivation_link = $links['deactivate'];
860 /**
861 * Change the default deactivate button link.
862 */
863 $deactivation_link = str_replace('<a ', '<div class="wpinsights-goodbye-form-wrapper-' . esc_attr($this->plugin_name) . '"><div class="wpinsights-goodbye-form-bg"></div><span class="wpinsights-goodbye-form" id="wpinsights-goodbye-form"></span></div><a onclick="javascript:event.preventDefault();" id="wpinsights-goodbye-link-' . esc_attr($this->plugin_name) . '" ', $deactivation_link);
864 $links['deactivate'] = $deactivation_link;
865 }
866 return $links;
867 }
868 /**
869 * ALL Deactivate Reasons.
870 * @since 3.0.0
871 */
872 public function deactivation_reasons()
873 {
874 $form = array();
875 $form['heading'] = esc_html__('Sorry to see you go', 'better-payment');
876 $form['body'] = esc_html__('Before you deactivate the plugin, would you quickly give us your reason for doing so?', 'better-payment');
877
878 $form['options'] = array(
879 esc_html__('I no longer need the plugin', 'better-payment'),
880 [
881 'label' => esc_html__('I found a better plugin', 'better-payment'),
882 'extra_field' => esc_html__('Please share which plugin', 'better-payment')
883 ],
884 esc_html__("I couldn't get the plugin to work", 'better-payment'),
885 esc_html__('It\'s a temporary deactivation', 'better-payment'),
886 [
887 'label' => esc_html__('Other', 'better-payment'),
888 'extra_field' => esc_html__('Please share the reason', 'better-payment'),
889 'type' => 'textarea'
890 ]
891 );
892 return apply_filters('wpins_form_text_' . $this->plugin_name, $form);
893 }
894 /**
895 * Deactivate Reasons Form.
896 * This form will appears when user wants to deactivate the plugin to send you deactivated reasons.
897 *
898 * @since 3.0.0
899 */
900 public function deactivate_reasons_form()
901 {
902 $form = $this->deactivation_reasons();
903 $class_plugin_name = esc_attr( $this->plugin_name );
904 $html = '<div class="wpinsights-goodbye-form-head"><strong>' . esc_html($form['heading']) . '</strong></div>';
905 $html .= '<div class="wpinsights-goodbye-form-body"><p class="wpinsights-goodbye-form-caption">' . esc_html($form['body']) . '</p>';
906 if (is_array($form['options'])) {
907 $html .= '<div id="wpinsights-goodbye-options" class="wpinsights-goodbye-options"><ul>';
908 foreach ($form['options'] as $option) {
909 if (is_array($option)) {
910 $id = strtolower(str_replace(" ", "_", esc_attr($option['label'])));
911 $id = $id . '_' . esc_attr( $class_plugin_name );
912 $html .= '<li class="has-goodbye-extra">';
913 $html .= '<input type="radio" name="wpinsights-' . esc_attr( $class_plugin_name ) . '-goodbye-options" id="' . $id . '" value="' . esc_attr($option['label']) . '">';
914 $html .= '<div><label for="' . $id . '">' . esc_attr($option['label']) . '</label>';
915 if (isset($option['extra_field']) && !isset($option['type'])) {
916 $html .= '<input type="text" style="display: none" name="' . $id . '" id="' . str_replace(" ", "", esc_attr($option['extra_field'])) . '" placeholder="' . esc_attr($option['extra_field']) . '">';
917 }
918 if (isset($option['extra_field']) && isset($option['type'])) {
919 $html .= '<' . $option['type'] . ' style="display: none" type="text" name="' . $id . '" id="' . str_replace(" ", "", esc_attr($option['extra_field'])) . '" placeholder="' . esc_attr($option['extra_field']) . '"></' . $option['type'] . '>';
920 }
921 $html .= '</div></li>';
922 } else {
923 $id = strtolower(str_replace(" ", "_", esc_attr($option)));
924 $id = $id . '_' . esc_attr( $class_plugin_name );
925 $html .= '<li><input type="radio" name="wpinsights-' . esc_attr( $class_plugin_name ) . '-goodbye-options" id="' . $id . '" value="' . esc_attr($option) . '"> <label for="' . $id . '">' . esc_attr($option) . '</label></li>';
926 }
927 }
928 $html .= '</ul></div><!-- .wpinsights-' . esc_attr( $class_plugin_name ) . '-goodbye-options -->';
929 }
930 $html .= '</div><!-- .wpinsights-goodbye-form-body -->';
931 $html .= '<p class="deactivating-spinner"><span class="spinner"></span> ' . esc_html__('Submitting form', 'better-payment') . '</p>';
932
933 $wrapper_class = '.wpinsights-goodbye-form-wrapper-' . esc_attr( $class_plugin_name );
934
935 $styles = '';
936 $styles .= '<style type="text/css">';
937 $styles .= '.wpinsights-form-active-' . esc_attr( $class_plugin_name ) . ' .wpinsights-goodbye-form-bg {';
938 $styles .= 'background: rgba( 0, 0, 0, .8 );position: fixed;top: 0;left: 0;width: 100%;height: 100%;z-index: 9;';
939 $styles .= '}';
940 $styles .= $wrapper_class . '{';
941 $styles .= 'position: relative; display: none;';
942 $styles .= '}';
943 $styles .= '.wpinsights-form-active-' . esc_attr( $class_plugin_name ) . ' ' . $wrapper_class . '{';
944 $styles .= 'display: flex !important; position: fixed;top: 0;left: 0;width: 100%;height: 100%; justify-content: center; align-items: center; z-index: 99999';
945 $styles .= '}';
946 $styles .= $wrapper_class . ' .wpinsights-goodbye-form { display: none; }';
947 $styles .= '.wpinsights-form-active-' . esc_attr( $class_plugin_name ) . ' .wpinsights-goodbye-form {';
948 $styles .= 'position: relative !important; width: 550px; max-width: 80%; background: #fff; box-shadow: 2px 8px 23px 3px rgba(0,0,0,.2); border-radius: 3px; white-space: normal; overflow: hidden; display: block; z-index: 999999;';
949 $styles .= '}';
950 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-head {';
951 $styles .= 'background: #fff; color: #495157; padding: 18px; box-shadow: 0 0 8px rgba(0,0,0,.1); font-size: 15px;';
952 $styles .= '}';
953 $styles .= $wrapper_class . ' .wpinsights-goodbye-form .wpinsights-goodbye-form-head strong { font-size: 15px; }';
954 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-body { padding: 8px 18px; color: #333; }';
955 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-body label { padding-left: 5px; color: #6d7882; }';
956 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-body .wpinsights-goodbye-form-caption {';
957 $styles .= 'font-weight: 500; font-size: 15px; color: #495157; line-height: 1.4;';
958 $styles .= '}';
959 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options { padding-top: 5px; }';
960 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options ul > li { margin-bottom: 15px; }';
961 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options input[type=radio] { border-color: #6B59EE; box-shadow: none; outline: 0; }';
962 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options input[type=radio]:checked::before { content: ""; border-radius: 50%; width: .6rem; height: .6rem; margin: 2.45px; background-color: #6B59EE; line-height: 1.14285714; outline: 0; }';
963 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options ul > li > div { display: inline; padding-left: 3px; }';
964 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options ul > li > div > input, ' . $wrapper_class . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options ul > li > div > textarea {';
965 $styles .= 'margin: 10px 18px; padding: 8px; width: 80%;';
966 $styles .= '}';
967 $styles .= $wrapper_class . ' .deactivating-spinner { display: none; padding-bottom: 20px !important; }';
968 $styles .= $wrapper_class . ' .deactivating-spinner .spinner { float: none; margin: 4px 4px 0 18px; vertical-align: bottom; visibility: visible; }';
969 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-footer { padding: 8px 18px; margin-bottom: 15px; }';
970 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-footer > .wpinsights-goodbye-form-buttons { display: flex; align-items: center; justify-content: space-between; }';
971 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-footer .wpinsights-submit-btn {';
972 $styles .= 'background-color: #6B59EE; -webkit-border-radius: 8px; border-radius: 8px; color: #fff; line-height: 1; padding: 12px 18px; font-size: 13px;';
973 $styles .= '}';
974 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-footer .wpsp-put-deactivate-btn {';
975 $styles .= 'font-size: 13px; color: #a4afb7; background: none; float: right; padding-right: 10px; width: auto; text-decoration: underline;';
976 $styles .= '}';
977 $styles .= $wrapper_class . ' .test {';
978 $styles .= '}';
979 $styles .= '</style>';
980 $styles .= '';
981
982 echo $styles;
983 ?>
984 <script type="text/javascript">
985 jQuery(document).ready(function($) {
986 $("#wpinsights-goodbye-link-<?php echo esc_attr( $class_plugin_name ); ?>").on("click", function() {
987 // We'll send the user to this deactivation link when they've completed or dismissed the form
988 var url = document.getElementById("wpinsights-goodbye-link-<?php echo esc_attr( $class_plugin_name ); ?>");
989 $('body').toggleClass('wpinsights-form-active-<?php echo esc_attr( $class_plugin_name ); ?>');
990 $(".wpinsights-goodbye-form-wrapper-<?php echo esc_attr( $class_plugin_name ); ?> #wpinsights-goodbye-form").fadeIn();
991 $(".wpinsights-goodbye-form-wrapper-<?php echo esc_attr( $class_plugin_name ); ?> #wpinsights-goodbye-form").html('<?php echo $html; ?>' + '<div class="wpinsights-goodbye-form-footer"><div class="wpinsights-goodbye-form-buttons"><a id="wpinsights-submit-form-<?php echo esc_attr( $class_plugin_name ); ?>" class="wpinsights-submit-btn" href="#"><?php esc_html_e('Submit and Deactivate', 'better-payment'); ?></a>&nbsp;<a class="wpsp-put-deactivate-btn" href="' + url + '"><?php esc_html_e('Just Deactivate', 'better-payment'); ?></a></div></div>');
992 $('#wpinsights-submit-form-<?php echo esc_attr( $class_plugin_name ); ?>').on('click', function(e) {
993 // As soon as we click, the body of the form should disappear
994 $("#wpinsights-goodbye-form-<?php echo esc_attr( $class_plugin_name ); ?> .wpinsights-goodbye-form-body").fadeOut();
995 $("#wpinsights-goodbye-form-<?php echo esc_attr( $class_plugin_name ); ?> .wpinsights-goodbye-form-footer").fadeOut();
996 // Fade in spinner
997 $("#wpinsights-goodbye-form-<?php echo esc_attr( $class_plugin_name ); ?> .deactivating-spinner").fadeIn();
998 e.preventDefault();
999 var checkedInput = $("input[name='wpinsights-<?php echo esc_attr( $class_plugin_name ); ?>-goodbye-options']:checked"),
1000 checkedInputVal, details;
1001 if (checkedInput.length > 0) {
1002 checkedInputVal = checkedInput.val();
1003 details = $('input[name="' + checkedInput[0].id + '"], textarea[name="' + checkedInput[0].id + '"]').val();
1004 }
1005
1006 if (typeof details === 'undefined') {
1007 details = '';
1008 }
1009 if (typeof checkedInputVal === 'undefined') {
1010 checkedInputVal = 'No Reason';
1011 }
1012
1013 var data = {
1014 'action': 'deactivation_form_<?php echo esc_attr( $class_plugin_name ); ?>',
1015 'values': checkedInputVal,
1016 'details': details,
1017 'security': "<?php echo esc_attr( wp_create_nonce('wpins_deactivation_nonce') ); ?>",
1018 'dataType': "json"
1019 }
1020
1021 $.post(
1022 ajaxurl,
1023 data,
1024 function(response) {
1025 // Redirect to original deactivation URL
1026 window.location.href = url;
1027 }
1028 );
1029 });
1030 $('#wpinsights-goodbye-options > ul ').on('click', 'li label, li > input', function(e) {
1031 var parent = $(this).parents('li');
1032 parent.siblings().find('label').next('input, textarea').css('display', 'none');
1033 parent.find('label').next('input, textarea').css('display', 'block');
1034 });
1035 // If we click outside the form, the form will close
1036 $('.wpinsights-goodbye-form-bg').on('click', function() {
1037 $("#wpinsights-goodbye-form").fadeOut();
1038 $('body').removeClass('wpinsights-form-active-<?php echo esc_attr( $class_plugin_name ); ?>');
1039 });
1040 });
1041 });
1042 </script>
1043 <?php }
1044 }
1045