PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.6
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.6
1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 All 32 releases
← All changes | includes/class-usage-tracker.php +97 -17 1.1.0 → 1.3.6 View file →
@@ -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.