| @@ -2,10 +2,12 @@ | ||
| 2 | 2 | /** |
| 3 | 3 | * Usage_Tracker — anonymous, 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). Trimmed for xSpeed: no email marketing capture by default. | |
| 7 | + * The deactivation "goodbye" survey UI lives in Deactivation_Feedback (shown to | |
| 8 | + * every admin); it stores the reason in the canonical WPInsight options and | |
| 9 | + * deactivate_this_plugin() transmits it — see that method. | |
| 8 | 10 | * |
| 9 | 11 | * PRIVACY CONTRACT (see CLAUDE.md "Hard do-not" + readme.txt): |
| 10 | 12 | * Nothing is collected or sent until the site admin EXPLICITLY opts in via |
| 11 | 13 | * the setup wizard. `require_optin` is always true. Until `opt_in( true )` |
| @@ -122,23 +124,94 @@ | ||
| 122 | 124 | } |
| 123 | 125 | } |
| 124 | 126 | |
| 125 | 127 | /** |
| 126 | - * On deactivation: tell insights we went inactive (only if opted in), | |
| 127 | - * then clear the cron. No reason survey is collected. | |
| 128 | + * On deactivation: report to WPInsight that we went inactive, carrying | |
| 129 | + * the deactivation reason the admin submitted on the Plugins screen (if | |
| 130 | + * any). Deactivation_Feedback stores that reason in the canonical | |
| 131 | + * `wpins_deactivation_reason_<slug>` / `wpins_deactivation_details_<slug>` | |
| 132 | + * options; we read + transmit + delete them here. | |
| 133 | + * | |
| 134 | + * Two send paths: | |
| 135 | + * - Usage analytics ON → the full, site-correlated body (get_data()) | |
| 136 | + * with the reason appended, via the normal send_data() handshake. | |
| 137 | + * This is the canonical WPInsight deactivation record. | |
| 138 | + * - Usage analytics OFF → nothing is sent UNLESS the admin explicitly | |
| 139 | + * submitted the survey; in that case a minimal, reason-only payload | |
| 140 | + * goes out as per-action consent (no diagnostics inventory). | |
| 128 | 141 | */ |
| 129 | 142 | public function deactivate_this_plugin() { |
| 130 | - if ( ! $this->is_tracking_allowed() ) { | |
| 143 | + $reason_key = 'wpins_deactivation_reason_' . $this->plugin_name; | |
| 144 | + $details_key = 'wpins_deactivation_details_' . $this->plugin_name; | |
| 145 | + $reason = get_option( $reason_key, false ); | |
| 146 | + $details = get_option( $details_key, false ); | |
| 147 | + | |
| 148 | + if ( $this->is_tracking_allowed() ) { | |
| 149 | + $body = $this->get_data(); | |
| 150 | + $body['status'] = 'Deactivated'; | |
| 151 | + $body['deactivated_date'] = time(); | |
| 152 | + if ( false !== $reason ) { | |
| 153 | + $body['deactivation_reason'] = $reason; | |
| 154 | + } | |
| 155 | + if ( false !== $details ) { | |
| 156 | + $body['deactivation_details'] = $details; | |
| 157 | + } | |
| 158 | + $this->send_data( $body ); | |
| 159 | + | |
| 160 | + if ( ! $this->disabled_wp_cron ) { | |
| 161 | + wp_clear_scheduled_hook( self::EVENT_HOOK ); | |
| 162 | + } | |
| 163 | + } elseif ( false !== $reason || false !== $details ) { | |
| 164 | + $this->send_deactivation_feedback( $reason, $details ); | |
| 165 | + } | |
| 166 | + | |
| 167 | + // Never let a stored reason linger or double-send on the next cycle. | |
| 168 | + delete_option( $reason_key ); | |
| 169 | + delete_option( $details_key ); | |
| 170 | + } | |
| 171 | + | |
| 172 | + /** | |
| 173 | + * Minimal, reason-only deactivation report for when usage analytics is | |
| 174 | + * OFF but the admin submitted the deactivation survey. Sends only plugin | |
| 175 | + * identity, WP/PHP version, and the reason/details — never the full | |
| 176 | + * diagnostic body get_data() assembles (no plugin inventory, no theme, | |
| 177 | + * no xSpeed config). Per-action consent; see the privacy contract at the | |
| 178 | + * top of this file and readme.txt "External services". | |
| 179 | + * | |
| 180 | + * @param string|false $reason Stored deactivation reason label, or false. | |
| 181 | + * @param string|false $details Stored free-text detail, or false. | |
| 182 | + */ | |
| 183 | + private function send_deactivation_feedback( $reason, $details ) { | |
| 184 | + if ( empty( self::API_URL ) ) { | |
| 131 | 185 | return; |
| 132 | 186 | } |
| 133 | - $body = $this->get_data(); | |
| 134 | - $body['status'] = 'Deactivated'; | |
| 135 | - $body['deactivated_date'] = time(); | |
| 136 | - $this->send_data( $body ); | |
| 187 | + $plugin = $this->plugin_data(); | |
| 188 | + $body = array( | |
| 189 | + 'plugin_slug' => sanitize_text_field( $this->plugin_name ), | |
| 190 | + 'url' => get_bloginfo( 'url' ), | |
| 191 | + 'status' => 'Deactivated', | |
| 192 | + 'deactivated_date' => time(), | |
| 193 | + 'site_version' => get_bloginfo( 'version' ), | |
| 194 | + 'php_version' => phpversion(), | |
| 195 | + 'wpins_version' => self::WPINS_VERSION, | |
| 196 | + ); | |
| 197 | + if ( ! empty( $plugin['Name'] ) ) { | |
| 198 | + $body['plugin'] = sanitize_text_field( $plugin['Name'] ); | |
| 199 | + } | |
| 200 | + if ( ! empty( $plugin['Version'] ) ) { | |
| 201 | + $body['version'] = sanitize_text_field( $plugin['Version'] ); | |
| 202 | + } | |
| 203 | + if ( false !== $this->item_id ) { | |
| 204 | + $body['item_id'] = $this->item_id; | |
| 205 | + } | |
| 206 | + if ( false !== $reason ) { | |
| 207 | + $body['deactivation_reason'] = sanitize_text_field( $reason ); | |
| 208 | + } | |
| 209 | + if ( false !== $details ) { | |
| 210 | + $body['deactivation_details'] = sanitize_text_field( $details ); | |
| 211 | + } | |
| 137 | 212 | |
| 138 | - if ( ! $this->disabled_wp_cron ) { | |
| 139 | - wp_clear_scheduled_hook( self::EVENT_HOOK ); | |
| 140 | - } | |
| 213 | + $this->remote_post( $body ); | |
| 141 | 214 | } |
| 142 | 215 | |
| 143 | 216 | /** |
| 144 | 217 | * Cron callback. Bails before any HTTP unless tracking is allowed and |