plugin_file = $plugin_file; $this->plugin_name = basename($this->plugin_file, '.php'); $this->disabled_wp_cron = defined('DISABLE_WP_CRON') && DISABLE_WP_CRON == true; $this->enable_self_cron = $this->disabled_wp_cron == true ? true : false; $this->event_hook = 'thinkrank_put_do_daily_action'; $this->require_optin = isset($args['opt_in']) ? $args['opt_in'] : true; $this->include_goodbye_form = isset($args['goodbye_form']) ? $args['goodbye_form'] : true; $this->marketing = isset($args['email_marketing']) ? $args['email_marketing'] : true; $this->options = isset($args['options']) ? $args['options'] : []; $this->item_id = isset($args['item_id']) ? $args['item_id'] : false; /** * Activation Hook */ register_activation_hook($this->plugin_file, array($this, 'activate_this_plugin')); /** * Deactivation Hook */ register_deactivation_hook($this->plugin_file, array($this, 'deactivate_this_plugin')); } /** * When user agreed to opt-in tracking schedule is enabled. * * @since 1.12.0 */ public function schedule_tracking() { if ($this->disabled_wp_cron) { return; } if (!wp_next_scheduled($this->event_hook)) { wp_schedule_event(time(), $this->recurrence, $this->event_hook); } } /** * Add the schedule event if the plugin is tracked. * * @return void */ public function activate_this_plugin() { $allow_tracking = $this->is_tracking_allowed(); if (!$allow_tracking) { return; } $this->schedule_tracking(); } /** * Remove the schedule event when plugin is deactivated and send the * deactivated reason to insights if user submitted. * * @since 1.12.0 */ public function deactivate_this_plugin() { /** * Check tracking is allowed or not. */ $allow_tracking = $this->is_tracking_allowed(); if (!$allow_tracking) { return; } $body = $this->get_data(); $body['status'] = 'Deactivated'; $body['deactivated_date'] = time(); // Check deactivation reason and add for insights data. if (false !== get_option('wpins_deactivation_reason_' . $this->plugin_name)) { $body['deactivation_reason'] = get_option('wpins_deactivation_reason_' . $this->plugin_name); } if (false !== get_option('wpins_deactivation_details_' . $this->plugin_name)) { $body['deactivation_details'] = get_option('wpins_deactivation_details_' . $this->plugin_name); } $this->send_data($body); delete_option('wpins_deactivation_reason_' . $this->plugin_name); delete_option('wpins_deactivation_details_' . $this->plugin_name); /** * Clear the event schedule. */ if (!$this->disabled_wp_cron) { wp_clear_scheduled_hook($this->event_hook); } } /** * Initial Method to Hook Everything. * * @return void */ public function init() { add_action('admin_init', array($this, 'clicked')); add_action($this->event_hook, array($this, 'do_tracking')); add_action('admin_notices', array($this, 'notice')); /** * Deactivation Reason Form and Submit Data to Insights. */ add_filter('plugin_action_links_' . plugin_basename($this->plugin_file), array($this, 'deactivate_action_links')); add_action('admin_footer-plugins.php', array($this, 'deactivate_reasons_form')); add_action('wp_ajax_deactivation_form_' . esc_attr($this->plugin_name), array($this, 'deactivate_reasons_form_submit')); } /** * For Redirecting Current Page without Arguments! * * @return string */ private function redirect_to() { $request_uri = !empty($_SERVER['REQUEST_URI']) ? wp_parse_url(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])), PHP_URL_PATH) : ''; $query_string = !empty($_SERVER['REQUEST_URI']) ? wp_parse_url(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])), PHP_URL_QUERY) : ''; parse_str((string) $query_string, $current_url); $unset_array = array('dismiss', 'plugin', '_wpnonce', 'later', 'plugin_action', 'marketing_optin'); foreach ($unset_array as $value) { if (isset($current_url[$value])) { unset($current_url[$value]); } } $current_url = http_build_query($current_url); $redirect_url = $request_uri . '?' . $current_url; return $redirect_url; } /** * This method forcing the do_tracking method to execute instant. * * @return void */ public function force_tracking() { $this->do_tracking(true); } /** * This method is responsible for all the magic from the front of the plugin. * * @since 1.12.0 * @param bool $force Force tracking if it's not the correct time to track. */ public function do_tracking($force = false) { /** * Check URL is set or not. */ if (empty(self::API_URL)) { return; } /** * Check is tracking allowed or not. */ if (!$this->is_tracking_allowed()) { return; } /** * Check is this the correct time to track or not. * or Force to track. */ if (!$this->is_time_to_track() && !$force) { return; } /** * Get All Data. */ $body = $this->get_data(); /** * Send all data. */ return $this->send_data($body); } /** * Is tracking allowed? * * @since 1.12.0 */ private function is_tracking_allowed() { // First, check if the user has changed their mind and opted out of tracking if ($this->has_user_opted_out()) { $this->set_is_tracking_allowed(false, $this->plugin_name); return false; } // The wpins_allow_tracking option is an array of plugins that are being tracked $allow_tracking = get_option('wpins_allow_tracking'); // If this plugin is in the array, then tracking is allowed if (isset($allow_tracking[$this->plugin_name])) { return true; } return false; } /** * Set a flag in DB If tracking is allowed. * * @since 1.12.0 * @param bool $is_allowed true if is allowed. * @param string|null $plugin */ public function set_is_tracking_allowed($is_allowed, $plugin = null) { if (empty($plugin)) { $plugin = $this->plugin_name; } /** * Get All Tracked Plugin List using this Tracker. */ $allow_tracking = get_option('wpins_allow_tracking'); /** * Check user is opted out for tracking or not. */ if ($this->has_user_opted_out()) { if (isset($allow_tracking[$plugin])) { unset($allow_tracking[$plugin]); } } elseif ($is_allowed || !$this->require_optin) { /** * If user has agreed to allow tracking */ if (empty($allow_tracking) || !is_array($allow_tracking)) { $allow_tracking = array($plugin => $plugin); } else { $allow_tracking[$plugin] = $plugin; } } else { if (isset($allow_tracking[$plugin])) { unset($allow_tracking[$plugin]); } } update_option('wpins_allow_tracking', $allow_tracking); } /** * Check the user has opted out or not. * * @since 1.12.0 * @return bool */ protected function has_user_opted_out() { if (!empty($this->options)) { foreach ($this->options as $option_name) { $options = get_option($option_name); if (!empty($options['wpins_opt_out'])) { return true; } } } return false; } /** * Check if it's time to track * * @since 1.12.0 */ public function is_time_to_track() { $track_times = get_option('wpins_last_track_time', array()); return !isset($track_times[$this->plugin_name]) ? true : (($track_times[$this->plugin_name]) < strtotime('-1 day') ? true : false); } /** * Set tracking time. * * @since 1.12.0 */ public function set_track_time() { $track_times = get_option('wpins_last_track_time', array()); $track_times[$this->plugin_name] = time(); update_option('wpins_last_track_time', $track_times); } /** * This method is responsible for collecting all data. * * @since 1.12.0 */ public function get_data() { $body = array( 'plugin_slug' => sanitize_text_field($this->plugin_name), 'url' => get_bloginfo('url'), 'site_name' => get_bloginfo('name'), 'site_version' => get_bloginfo('version'), 'site_language' => get_bloginfo('language'), 'charset' => get_bloginfo('charset'), 'wpins_version' => self::WPINS_VERSION, 'php_version' => phpversion(), 'multisite' => is_multisite(), 'file_location' => __FILE__, ); // Collect the email if the correct option has been set if ($this->marketing) { if (!function_exists('wp_get_current_user')) { include ABSPATH . 'wp-includes/pluggable.php'; } $current_user = wp_get_current_user(); $email = $current_user->user_email; if (is_email($email)) { $body['email'] = $email; } else { $email = get_option('admin_email'); if (is_email($email)) { $body['email'] = $email; } } } $body['marketing_method'] = $this->marketing; $body['server'] = isset($_SERVER['SERVER_SOFTWARE']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_SOFTWARE'])) : ''; /** * Collect all active and inactive plugins */ if (!function_exists('get_plugins')) { include ABSPATH . '/wp-admin/includes/plugin.php'; } $plugins = array_keys(get_plugins()); $active_plugins = is_network_admin() ? array_keys(get_site_option('active_sitewide_plugins', array())) : get_option('active_plugins', array()); foreach ($plugins as $key => $plugin) { if (in_array($plugin, $active_plugins)) { unset($plugins[$key]); } } $body['active_plugins'] = $active_plugins; $body['inactive_plugins'] = $plugins; /** * Text Direction. */ $body['text_direction'] = (function_exists('is_rtl') ? (is_rtl() ? 'RTL' : 'LTR') : 'NOT SET'); /** * Get Our Plugin Data. * * @since 1.12.0 */ $plugin = $this->plugin_data(); if (empty($plugin)) { $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'); $body['status'] = 'NOT FOUND'; } else { if (isset($plugin['Name'])) { $body['plugin'] = sanitize_text_field($plugin['Name']); } if (isset($plugin['Version'])) { $body['version'] = sanitize_text_field($plugin['Version']); } $body['status'] = 'Active'; } /** * Get active theme name and version * * @since 1.12.0 */ $theme = wp_get_theme(); if ($theme->Name) { $body['theme'] = sanitize_text_field($theme->Name); } if ($theme->Version) { $body['theme_version'] = sanitize_text_field($theme->Version); } $optional_data = $this->get_used_elements_count(); if (!empty($optional_data)) { $body['optional_data'] = $optional_data; } return $body; } /** * Collect plugin data, * Retrieve current plugin information * * @since 1.12.0 */ public function plugin_data() { if (!function_exists('get_plugin_data')) { include ABSPATH . '/wp-admin/includes/plugin.php'; } $plugin = get_plugin_data($this->plugin_file); return $plugin; } /** * Send the data to insights. * * @since 1.12.0 */ public function send_data($body) { /** * Get SITE ID */ $site_id_key = "wpins_{$this->plugin_name}_site_id"; $site_id = get_option($site_id_key, false); $failed_data = []; $diff_data = []; $site_url = get_bloginfo('url'); $original_site_url = get_option("wpins_{$this->plugin_name}_original_url", false); if (($original_site_url === false || $original_site_url != $site_url) && version_compare($body['wpins_version'], '3.0.1', '>=')) { $site_id = false; } /** * Send Initial Data to API */ if ($site_id == false && $this->item_id !== false) { if (isset($_SERVER['REMOTE_ADDR']) && !empty($_SERVER['REMOTE_ADDR'] && $_SERVER['REMOTE_ADDR'] != '127.0.0.1')) { $country_request = wp_remote_get('https://ip-api.com/json/' . sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) . '?fields=country'); if (!is_wp_error($country_request) && $country_request['response']['code'] == 200) { $ip_data = json_decode($country_request["body"]); $body['country'] = isset($ip_data->country) ? $ip_data->country : 'NOT SET'; } } $body['plugin_slug'] = $this->plugin_name; $body['url'] = $site_url; $body['item_id'] = $this->item_id; $request = $this->remote_post($body); if (!is_wp_error($request) && $request['response']['code'] == 200) { $retrieved_body = json_decode(wp_remote_retrieve_body($request), true); if (is_array($retrieved_body) && isset($retrieved_body['siteId'])) { update_option($site_id_key, $retrieved_body['siteId']); update_option("wpins_{$this->plugin_name}_original_url", $site_url); update_option("wpins_{$this->plugin_name}_{$retrieved_body['siteId']}", $body); } } else { $failed_data = $body; } } $site_id_data_key = "wpins_{$this->plugin_name}_{$site_id}"; $site_id_data_failed_key = "wpins_{$this->plugin_name}_{$site_id}_send_failed"; if ($site_id != false) { $old_sent_data = get_option($site_id_data_key, []); $diff_data = $this->diff($body, $old_sent_data); $failed_data = get_option($site_id_data_failed_key, []); if (!empty($failed_data) && $diff_data != $failed_data) { $failed_data = array_merge($failed_data, $diff_data); } } if (!empty($failed_data) && $site_id != false) { $failed_data['plugin_slug'] = $this->plugin_name; $failed_data['url'] = $site_url; $failed_data['site_id'] = $site_id; if ($original_site_url != false) { $failed_data['original_url'] = $original_site_url; } $request = $this->remote_post($failed_data); if (!is_wp_error($request)) { delete_option($site_id_data_failed_key); $replaced_data = array_merge($old_sent_data, $failed_data); update_option($site_id_data_key, $replaced_data); } } if (!empty($diff_data) && $site_id != false && empty($failed_data)) { $diff_data['plugin_slug'] = $this->plugin_name; $diff_data['url'] = $site_url; $diff_data['site_id'] = $site_id; if ($original_site_url != false) { $diff_data['original_url'] = $original_site_url; } $request = $this->remote_post($diff_data); if (is_wp_error($request)) { update_option($site_id_data_failed_key, $diff_data); } else { $replaced_data = array_merge($old_sent_data, $diff_data); update_option($site_id_data_key, $replaced_data); } } $this->set_track_time(); if (isset($request) && is_wp_error($request)) { return $request; } if (isset($request)) { return true; } return false; } /** * WP_REMOTE_POST method responsible for send data to the API_URL * * @param array $data * @param array $args * @return mixed */ protected function remote_post($data = array(), $args = array()) { if (empty($data)) { return; } $args = wp_parse_args($args, array( 'method' => 'POST', 'timeout' => 30, 'redirection' => 5, 'httpversion' => '1.1', 'blocking' => true, 'body' => $data, 'user-agent' => 'PUT/1.0.0; ' . get_bloginfo('url'), )); $request = wp_remote_post(esc_url(self::API_URL), $args); if (is_wp_error($request) || (isset($request['response'], $request['response']['code']) && $request['response']['code'] != 200)) { return new \WP_Error(500, 'Something went wrong.'); } return $request; } /** * Difference between old and new data * * @param array $new_data * @param array $old_data * @return array */ protected function diff($new_data, $old_data) { $data = []; if (!empty($new_data)) { foreach ($new_data as $key => $value) { if (isset($old_data[$key])) { if ($old_data[$key] == $value) { continue; } } $data[$key] = $value; } } return $data; } /** * Display the admin notice to users to allow them to opt in * * @since 1.12.0 */ public function notice() { /** * Return if notice is not set. */ if (!isset($this->notice_options['notice'])) { return; } /** * Already opted in — there is nothing left to ask for. */ if ($this->is_tracking_allowed()) { return; } /** * Check is allowed or blocked for notice. */ $block_notice = get_option('wpins_block_notice'); if (isset($block_notice[$this->plugin_name])) { return; } if (!current_user_can('manage_options')) { return; } $url_yes = add_query_arg([ 'plugin' => $this->plugin_name, 'plugin_action' => 'yes', ]); $url_no = add_query_arg(array( 'plugin' => $this->plugin_name, 'plugin_action' => 'no', )); $url_yes = wp_nonce_url($url_yes, '_wpnonce_optin_' . $this->plugin_name); $url_no = wp_nonce_url($url_no, '_wpnonce_optin_' . $this->plugin_name); // Shared branded notice styles (same card layout as the welcome notice). wp_enqueue_style( 'thinkrank-admin-notices', THINKRANK_PLUGIN_URL . 'static/css/admin-notices.css', [], THINKRANK_VERSION ); // Decide on notice text $notice_title = $this->notice_options['notice_title'] ?? ''; $notice_text = $this->notice_options['notice'] . ' ' . $this->notice_options['consent_button_text'] . ''; $extra_notice_text = $this->notice_options['extra_notice'] ?? ''; $output = ''; $output .= '
'; $output .= '
'; $output .= '
'; if ($notice_title !== '') { $output .= '

' . esc_html($notice_title) . '

'; } $output .= '

' . $notice_text . '

'; $output .= ''; $output .= '

'; $output .= '' . $this->notice_options['yes'] . ''; $output .= '' . $this->notice_options['no'] . ''; $output .= '

'; $output .= '
'; $output .= '
'; $output .= ""; $output .= '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $output; } /** * Set all notice options to customized notice. * * @since 1.12.0 * @param array $options * @return void */ public function set_notice_options($options = []) { $default_options = [ 'consent_button_text' => __('What we collect.', 'thinkrank'), 'yes' => __('Sure, I\'d like to help', 'thinkrank'), 'no' => __('No Thanks.', 'thinkrank'), ]; $options = wp_parse_args($options, $default_options); $this->notice_options = $options; } /** * Responsible for track the click from Notice. * * @return void */ public function clicked() { if (isset($_GET['_wpnonce']) && isset($_GET['plugin']) && sanitize_text_field(wp_unslash($_GET['plugin'])) === $this->plugin_name && isset($_GET['plugin_action'])) { if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), '_wpnonce_optin_' . $this->plugin_name)) { return; } if (isset($_GET['tab']) && $_GET['tab'] === 'plugin-information') { return; } $plugin = sanitize_text_field(wp_unslash($_GET['plugin'])); $action = sanitize_text_field(wp_unslash($_GET['plugin_action'])); if ($action == 'yes') { $this->schedule_tracking(); $this->set_is_tracking_allowed(true, $plugin); if ($this->do_tracking(true)) { $this->update_block_notice($plugin); } /** * Redirect User To the Current URL, but without set query arguments. */ wp_safe_redirect($this->redirect_to()); } else { $this->set_is_tracking_allowed(false, $plugin); $this->update_block_notice($plugin); } } } /** * Programmatically opt the site in to tracking. * * Mirrors the "yes" branch of clicked() without the request redirect, so * other flows (e.g. the Setup Wizard "Get Started" action) can record * consent: schedule the cron, flag tracking allowed, send the first * payload and suppress the opt-in notice from then on. * * @since 1.12.0 * @return void */ public function opt_in() { $this->schedule_tracking(); $this->set_is_tracking_allowed(true, $this->plugin_name); $this->do_tracking(true); $this->update_block_notice($this->plugin_name); } /** * Set if we should block the opt-in notice for this plugin * * @since 1.12.0 */ public function update_block_notice($plugin = null) { if (empty($plugin)) { $plugin = $this->plugin_name; } $block_notice = get_option('wpins_block_notice'); if (empty($block_notice) || !is_array($block_notice)) { $block_notice = array($plugin => $plugin); } else { $block_notice[$plugin] = $plugin; } update_option('wpins_block_notice', $block_notice); } /** * AJAX callback when the deactivated form is submitted. * * @since 1.12.0 */ public function deactivate_reasons_form_submit() { check_ajax_referer('wpins_deactivation_nonce', 'security'); // The form is only reachable from the plugins screen; require the same // capability that screen does so the nonce isn't the only control. if (!current_user_can('activate_plugins')) { wp_die('', '', ['response' => 403]); } if (isset($_POST['values'])) { $values = sanitize_text_field(wp_unslash($_POST['values'])); update_option('wpins_deactivation_reason_' . $this->plugin_name, $values); } if (isset($_POST['details'])) { $details = sanitize_text_field(wp_unslash($_POST['details'])); update_option('wpins_deactivation_details_' . $this->plugin_name, $details); } echo 'success'; wp_die(); } /** * Filter the deactivation link to allow us to present a form when the user * deactivates the plugin * * @since 1.12.0 */ public function deactivate_action_links($links) { /** * Check is tracking allowed or not. */ if (!$this->is_tracking_allowed()) { return $links; } if (isset($links['deactivate']) && $this->include_goodbye_form) { $deactivation_link = $links['deactivate']; /** * Change the default deactivate button link. */ $deactivation_link = str_replace('
__('I found a better plugin', 'thinkrank'), 'extra_field' => __('Please share which plugin', 'thinkrank'), ], __("I couldn't get the plugin to work", 'thinkrank'), __('It\'s a temporary deactivation', 'thinkrank'), [ 'label' => __('Other', 'thinkrank'), 'extra_field' => __('Please share the reason', 'thinkrank'), 'type' => 'textarea', ], ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound return apply_filters('wpins_form_text_' . $this->plugin_name, $form); } /** * Deactivate Reasons Form. * This form will appears when user wants to deactivate the plugin to send * you deactivated reasons. * * @since 1.12.0 */ public function deactivate_reasons_form() { $form = $this->deactivation_reasons(); $class_plugin_name = esc_attr($this->plugin_name); $html = '

' . esc_html($form['heading']) . '

'; $html .= '

' . esc_html($form['body']) . '

'; if (is_array($form['options'])) { $html .= '
    '; foreach ($form['options'] as $option) { if (is_array($option)) { $id = strtolower(str_replace(" ", "_", esc_attr($option['label']))); $id = $id . '_' . $class_plugin_name; $html .= '
  • '; $html .= ''; $html .= '
    '; if (isset($option['extra_field']) && !isset($option['type'])) { $html .= ''; } if (isset($option['extra_field']) && isset($option['type'])) { $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']) . '">'; } $html .= '
  • '; } else { $id = strtolower(str_replace(" ", "_", esc_attr($option))); $id = $id . '_' . $class_plugin_name; $html .= '
  • '; } } $html .= '
'; } $html .= '
'; $html .= '

' . esc_html__('Submitting form', 'thinkrank') . '

'; $wrapper_class = '.wpinsights-goodbye-form-wrapper-' . $class_plugin_name; $styles = ''; $styles .= ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $styles; ?> count map (1 = enabled) so the * insights backend can aggregate feature adoption. * * @return array * * @since 1.12.0 */ public static function get_used_elements_count() { $used = []; // AI provider in use. $ai_provider = get_option('thinkrank_ai_provider', ''); if (!empty($ai_provider)) { $used['ai_provider_' . sanitize_key($ai_provider)] = 1; } // Boolean feature flags stored as standalone options. $flag_options = [ 'thinkrank_auto_optimize' => 'auto_optimize', 'thinkrank_enable_logging' => 'logging', ]; foreach ($flag_options as $option_name => $label) { if (!empty(get_option($option_name))) { $used[$label] = 1; } } // Feature settings groups: report when configured/enabled. $feature_settings = [ 'thinkrank_instant_indexing_settings' => 'instant_indexing', 'thinkrank_author_archives_settings' => 'author_archives', 'thinkrank_image_seo_settings' => 'image_seo', ]; foreach ($feature_settings as $option_name => $label) { $settings = get_option($option_name, []); if (is_array($settings) && !empty($settings['enabled'])) { $used[$label] = 1; } } // Indexed content volume buckets (lightweight published-post count). $published = (int) wp_count_posts('post')->publish; if ($published > 0) { $used['published_posts'] = $published; } return $used; } }