PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.6.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.6.1
1.6.1 1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
← All changes | inc/privacy/privacy-data.php +69 -8 1.2.0 → 1.6.1 View file →
@@ -29,8 +29,24 @@
29 29 *
30 30 * @since 1.2.0
31 31 */
32 32 class Privacy_Data {
33 + /**
34 + * The donation_data keys that hold the donor's own personal data and are
35 + * removed on erasure. Everything else in the column is an operational
36 + * record (refunds, notes, subscription metadata) and is written back.
37 + *
38 + * `gift_aid` is written by SureDonation Pro: a UK Gift Aid declaration
39 + * carries the donor's legal name and home address. It is named here, in
40 + * free, because an eraser that ran only while Pro was active would report
41 + * a completed erasure with that address still on the row the moment Pro
42 + * was deactivated, and the anonymised email would then hide the row from
43 + * every later request.
44 + *
45 + * @since 1.6.1
46 + */
47 + public const ERASED_DONATION_DATA_KEYS = [ 'fields', 'gift_aid' ];
48 +
33 49 use Get_Instance;
34 50
35 51 /**
36 52 * Constructor — register the exporter + eraser with WordPress.
@@ -155,8 +171,27 @@
155 171 }
156 172 $rows[ $unique ] = $field['value'] ?? '';
157 173 }
158 174
175 + // The Gift Aid declaration (SureDonation Pro) is the record actually
176 + // filed with HMRC and holds the corrected name and address; the
177 + // submitted fields above keep whatever the donor first typed.
178 + $gift_aid = isset( $donation['donation_data']['gift_aid'] ) && is_array( $donation['donation_data']['gift_aid'] ) ? $donation['donation_data']['gift_aid'] : [];
179 + if ( [] !== $gift_aid ) {
180 + $rows[ __( 'Gift Aid Declared', 'suredonation' ) ] = true === ( $gift_aid['declared'] ?? false ) ? __( 'Yes', 'suredonation' ) : __( 'No', 'suredonation' );
181 + foreach ( [
182 + 'first_name' => __( 'Gift Aid First Name', 'suredonation' ),
183 + 'last_name' => __( 'Gift Aid Last Name', 'suredonation' ),
184 + 'house' => __( 'Gift Aid House Name or Number', 'suredonation' ),
185 + 'postcode' => __( 'Gift Aid Postcode', 'suredonation' ),
186 + 'country' => __( 'Gift Aid Country', 'suredonation' ),
187 + ] as $key => $label ) {
188 + if ( isset( $gift_aid[ $key ] ) && is_scalar( $gift_aid[ $key ] ) ) {
189 + $rows[ $label ] = (string) $gift_aid[ $key ];
190 + }
191 + }
192 + }
193 +
159 194 $export[] = [
160 195 'group_id' => 'suredonation-donations',
161 196 'group_label' => __( 'SureDonation Donations', 'suredonation' ),
162 197 'item_id' => 'suredonation-donation-' . absint( Helper::get_string_value( $donation['id'] ?? 0 ) ),
@@ -219,9 +254,11 @@
219 254
220 255 // Strip the personal-data keys from donation_data, keep the rest
221 256 // (e.g. refunds/notes are operational records, not donor PII).
222 257 $donation_data = isset( $donation['donation_data'] ) && is_array( $donation['donation_data'] ) ? $donation['donation_data'] : [];
223 - unset( $donation_data['fields'] );
258 + foreach ( self::ERASED_DONATION_DATA_KEYS as $key ) {
259 + unset( $donation_data[ $key ] );
260 + }
224 261
225 262 // The receipt PDF is generated from the donor's name/email/address —
226 263 // erasure must remove the file from disk, not just the DB columns.
227 264 $receipt_deleted = Receipt_Generator::delete_receipt( Helper::get_string_value( $donation['receipt_pdf_url'] ?? '' ) );
@@ -247,10 +284,13 @@
247 284 'parent_subscription_id' => 0,
248 285 'log' => '',
249 286 ];
250 287
251 - // Clear the receipt pointer only when the file is actually gone —
252 - // otherwise keep it so a retried erasure can still find the file.
288 + // Clear the receipt pointer once nothing further will be done with
289 + // it — otherwise keep it so a retried erasure can still find the
290 + // file. A pointer refused by containment also reports true and is
291 + // dropped deliberately: no caller can act on it, so retaining it
292 + // would fail the erasure forever with no remedy for the admin.
253 293 if ( $receipt_deleted ) {
254 294 $anonymized['receipt_pdf_url'] = '';
255 295 }
256 296
@@ -276,17 +316,38 @@
276 316 if ( is_array( $donor ) && ! empty( $donor ) && ! $items_retained && ! $erase_failed ) {
277 317 $donor_id = absint( Helper::get_string_value( $donor['id'] ?? 0 ) );
278 318 if ( $donor_id > 0 ) {
279 319 Donors::clear_stripe_customer_id_by_email( $email );
320 +
321 + // Clear the per-account Stripe customer identifiers added with
322 + // multi-account support: the donor_data map (folded into the
323 + // anonymization write below) and the per-account user-meta cache.
324 + $donor_data = isset( $donor['donor_data'] ) && is_array( $donor['donor_data'] ) ? $donor['donor_data'] : [];
325 + unset( $donor_data['stripe_customers'] );
326 +
327 + $wp_user = get_user_by( 'email', $email );
328 + if ( $wp_user instanceof \WP_User ) {
329 + delete_user_meta( $wp_user->ID, '_stripe_customer_id' );
330 + global $wpdb;
331 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- One-off GDPR erase of prefixed per-account customer-id meta; $wpdb->usermeta is trusted.
332 + $meta_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->usermeta} WHERE user_id = %d AND meta_key LIKE %s", $wp_user->ID, $wpdb->esc_like( '_suredonation_stripe_customer_id_' ) . '%' ) );
333 + if ( is_array( $meta_keys ) ) {
334 + foreach ( $meta_keys as $meta_key ) {
335 + delete_user_meta( $wp_user->ID, (string) $meta_key );
336 + }
337 + }
338 + }
339 +
280 340 $updated = Donors::update(
281 341 $donor_id,
282 342 [
283 343 // Keep the email unique per donor to respect the UNIQUE column.
284 - 'email' => 'deleted-' . $donor_id . '@site.invalid',
285 - 'name' => wp_privacy_anonymize_data( 'text', Helper::get_string_value( $donor['name'] ?? '' ) ),
286 - 'phone' => '',
287 - 'company' => '',
288 - 'address' => '',
344 + 'email' => 'deleted-' . $donor_id . '@site.invalid',
345 + 'name' => wp_privacy_anonymize_data( 'text', Helper::get_string_value( $donor['name'] ?? '' ) ),
346 + 'phone' => '',
347 + 'company' => '',
348 + 'address' => '',
349 + 'donor_data' => $donor_data,
289 350 ]
290 351 );
291 352
292 353 // Same int|false contract as the donation updates above.