| @@ -1,11 +1,15 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | - * Usage_Tracker — anonymous, opt-in plugin usage analytics. | |
| 3 | + * Usage_Tracker — opt-in plugin usage analytics. | |
| 4 | 4 | * |
| 5 | 5 | * Ported from the WP Insights SDK (the same engine WPDeveloper plugins such as |
| 6 | - * EmbedPress ship). Trimmed for xSpeed: no deactivation "goodbye" survey, no | |
| 7 | - * email marketing capture by default. | |
| 6 | + * EmbedPress ship). With email_marketing enabled (Plugin passes it), the | |
| 7 | + * opted-in admin's email + display name are included and disclosed in the | |
| 8 | + * consent copy. | |
| 9 | + * The deactivation "goodbye" survey UI lives in Deactivation_Feedback (shown to | |
| 10 | + * every admin); it stores the reason in the canonical WPInsight options and | |
| 11 | + * deactivate_this_plugin() transmits it — see that method. | |
| 8 | 12 | * |
| 9 | 13 | * PRIVACY CONTRACT (see CLAUDE.md "Hard do-not" + readme.txt): |
| 10 | 14 | * Nothing is collected or sent until the site admin EXPLICITLY opts in via |
| 11 | 15 | * the setup wizard. `require_optin` is always true. Until `opt_in( true )` |
| @@ -67,11 +71,12 @@ | ||
| 67 | 71 | |
| 68 | 72 | // require_optin is intentionally forced true — never honor a caller |
| 69 | 73 | // that tries to disable consent gating. |
| 70 | 74 | $this->require_optin = true; |
| 71 | - // Email marketing capture is OFF by default in xSpeed (EmbedPress | |
| 72 | - // defaults it on to send a discount coupon; we collect no email | |
| 73 | - // unless a caller explicitly turns it on). | |
| 75 | + // Email/name capture ships OFF here and is enabled by the caller | |
| 76 | + // (Plugin::start_plugin_tracking() passes email_marketing => true). | |
| 77 | + // When on, the opted-in admin's email + display name ride along — | |
| 78 | + // disclosed in the consent copy and readme.txt "External services". | |
| 74 | 79 | $this->marketing = isset( $args['email_marketing'] ) ? (bool) $args['email_marketing'] : false; |
| 75 | 80 | $this->item_id = ! empty( $args['item_id'] ) ? $args['item_id'] : false; |
| 76 | 81 | |
| 77 | 82 | register_deactivation_hook( $this->plugin_file, array( $this, 'deactivate_this_plugin' ) ); |
| @@ -122,23 +127,94 @@ | ||
| 122 | 127 | } |
| 123 | 128 | } |
| 124 | 129 | |
| 125 | 130 | /** |
| 126 | - * On deactivation: tell insights we went inactive (only if opted in), | |
| 127 | - * then clear the cron. No reason survey is collected. | |
| 131 | + * On deactivation: report to WPInsight that we went inactive, carrying | |
| 132 | + * the deactivation reason the admin submitted on the Plugins screen (if | |
| 133 | + * any). Deactivation_Feedback stores that reason in the canonical | |
| 134 | + * `wpins_deactivation_reason_<slug>` / `wpins_deactivation_details_<slug>` | |
| 135 | + * options; we read + transmit + delete them here. | |
| 136 | + * | |
| 137 | + * Two send paths: | |
| 138 | + * - Usage analytics ON → the full, site-correlated body (get_data()) | |
| 139 | + * with the reason appended, via the normal send_data() handshake. | |
| 140 | + * This is the canonical WPInsight deactivation record. | |
| 141 | + * - Usage analytics OFF → nothing is sent UNLESS the admin explicitly | |
| 142 | + * submitted the survey; in that case a minimal, reason-only payload | |
| 143 | + * goes out as per-action consent (no diagnostics inventory). | |
| 128 | 144 | */ |
| 129 | 145 | public function deactivate_this_plugin() { |
| 130 | - if ( ! $this->is_tracking_allowed() ) { | |
| 146 | + $reason_key = 'wpins_deactivation_reason_' . $this->plugin_name; | |
| 147 | + $details_key = 'wpins_deactivation_details_' . $this->plugin_name; | |
| 148 | + $reason = get_option( $reason_key, false ); | |
| 149 | + $details = get_option( $details_key, false ); | |
| 150 | + | |
| 151 | + if ( $this->is_tracking_allowed() ) { | |
| 152 | + $body = $this->get_data(); | |
| 153 | + $body['status'] = 'Deactivated'; | |
| 154 | + $body['deactivated_date'] = time(); | |
| 155 | + if ( false !== $reason ) { | |
| 156 | + $body['deactivation_reason'] = $reason; | |
| 157 | + } | |
| 158 | + if ( false !== $details ) { | |
| 159 | + $body['deactivation_details'] = $details; | |
| 160 | + } | |
| 161 | + $this->send_data( $body ); | |
| 162 | + | |
| 163 | + if ( ! $this->disabled_wp_cron ) { | |
| 164 | + wp_clear_scheduled_hook( self::EVENT_HOOK ); | |
| 165 | + } | |
| 166 | + } elseif ( false !== $reason || false !== $details ) { | |
| 167 | + $this->send_deactivation_feedback( $reason, $details ); | |
| 168 | + } | |
| 169 | + | |
| 170 | + // Never let a stored reason linger or double-send on the next cycle. | |
| 171 | + delete_option( $reason_key ); | |
| 172 | + delete_option( $details_key ); | |
| 173 | + } | |
| 174 | + | |
| 175 | + /** | |
| 176 | + * Minimal, reason-only deactivation report for when usage analytics is | |
| 177 | + * OFF but the admin submitted the deactivation survey. Sends only plugin | |
| 178 | + * identity, WP/PHP version, and the reason/details — never the full | |
| 179 | + * diagnostic body get_data() assembles (no plugin inventory, no theme, | |
| 180 | + * no xSpeed config). Per-action consent; see the privacy contract at the | |
| 181 | + * top of this file and readme.txt "External services". | |
| 182 | + * | |
| 183 | + * @param string|false $reason Stored deactivation reason label, or false. | |
| 184 | + * @param string|false $details Stored free-text detail, or false. | |
| 185 | + */ | |
| 186 | + private function send_deactivation_feedback( $reason, $details ) { | |
| 187 | + if ( empty( self::API_URL ) ) { | |
| 131 | 188 | return; |
| 132 | 189 | } |
| 133 | - $body = $this->get_data(); | |
| 134 | - $body['status'] = 'Deactivated'; | |
| 135 | - $body['deactivated_date'] = time(); | |
| 136 | - $this->send_data( $body ); | |
| 190 | + $plugin = $this->plugin_data(); | |
| 191 | + $body = array( | |
| 192 | + 'plugin_slug' => sanitize_text_field( $this->plugin_name ), | |
| 193 | + 'url' => get_bloginfo( 'url' ), | |
| 194 | + 'status' => 'Deactivated', | |
| 195 | + 'deactivated_date' => time(), | |
| 196 | + 'site_version' => get_bloginfo( 'version' ), | |
| 197 | + 'php_version' => phpversion(), | |
| 198 | + 'wpins_version' => self::WPINS_VERSION, | |
| 199 | + ); | |
| 200 | + if ( ! empty( $plugin['Name'] ) ) { | |
| 201 | + $body['plugin'] = sanitize_text_field( $plugin['Name'] ); | |
| 202 | + } | |
| 203 | + if ( ! empty( $plugin['Version'] ) ) { | |
| 204 | + $body['version'] = sanitize_text_field( $plugin['Version'] ); | |
| 205 | + } | |
| 206 | + if ( false !== $this->item_id ) { | |
| 207 | + $body['item_id'] = $this->item_id; | |
| 208 | + } | |
| 209 | + if ( false !== $reason ) { | |
| 210 | + $body['deactivation_reason'] = sanitize_text_field( $reason ); | |
| 211 | + } | |
| 212 | + if ( false !== $details ) { | |
| 213 | + $body['deactivation_details'] = sanitize_text_field( $details ); | |
| 214 | + } | |
| 137 | 215 | |
| 138 | - if ( ! $this->disabled_wp_cron ) { | |
| 139 | - wp_clear_scheduled_hook( self::EVENT_HOOK ); | |
| 140 | - } | |
| 216 | + $this->remote_post( $body ); | |
| 141 | 217 | } |
| 142 | 218 | |
| 143 | 219 | /** |
| 144 | 220 | * Cron callback. Bails before any HTTP unless tracking is allowed and |
| @@ -214,11 +290,15 @@ | ||
| 214 | 290 | if ( $this->marketing ) { |
| 215 | 291 | if ( ! function_exists( 'wp_get_current_user' ) ) { |
| 216 | 292 | include ABSPATH . 'wp-includes/pluggable.php'; |
| 217 | 293 | } |
| 218 | - $email = wp_get_current_user()->user_email; | |
| 294 | + $user = wp_get_current_user(); | |
| 295 | + $email = $user->user_email; | |
| 219 | 296 | if ( is_email( $email ) ) { |
| 220 | 297 | $body['email'] = $email; |
| 298 | + } | |
| 299 | + if ( ! empty( $user->display_name ) ) { | |
| 300 | + $body['name'] = sanitize_text_field( $user->display_name ); | |
| 221 | 301 | } |
| 222 | 302 | } |
| 223 | 303 | $body['marketing_method'] = $this->marketing; |
| 224 | 304 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- server software string, reported as-is to insights. |