PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.9.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.9.0
2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 All 50 releases
thinkrank / includes / core / class-plugin-usage-tracker.php

class-plugin-usage-tracker.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.9.0, at includes/core/class-plugin-usage-tracker.php

1,349 lines 57.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Plugin_Usage_Tracker
5 *
6 * Opt-in usage tracking SDK (WP Insights). Responsible for collecting
7 * non-sensitive diagnostic data and sending it to the insights endpoint
8 * when the site administrator has explicitly opted in.
9 *
10 * Ported from the WPDeveloper WP Insights SDK and adapted for ThinkRank:
11 * standalone notice wiring (no external notice library) and ThinkRank
12 * feature usage as the optional data payload.
13 *
14 * @package ThinkRank\Core
15 * @since 1.12.0
16 * @version 3.0.3
17 */
18
19 declare(strict_types=1);
20
21 namespace ThinkRank\Core;
22
23 // Prevent direct access
24 if (!defined('ABSPATH')) {
25 exit;
26 }
27
28 /**
29 * Main SDK for Plugin_Usage_Tracker.
30 */
31 class Plugin_Usage_Tracker {
32
33 /**
34 * WP Insights Version
35 */
36 const WPINS_VERSION = '3.0.3';
37
38 /**
39 * API URL
40 */
41 const API_URL = 'https://send.wpinsight.com/process-plugin-data';
42
43 /**
44 * Installed Plugin File
45 *
46 * @var string
47 */
48 private $plugin_file = null;
49
50 /**
51 * Installed Plugin Name
52 *
53 * @var string
54 */
55 private $plugin_name = null;
56
57 /**
58 * How often the event should subsequently
59 *
60 * @var string
61 */
62 public $recurrence = 'daily';
63
64 private $event_hook = null;
65
66 /**
67 * Instance of Plugin_Usage_Tracker
68 *
69 * @var Plugin_Usage_Tracker
70 */
71 private static $_instance = null;
72
73 private $disabled_wp_cron;
74 private $enable_self_cron;
75 private $require_optin;
76 private $include_goodbye_form;
77 private $marketing;
78 private $options;
79 private $item_id;
80 private $notice_options;
81
82 /**
83 * Unix timestamp before which the opt-in notice stays hidden.
84 *
85 * 0 keeps the SDK's original behaviour (ask on the first admin page load).
86 * ThinkRank sets it to install time + a grace period via
87 * {@see Usage_Tracker_Manager::start_tracking()}.
88 *
89 * @var int
90 */
91 private $notice_after = 0;
92
93 /**
94 * Get Instance of Plugin_Usage_Tracker
95 *
96 * @return Plugin_Usage_Tracker
97 */
98 public static function get_instance($plugin_file, $args = []) {
99 if (is_null(static::$_instance)) {
100 static::$_instance = new static($plugin_file, $args);
101 }
102 return static::$_instance;
103 }
104
105 /**
106 * Automatically Invoked when initialized.
107 *
108 * @param string $plugin_file
109 * @param array $args
110 */
111 public function __construct($plugin_file, $args = []) {
112 $this->plugin_file = $plugin_file;
113 $this->plugin_name = basename($this->plugin_file, '.php');
114 $this->disabled_wp_cron = defined('DISABLE_WP_CRON') && DISABLE_WP_CRON == true;
115 $this->enable_self_cron = $this->disabled_wp_cron == true ? true : false;
116
117 $this->event_hook = 'thinkrank_put_do_daily_action';
118
119 $this->require_optin = isset($args['opt_in']) ? $args['opt_in'] : true;
120 $this->include_goodbye_form = isset($args['goodbye_form']) ? $args['goodbye_form'] : true;
121 $this->marketing = isset($args['email_marketing']) ? $args['email_marketing'] : true;
122 $this->options = isset($args['options']) ? $args['options'] : [];
123 $this->item_id = isset($args['item_id']) ? $args['item_id'] : false;
124
125 /**
126 * Activation Hook
127 */
128 register_activation_hook($this->plugin_file, array($this, 'activate_this_plugin'));
129 /**
130 * Deactivation Hook
131 */
132 register_deactivation_hook($this->plugin_file, array($this, 'deactivate_this_plugin'));
133 }
134
135 /**
136 * When user agreed to opt-in tracking schedule is enabled.
137 *
138 * @since 1.12.0
139 */
140 public function schedule_tracking() {
141 if ($this->disabled_wp_cron) {
142 return;
143 }
144 if (!wp_next_scheduled($this->event_hook)) {
145 wp_schedule_event(time(), $this->recurrence, $this->event_hook);
146 }
147 }
148
149 /**
150 * Add the schedule event if the plugin is tracked.
151 *
152 * @return void
153 */
154 public function activate_this_plugin() {
155 $allow_tracking = $this->is_tracking_allowed();
156 if (!$allow_tracking) {
157 return;
158 }
159 $this->schedule_tracking();
160 }
161
162 /**
163 * Remove the schedule event when plugin is deactivated and send the
164 * deactivated reason to insights if user submitted.
165 *
166 * @since 1.12.0
167 */
168 public function deactivate_this_plugin() {
169 /**
170 * Check tracking is allowed or not.
171 */
172 $allow_tracking = $this->is_tracking_allowed();
173 if (!$allow_tracking) {
174 return;
175 }
176 $body = $this->get_data();
177 $body['status'] = 'Deactivated';
178 $body['deactivated_date'] = time();
179
180 // Check deactivation reason and add for insights data.
181 if (false !== get_option('wpins_deactivation_reason_' . $this->plugin_name)) {
182 $body['deactivation_reason'] = get_option('wpins_deactivation_reason_' . $this->plugin_name);
183 }
184 if (false !== get_option('wpins_deactivation_details_' . $this->plugin_name)) {
185 $body['deactivation_details'] = get_option('wpins_deactivation_details_' . $this->plugin_name);
186 }
187
188 $this->send_data($body);
189 delete_option('wpins_deactivation_reason_' . $this->plugin_name);
190 delete_option('wpins_deactivation_details_' . $this->plugin_name);
191 /**
192 * Clear the event schedule.
193 */
194 if (!$this->disabled_wp_cron) {
195 wp_clear_scheduled_hook($this->event_hook);
196 }
197 }
198
199 /**
200 * Initial Method to Hook Everything.
201 *
202 * @return void
203 */
204 public function init() {
205 add_action('admin_init', array($this, 'clicked'));
206 add_action($this->event_hook, array($this, 'do_tracking'));
207
208 // Both hooks, deliberately: Admin\Manager::remove_admin_notice() strips
209 // every `admin_notices` callback on ThinkRank's own screens and re-fires
210 // `thinkrank_admin_notices` in their place. Since notice() now renders
211 // only on those screens, registering on `admin_notices` alone would mean
212 // it never renders anywhere. The two never both run on one request.
213 add_action('admin_notices', array($this, 'notice'));
214 add_action('thinkrank_admin_notices', array($this, 'notice'));
215 /**
216 * Deactivation Reason Form and Submit Data to Insights.
217 */
218 add_filter('plugin_action_links_' . plugin_basename($this->plugin_file), array($this, 'deactivate_action_links'));
219 add_action('admin_footer-plugins.php', array($this, 'deactivate_reasons_form'));
220 add_action('wp_ajax_deactivation_form_' . esc_attr($this->plugin_name), array($this, 'deactivate_reasons_form_submit'));
221 }
222
223 /**
224 * For Redirecting Current Page without Arguments!
225 *
226 * @return string
227 */
228 private function redirect_to() {
229 $request_uri = !empty($_SERVER['REQUEST_URI']) ? wp_parse_url(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])), PHP_URL_PATH) : '';
230 $query_string = !empty($_SERVER['REQUEST_URI']) ? wp_parse_url(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])), PHP_URL_QUERY) : '';
231 parse_str((string) $query_string, $current_url);
232
233 $unset_array = array('dismiss', 'plugin', '_wpnonce', 'later', 'plugin_action', 'marketing_optin');
234
235 foreach ($unset_array as $value) {
236 if (isset($current_url[$value])) {
237 unset($current_url[$value]);
238 }
239 }
240
241 $current_url = http_build_query($current_url);
242 $redirect_url = $request_uri . '?' . $current_url;
243 return $redirect_url;
244 }
245
246 /**
247 * This method forcing the do_tracking method to execute instant.
248 *
249 * @return void
250 */
251 public function force_tracking() {
252 $this->do_tracking(true);
253 }
254
255 /**
256 * This method is responsible for all the magic from the front of the plugin.
257 *
258 * @since 1.12.0
259 * @param bool $force Force tracking if it's not the correct time to track.
260 */
261 public function do_tracking($force = false) {
262 /**
263 * Check URL is set or not.
264 */
265 if (empty(self::API_URL)) {
266 return;
267 }
268 /**
269 * Check is tracking allowed or not.
270 */
271 if (!$this->is_tracking_allowed()) {
272 return;
273 }
274 /**
275 * Check is this the correct time to track or not.
276 * or Force to track.
277 */
278 if (!$this->is_time_to_track() && !$force) {
279 return;
280 }
281 /**
282 * Get All Data.
283 */
284 $body = $this->get_data();
285 /**
286 * Send all data.
287 */
288 return $this->send_data($body);
289 }
290
291 /**
292 * Is tracking allowed?
293 *
294 * @since 1.12.0
295 */
296 private function is_tracking_allowed() {
297 // First, check if the user has changed their mind and opted out of tracking
298 if ($this->has_user_opted_out()) {
299 $this->set_is_tracking_allowed(false, $this->plugin_name);
300 return false;
301 }
302 // The wpins_allow_tracking option is an array of plugins that are being tracked
303 $allow_tracking = get_option('wpins_allow_tracking');
304 // If this plugin is in the array, then tracking is allowed
305 if (isset($allow_tracking[$this->plugin_name])) {
306 return true;
307 }
308 return false;
309 }
310
311 /**
312 * Set a flag in DB If tracking is allowed.
313 *
314 * @since 1.12.0
315 * @param bool $is_allowed true if is allowed.
316 * @param string|null $plugin
317 */
318 public function set_is_tracking_allowed($is_allowed, $plugin = null) {
319 if (empty($plugin)) {
320 $plugin = $this->plugin_name;
321 }
322 /**
323 * Get All Tracked Plugin List using this Tracker.
324 */
325 $allow_tracking = get_option('wpins_allow_tracking');
326 /**
327 * Check user is opted out for tracking or not.
328 */
329 if ($this->has_user_opted_out()) {
330 if (isset($allow_tracking[$plugin])) {
331 unset($allow_tracking[$plugin]);
332 }
333 } elseif ($is_allowed || !$this->require_optin) {
334 /**
335 * If user has agreed to allow tracking
336 */
337 if (empty($allow_tracking) || !is_array($allow_tracking)) {
338 $allow_tracking = array($plugin => $plugin);
339 } else {
340 $allow_tracking[$plugin] = $plugin;
341 }
342 } else {
343 if (isset($allow_tracking[$plugin])) {
344 unset($allow_tracking[$plugin]);
345 }
346 }
347 update_option('wpins_allow_tracking', $allow_tracking);
348 }
349
350 /**
351 * Check the user has opted out or not.
352 *
353 * @since 1.12.0
354 * @return bool
355 */
356 protected function has_user_opted_out() {
357 if (!empty($this->options)) {
358 foreach ($this->options as $option_name) {
359 $options = get_option($option_name);
360 if (!empty($options['wpins_opt_out'])) {
361 return true;
362 }
363 }
364 }
365 return false;
366 }
367
368 /**
369 * Check if it's time to track
370 *
371 * @since 1.12.0
372 */
373 public function is_time_to_track() {
374 $track_times = get_option('wpins_last_track_time', array());
375 return !isset($track_times[$this->plugin_name]) ? true :
376 (($track_times[$this->plugin_name]) < strtotime('-1 day') ? true : false);
377 }
378
379 /**
380 * Set tracking time.
381 *
382 * @since 1.12.0
383 */
384 public function set_track_time() {
385 $track_times = get_option('wpins_last_track_time', array());
386 $track_times[$this->plugin_name] = time();
387 update_option('wpins_last_track_time', $track_times);
388 }
389
390 /**
391 * This method is responsible for collecting all data.
392 *
393 * @since 1.12.0
394 */
395 public function get_data() {
396 $body = array(
397 'plugin_slug' => sanitize_text_field($this->plugin_name),
398 'url' => get_bloginfo('url'),
399 'site_name' => get_bloginfo('name'),
400 'site_version' => get_bloginfo('version'),
401 'site_language' => get_bloginfo('language'),
402 'charset' => get_bloginfo('charset'),
403 'wpins_version' => self::WPINS_VERSION,
404 'php_version' => phpversion(),
405 'multisite' => is_multisite(),
406 'file_location' => __FILE__,
407 );
408
409 // Collect the email if the correct option has been set
410 if ($this->marketing) {
411 if (!function_exists('wp_get_current_user')) {
412 include ABSPATH . 'wp-includes/pluggable.php';
413 }
414 $current_user = wp_get_current_user();
415 $email = $current_user->user_email;
416 if (is_email($email)) {
417 $body['email'] = $email;
418 } else {
419 $email = get_option('admin_email');
420 if (is_email($email)) {
421 $body['email'] = $email;
422 }
423 }
424 }
425 $body['marketing_method'] = $this->marketing;
426 $body['server'] = isset($_SERVER['SERVER_SOFTWARE']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_SOFTWARE'])) : '';
427
428 /**
429 * Collect all active and inactive plugins
430 */
431 if (!function_exists('get_plugins')) {
432 include ABSPATH . '/wp-admin/includes/plugin.php';
433 }
434 $plugins = array_keys(get_plugins());
435 $active_plugins = is_network_admin() ? array_keys(get_site_option('active_sitewide_plugins', array())) : get_option('active_plugins', array());
436 foreach ($plugins as $key => $plugin) {
437 if (in_array($plugin, $active_plugins)) {
438 unset($plugins[$key]);
439 }
440 }
441 $body['active_plugins'] = $active_plugins;
442 $body['inactive_plugins'] = $plugins;
443
444 /**
445 * Text Direction.
446 */
447 $body['text_direction'] = (function_exists('is_rtl') ? (is_rtl() ? 'RTL' : 'LTR') : 'NOT SET');
448 /**
449 * Get Our Plugin Data.
450 *
451 * @since 1.12.0
452 */
453 $plugin = $this->plugin_data();
454 if (empty($plugin)) {
455 $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.', 'thinkrank');
456 $body['status'] = 'NOT FOUND';
457 } else {
458 if (isset($plugin['Name'])) {
459 $body['plugin'] = sanitize_text_field($plugin['Name']);
460 }
461 if (isset($plugin['Version'])) {
462 $body['version'] = sanitize_text_field($plugin['Version']);
463 }
464 $body['status'] = 'Active';
465 }
466
467 /**
468 * Get active theme name and version
469 *
470 * @since 1.12.0
471 */
472 $theme = wp_get_theme();
473 if ($theme->Name) {
474 $body['theme'] = sanitize_text_field($theme->Name);
475 }
476 if ($theme->Version) {
477 $body['theme_version'] = sanitize_text_field($theme->Version);
478 }
479
480 $optional_data = $this->get_used_elements_count();
481 if (!empty($optional_data)) {
482 $body['optional_data'] = $optional_data;
483 }
484
485 return $body;
486 }
487
488 /**
489 * Collect plugin data,
490 * Retrieve current plugin information
491 *
492 * @since 1.12.0
493 */
494 public function plugin_data() {
495 if (!function_exists('get_plugin_data')) {
496 include ABSPATH . '/wp-admin/includes/plugin.php';
497 }
498 $plugin = get_plugin_data($this->plugin_file);
499 return $plugin;
500 }
501
502 /**
503 * Send the data to insights.
504 *
505 * @since 1.12.0
506 */
507 public function send_data($body) {
508 /**
509 * Get SITE ID
510 */
511 $site_id_key = "wpins_{$this->plugin_name}_site_id";
512 $site_id = get_option($site_id_key, false);
513 $failed_data = [];
514 $diff_data = [];
515 $site_url = get_bloginfo('url');
516 $original_site_url = get_option("wpins_{$this->plugin_name}_original_url", false);
517
518 if (($original_site_url === false || $original_site_url != $site_url) && version_compare($body['wpins_version'], '3.0.1', '>=')) {
519 $site_id = false;
520 }
521 /**
522 * Send Initial Data to API
523 */
524 if ($site_id == false && $this->item_id !== false) {
525 if (isset($_SERVER['REMOTE_ADDR']) && !empty($_SERVER['REMOTE_ADDR'] && $_SERVER['REMOTE_ADDR'] != '127.0.0.1')) {
526 $country_request = wp_remote_get('https://ip-api.com/json/' . sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) . '?fields=country');
527 if (!is_wp_error($country_request) && $country_request['response']['code'] == 200) {
528 $ip_data = json_decode($country_request["body"]);
529 $body['country'] = isset($ip_data->country) ? $ip_data->country : 'NOT SET';
530 }
531 }
532
533 $body['plugin_slug'] = $this->plugin_name;
534 $body['url'] = $site_url;
535 $body['item_id'] = $this->item_id;
536
537 $request = $this->remote_post($body);
538 if (!is_wp_error($request) && $request['response']['code'] == 200) {
539 $retrieved_body = json_decode(wp_remote_retrieve_body($request), true);
540 if (is_array($retrieved_body) && isset($retrieved_body['siteId'])) {
541 update_option($site_id_key, $retrieved_body['siteId']);
542 update_option("wpins_{$this->plugin_name}_original_url", $site_url);
543 update_option("wpins_{$this->plugin_name}_{$retrieved_body['siteId']}", $body);
544 }
545 } else {
546 $failed_data = $body;
547 }
548 }
549
550 $site_id_data_key = "wpins_{$this->plugin_name}_{$site_id}";
551 $site_id_data_failed_key = "wpins_{$this->plugin_name}_{$site_id}_send_failed";
552
553 if ($site_id != false) {
554 $old_sent_data = get_option($site_id_data_key, []);
555 $diff_data = $this->diff($body, $old_sent_data);
556 $failed_data = get_option($site_id_data_failed_key, []);
557 if (!empty($failed_data) && $diff_data != $failed_data) {
558 $failed_data = array_merge($failed_data, $diff_data);
559 }
560 }
561
562 if (!empty($failed_data) && $site_id != false) {
563 $failed_data['plugin_slug'] = $this->plugin_name;
564 $failed_data['url'] = $site_url;
565 $failed_data['site_id'] = $site_id;
566 if ($original_site_url != false) {
567 $failed_data['original_url'] = $original_site_url;
568 }
569
570 $request = $this->remote_post($failed_data);
571 if (!is_wp_error($request)) {
572 delete_option($site_id_data_failed_key);
573 $replaced_data = array_merge($old_sent_data, $failed_data);
574 update_option($site_id_data_key, $replaced_data);
575 }
576 }
577
578 if (!empty($diff_data) && $site_id != false && empty($failed_data)) {
579 $diff_data['plugin_slug'] = $this->plugin_name;
580 $diff_data['url'] = $site_url;
581 $diff_data['site_id'] = $site_id;
582 if ($original_site_url != false) {
583 $diff_data['original_url'] = $original_site_url;
584 }
585
586 $request = $this->remote_post($diff_data);
587 if (is_wp_error($request)) {
588 update_option($site_id_data_failed_key, $diff_data);
589 } else {
590 $replaced_data = array_merge($old_sent_data, $diff_data);
591 update_option($site_id_data_key, $replaced_data);
592 }
593 }
594
595 $this->set_track_time();
596
597 if (isset($request) && is_wp_error($request)) {
598 return $request;
599 }
600
601 if (isset($request)) {
602 return true;
603 }
604 return false;
605 }
606
607 /**
608 * WP_REMOTE_POST method responsible for send data to the API_URL
609 *
610 * @param array $data
611 * @param array $args
612 * @return mixed
613 */
614 protected function remote_post($data = array(), $args = array()) {
615 if (empty($data)) {
616 return;
617 }
618
619 $args = wp_parse_args($args, array(
620 'method' => 'POST',
621 'timeout' => 30,
622 'redirection' => 5,
623 'httpversion' => '1.1',
624 'blocking' => true,
625 'body' => $data,
626 'user-agent' => 'PUT/1.0.0; ' . get_bloginfo('url'),
627 ));
628 $request = wp_remote_post(esc_url(self::API_URL), $args);
629 if (is_wp_error($request) || (isset($request['response'], $request['response']['code']) && $request['response']['code'] != 200)) {
630 return new \WP_Error(500, 'Something went wrong.');
631 }
632 return $request;
633 }
634
635 /**
636 * Difference between old and new data
637 *
638 * @param array $new_data
639 * @param array $old_data
640 * @return array
641 */
642 protected function diff($new_data, $old_data) {
643 $data = [];
644 if (!empty($new_data)) {
645 foreach ($new_data as $key => $value) {
646 if (isset($old_data[$key])) {
647 if ($old_data[$key] == $value) {
648 continue;
649 }
650 }
651 $data[$key] = $value;
652 }
653 }
654 return $data;
655 }
656
657 /**
658 * Display the admin notice to users to allow them to opt in
659 *
660 * @since 1.12.0
661 */
662 public function notice() {
663 /**
664 * Return if notice is not set.
665 */
666 if (!isset($this->notice_options['notice'])) {
667 return;
668 }
669 /**
670 * Already opted in — there is nothing left to ask for.
671 */
672 if ($this->is_tracking_allowed()) {
673 return;
674 }
675 /**
676 * Check is allowed or blocked for notice.
677 */
678 $block_notice = get_option('wpins_block_notice');
679 if (isset($block_notice[$this->plugin_name])) {
680 return;
681 }
682 if (!current_user_can('manage_options')) {
683 return;
684 }
685 /**
686 * Too soon after install to ask. A consent request on the user's very
687 * first admin page load is the loudest possible first impression, and
688 * it competes with the Setup Wizard the activation redirect just put
689 * them in.
690 */
691 if ($this->notice_after > 0 && time() < $this->notice_after) {
692 return;
693 }
694 /**
695 * Ask on ThinkRank's own screens only. The card used to render on every
696 * admin screen in the site, including other plugins' settings pages.
697 */
698 if (!$this->is_own_screen()) {
699 return;
700 }
701
702 $url_yes = add_query_arg([
703 'plugin' => $this->plugin_name,
704 'plugin_action' => 'yes',
705 ]);
706 $url_no = add_query_arg(array(
707 'plugin' => $this->plugin_name,
708 'plugin_action' => 'no',
709 ));
710
711 $url_yes = wp_nonce_url($url_yes, '_wpnonce_optin_' . $this->plugin_name);
712 $url_no = wp_nonce_url($url_no, '_wpnonce_optin_' . $this->plugin_name);
713
714 // Shared branded notice styles (same card layout as the welcome notice).
715 wp_enqueue_style(
716 'thinkrank-admin-notices',
717 THINKRANK_PLUGIN_URL . 'static/css/admin-notices.css',
718 [],
719 THINKRANK_VERSION
720 );
721
722 // Decide on notice text
723 $notice_title = $this->notice_options['notice_title'] ?? '';
724 $notice_text = $this->notice_options['notice'] . ' <a href="#" class="wpinsights-' . esc_attr($this->plugin_name) . '-collect">' . $this->notice_options['consent_button_text'] . '</a>';
725 $extra_notice_text = $this->notice_options['extra_notice'] ?? '';
726
727 $output = '';
728 $output .= '<div class="notice notice-success put-dismiss-notice thinkrank-notice">';
729 $output .= '<div class="thinkrank-notice__inner">';
730 $output .= '<div class="thinkrank-notice__body">';
731 if ($notice_title !== '') {
732 $output .= '<p class="thinkrank-notice__title">' . esc_html($notice_title) . '</p>';
733 }
734 $output .= '<p class="thinkrank-notice__text">' . $notice_text . '</p>';
735 $output .= '<div class="wpinsights-data" style="display: none;">';
736 $output .= '<p class="thinkrank-notice__text">' . $extra_notice_text . '</p>';
737 $output .= '</div>';
738 $output .= '<p class="thinkrank-notice__actions">';
739 $output .= '<a href="' . esc_url($url_yes) . '" class="button button-primary">' . $this->notice_options['yes'] . '</a>';
740 $output .= '<a href="' . esc_url($url_no) . '" class="thinkrank-notice__dismiss">' . $this->notice_options['no'] . '</a>';
741 $output .= '</p>';
742 $output .= '</div>';
743 $output .= '</div>';
744 $output .= "<script type='text/javascript'>jQuery('.wpinsights-" . esc_attr($this->plugin_name) . "-collect').on('click', function(e) {e.preventDefault();jQuery('.wpinsights-data').slideToggle('fast');});</script>";
745 $output .= '</div>';
746
747 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
748 echo $output;
749 }
750
751 /**
752 * Hold the opt-in notice back until the given time.
753 *
754 * @since 2.8.1
755 * @param int $timestamp Unix timestamp; 0 restores "ask immediately".
756 * @return void
757 */
758 public function set_notice_after($timestamp) {
759 $this->notice_after = max(0, (int) $timestamp);
760 }
761
762 /**
763 * Whether the current admin screen belongs to ThinkRank.
764 *
765 * Matched on the screen id rather than against a list of page slugs:
766 * Admin\Manager already keeps such a list and it has drifted from the
767 * pages actually registered, and Pro adds screens of its own that a list
768 * living in free could not know about.
769 *
770 * The Setup Wizard screen is excluded — it suppresses admin notices
771 * wholesale and collects consent itself, so a card there would be both
772 * invisible and redundant.
773 *
774 * @since 2.8.1
775 * @return bool
776 */
777 private function is_own_screen() {
778 if (!function_exists('get_current_screen')) {
779 return false;
780 }
781
782 $screen = get_current_screen();
783 if (!$screen instanceof \WP_Screen || empty($screen->id)) {
784 return false;
785 }
786
787 $is_own = strpos($screen->id, 'thinkrank') !== false
788 && strpos($screen->id, 'thinkrank_setup_wizard') === false;
789
790 /**
791 * Filter whether the usage-tracking opt-in notice may render here.
792 *
793 * @since 2.8.1
794 * @param bool $is_own Whether this is a ThinkRank screen.
795 * @param \WP_Screen $screen Current screen.
796 */
797 return (bool) apply_filters('thinkrank_usage_notice_is_own_screen', $is_own, $screen);
798 }
799
800 /**
801 * Set all notice options to customized notice.
802 *
803 * @since 1.12.0
804 * @param array $options
805 * @return void
806 */
807 public function set_notice_options($options = []) {
808 $default_options = [
809 'consent_button_text' => __('What we collect.', 'thinkrank'),
810 'yes' => __('Sure, I\'d like to help', 'thinkrank'),
811 'no' => __('No Thanks.', 'thinkrank'),
812 ];
813 $options = wp_parse_args($options, $default_options);
814 $this->notice_options = $options;
815 }
816
817 /**
818 * Responsible for track the click from Notice.
819 *
820 * @return void
821 */
822 public function clicked() {
823 if (isset($_GET['_wpnonce']) && isset($_GET['plugin']) && sanitize_text_field(wp_unslash($_GET['plugin'])) === $this->plugin_name && isset($_GET['plugin_action'])) {
824 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), '_wpnonce_optin_' . $this->plugin_name)) {
825 return;
826 }
827
828 if (isset($_GET['tab']) && $_GET['tab'] === 'plugin-information') {
829 return;
830 }
831 $plugin = sanitize_text_field(wp_unslash($_GET['plugin']));
832 $action = sanitize_text_field(wp_unslash($_GET['plugin_action']));
833 if ($action == 'yes') {
834 $this->schedule_tracking();
835 $this->set_is_tracking_allowed(true, $plugin);
836 if ($this->do_tracking(true)) {
837 $this->update_block_notice($plugin);
838 }
839 /**
840 * Redirect User To the Current URL, but without set query arguments.
841 */
842 wp_safe_redirect($this->redirect_to());
843 } else {
844 $this->set_is_tracking_allowed(false, $plugin);
845 $this->update_block_notice($plugin);
846 }
847 }
848 }
849
850 /**
851 * Programmatically opt the site in to tracking.
852 *
853 * Mirrors the "yes" branch of clicked() without the request redirect, so
854 * other flows (e.g. the Setup Wizard "Get Started" action) can record
855 * consent: schedule the cron, flag tracking allowed, send the first
856 * payload and suppress the opt-in notice from then on.
857 *
858 * @since 1.12.0
859 * @return void
860 */
861 public function opt_in() {
862 $this->schedule_tracking();
863 $this->set_is_tracking_allowed(true, $this->plugin_name);
864 $this->do_tracking(true);
865 $this->update_block_notice($this->plugin_name);
866 }
867
868 /**
869 * Set if we should block the opt-in notice for this plugin
870 *
871 * @since 1.12.0
872 */
873 public function update_block_notice($plugin = null) {
874 if (empty($plugin)) {
875 $plugin = $this->plugin_name;
876 }
877 $block_notice = get_option('wpins_block_notice');
878 if (empty($block_notice) || !is_array($block_notice)) {
879 $block_notice = array($plugin => $plugin);
880 } else {
881 $block_notice[$plugin] = $plugin;
882 }
883 update_option('wpins_block_notice', $block_notice);
884 }
885
886 /**
887 * AJAX callback when the deactivated form is submitted.
888 *
889 * @since 1.12.0
890 */
891 public function deactivate_reasons_form_submit() {
892 check_ajax_referer('wpins_deactivation_nonce', 'security');
893
894 // The form is only reachable from the plugins screen; require the same
895 // capability that screen does so the nonce isn't the only control.
896 if (!current_user_can('activate_plugins')) {
897 wp_die('', '', ['response' => 403]);
898 }
899
900 if (isset($_POST['values'])) {
901 $values = sanitize_text_field(wp_unslash($_POST['values']));
902 update_option('wpins_deactivation_reason_' . $this->plugin_name, $values);
903 }
904 if (isset($_POST['details'])) {
905 $details = sanitize_text_field(wp_unslash($_POST['details']));
906 update_option('wpins_deactivation_details_' . $this->plugin_name, $details);
907 }
908 echo 'success';
909 wp_die();
910 }
911
912 /**
913 * Filter the deactivation link to allow us to present a form when the user
914 * deactivates the plugin
915 *
916 * @since 1.12.0
917 */
918 public function deactivate_action_links($links) {
919 /**
920 * Check is tracking allowed or not.
921 */
922 if (!$this->is_tracking_allowed()) {
923 return $links;
924 }
925 if (isset($links['deactivate']) && $this->include_goodbye_form) {
926 $deactivation_link = $links['deactivate'];
927 /**
928 * Change the default deactivate button link.
929 */
930 $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);
931 $links['deactivate'] = $deactivation_link;
932 }
933 return $links;
934 }
935
936 /**
937 * ALL Deactivate Reasons.
938 *
939 * @since 1.12.0
940 */
941 public function deactivation_reasons() {
942 $form = array();
943 $form['heading'] = __('We Value Your Feedback', 'thinkrank');
944 $form['body'] = __("Could you please tell us why you're deactivating our plugin? Your insights will help us to improve.", 'thinkrank');
945
946 $form['options'] = array(
947 __('I no longer need the plugin', 'thinkrank'),
948 [
949 'label' => __('I found a better plugin', 'thinkrank'),
950 'extra_field' => __('Please share which plugin', 'thinkrank'),
951 ],
952 __("I couldn't get the plugin to work", 'thinkrank'),
953 __('It\'s a temporary deactivation', 'thinkrank'),
954 [
955 'label' => __('Other', 'thinkrank'),
956 'extra_field' => __('Please share the reason', 'thinkrank'),
957 'type' => 'textarea',
958 ],
959 );
960 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
961 return apply_filters('wpins_form_text_' . $this->plugin_name, $form);
962 }
963
964 /**
965 * Deactivate Reasons Form.
966 * This form will appears when user wants to deactivate the plugin to send
967 * you deactivated reasons.
968 *
969 * @since 1.12.0
970 */
971 public function deactivate_reasons_form() {
972 $form = $this->deactivation_reasons();
973 $class_plugin_name = esc_attr($this->plugin_name);
974 $html = '<div class="tr__modal-close-btn"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z"/></svg></div><div class="wpinsights-goodbye-form-head"><span><svg width="65" height="64" viewBox="0 0 65 64" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="0.5" width="64" height="64" rx="16" fill="#EDEDFD"/><path d="M41.8346 22.668L45.8346 18.668M19.168 45.3346L23.168 41.3346M26.5013 34.0013L29.8346 30.668M30.5013 38.0013L33.8346 34.668M24.9013 43.068C25.1986 43.3663 25.5518 43.603 25.9408 43.7645C26.3298 43.926 26.7468 44.0092 27.168 44.0092C27.5891 44.0092 28.0062 43.926 28.3951 43.7645C28.7841 43.603 29.1373 43.3663 29.4346 43.068L32.5013 40.0013L24.5013 32.0013L21.4346 35.068C21.1363 35.3653 20.8996 35.7185 20.7381 36.1075C20.5766 36.4964 20.4934 36.9135 20.4934 37.3346C20.4934 37.7558 20.5766 38.1728 20.7381 38.5618C20.8996 38.9508 21.1363 39.304 21.4346 39.6013L24.9013 43.068ZM32.5013 24.0013L40.5013 32.0013L43.568 28.9346C43.8663 28.6373 44.103 28.2841 44.2645 27.8951C44.426 27.5062 44.5092 27.0891 44.5092 26.668C44.5092 26.2468 44.426 25.8298 44.2645 25.4408C44.103 25.0518 43.8663 24.6986 43.568 24.4013L40.1013 20.9346C39.804 20.6363 39.4508 20.3996 39.0618 20.2381C38.6728 20.0766 38.2558 19.9934 37.8346 19.9934C37.4135 19.9934 36.9964 20.0766 36.6075 20.2381C36.2185 20.3996 35.8653 20.6363 35.568 20.9346L32.5013 24.0013Z" stroke="#4451FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span><h1>' . esc_html($form['heading']) . '</h1></div>';
975 $html .= '<div class="wpinsights-goodbye-form-body"><p class="wpinsights-goodbye-form-caption">' . esc_html($form['body']) . '</p>';
976 if (is_array($form['options'])) {
977 $html .= '<div id="wpinsights-goodbye-options" class="wpinsights-goodbye-options"><ul>';
978 foreach ($form['options'] as $option) {
979 if (is_array($option)) {
980 $id = strtolower(str_replace(" ", "_", esc_attr($option['label'])));
981 $id = $id . '_' . $class_plugin_name;
982 $html .= '<li class="has-goodbye-extra">';
983 $html .= '<input type="radio" name="wpinsights-' . esc_attr($class_plugin_name) . '-goodbye-options" id="' . esc_attr($id) . '" value="' . esc_attr($option['label']) . '">';
984 $html .= '<div><label for="' . $id . '">' . esc_attr($option['label']) . '</label>';
985 if (isset($option['extra_field']) && !isset($option['type'])) {
986 $html .= '<input type="text" style="display: none" name="' . esc_attr($id) . '" id="' . str_replace(" ", "", esc_attr($option['extra_field'])) . '" placeholder="' . esc_attr($option['extra_field']) . '">';
987 }
988 if (isset($option['extra_field']) && isset($option['type'])) {
989 $html .= '<' . $option['type'] . ' style="display: none" type="text" name="' . esc_attr($id) . '" id="' . str_replace(" ", "", esc_attr($option['extra_field'])) . '" placeholder="' . esc_attr($option['extra_field']) . '"></' . $option['type'] . '>';
990 }
991 $html .= '</div></li>';
992 } else {
993 $id = strtolower(str_replace(" ", "_", esc_attr($option)));
994 $id = $id . '_' . $class_plugin_name;
995 $html .= '<li><input type="radio" name="wpinsights-' . $class_plugin_name . '-goodbye-options" id="' . esc_attr($id) . '" value="' . esc_attr($option) . '"> <label for="' . $id . '">' . esc_attr($option) . '</label></li>';
996 }
997 }
998 $html .= '</ul></div><!-- .wpinsights-' . $class_plugin_name . '-goodbye-options -->';
999 }
1000 $html .= '</div><!-- .wpinsights-goodbye-form-body -->';
1001 $html .= '<p class="deactivating-spinner"><span class="spinner"></span> ' . esc_html__('Submitting form', 'thinkrank') . '</p>';
1002
1003 $wrapper_class = '.wpinsights-goodbye-form-wrapper-' . $class_plugin_name;
1004
1005 $styles = '';
1006 $styles .= '<style type="text/css">';
1007 $styles .= '.wpinsights-form-active-' . $class_plugin_name . ' .wpinsights-goodbye-form-bg {';
1008 $styles .= 'background: rgba( 0, 0, 0, .8 );position: fixed;top: 0;left: 0;width: 100%;height: 100%;z-index: 9;';
1009 $styles .= '}';
1010 $styles .= $wrapper_class . '{';
1011 $styles .= 'position: relative; display: none;';
1012 $styles .= '}';
1013 $styles .= '.wpinsights-form-active-' . $class_plugin_name . ' ' . $wrapper_class . '{';
1014 $styles .= 'display: flex !important; position: fixed;top: 0;left: 0;width: 100%;height: 100%; justify-content: center; align-items: center;';
1015 $styles .= '}';
1016 $styles .= $wrapper_class . ' .wpinsights-goodbye-form { display: none; }';
1017 $styles .= '.wpinsights-form-active-' . $class_plugin_name . ' .wpinsights-goodbye-form {';
1018 $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;';
1019 $styles .= '}';
1020 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-head {';
1021 $styles .= 'background: #fff; color: #495157; padding: 18px; box-shadow: 0 0 8px rgba(0,0,0,.1); font-size: 15px;';
1022 $styles .= '}';
1023 $styles .= $wrapper_class . ' .wpinsights-goodbye-form .wpinsights-goodbye-form-head strong { font-size: 15px; }';
1024 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-body { padding: 8px 18px; color: #333; }';
1025 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-body label { padding-left: 5px; color: #6d7882; }';
1026 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-body .wpinsights-goodbye-form-caption {';
1027 $styles .= 'font-weight: 500; font-size: 15px; color: #495157; line-height: 1.4;';
1028 $styles .= '}';
1029 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options { padding-top: 5px; }';
1030 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options ul > li { margin-bottom: 15px; }';
1031 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options ul > li > div { display: inline; padding-left: 3px; }';
1032 $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 {';
1033 $styles .= 'margin: 10px 18px; padding: 8px; width: 80%;';
1034 $styles .= '}';
1035 $styles .= $wrapper_class . ' .deactivating-spinner { display: none; padding-bottom: 20px !important; }';
1036 $styles .= $wrapper_class . ' .deactivating-spinner .spinner { float: none; margin: 4px 4px 0 18px; vertical-align: bottom; visibility: visible; }';
1037 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-footer { padding: 8px 18px; margin-bottom: 15px; }';
1038 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-footer > .wpinsights-goodbye-form-buttons { display: flex; align-items: center; justify-content: space-between; }';
1039 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-footer .wpinsights-submit-btn {';
1040 $styles .= 'background-color: #4451ff; -webkit-border-radius: 4px; border-radius: 4px; color: #fff; line-height: 1; padding: 10px 20px; font-size: 13px; font-weight: 500; text-transform: uppercase; transition: .3s;';
1041 $styles .= '}';
1042 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-footer .wpinsights-submit-btn:hover {';
1043 $styles .= 'background-color: #3a46e0;';
1044 $styles .= '}';
1045 $styles .= $wrapper_class . ' .wpinsights-goodbye-form-footer .wpinsights-deactivate-btn {';
1046 $styles .= 'font-size: 13px; color: #a4afb7; background: none; float: right; padding-right: 10px; width: auto; text-decoration: underline;';
1047 $styles .= '}';
1048 $styles .= '
1049 .wpinsights-form-active-' . $class_plugin_name . ' .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' {
1050 display: flex !important;
1051 position: fixed;
1052 top: 0;
1053 left: 0;
1054 right: 0;
1055 bottom: 0;
1056 width: 100%;
1057 z-index: 9999;
1058 height: 100%;
1059 justify-content: center;
1060 align-items: center;
1061 }
1062 .wpinsights-form-active-' . $class_plugin_name . ' .wpinsights-goodbye-form {
1063 border-radius: 8px;
1064 width: 563px;
1065 overflow: visible;
1066 position: relative;
1067 font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
1068 }
1069 .wpinsights-goodbye-form .tr__modal-close-btn {
1070 position: absolute;
1071 top: 0;
1072 right: -55px;
1073 width: 40px;
1074 height: 40px;
1075 background: #fff;
1076 border-radius: 50%;
1077 display: flex;
1078 justify-content: center;
1079 align-items: center;
1080 font-size: 12px;
1081 font-weight: 500;
1082 color: #475467;
1083 cursor: pointer;
1084 }
1085 .wpinsights-goodbye-form .tr__modal-close-btn svg {
1086 width: 20px;
1087 }
1088 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-head {
1089 background: #fff;
1090 color: #495157;
1091 padding: 24px;
1092 border-radius: 8px 8px 0 0;
1093 box-shadow: none;
1094 border-bottom: 1px solid #EAECF0;
1095 display: flex;
1096 flex-direction: column;
1097 gap: 16px;
1098 align-items: center;
1099 }
1100 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-head > span {
1101 display: inline-flex;
1102 }
1103 .wpinsights-goodbye-form-head h1 {
1104 font-size: 22px;
1105 font-weight: 450;
1106 color: #1D2939;
1107 padding: 0;
1108 }
1109 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-body {
1110 padding: 32px 40px;
1111 max-height: calc(70vh - 230px);
1112 overflow: auto;
1113 }
1114 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-body .wpinsights-goodbye-form-caption {
1115 font-size: 18px;
1116 color: #1D2939;
1117 margin: 0;
1118 padding-bottom: 16px;
1119 font-weight: 400;
1120 }
1121 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options {
1122 padding-top: 0px;
1123 }
1124 .widefat td ul {
1125 font-size: 14px;
1126 margin: 0;
1127 color: #475467;
1128 line-height: 1.4em;
1129 }
1130 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options ul > li:not(:last-child) {
1131 margin-bottom: 16px;
1132 }
1133 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options ul > li:last-child {
1134 margin-bottom: 0;
1135 }
1136 input[type=radio] {
1137 border-radius: 50%;
1138 margin-right: .25rem;
1139 line-height: .71428571;
1140 margin: 0;
1141 }
1142 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-body label {
1143 padding-left: 4px;
1144 color: #475467;
1145 font-size: 14px;
1146 }
1147 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options ul > li > div > input, .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options ul > li > div > textarea {
1148 padding: 10px 16px;
1149 border-radius: 8px;
1150 width: 100%;
1151 border: 1px solid #D0D5DD;
1152 margin: 0;
1153 margin-top: 16px;
1154 color: #1D2939;
1155 font-size: 14px;
1156 line-height: 1.5em;
1157 resize: none;
1158 max-height: 44px;
1159 }
1160 input[type=radio] {
1161 border: 1px solid #D0D5DD;
1162 box-shadow: none;
1163 }
1164 input[type=radio]:checked::before {
1165 content: "";
1166 border-radius: 50%;
1167 width: .88rem;
1168 height: .88rem;
1169 margin: -.95px;
1170 background-color: #4451ff;
1171 border: 1px solid #c7ccff;
1172 line-height: 1.14285714;
1173 outline: 0;
1174 }
1175 input[type=radio]:focus {
1176 border-color: #D0D5DD;
1177 box-shadow: none;
1178 outline: 0;
1179 }
1180 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options ul > li > div > input::placeholder,
1181 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-body #wpinsights-goodbye-options ul > li > div > textarea::placeholder{
1182 color: #C6CBD2 !important;
1183 }
1184 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-footer {
1185 padding: 16px 36px;
1186 margin-bottom: 0;
1187 border-top: 1px solid #EAECF0;
1188 }
1189 input:focus,
1190 textarea:focus {
1191 box-shadow: none !important;
1192 outline: 0 !important;
1193 }
1194 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-footer > .wpinsights-goodbye-form-buttons {
1195 display: flex;
1196 align-items: center;
1197 justify-content: flex-start;
1198 }
1199 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-footer .wpinsights-submit-btn {
1200 background: #4451ff;
1201 -webkit-border-radius: 8px;
1202 border-radius: 8px;
1203 color: #fff;
1204 padding: 8px 16px;
1205 font-size: 14px;
1206 font-weight: 500;
1207 line-height: 1.5em;
1208 text-transform: capitalize;
1209 transition: .3s;
1210 }
1211 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-footer .wpinsights-submit-btn:hover {
1212 background: #3a46e0;
1213 }
1214 .wpinsights-goodbye-form-wrapper-' . $class_plugin_name . ' .wpinsights-goodbye-form-footer .wpsp-put-deactivate-btn {
1215 font-size: 14px;
1216 font-weight: 500;
1217 color: #344054;
1218 margin-left: 10px;
1219 }
1220 .wp-person a:focus .gravatar, a:focus, a:focus .media-icon img, a:focus .plugin-icon {
1221 color: #043959;
1222 box-shadow: none;
1223 outline: 0;
1224 }
1225 ';
1226 $styles .= '</style>';
1227
1228 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1229 echo $styles;
1230 ?>
1231 <script type="text/javascript">
1232 jQuery(document).ready(function($){
1233 $("#wpinsights-goodbye-link-<?php echo esc_js($class_plugin_name); ?>").on("click",function(){
1234 // We'll send the user to this deactivation link when they've completed or dismissed the form
1235 var url = document.getElementById("wpinsights-goodbye-link-<?php echo esc_js($class_plugin_name); ?>");
1236 $('body').toggleClass('wpinsights-form-active-<?php echo esc_js($class_plugin_name); ?>');
1237 $(".wpinsights-goodbye-form-wrapper-<?php echo esc_js($class_plugin_name); ?> #wpinsights-goodbye-form").fadeIn();
1238 $(".wpinsights-goodbye-form-wrapper-<?php echo esc_js($class_plugin_name); ?> #wpinsights-goodbye-form").html( '<?php
1239 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1240 echo $html; ?>' + '<div class="wpinsights-goodbye-form-footer"><div class="wpinsights-goodbye-form-buttons"><a id="wpinsights-submit-form-<?php echo esc_js($class_plugin_name); ?>" class="wpinsights-submit-btn" href="#"><?php esc_html_e('Submit and Deactivate', 'thinkrank'); ?></a>&nbsp;<a class="wpsp-put-deactivate-btn" href="'+url+'"><?php esc_html_e('Just Deactivate', 'thinkrank'); ?></a></div></div>');
1241 $('#wpinsights-submit-form-<?php echo esc_js($class_plugin_name); ?>').on('click', function(e){
1242 // As soon as we click, the body of the form should disappear
1243 $("#wpinsights-goodbye-form-<?php echo esc_js($class_plugin_name); ?> .wpinsights-goodbye-form-body").fadeOut();
1244 $("#wpinsights-goodbye-form-<?php echo esc_js($class_plugin_name); ?> .wpinsights-goodbye-form-footer").fadeOut();
1245 // Fade in spinner
1246 $("#wpinsights-goodbye-form-<?php echo esc_js($class_plugin_name); ?> .deactivating-spinner").fadeIn();
1247 e.preventDefault();
1248 var checkedInput = $("input[name='wpinsights-<?php echo esc_attr($class_plugin_name); ?>-goodbye-options']:checked"),
1249 checkedInputVal, details;
1250 if( checkedInput.length > 0 ) {
1251 checkedInputVal = checkedInput.val();
1252 details = $('input[name="'+ checkedInput[0].id +'"], textarea[name="'+ checkedInput[0].id +'"]').val();
1253 }
1254
1255 if( typeof details === 'undefined' ) {
1256 details = '';
1257 }
1258 if( typeof checkedInputVal === 'undefined' ) {
1259 checkedInputVal = 'No Reason';
1260 }
1261
1262 var data = {
1263 'action': 'deactivation_form_<?php echo esc_attr($class_plugin_name); ?>',
1264 'values': checkedInputVal,
1265 'details': details,
1266 'security': "<?php
1267 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1268 echo esc_js(wp_create_nonce('wpins_deactivation_nonce')); ?>",
1269 'dataType': "json"
1270 }
1271
1272 $.post(
1273 ajaxurl,
1274 data,
1275 function(response){
1276 // Redirect to original deactivation URL
1277 window.location.href = url;
1278 }
1279 );
1280 });
1281 $('#wpinsights-goodbye-options > ul ').on('click', 'li label, li > input', function( e ){
1282 var parent = $(this).parents('li');
1283 parent.siblings().find('label').next('input, textarea').css('display', 'none');
1284 parent.find('label').next('input, textarea').css('display', 'block');
1285 });
1286 // If we click outside the form, the form will close
1287 $('.wpinsights-goodbye-form-bg, #wpinsights-goodbye-form > .tr__modal-close-btn').on('click',function(){
1288 $("#wpinsights-goodbye-form").fadeOut();
1289 $('body').removeClass('wpinsights-form-active-<?php echo esc_attr($class_plugin_name); ?>');
1290 });
1291 });
1292 });
1293 </script>
1294 <?php }
1295
1296 /**
1297 * Get Used Elements Count
1298 *
1299 * Optional data payload describing which ThinkRank features are enabled
1300 * on the site. Reported as a name => count map (1 = enabled) so the
1301 * insights backend can aggregate feature adoption.
1302 *
1303 * @return array
1304 *
1305 * @since 1.12.0
1306 */
1307 public static function get_used_elements_count() {
1308 $used = [];
1309
1310 // AI provider in use.
1311 $ai_provider = get_option('thinkrank_ai_provider', '');
1312 if (!empty($ai_provider)) {
1313 $used['ai_provider_' . sanitize_key($ai_provider)] = 1;
1314 }
1315
1316 // Boolean feature flags stored as standalone options.
1317 $flag_options = [
1318 'thinkrank_auto_optimize' => 'auto_optimize',
1319 'thinkrank_enable_logging' => 'logging',
1320 ];
1321 foreach ($flag_options as $option_name => $label) {
1322 if (!empty(get_option($option_name))) {
1323 $used[$label] = 1;
1324 }
1325 }
1326
1327 // Feature settings groups: report when configured/enabled.
1328 $feature_settings = [
1329 'thinkrank_instant_indexing_settings' => 'instant_indexing',
1330 'thinkrank_author_archives_settings' => 'author_archives',
1331 'thinkrank_image_seo_settings' => 'image_seo',
1332 ];
1333 foreach ($feature_settings as $option_name => $label) {
1334 $settings = get_option($option_name, []);
1335 if (is_array($settings) && !empty($settings['enabled'])) {
1336 $used[$label] = 1;
1337 }
1338 }
1339
1340 // Indexed content volume buckets (lightweight published-post count).
1341 $published = (int) wp_count_posts('post')->publish;
1342 if ($published > 0) {
1343 $used['published_posts'] = $published;
1344 }
1345
1346 return $used;
1347 }
1348 }
1349