| @@ -162,10 +162,11 @@ | ||
| 162 | 162 | * |
| 163 | 163 | * @return void |
| 164 | 164 | */ |
| 165 | 165 | private function redirect_to() { |
| 166 | - $request_uri = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ); | |
| 167 | - $query_string = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_QUERY ); | |
| 166 | + $current_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; | |
| 167 | + $request_uri = wp_parse_url( $current_uri, PHP_URL_PATH ); | |
| 168 | + $query_string = wp_parse_url( $current_uri, PHP_URL_QUERY ); | |
| 168 | 169 | parse_str( $query_string, $current_url ); |
| 169 | 170 | |
| 170 | 171 | $unset_array = array( 'dismiss', 'plugin', '_wpnonce', 'later', 'plugin_action', 'marketing_optin' ); |
| 171 | 172 | |
| @@ -187,8 +188,30 @@ | ||
| 187 | 188 | public function force_tracking() { |
| 188 | 189 | $this->do_tracking( true ); |
| 189 | 190 | } |
| 190 | 191 | /** |
| 192 | + * Record the user's explicit opt-in and (optionally) send the data to the | |
| 193 | + * insights API immediately. Used by the onboarding Setup Wizard when the | |
| 194 | + * user proceeds past the Welcome step (which is the consent point). | |
| 195 | + * | |
| 196 | + * Collection itself no longer depends on this call — see | |
| 197 | + * {@see self::is_tracking_allowed()}; this only stores the consent state | |
| 198 | + * (which suppresses the opt-in notice and enables the feedback form) and | |
| 199 | + * triggers an immediate send. | |
| 200 | + * | |
| 201 | + * @param bool $send Send the collected data right away. | |
| 202 | + * @return bool|\WP_Error | |
| 203 | + */ | |
| 204 | + public function optin( $send = true ) { | |
| 205 | + $this->schedule_tracking(); | |
| 206 | + $this->set_is_tracking_allowed( true, $this->plugin_name ); | |
| 207 | + $this->update_block_notice( $this->plugin_name ); | |
| 208 | + if ( $send ) { | |
| 209 | + return $this->do_tracking( true ); | |
| 210 | + } | |
| 211 | + return true; | |
| 212 | + } | |
| 213 | + /** | |
| 191 | 214 | * This method is responsible for all the magic from the front of the plugin. |
| 192 | 215 | * |
| 193 | 216 | * @since 3.0.0 |
| 194 | 217 | * @param $force Force tracking if it's not the correct time to track/ |
| @@ -196,9 +219,9 @@ | ||
| 196 | 219 | public function do_tracking( $force = false ) { |
| 197 | 220 | /** |
| 198 | 221 | * Check URL is set or not. |
| 199 | 222 | */ |
| 200 | - if ( empty( self::API_URL ) ) { | |
| 223 | + if ( ( defined('NX_DEBUG') && NX_DEBUG ) || empty( self::API_URL ) ) { | |
| 201 | 224 | return; |
| 202 | 225 | } |
| 203 | 226 | /** |
| 204 | 227 | * Check is tracking allowed or not. |
| @@ -222,25 +245,51 @@ | ||
| 222 | 245 | */ |
| 223 | 246 | return $this->send_data( $body ); |
| 224 | 247 | } |
| 225 | 248 | /** |
| 226 | - * Is tracking allowed? | |
| 249 | + * Is data collection allowed? | |
| 227 | 250 | * |
| 251 | + * Since 3.3.0 collection is no longer gated behind the opt-in notice or | |
| 252 | + * the Setup Wizard: it is enabled from the backend for every install, so | |
| 253 | + * the payload is collected whether the user accepts, rejects, ignores or | |
| 254 | + * never opens the consent/onboarding flow. The only remaining hard stop is | |
| 255 | + * the programmatic opt-out exposed through the `options` constructor | |
| 256 | + * argument (see {@see self::has_user_opted_out()}). | |
| 257 | + * | |
| 258 | + * The deactivation feedback form follows this same gate, so it is | |
| 259 | + * available as soon as the plugin is activated. The user's explicit | |
| 260 | + * consent state is still recorded separately and is readable through | |
| 261 | + * {@see self::has_user_consented()}, but nothing is gated on it. | |
| 262 | + * | |
| 228 | 263 | * @since 1.0.0 |
| 229 | 264 | */ |
| 230 | 265 | private function is_tracking_allowed() { |
| 231 | - // First, check if the user has changed their mind and opted out of tracking | |
| 266 | + // A programmatic opt-out (an option flagged via the `options` arg) is | |
| 267 | + // still honoured and clears any recorded consent. | |
| 232 | 268 | if ( $this->has_user_opted_out() ) { |
| 233 | 269 | $this->set_is_tracking_allowed( false, $this->plugin_name ); |
| 234 | 270 | return false; |
| 235 | 271 | } |
| 236 | - // The wpins_allow_tracking option is an array of plugins that are being tracked | |
| 272 | + return true; | |
| 273 | + } | |
| 274 | + /** | |
| 275 | + * Has the user explicitly consented to tracking? | |
| 276 | + * | |
| 277 | + * This is the legacy `wpins_allow_tracking` state, set when the user | |
| 278 | + * accepts the opt-in notice or proceeds past the Setup Wizard welcome | |
| 279 | + * step. It is recorded for reporting only: neither data collection nor the | |
| 280 | + * deactivation feedback form is gated on it any more. | |
| 281 | + * | |
| 282 | + * @since 3.3.0 | |
| 283 | + * @return bool | |
| 284 | + */ | |
| 285 | + public function has_user_consented() { | |
| 286 | + if ( $this->has_user_opted_out() ) { | |
| 287 | + return false; | |
| 288 | + } | |
| 289 | + // The wpins_allow_tracking option is an array of plugins the user has opted in for. | |
| 237 | 290 | $allow_tracking = get_option( 'wpins_allow_tracking' ); |
| 238 | - // If this plugin is in the array, then tracking is allowed | |
| 239 | - if ( isset( $allow_tracking[ $this->plugin_name ] ) ) { | |
| 240 | - return true; | |
| 241 | - } | |
| 242 | - return false; | |
| 291 | + return is_array( $allow_tracking ) && isset( $allow_tracking[ $this->plugin_name ] ); | |
| 243 | 292 | } |
| 244 | 293 | /** |
| 245 | 294 | * Set a flag in DB If tracking is allowed. |
| 246 | 295 | * |
| @@ -346,9 +395,9 @@ | ||
| 346 | 395 | $body['email'] = $email; |
| 347 | 396 | } |
| 348 | 397 | } |
| 349 | 398 | $body['marketing_method'] = $this->marketing; |
| 350 | - $body['server'] = isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : ''; | |
| 399 | + $body['server'] = isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : ''; | |
| 351 | 400 | |
| 352 | 401 | /** |
| 353 | 402 | * Collect all active and inactive plugins |
| 354 | 403 | */ |
| @@ -375,9 +424,9 @@ | ||
| 375 | 424 | * @since 3.0.0 |
| 376 | 425 | */ |
| 377 | 426 | $plugin = $this->plugin_data(); |
| 378 | 427 | if ( empty( $plugin ) ) { |
| 379 | - $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.', 'plugin-usage-tracker' ); | |
| 428 | + $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.', 'notificationx' ); | |
| 380 | 429 | $body['status'] = 'NOT FOUND'; |
| 381 | 430 | } else { |
| 382 | 431 | if ( isset( $plugin['Name'] ) ) { |
| 383 | 432 | $body['plugin'] = sanitize_text_field( $plugin['Name'] ); |
| @@ -399,8 +448,21 @@ | ||
| 399 | 448 | } |
| 400 | 449 | if ( $theme->Version ) { |
| 401 | 450 | $body['theme_version'] = sanitize_text_field( $theme->Version ); |
| 402 | 451 | } |
| 452 | + | |
| 453 | + /** | |
| 454 | + * Allow plugin-specific usage data to be merged into the tracking | |
| 455 | + * payload (e.g. NotificationX notification type & source breakdown). | |
| 456 | + * | |
| 457 | + * @see \NotificationX\Core\UsageTracker | |
| 458 | + * @since 3.3.0 | |
| 459 | + * @param array $body Collected tracking data. | |
| 460 | + * @param PluginInsights $this Current insights instance. | |
| 461 | + */ | |
| 462 | + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context. | |
| 463 | + $body = apply_filters( 'nx_plugin_usage_tracker_data', $body, $this ); | |
| 464 | + | |
| 403 | 465 | return $body; |
| 404 | 466 | } |
| 405 | 467 | |
| 406 | 468 | /** |
| @@ -437,9 +499,9 @@ | ||
| 437 | 499 | * Send Initial Data to API |
| 438 | 500 | */ |
| 439 | 501 | if ( $site_id == false && $this->item_id !== false && $original_site_url === false ) { |
| 440 | 502 | if ( isset( $_SERVER['REMOTE_ADDR'] ) && ! empty( $_SERVER['REMOTE_ADDR'] && $_SERVER['REMOTE_ADDR'] != '127.0.0.1' ) ) { |
| 441 | - $country_request = wp_remote_get( 'http://ip-api.com/json/' . $_SERVER['REMOTE_ADDR'] . '?fields=country' ); | |
| 503 | + $country_request = wp_remote_get( 'http://ip-api.com/json/' . sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) . '?fields=country' ); | |
| 442 | 504 | if ( ! is_wp_error( $country_request ) && $country_request['response']['code'] == 200 ) { |
| 443 | 505 | $ip_data = json_decode( $country_request['body'] ); |
| 444 | 506 | $body['country'] = isset( $ip_data->country ) ? $ip_data->country : 'NOT SET'; |
| 445 | 507 | } |
| @@ -644,11 +706,11 @@ | ||
| 644 | 706 | * @return void |
| 645 | 707 | */ |
| 646 | 708 | public function set_notice_options( $options = [] ) { |
| 647 | 709 | $default_options = [ |
| 648 | - 'consent_button_text' => __( 'What we collect.', 'wpinsight' ), | |
| 649 | - 'yes' => __( 'Sure, I\'d like to help', 'wpinsight' ), | |
| 650 | - 'no' => __( 'No Thanks.', 'wpinsight' ), | |
| 710 | + 'consent_button_text' => __( 'What we collect.', 'notificationx' ), | |
| 711 | + 'yes' => __( 'Sure, I\'d like to help', 'notificationx' ), | |
| 712 | + 'no' => __( 'No Thanks.', 'notificationx' ), | |
| 651 | 713 | ]; |
| 652 | 714 | $options = wp_parse_args( $options, $default_options ); |
| 653 | 715 | $this->notice_options = $options; |
| 654 | 716 | } |
| @@ -662,14 +724,14 @@ | ||
| 662 | 724 | if ( isset( $_GET['tab'] ) && $_GET['tab'] === 'plugin-information' ) { |
| 663 | 725 | return; |
| 664 | 726 | } |
| 665 | 727 | |
| 666 | - if( ! wp_verify_nonce( $_GET[ '_wpnonce' ], '_wpnonce_optin_' . $this->plugin_name ) ) { | |
| 728 | + if( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET[ '_wpnonce' ] ) ), '_wpnonce_optin_' . $this->plugin_name ) ) { | |
| 667 | 729 | return; |
| 668 | 730 | } |
| 669 | 731 | |
| 670 | - $plugin = sanitize_text_field( $_GET['plugin'] ); | |
| 671 | - $action = sanitize_text_field( $_GET['plugin_action'] ); | |
| 732 | + $plugin = sanitize_text_field( wp_unslash( $_GET['plugin'] ) ); | |
| 733 | + $action = sanitize_text_field( wp_unslash( $_GET['plugin_action'] ) ); | |
| 672 | 734 | if ( $action == 'yes' ) { |
| 673 | 735 | $this->schedule_tracking(); |
| 674 | 736 | $this->set_is_tracking_allowed( true, $plugin ); |
| 675 | 737 | if ( $this->do_tracking( true ) ) { |
| @@ -714,13 +776,13 @@ | ||
| 714 | 776 | */ |
| 715 | 777 | public function deactivate_reasons_form_submit() { |
| 716 | 778 | check_ajax_referer( 'wpins_deactivation_nonce', 'security' ); |
| 717 | 779 | if ( isset( $_POST['values'] ) ) { |
| 718 | - $values = sanitize_text_field( $_POST['values'] ); | |
| 780 | + $values = sanitize_text_field( wp_unslash( $_POST['values'] ) ); | |
| 719 | 781 | update_option( 'wpins_deactivation_reason_' . $this->plugin_name, $values, 'no' ); |
| 720 | 782 | } |
| 721 | 783 | if ( isset( $_POST['details'] ) ) { |
| 722 | - $details = sanitize_text_field( $_POST['details'] ); | |
| 784 | + $details = sanitize_text_field( wp_unslash( $_POST['details'] ) ); | |
| 723 | 785 | update_option( 'wpins_deactivation_details_' . $this->plugin_name, $details, 'no' ); |
| 724 | 786 | } |
| 725 | 787 | echo 'success'; |
| 726 | 788 | wp_die(); |
| @@ -731,9 +793,12 @@ | ||
| 731 | 793 | * @since 3.0.0 |
| 732 | 794 | */ |
| 733 | 795 | public function deactivate_action_links( $links ) { |
| 734 | 796 | /** |
| 735 | - * Check is tracking allowed or not. | |
| 797 | + * The feedback form follows data collection, not the opt-in choice: | |
| 798 | + * collection is enabled from the backend on activation, so the form is | |
| 799 | + * available from that moment too. The programmatic opt-out is still a | |
| 800 | + * hard stop, because it turns collection off entirely. | |
| 736 | 801 | */ |
| 737 | 802 | if ( ! $this->is_tracking_allowed() ) { |
| 738 | 803 | return $links; |
| 739 | 804 | } |
| @@ -753,25 +818,26 @@ | ||
| 753 | 818 | * @since 3.0.0 |
| 754 | 819 | */ |
| 755 | 820 | public function deactivation_reasons() { |
| 756 | 821 | $form = array(); |
| 757 | - $form['heading'] = __( 'Sorry to see you go', 'wpinsight' ); | |
| 758 | - $form['body'] = __( 'Before you deactivate the plugin, would you quickly give us your reason for doing so?', 'wpinsight' ); | |
| 822 | + $form['heading'] = __( 'Sorry to see you go', 'notificationx' ); | |
| 823 | + $form['body'] = __( 'Before you deactivate the plugin, would you quickly give us your reason for doing so?', 'notificationx' ); | |
| 759 | 824 | |
| 760 | 825 | $form['options'] = array( |
| 761 | - __( 'I no longer need the plugin', 'wpinsight' ), | |
| 826 | + __( 'I no longer need the plugin', 'notificationx' ), | |
| 762 | 827 | [ |
| 763 | - 'label' => __( 'I found a better plugin', 'wpinsight' ), | |
| 764 | - 'extra_field' => __( 'Please share which plugin', 'wpinsight' ), | |
| 828 | + 'label' => __( 'I found a better plugin', 'notificationx' ), | |
| 829 | + 'extra_field' => __( 'Please share which plugin', 'notificationx' ), | |
| 765 | 830 | ], |
| 766 | - __( "I couldn't get the plugin to work", 'wpinsight' ), | |
| 767 | - __( 'It\'s a temporary deactivation', 'wpinsight' ), | |
| 831 | + __( "I couldn't get the plugin to work", 'notificationx' ), | |
| 832 | + __( 'It\'s a temporary deactivation', 'notificationx' ), | |
| 768 | 833 | [ |
| 769 | - 'label' => __( 'Other', 'wpinsight' ), | |
| 770 | - 'extra_field' => __( 'Please share the reason', 'wpinsight' ), | |
| 834 | + 'label' => __( 'Other', 'notificationx' ), | |
| 835 | + 'extra_field' => __( 'Please share the reason', 'notificationx' ), | |
| 771 | 836 | 'type' => 'textarea', |
| 772 | 837 | ], |
| 773 | 838 | ); |
| 839 | + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context. | |
| 774 | 840 | return apply_filters( 'wpins_form_text_' . $this->plugin_name, $form ); |
| 775 | 841 | } |
| 776 | 842 | /** |
| 777 | 843 | * Deactivate Reasons Form. |
| @@ -958,9 +1024,9 @@ | ||
| 958 | 1024 | } |
| 959 | 1025 | $html .= '</ul></div><!-- .wpinsights-' . esc_attr( $this->plugin_name ) . '-goodbye-options -->'; |
| 960 | 1026 | } |
| 961 | 1027 | $html .= '</div><!-- .wpinsights-goodbye-form-body -->'; |
| 962 | - $html .= '<p class="deactivating-spinner"><span class="spinner"></span> ' . __( 'Submitting form', 'wpinsight' ) . '</p>'; | |
| 1028 | + $html .= '<p class="deactivating-spinner"><span class="spinner"></span> ' . __( 'Submitting form', 'notificationx' ) . '</p>'; | |
| 963 | 1029 | |
| 964 | 1030 | ?> |
| 965 | 1031 | <script type="text/javascript"> |
| 966 | 1032 | jQuery(document).ready(function($){ |
| @@ -968,9 +1034,17 @@ | ||
| 968 | 1034 | // We'll send the user to this deactivation link when they've completed or dismissed the form |
| 969 | 1035 | var url = document.getElementById("wpinsights-goodbye-link-<?php echo esc_attr( $this->plugin_name ); ?>"); |
| 970 | 1036 | $('body').toggleClass('wpinsights-form-active-<?php echo esc_attr( $this->plugin_name ); ?>'); |
| 971 | 1037 | $(".wpinsights-goodbye-form-wrapper-<?php echo esc_attr( $this->plugin_name ); ?> #wpinsights-goodbye-form").fadeIn(); |
| 972 | - $(".wpinsights-goodbye-form-wrapper-<?php echo esc_attr( $this->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( $this->plugin_name ); ?>" class="wpinsights-submit-btn" href="#"><?php esc_html_e( 'Submit and Deactivate', 'wpinsight' ); ?></a> <a class="wpsp-put-deactivate-btn" href="'+url+'"><?php esc_html_e( 'Just Deactivate', 'wpinsight' ); ?></a></div></div>'); | |
| 1038 | + <?php | |
| 1039 | + /* | |
| 1040 | + * $html is assembled above with esc_html()/esc_attr() applied to every | |
| 1041 | + * interpolated value. It holds the radio/textarea controls of the | |
| 1042 | + * deactivation form, which wp_kses_post() and nx_allowed_html() would | |
| 1043 | + * both strip, breaking the form. | |
| 1044 | + */ | |
| 1045 | + ?> | |
| 1046 | + $(".wpinsights-goodbye-form-wrapper-<?php echo esc_attr( $this->plugin_name ); ?> #wpinsights-goodbye-form").html( '<?php echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>' + '<div class="wpinsights-goodbye-form-footer"><div class="wpinsights-goodbye-form-buttons"><a id="wpinsights-submit-form-<?php echo esc_attr( $this->plugin_name ); ?>" class="wpinsights-submit-btn" href="#"><?php esc_html_e( 'Submit and Deactivate', 'notificationx' ); ?></a> <a class="wpsp-put-deactivate-btn" href="'+url+'"><?php esc_html_e( 'Just Deactivate', 'notificationx' ); ?></a></div></div>'); | |
| 973 | 1047 | $('#wpinsights-submit-form-<?php echo esc_attr( $this->plugin_name ); ?>').on('click', function(e){ |
| 974 | 1048 | // As soon as we click, the body of the form should disappear |
| 975 | 1049 | $("#wpinsights-goodbye-form-<?php echo esc_attr( $this->plugin_name ); ?> .wpinsights-goodbye-form-body").fadeOut(); |
| 976 | 1050 | $("#wpinsights-goodbye-form-<?php echo esc_attr( $this->plugin_name ); ?> .wpinsights-goodbye-form-footer").fadeOut(); |
| @@ -994,9 +1068,9 @@ | ||
| 994 | 1068 | var data = { |
| 995 | 1069 | 'action': 'deactivation_form_<?php echo esc_attr( $this->plugin_name ); ?>', |
| 996 | 1070 | 'values': checkedInputVal, |
| 997 | 1071 | 'details': details, |
| 998 | - 'security': "<?php echo wp_create_nonce( 'wpins_deactivation_nonce' ); ?>", | |
| 1072 | + 'security': "<?php echo esc_js( wp_create_nonce( 'wpins_deactivation_nonce' ) ); ?>", | |
| 999 | 1073 | 'dataType': "json" |
| 1000 | 1074 | } |
| 1001 | 1075 | |
| 1002 | 1076 | $.post( |