| @@ -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,12 +188,17 @@ | ||
| 187 | 188 | public function force_tracking() { |
| 188 | 189 | $this->do_tracking( true ); |
| 189 | 190 | } |
| 190 | 191 | /** |
| 191 | - * Programmatically opt the site into tracking and (optionally) send the | |
| 192 | - * data to the insights API immediately. Used by the onboarding Setup Wizard | |
| 193 | - * when the user proceeds past the Welcome step (which is the consent point). | |
| 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). | |
| 194 | 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 | + * | |
| 195 | 201 | * @param bool $send Send the collected data right away. |
| 196 | 202 | * @return bool|\WP_Error |
| 197 | 203 | */ |
| 198 | 204 | public function optin( $send = true ) { |
| @@ -239,25 +245,51 @@ | ||
| 239 | 245 | */ |
| 240 | 246 | return $this->send_data( $body ); |
| 241 | 247 | } |
| 242 | 248 | /** |
| 243 | - * Is tracking allowed? | |
| 249 | + * Is data collection allowed? | |
| 244 | 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 | + * | |
| 245 | 263 | * @since 1.0.0 |
| 246 | 264 | */ |
| 247 | 265 | private function is_tracking_allowed() { |
| 248 | - // 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. | |
| 249 | 268 | if ( $this->has_user_opted_out() ) { |
| 250 | 269 | $this->set_is_tracking_allowed( false, $this->plugin_name ); |
| 251 | 270 | return false; |
| 252 | 271 | } |
| 253 | - // 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. | |
| 254 | 290 | $allow_tracking = get_option( 'wpins_allow_tracking' ); |
| 255 | - // If this plugin is in the array, then tracking is allowed | |
| 256 | - if ( isset( $allow_tracking[ $this->plugin_name ] ) ) { | |
| 257 | - return true; | |
| 258 | - } | |
| 259 | - return false; | |
| 291 | + return is_array( $allow_tracking ) && isset( $allow_tracking[ $this->plugin_name ] ); | |
| 260 | 292 | } |
| 261 | 293 | /** |
| 262 | 294 | * Set a flag in DB If tracking is allowed. |
| 263 | 295 | * |
| @@ -363,9 +395,9 @@ | ||
| 363 | 395 | $body['email'] = $email; |
| 364 | 396 | } |
| 365 | 397 | } |
| 366 | 398 | $body['marketing_method'] = $this->marketing; |
| 367 | - $body['server'] = isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : ''; | |
| 399 | + $body['server'] = isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : ''; | |
| 368 | 400 | |
| 369 | 401 | /** |
| 370 | 402 | * Collect all active and inactive plugins |
| 371 | 403 | */ |
| @@ -392,9 +424,9 @@ | ||
| 392 | 424 | * @since 3.0.0 |
| 393 | 425 | */ |
| 394 | 426 | $plugin = $this->plugin_data(); |
| 395 | 427 | if ( empty( $plugin ) ) { |
| 396 | - $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' ); | |
| 397 | 429 | $body['status'] = 'NOT FOUND'; |
| 398 | 430 | } else { |
| 399 | 431 | if ( isset( $plugin['Name'] ) ) { |
| 400 | 432 | $body['plugin'] = sanitize_text_field( $plugin['Name'] ); |
| @@ -426,8 +458,9 @@ | ||
| 426 | 458 | * @since 3.3.0 |
| 427 | 459 | * @param array $body Collected tracking data. |
| 428 | 460 | * @param PluginInsights $this Current insights instance. |
| 429 | 461 | */ |
| 462 | + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context. | |
| 430 | 463 | $body = apply_filters( 'nx_plugin_usage_tracker_data', $body, $this ); |
| 431 | 464 | |
| 432 | 465 | return $body; |
| 433 | 466 | } |
| @@ -466,9 +499,9 @@ | ||
| 466 | 499 | * Send Initial Data to API |
| 467 | 500 | */ |
| 468 | 501 | if ( $site_id == false && $this->item_id !== false && $original_site_url === false ) { |
| 469 | 502 | if ( isset( $_SERVER['REMOTE_ADDR'] ) && ! empty( $_SERVER['REMOTE_ADDR'] && $_SERVER['REMOTE_ADDR'] != '127.0.0.1' ) ) { |
| 470 | - $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' ); | |
| 471 | 504 | if ( ! is_wp_error( $country_request ) && $country_request['response']['code'] == 200 ) { |
| 472 | 505 | $ip_data = json_decode( $country_request['body'] ); |
| 473 | 506 | $body['country'] = isset( $ip_data->country ) ? $ip_data->country : 'NOT SET'; |
| 474 | 507 | } |
| @@ -673,11 +706,11 @@ | ||
| 673 | 706 | * @return void |
| 674 | 707 | */ |
| 675 | 708 | public function set_notice_options( $options = [] ) { |
| 676 | 709 | $default_options = [ |
| 677 | - 'consent_button_text' => __( 'What we collect.', 'wpinsight' ), | |
| 678 | - 'yes' => __( 'Sure, I\'d like to help', 'wpinsight' ), | |
| 679 | - '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' ), | |
| 680 | 713 | ]; |
| 681 | 714 | $options = wp_parse_args( $options, $default_options ); |
| 682 | 715 | $this->notice_options = $options; |
| 683 | 716 | } |
| @@ -691,14 +724,14 @@ | ||
| 691 | 724 | if ( isset( $_GET['tab'] ) && $_GET['tab'] === 'plugin-information' ) { |
| 692 | 725 | return; |
| 693 | 726 | } |
| 694 | 727 | |
| 695 | - 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 ) ) { | |
| 696 | 729 | return; |
| 697 | 730 | } |
| 698 | 731 | |
| 699 | - $plugin = sanitize_text_field( $_GET['plugin'] ); | |
| 700 | - $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'] ) ); | |
| 701 | 734 | if ( $action == 'yes' ) { |
| 702 | 735 | $this->schedule_tracking(); |
| 703 | 736 | $this->set_is_tracking_allowed( true, $plugin ); |
| 704 | 737 | if ( $this->do_tracking( true ) ) { |
| @@ -743,13 +776,13 @@ | ||
| 743 | 776 | */ |
| 744 | 777 | public function deactivate_reasons_form_submit() { |
| 745 | 778 | check_ajax_referer( 'wpins_deactivation_nonce', 'security' ); |
| 746 | 779 | if ( isset( $_POST['values'] ) ) { |
| 747 | - $values = sanitize_text_field( $_POST['values'] ); | |
| 780 | + $values = sanitize_text_field( wp_unslash( $_POST['values'] ) ); | |
| 748 | 781 | update_option( 'wpins_deactivation_reason_' . $this->plugin_name, $values, 'no' ); |
| 749 | 782 | } |
| 750 | 783 | if ( isset( $_POST['details'] ) ) { |
| 751 | - $details = sanitize_text_field( $_POST['details'] ); | |
| 784 | + $details = sanitize_text_field( wp_unslash( $_POST['details'] ) ); | |
| 752 | 785 | update_option( 'wpins_deactivation_details_' . $this->plugin_name, $details, 'no' ); |
| 753 | 786 | } |
| 754 | 787 | echo 'success'; |
| 755 | 788 | wp_die(); |
| @@ -760,9 +793,12 @@ | ||
| 760 | 793 | * @since 3.0.0 |
| 761 | 794 | */ |
| 762 | 795 | public function deactivate_action_links( $links ) { |
| 763 | 796 | /** |
| 764 | - * 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. | |
| 765 | 801 | */ |
| 766 | 802 | if ( ! $this->is_tracking_allowed() ) { |
| 767 | 803 | return $links; |
| 768 | 804 | } |
| @@ -782,25 +818,26 @@ | ||
| 782 | 818 | * @since 3.0.0 |
| 783 | 819 | */ |
| 784 | 820 | public function deactivation_reasons() { |
| 785 | 821 | $form = array(); |
| 786 | - $form['heading'] = __( 'Sorry to see you go', 'wpinsight' ); | |
| 787 | - $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' ); | |
| 788 | 824 | |
| 789 | 825 | $form['options'] = array( |
| 790 | - __( 'I no longer need the plugin', 'wpinsight' ), | |
| 826 | + __( 'I no longer need the plugin', 'notificationx' ), | |
| 791 | 827 | [ |
| 792 | - 'label' => __( 'I found a better plugin', 'wpinsight' ), | |
| 793 | - 'extra_field' => __( 'Please share which plugin', 'wpinsight' ), | |
| 828 | + 'label' => __( 'I found a better plugin', 'notificationx' ), | |
| 829 | + 'extra_field' => __( 'Please share which plugin', 'notificationx' ), | |
| 794 | 830 | ], |
| 795 | - __( "I couldn't get the plugin to work", 'wpinsight' ), | |
| 796 | - __( 'It\'s a temporary deactivation', 'wpinsight' ), | |
| 831 | + __( "I couldn't get the plugin to work", 'notificationx' ), | |
| 832 | + __( 'It\'s a temporary deactivation', 'notificationx' ), | |
| 797 | 833 | [ |
| 798 | - 'label' => __( 'Other', 'wpinsight' ), | |
| 799 | - 'extra_field' => __( 'Please share the reason', 'wpinsight' ), | |
| 834 | + 'label' => __( 'Other', 'notificationx' ), | |
| 835 | + 'extra_field' => __( 'Please share the reason', 'notificationx' ), | |
| 800 | 836 | 'type' => 'textarea', |
| 801 | 837 | ], |
| 802 | 838 | ); |
| 839 | + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context. | |
| 803 | 840 | return apply_filters( 'wpins_form_text_' . $this->plugin_name, $form ); |
| 804 | 841 | } |
| 805 | 842 | /** |
| 806 | 843 | * Deactivate Reasons Form. |
| @@ -987,9 +1024,9 @@ | ||
| 987 | 1024 | } |
| 988 | 1025 | $html .= '</ul></div><!-- .wpinsights-' . esc_attr( $this->plugin_name ) . '-goodbye-options -->'; |
| 989 | 1026 | } |
| 990 | 1027 | $html .= '</div><!-- .wpinsights-goodbye-form-body -->'; |
| 991 | - $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>'; | |
| 992 | 1029 | |
| 993 | 1030 | ?> |
| 994 | 1031 | <script type="text/javascript"> |
| 995 | 1032 | jQuery(document).ready(function($){ |
| @@ -997,9 +1034,17 @@ | ||
| 997 | 1034 | // We'll send the user to this deactivation link when they've completed or dismissed the form |
| 998 | 1035 | var url = document.getElementById("wpinsights-goodbye-link-<?php echo esc_attr( $this->plugin_name ); ?>"); |
| 999 | 1036 | $('body').toggleClass('wpinsights-form-active-<?php echo esc_attr( $this->plugin_name ); ?>'); |
| 1000 | 1037 | $(".wpinsights-goodbye-form-wrapper-<?php echo esc_attr( $this->plugin_name ); ?> #wpinsights-goodbye-form").fadeIn(); |
| 1001 | - $(".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>'); | |
| 1002 | 1047 | $('#wpinsights-submit-form-<?php echo esc_attr( $this->plugin_name ); ?>').on('click', function(e){ |
| 1003 | 1048 | // As soon as we click, the body of the form should disappear |
| 1004 | 1049 | $("#wpinsights-goodbye-form-<?php echo esc_attr( $this->plugin_name ); ?> .wpinsights-goodbye-form-body").fadeOut(); |
| 1005 | 1050 | $("#wpinsights-goodbye-form-<?php echo esc_attr( $this->plugin_name ); ?> .wpinsights-goodbye-form-footer").fadeOut(); |
| @@ -1023,9 +1068,9 @@ | ||
| 1023 | 1068 | var data = { |
| 1024 | 1069 | 'action': 'deactivation_form_<?php echo esc_attr( $this->plugin_name ); ?>', |
| 1025 | 1070 | 'values': checkedInputVal, |
| 1026 | 1071 | 'details': details, |
| 1027 | - 'security': "<?php echo wp_create_nonce( 'wpins_deactivation_nonce' ); ?>", | |
| 1072 | + 'security': "<?php echo esc_js( wp_create_nonce( 'wpins_deactivation_nonce' ) ); ?>", | |
| 1028 | 1073 | 'dataType': "json" |
| 1029 | 1074 | } |
| 1030 | 1075 | |
| 1031 | 1076 | $.post( |