PluginProbe
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar / 3.3.3
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar v3.3.3
3.3.3 3.3.2 3.3.1 3.3.0 3.2.14 3.2.13 3.2.12 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 trunk 0.2.5.5 0.2.5.6 0.2.5.7 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 All 158 releases
← All changes | includes/Admin/PluginInsights.php +109 -35 3.2.8 → 3.3.3 View file →
@@ -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>&nbsp;<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>&nbsp;<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(