PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.11.3
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.11.3
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / includes / privacy / class-charitable-privacy.php

class-charitable-privacy.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.11.3, at includes/privacy/class-charitable-privacy.php

717 lines 19.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * A class providing privacy tools for Charitable.
4 *
5 * @package Charitable/Classes/Charitable_Privacy
6 * @author David Bisset
7 * @copyright Copyright (c) 2023, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.6.0
10 * @version 1.6.0
11 */
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 if ( ! class_exists( 'Charitable_Privacy' ) ) :
19
20 /**
21 * Charitable_Privacy
22 *
23 * @since 1.6.0
24 */
25 class Charitable_Privacy {
26
27 /**
28 * User donation fields.
29 *
30 * @since 1.6.0
31 *
32 * @var Charitable_Donation_Field[]
33 */
34 protected $user_donation_fields;
35
36 /**
37 * Fields that should be retained during the Data Retention Period.
38 *
39 * @since 1.6.0
40 *
41 * @var Charitable_Donation_Field[]
42 */
43 protected $data_retention_fields;
44
45 /**
46 * Whether data was retained during erasure.
47 *
48 * @since 1.6.0
49 *
50 * @var boolean
51 */
52 protected $retained;
53
54 /**
55 * Whether data was removed during erasure.
56 *
57 * @since 1.6.0
58 *
59 * @var boolean
60 */
61 protected $removed;
62
63 /**
64 * Set up class instance.
65 *
66 * @since 1.6.0
67 */
68 public function __construct() {
69 if ( ! function_exists( 'wp_privacy_anonymize_data' ) ) {
70 return;
71 }
72
73 /* Register our data exporter. */
74 add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_exporter' ) );
75
76 /* Register our data eraser. */
77 add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_eraser' ) );
78
79 /* Add our privacy policy content. */
80 add_action( 'admin_init', array( $this, 'add_privacy_policy_content' ) );
81 }
82
83 /**
84 * Return the data to be retained for donations within the Data Retention Period.
85 *
86 * @since 1.6.0
87 *
88 * @return array
89 */
90 public function get_data_retention_fields() {
91 if ( ! isset( $this->data_retention_fields ) ) {
92 $this->data_retention_fields = charitable_get_option( 'data_retention_fields', array_keys( $this->get_user_donation_fields() ) );
93 }
94
95 return $this->data_retention_fields;
96 }
97
98 /**
99 * Return the Data Retention Period.
100 *
101 * @since 1.6.0
102 *
103 * @return int|string
104 */
105 public function get_data_retention_period() {
106 return charitable_get_option( 'minimum_data_retention_period', 2 );
107 }
108
109 /**
110 * Get the user donation fields.
111 *
112 * @since 1.6.0
113 *
114 * @return Charitable_Donation_Field[]
115 */
116 public function get_user_donation_fields() {
117 if ( ! isset( $this->user_donation_fields ) ) {
118 $this->user_donation_fields = charitable()->donation_fields()->get_data_type_fields( 'user' );
119 }
120
121 return $this->user_donation_fields;
122 }
123
124 /**
125 * Return all erasable fields for a particular donation.
126 *
127 * @since 1.6.0
128 *
129 * @param int $donation_id The donation ID.
130 * @return Charitable_Donation_Field[]
131 */
132 public function get_erasable_fields_for_donation( $donation_id ) {
133 if ( ! $this->is_donation_in_data_retention_period( $donation_id ) ) {
134 return $this->get_user_donation_fields();
135 }
136
137 return $this->get_erasable_fields_in_data_retention_period();
138 }
139
140 /**
141 * Checks whether a donation is in the Data Retention Period.
142 *
143 * @since 1.6.0
144 *
145 * @param int $donation_id The donation ID.
146 * @return boolean
147 */
148 public function is_donation_in_data_retention_period( $donation_id ) {
149 $period = $this->get_data_retention_period();
150
151 if ( '0' == (string) $period ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
152 return false;
153 }
154
155 if ( 'endless' == $period ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
156 return true;
157 }
158
159 $diff = (int) abs( time() - strtotime( get_post_field( 'post_date_gmt', $donation_id, 'raw' ) ) );
160
161 return floor( $diff / YEAR_IN_SECONDS ) < $period;
162 }
163
164 /**
165 * Register the data exporter.
166 *
167 * @since 1.6.0
168 *
169 * @param array $exporters The list of exporters.
170 * @return array
171 */
172 public function register_exporter( $exporters ) {
173 return array_merge(
174 $exporters,
175 array(
176 array(
177 'exporter_friendly_name' => __( 'Charitable Donor Data', 'charitable' ),
178 'callback' => array( $this, 'export_user_data' ),
179 ),
180 )
181 );
182 }
183
184 /**
185 * Register the data eraser.
186 *
187 * @since 1.6.0
188 *
189 * @param array $erasers The list of registered data erasers.
190 * @return array
191 */
192 public function register_eraser( $erasers ) {
193 return array_merge(
194 $erasers,
195 array(
196 array(
197 'eraser_friendly_name' => __( 'Charitable Donor Data Eraser', 'charitable' ),
198 'callback' => array( $this, 'erase_user_data' ),
199 ),
200 )
201 );
202 }
203
204 /**
205 * Add privacy policy content for Charitable.
206 *
207 * @since 1.6.0
208 *
209 * @return void
210 */
211 public function add_privacy_policy_content() {
212 if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
213 return;
214 }
215
216 /**
217 * Filter the Charitable privacy policy content.
218 *
219 * @since 1.6.0
220 *
221 * @param string $content The privacy policy content.
222 */
223 $content = wp_kses_post(
224 apply_filters(
225 'charitable_privacy_policy_content',
226 wpautop(
227 __(
228 '
229 We collect information about you during the donation process on our website. This information may include, but is not limited to, your name, address, phone number, credit card/payment details and any other details that might be requested from you for the purpose of processing your donations.
230
231 Handling this data allows us to:
232
233 - Send you important account/donation/service information.
234 - Respond to your queries, refund requests or complaints.
235 - Process payments and prevent fraudulent transactions. We do this on the basis of our legitimate organizational interests.
236 - Set up and administer your account, provide technical and donor support, and to verify your identity.
237
238 Additionally, we may also collect the following information:
239
240 - Your donor comments if you choose to leave them on our website.
241 - Cookies which are essential to keep track of your donation history whilst your session is active. This allows you to access your donation receipt without being a registered, logged in user.
242 - Account email/password to allow access to your account, if you have one.
243 - If you choose to create an account with us, your name, address, email and phone number, which will be used to populate the donation form for future donations.
244 ',
245 'charitable'
246 )
247 )
248 )
249 );
250
251 wp_add_privacy_policy_content( 'Charitable', $content );
252 }
253
254 /**
255 * Return a user's data.
256 *
257 * We export the following pieces of data:
258 *
259 * 1. Registered donor meta. (*_usermeta)
260 * 2. Donor profile records. (*_charitable_donors table)
261 * 3. Donation donor meta. (*_postmeta)
262 *
263 * @since 1.6.0
264 *
265 * @param string $email The user's email address.
266 * @param int $page The page of data to retrieve.
267 * @return array
268 */
269 public function export_user_data( $email, $page = 1 ) {
270 $export_items = array();
271
272 if ( 1 === $page ) {
273 /* 1. Get registered donor meta. */
274 $user = get_user_by( 'email', $email );
275
276 if ( $user instanceof WP_User ) {
277 $export_items[] = $this->get_registered_donor_data( $user );
278 }
279 }
280
281 /* 2. Get donor profile. */
282 $profiles = charitable_get_table( 'donors' )->get_personal_data( $email );
283
284 if ( is_array( $profiles ) && 1 === $page ) {
285 $export_items = array_merge(
286 $export_items,
287 array_map( array( $this, 'get_donor_profile_data' ), $profiles )
288 );
289 }
290
291 /* If there are no donor profiles, there are no donations; return whatever we have. */
292 if ( empty( $profiles ) ) {
293 return array(
294 'data' => $export_items,
295 'done' => true,
296 );
297 }
298
299 /* 3. Donation donor meta */
300 $user_donation_fields = $this->get_user_donation_fields();
301
302 if ( ! empty( $user_donation_fields ) ) {
303 $donor_id = wp_list_pluck( $profiles, 'donor_id' );
304 $donations = charitable_get_table( 'campaign_donations' )->get_distinct_ids( 'donation_id', $donor_id, 'donor_id' );
305 $export_items = array_merge(
306 $export_items,
307 array_map( array( $this, 'get_personal_donation_meta_data' ), $donations )
308 );
309 }
310
311 return array(
312 'data' => $export_items,
313 'done' => true,
314 );
315 }
316
317 /**
318 * Remove a user's personal data.
319 *
320 * @since 1.6.0
321 *
322 * @param string $email The user's email address.
323 * @param int $page The page of data to retrieve.
324 * @return array
325 */
326 public function erase_user_data( $email, $page = 1 ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
327 $this->retained = false;
328 $this->removed = false;
329
330 /* 1. Remove registered donor meta. */
331 $user = get_user_by( 'email', $email );
332
333 if ( $user instanceof WP_User ) {
334 $this->erase_registered_donor_personal_data( $user );
335 }
336
337 /* 2. Remove donor profile data. */
338 $profiles = charitable_get_table( 'donors' )->get_personal_data( $email );
339
340 if ( is_array( $profiles ) ) {
341 charitable_get_table( 'donors' )->erase_donor_data( wp_list_pluck( $profiles, 'donor_id' ) );
342
343 $this->removed = true;
344 }
345
346 /* If there are no donor profiles, there are no donations. */
347 if ( empty( $profiles ) ) {
348 return array(
349 'items_removed' => $this->removed,
350 'items_retained' => $this->retained,
351 'messages' => array(),
352 'done' => true,
353 );
354 }
355
356 /* 3. Donation donor meta */
357 $user_donation_fields = $this->get_user_donation_fields();
358
359 if ( ! empty( $user_donation_fields ) ) {
360 $donations = $this->get_donations_from_profiles( $profiles );
361
362 array_walk( $donations, array( $this, 'erase_donation_personal_data' ) );
363 }
364
365 return array(
366 'items_removed' => $this->removed,
367 'items_retained' => $this->retained,
368 'messages' => array(),
369 'done' => true,
370 );
371 }
372
373 /**
374 * Return a registered donor's data.
375 *
376 * @since 1.6.0
377 *
378 * @param WP_User $user An instance of `WP_User`.
379 * @return array
380 */
381 public function get_registered_donor_data( $user ) {
382 $data = array();
383 $form = new Charitable_Profile_Form();
384 $methods = array(
385 'get_user_fields',
386 'get_address_fields',
387 'get_social_fields',
388 );
389 $key_map = charitable_get_user_mapped_keys();
390
391 foreach ( $methods as $method ) {
392 $fields = call_user_func( array( $form, $method ) );
393
394 if ( ! is_array( $fields ) ) {
395 continue;
396 }
397
398 foreach ( $fields as $key => $field ) {
399 $key = array_key_exists( $key, $key_map ) ? $key_map[ $key ] : $key;
400
401 if ( ! $user->has_prop( $key ) ) {
402 continue;
403 }
404
405 $data[] = array(
406 'name' => array_key_exists( 'label', $field ) ? $field['label'] : ucfirst( str_replace( '_', ' ', $key ) ),
407 'value' => $user->{$key},
408 );
409 }
410 }
411
412 /**
413 * Filter the personal donor meta data for a registered donor.
414 *
415 * @since 1.6.0
416 *
417 * @param array $data Set of personal donor data.
418 * @param WP_User $user An instance of `WP_User`.
419 */
420 $data = apply_filters( 'charitable_privacy_export_personal_registered_donor_data', $data, $user );
421
422 return array(
423 'item_id' => 'user',
424 'group_id' => 'charitable_users',
425 'group_label' => __( 'Donor Meta', 'charitable' ),
426 'data' => $data,
427 );
428 }
429
430 /**
431 * Returns donor profile data for export.
432 *
433 * @since 1.6.0
434 *
435 * @param object $profile The profile record.
436 * @return array
437 */
438 protected function get_donor_profile_data( $profile ) {
439 /**
440 * Filter the personal donor record data.
441 *
442 * @since 1.6.0
443 *
444 * @param array $data Set of personal donor data.
445 * @param object $profile The profile record.
446 */
447 $data = apply_filters(
448 'charitable_privacy_export_personal_donor_profile_data',
449 array(
450 array(
451 'name' => __( 'First Name', 'charitable' ),
452 'value' => $profile->first_name,
453 ),
454 array(
455 'name' => __( 'Last Name', 'charitable' ),
456 'value' => $profile->last_name,
457 ),
458 array(
459 'name' => __( 'Email', 'charitable' ),
460 'value' => $profile->email,
461 ),
462 )
463 );
464
465 return array(
466 'item_id' => 'donor-' . $profile->donor_id,
467 'group_id' => 'charitable_donors',
468 'group_label' => __( 'Donor Profiles', 'charitable' ),
469 'data' => $data,
470 );
471 }
472
473 /**
474 * Return the donor meta stored with a donor's donation records.
475 *
476 * @since 1.6.0
477 *
478 * @param int $donation_id A donation ID.
479 * @return array
480 */
481 protected function get_personal_donation_meta_data( $donation_id ) {
482 $meta = get_post_meta( $donation_id, 'donor', true );
483
484 if ( ! is_array( $meta ) ) {
485 return array();
486 }
487
488 $data = array();
489
490 foreach ( $this->get_user_donation_fields() as $field_id => $field ) {
491 if ( ! array_key_exists( $field_id, $meta ) ) {
492 continue;
493 }
494
495 $data[] = array(
496 'name' => $field->label,
497 'value' => $meta[ $field_id ],
498 );
499 }
500
501 /**
502 * Filter the personal donation meta data for a particular donation.
503 *
504 * @since 1.6.0
505 *
506 * @param array $data Set of personal donation meta data.
507 * @param array $data The donor meta array.
508 * @param int $donation_id The donation ID.
509 */
510 $data = apply_filters( 'charitable_privacy_export_personal_donation_meta_data', $data, $meta, $donation_id );
511
512 return array(
513 'item_id' => 'donation-' . $donation_id,
514 'group_id' => 'charitable_donations',
515 'group_label' => __( 'Donation Meta', 'charitable' ),
516 'data' => $data,
517 );
518 }
519
520 /**
521 * Remove personal data for a registered donor.
522 *
523 * @since 1.6.0
524 *
525 * @param WP_User $user The instance of `WP_User`.
526 * @return void
527 */
528 protected function erase_registered_donor_personal_data( WP_User $user ) {
529 $form = new Charitable_Profile_Form();
530 $methods = array(
531 'get_user_fields',
532 'get_address_fields',
533 'get_social_fields',
534 );
535 $key_map = charitable_get_user_mapped_keys();
536
537 foreach ( $methods as $method ) {
538 $fields = call_user_func( array( $form, $method ) );
539
540 if ( ! is_array( $fields ) ) {
541 continue;
542 }
543
544 foreach ( $fields as $key => $field ) {
545 $key = array_key_exists( $key, $key_map ) ? $key_map[ $key ] : $key;
546
547 if ( ! $user->has_prop( $key ) ) {
548 continue;
549 }
550
551 /* Get the data type we will use for erasing this particular data field. */
552 $data_type = $this->get_field_data_type( (object) $field );
553
554 /**
555 * Filter the placeholder data that will replace the personal data.
556 *
557 * @since 1.6.0
558 *
559 * @param string $data The anonymous data.
560 * @param mixed $value The current value of the field.
561 * @param string $key The key of the field.
562 * @param array $field The field definition.
563 */
564 $data = apply_filters( 'charitable_privacy_erasure_registered_user_data_prop_value', wp_privacy_anonymize_data( $data_type, $user->{$key} ), $user->{$key}, $key, $field );
565
566 update_user_meta( $user->ID, $key, $data );
567
568 $this->removed = true;
569 }
570 }
571 }
572
573 /**
574 * Remove personal data from a single donation.
575 *
576 * @since 1.6.0
577 *
578 * @param int $donation_id The donation ID.
579 * @return void
580 */
581 protected function erase_donation_personal_data( $donation_id ) {
582 $meta = get_post_meta( $donation_id, 'donor', true );
583
584 foreach ( $this->get_erasable_fields_for_donation( $donation_id ) as $field_id => $field ) {
585 if ( ! array_key_exists( $field_id, $meta ) ) {
586 continue;
587 }
588
589 /* Get the data type we will use for erasing this particular data field. */
590 $data_type = $this->get_field_data_type( $field );
591 $replace = empty( $meta[ $field_id ] ) ? '' : wp_privacy_anonymize_data( $data_type, $meta[ $field_id ] );
592
593 /**
594 * Filter the placeholder data that will replace the personal data.
595 *
596 * @since 1.6.0
597 *
598 * @param string $data The anonymous data.
599 * @param mixed $value The current value of the field.
600 * @param string $key The key of the field.
601 * @param Charitable_Donation_Field $field The field definition.
602 */
603 $meta[ $field_id ] = apply_filters( 'charitable_privacy_erasure_donation_donor_data_prop_value', $replace, $meta[ $field_id ], $field_id, $field );
604 }
605
606 update_post_meta( $donation_id, 'donor', $meta );
607 update_post_meta( $donation_id, 'data_erased', current_time( 'mysql', 0 ) );
608
609 /* Change the post_title since that otherwise has the donor's name. */
610 wp_update_post(
611 array(
612 'ID' => $donation_id,
613 'post_title' => sprintf(
614 /* translators: %s: campaign names */
615 __( 'Anonymous &ndash; %s', 'charitable' ),
616 charitable_get_donation( $donation_id )->get_campaigns_donated_to()
617 ),
618 )
619 );
620
621 $log = new Charitable_Donation_Log( $donation_id );
622 $log->add( __( 'Personal data erased.', 'charitable' ) );
623 }
624
625 /**
626 * Checks whether there are any user donation fields that can be erased
627 * for donations within the data retention period.
628 *
629 * @since 1.6.0
630 *
631 * @return boolean
632 */
633 protected function has_erasable_fields_within_data_retention_period() {
634 return 0 < count(
635 array_unique(
636 array_merge(
637 array_keys( $this->get_user_donation_fields() ),
638 $this->get_data_retention_fields()
639 )
640 )
641 );
642 }
643
644 /**
645 * Returns the erasable fields within the Data Retention Period.
646 *
647 * @since 1.6.0
648 *
649 * @return Charitable_Donation_Field[]
650 */
651 protected function get_erasable_fields_in_data_retention_period() {
652 return array_diff_key(
653 $this->get_user_donation_fields(),
654 array_flip( $this->get_data_retention_fields() )
655 );
656 }
657
658 /**
659 * Return the donation IDs for one or more profiles.
660 *
661 * @since 1.6.0
662 *
663 * @param object[] $profiles The donor profiles.
664 * @return int[]
665 */
666 protected function get_donations_from_profiles( $profiles ) {
667 $donor_id = wp_list_pluck( $profiles, 'donor_id' );
668
669 return charitable_get_table( 'campaign_donations' )->get_distinct_ids( 'donation_id', $donor_id, 'donor_id' );
670 }
671
672 /**
673 * Return the data type for a field.
674 *
675 * @since 1.6.0
676 *
677 * @param object $field The field attributes. This may be a Charitable_Donation_Field
678 * instance, or a simple object cast from an array.
679 * @return string
680 */
681 protected function get_field_data_type( $field ) {
682 if ( isset( $field->type ) ) {
683 $type = $field->type;
684 } else {
685 $type = is_array( $field->donation_form ) ? $field->donation_form['type'] : $field->admin_form['type'];
686 }
687
688 switch ( $type ) {
689 case 'select':
690 case 'multi-checkbox':
691 $data_type = '';
692 break;
693
694 case 'textarea':
695 case 'editor':
696 $data_type = 'longtext';
697 break;
698
699 default:
700 $data_type = $type;
701 }
702
703 /**
704 * Filter the data type to use for a field to be erased.
705 *
706 * @since 1.6.0
707 *
708 * @param string $data_type The data type.
709 * @param object $field The field attributes. This may be a Charitable_Donation_Field
710 * instance, or a simple object cast from an array.
711 */
712 return apply_filters( 'charitable_privacy_erasure_field_data_type', $data_type, $field );
713 }
714 }
715
716 endif;
717