PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.12.2
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.12.2
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 / admin / activities / class-charitable-admin-activities.php

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

1,274 lines 41.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * This class is responsible for setting up and general maintenance of acitivity logs.
4 *
5 * @package Charitable/Classes/Charitable_Admin_Activities
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.8.1
10 */
11
12 // Exit if accessed directly.
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 if ( ! class_exists( 'Charitable_Admin_Activities' ) ) :
18
19 /**
20 * Charitable_Admin_Activities
21 *
22 * @since 1.8.1
23 */
24 final class Charitable_Admin_Activities {
25
26 /**
27 * The name of our database table
28 *
29 * @since 1.0.0
30 *
31 * @var string
32 */
33 public $table_name;
34
35 /**
36 * The activities.
37 *
38 * @since 1.0.0
39 *
40 * @var array
41 */
42 public $activities;
43
44 /**
45 * The database tables that store activity data.
46 *
47 * @since 1.8.1
48 *
49 * @var array
50 */
51 public $activity_db_tables = array( 'donation', 'campaign' );
52
53 /**
54 * The single instance of this class.
55 *
56 * @var Charitable_Admin_Activities|null
57 */
58 private static $instance = null;
59
60
61 /**
62 * Create class object.
63 *
64 * @since 1.8.1
65 */
66 public function __construct() {
67 }
68
69 /**
70 * Create the table.
71 *
72 * @since 1.0.0
73 *
74 * @global WPDB $wpdb
75 */
76 public function create_table() {
77 global $wpdb;
78
79 $charset_collate = $wpdb->get_charset_collate();
80
81 $sql = "CREATE TABLE {$this->table_name} (
82 donor_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
83 user_id bigint(20) unsigned NOT NULL,
84 email varchar(100) NOT NULL,
85 first_name varchar(255) default '',
86 last_name varchar(255) default '',
87 date_joined datetime NOT NULL default '0000-00-00 00:00:00',
88 data_erased datetime default '0000-00-00 00:00:00',
89 contact_consent tinyint(1) unsigned default NULL,
90 PRIMARY KEY (donor_id),
91 KEY user_id (user_id),
92 KEY email (email),
93 KEY data_erased (data_erased),
94 KEY contact_consent (contact_consent)
95 ) $charset_collate;";
96
97 // $this->_create_table( $sql );
98 }
99
100 /**
101 * Determine if we load the activity data (depending on if it's the lite version or not, and what page we are on).
102 *
103 * @since 1.8.1
104 *
105 * @return bool
106 */
107 public function maybe_use_activity_data_sample() {
108 // If lite version, give sample data to populate restricted area.
109 if ( ! charitable_is_pro() && ( ! isset( $_GET['page'] ) || ( 'charitable-reports' === $_GET['page'] && isset( $_GET['tab'] ) && 'activity' === $_GET['tab'] ) ) ) { // phpcs:ignore
110 return true;
111 }
112
113 return false;
114 }
115
116 /**
117 * Get activities.
118 *
119 * @since 1.8.1
120 *
121 * @param array|string $args {
122 *
123 * An array of arguments.
124 * @type array $activity_filter_types The types of activity information you want. Accepts '' (all), 'donations-made', 'donations-made-paid', 'refunds', 'campaigns-created', 'user-comments'. Default: array().
125 * @type array $activity_action_types The action types of activities in the custom databae tables (primary and/or secondary). Empty array returns all. Default: array().
126 * @type int $campaign_id Optional. Campaign ID that narrows activity to that campaign. Default: 0.
127 * @type int $category_id Optional. For campaigns, this is the term ID for the custom Charitable category taxonomy. Default: 0.
128 * @type string $start_date The start date for the activity. Accepts 'Y/m/d'. Default: 7 days ago.
129 * @type string $end_date The end date for the activity. Accepts 'Y/m/d'. Default: today.
130 * @type int $limit Optional. The number of activities to return. Default: 0.
131 * @type int $offset Optional. The number of activities to offset the query. Default: 0.
132 * @type string $sort Optional. The column to sort by. Default: 'date_recorded'.
133 *
134 * }
135 * @return int|bool The ID of the group on success. False on error.
136 */
137 public function get_activity( $args = array() ) {
138
139 global $wpdb;
140
141 // If lite version, give sample data to populate restricted area.
142 if ( $this->maybe_use_activity_data_sample() ) {
143 $this->activities = $this->get_activity_data_sample( 'donation' );
144 return $this->activities;
145 }
146
147 // Gather the arguments passed to the function.
148 $args = wp_parse_args(
149 $args,
150 array(
151 'activity_filter_types' => array(),
152 'activity_action_types' => array(),
153 'campaign_id' => 0,
154 'category_id' => 0,
155 'start_date' => gmdate( 'Y/m/d', strtotime( '-7 days' ) ),
156 'end_date' => gmdate( 'Y/m/d' ),
157 'limit' => 0,
158 'offset' => 0,
159 'sort_by' => 'date_to_sort',
160 'order' => 'DESC',
161 )
162 );
163
164 // Santitize or check incoming args.
165 $args['campaign_id'] = ( $args['campaign_id'] < 0 ) ? 0 : intval( $args['campaign_id'] ); // make sure -1 doesn't get passed as campaign_id.
166
167 // convert state and end date to mysql format.
168 $args['start_date'] = gmdate( 'Y-m-d', strtotime( $args['start_date'] ) ) . ' 00:00:00';
169 $args['end_date'] = gmdate( 'Y-m-d', strtotime( $args['end_date'] ) ) . ' 23:59:59';
170 $limit_sql = $args['limit'] ? 'LIMIT ' . $args['limit'] : '';
171
172 // Define the custom database table names and shortcuts.
173 $activity_db_tables = $this->activity_db_tables;
174 $table_names = array();
175 $table_shortcuts = array();
176 foreach ( $activity_db_tables as $activity_db_table ) {
177 $table_shortcuts[ $activity_db_table ] = $activity_db_table . 's_a';
178 $table_names[ $activity_db_table ] = $wpdb->prefix . 'charitable_' . $activity_db_table . '_activities ' . $table_shortcuts[ $activity_db_table ];
179 }
180
181 // 1. build the WHERE statement for mysql based on the passed in values.
182 $where_sql = array(); // init the where.
183 $where_sql[] = ( $args['campaign_id'] > 0 ) ? $table_shortcuts['donation'] . '.campaign_id = ' . $args['campaign_id'] : false;
184 $where_sql[] = "a.primary_action != ''";
185
186 // remove all empty values from the array.
187 $where_sql = array_filter( $where_sql );
188 $where_args = implode( ' AND ', $where_sql );
189
190 $db_types = array();
191 $db_actions = array(
192 'donation' => array(),
193 'campaign' => array(),
194 );
195
196 // Extract individual variables from args array.
197 $activity_filter_types = $args['activity_filter_types'] ?? array();
198 $activity_action_types = $args['activity_action_types'] ?? array();
199 $campaign_id = $args['campaign_id'] ?? 0;
200 $category_id = $args['category_id'] ?? 0;
201 $start_date = $args['start_date'] ?? gmdate( 'Y/m/d', strtotime( '-7 days' ) );
202 $end_date = $args['end_date'] ?? gmdate( 'Y/m/d' );
203 $limit = $args['limit'] ?? 0;
204 $offset = $args['offset'] ?? 0;
205 $sort_by = $args['sort_by'] ?? 'date_to_sort';
206 $order = $args['order'] ?? 'DESC';
207
208 if ( ! empty( $activity_filter_types ) ) :
209
210 foreach ( $activity_filter_types as $activity_filter_type ) :
211
212 switch ( sanitize_title( $activity_filter_type ) ) {
213 case 'donations-made':
214 $db_types = $db_types + array( 'donation' );
215 $db_actions['donation'] = $db_actions['donation']
216 + array(
217 'primary_action' => 'charitable-completed',
218 'secondary_action' => false,
219 )
220 + array(
221 'primary_action' => 'charitable-pending',
222 'secondary_action' => false,
223 )
224 + array(
225 'primary_action' => 'charitable-failed',
226 'secondary_action' => false,
227 )
228 + array(
229 'primary_action' => 'charitable-refunded',
230 'secondary_action' => false,
231 );
232 break;
233
234 case 'donations-made-paid':
235 $db_types = $db_types + array( 'donation' );
236 $db_actions['donation'] = $db_actions['donation']
237 + array(
238 'primary_action' => 'charitable-completed',
239 'secondary_action' => 'add_donation',
240 );
241 break;
242
243 case 'refunds':
244 $db_types = $db_types + array( 'donation' );
245 $db_actions['donation'] = $db_actions['donation']
246 + array(
247 'primary_action' => 'charitable-refunded',
248 'secondary_action' => false,
249 );
250 break;
251
252 case 'campaigns-created':
253 $db_types = $db_types + array( 'campaign' );
254 $db_actions['campaign'][] = array(
255 'primary_action' => 'update',
256 'secondary_action' => 'publish',
257 );
258 break;
259
260 default:
261 break;
262 }
263
264 endforeach;
265
266 endif;
267
268 $activities_data = $this->get_activity_db_data( $db_types, $db_actions, $args );
269
270 // Merge results arrays (donation, campaign, etc.).
271 $this->activities = $this->merge_activities( $activities_data );
272
273 // Sort the results based on 'sort' and 'order' args in $args.
274 $this->activities = $this->sort_activities( $this->activities, $args );
275
276 // Filter for plugins to add/remove activities, sort.
277 $this->activities = apply_filters( 'charitable_reports_activity_activities', $this->activities, $args );
278
279 return $this->activities;
280 }
281
282 /**
283 * Generate sql for getting campaign activity.
284 *
285 * @since 1.8.1
286 *
287 * @param array $args The arguments for the sql.
288 * @param array $db_actions The actions used to assemble the sql.
289 *
290 * @return string
291 */
292 public function get_activity_campaign_sql( $args, $db_actions = array() ) {
293
294 global $wpdb;
295
296 // Define the custom database table names and shortcuts.
297 $activity_db_tables = $this->activity_db_tables;
298 $table_names = array();
299 $table_shortcuts = array();
300 foreach ( $activity_db_tables as $activity_db_table ) {
301 $table_shortcuts[ $activity_db_table ] = $activity_db_table . 's_a';
302 $table_names[ $activity_db_table ] = $wpdb->prefix . 'charitable_' . $activity_db_table . '_activities ' . $table_shortcuts[ $activity_db_table ];
303 }
304
305 // 1. build the WHERE statement for mysql based on the passed in values.
306 $where_sql = array( '1=1' ); // init the where.
307 $where_sql[] = ( $args['campaign_id'] > 0 ) ? $table_shortcuts['campaign'] . '.campaign_id = ' . $args['campaign_id'] : false;
308 $where_sql[] = "{$table_shortcuts['campaign']}.primary_action != ''";
309
310 if ( ! empty( $db_actions ) ) {
311 foreach ( $db_actions as $actions ) {
312 $where_sql[] = "{$table_shortcuts['campaign']}.primary_action = '" . $actions['primary_action'] . "' AND {$table_shortcuts['campaign']}.secondary_action = '" . $actions['secondary_action'] . "'";
313 }
314 }
315
316 // remove all empty values from the array.
317 $where_sql = array_filter( $where_sql );
318 $where_args = implode( ' AND ', $where_sql );
319
320 // 2. build LEFT JOIN
321 $left_join = array();
322 $left_join['campaign'][] = $wpdb->prefix . 'posts p ON p.ID = ' . $table_shortcuts['campaign'] . '.campaign_id';
323 $left_join['campaign'] = array_filter( $left_join['campaign'] ); // remove all empty values from the array.
324 $left_join_campaign_args = 'LEFT JOIN ' . implode( ' LEFT JOIN ', $left_join['campaign'] );
325
326 $start_date = $args['start_date'] ?? '';
327 $end_date = $args['end_date'] ?? '';
328
329 $sql = "SELECT {$table_shortcuts['campaign']}.activity_id,
330 {$table_shortcuts['campaign']}.campaign_id,
331 null AS donor_id,
332 {$table_shortcuts['campaign']}.type,
333 {$table_shortcuts['campaign']}.primary_action,
334 {$table_shortcuts['campaign']}.secondary_action,
335 null AS amount,
336 {$table_shortcuts['campaign']}.created_by,
337 {$table_shortcuts['campaign']}.date_recorded,
338 {$table_shortcuts['campaign']}.date_recorded AS date_to_sort,
339 p.post_date AS campaign_date
340 FROM {$table_names['campaign']} {$left_join_campaign_args} WHERE $where_args AND p.post_date >= %s AND p.post_date <= %s";
341
342 return array(
343 'sql' => $sql,
344 'start_date' => $start_date,
345 'end_date' => $end_date,
346 );
347 }
348
349 /**
350 * Generate sql for getting donation activity.
351 *
352 * @since 1.8.1
353 *
354 * @param array $args The arguments for the sql.
355 *
356 * @return string
357 */
358 public function get_activity_donation_sql( $args = array() ) {
359
360 global $wpdb;
361
362 if ( empty( $args ) ) {
363 return array(
364 'sql' => false,
365 'start_date' => false,
366 'end_date' => false,
367 );
368 }
369
370 // Define the custom database table names and shortcuts.
371 $activity_db_tables = $this->activity_db_tables;
372 $table_names = array();
373 $table_shortcuts = array();
374 foreach ( $activity_db_tables as $activity_db_table ) {
375 $table_shortcuts[ $activity_db_table ] = $activity_db_table . 's_a';
376 $table_names[ $activity_db_table ] = $wpdb->prefix . 'charitable_' . $activity_db_table . '_activities ' . $table_shortcuts[ $activity_db_table ];
377 }
378
379 // 1. build the WHERE statement for mysql based on the passed in values.
380 $where_sql = array( '1=1' ); // init the where.
381 $where_sql[] = ( $args['campaign_id'] > 0 ) ? $table_shortcuts['donation'] . '.campaign_id = ' . $args['campaign_id'] : false;
382 $where_sql[] = "{$table_shortcuts['donation']}.primary_action != ''";
383 if ( ! empty( $args['activity_filter_types'] ) ) :
384 foreach ( $args['activity_filter_types'] as $activity_filter_type ) :
385 if ( 'refunds' === $activity_filter_type ) {
386 $where_sql[] = "{$table_shortcuts['donation']}.primary_action = 'charitable-refunded'";
387 }
388 if ( 'donations-made-paid' === $activity_filter_type ) {
389 $where_sql[] = "{$table_shortcuts['donation']}.primary_action = 'charitable-completed'";
390 }
391 endforeach;
392 endif;
393
394 // remove all empty values from the array.
395 $where_sql = array_filter( $where_sql );
396 $where_args = implode( ' AND ', $where_sql );
397
398 // 2. build LEFT JOIN
399 $left_join = array();
400 $left_join['donation'][] = $wpdb->prefix . 'posts p ON p.ID = ' . $table_shortcuts['donation'] . '.donation_id';
401 $left_join['donation'] = array_filter( $left_join['donation'] ); // remove all empty values from the array.
402 $left_join_donation_args = 'LEFT JOIN ' . implode( ' LEFT JOIN ', $left_join['donation'] );
403
404 $start_date = $args['start_date'] ?? '';
405 $end_date = $args['end_date'] ?? '';
406
407 $sql = "SELECT {$table_shortcuts['donation']}.activity_id,
408 {$table_shortcuts['donation']}.campaign_id,
409 {$table_shortcuts['donation']}.donation_id,
410 {$table_shortcuts['donation']}.donor_id,
411 {$table_shortcuts['donation']}.type,
412 {$table_shortcuts['donation']}.primary_action,
413 {$table_shortcuts['donation']}.secondary_action,
414 {$table_shortcuts['donation']}.amount,
415 {$table_shortcuts['donation']}.created_by,
416 {$table_shortcuts['donation']}.date_recorded,
417 {$table_shortcuts['donation']}.date_recorded AS date_to_sort,
418 p.post_date AS donation_date
419 FROM {$table_names['donation']} {$left_join_donation_args} WHERE $where_args AND {$table_shortcuts['donation']}.date_recorded >= %s AND {$table_shortcuts['donation']}.date_recorded <= %s";
420
421 return array(
422 'sql' => $sql,
423 'start_date' => $start_date,
424 'end_date' => $end_date,
425 );
426 }
427
428 /**
429 * Get "raw" data for the activity report based on the types, actions, and args passed in.
430 *
431 * @since 1.8.1
432 *
433 * @param array $db_types The types of activity information you want. Accepts '' (all), 'donations-made', 'donations-made-paid', 'refunds', 'campaigns-created', 'user-comments'. Default: array().
434 * @param array $db_actions The actions used to assemble the sql.
435 * @param array $args The arguments for the sql.
436 *
437 * @return array
438 */
439 public function get_activity_db_data( $db_types = array(), $db_actions = array(), $args = array() ) {
440
441 global $wpdb;
442
443 $campaign_results = array();
444 $donation_results = array();
445
446 $activity_db_tables = (array) $this->activity_db_tables;
447 $sql_results = array();
448
449 if ( empty( $db_types ) ) {
450
451 $campaign_sql = (array) $this->get_activity_campaign_sql( $args );
452 $campaign_results = $wpdb->get_results( // phpcs:ignore
453 $wpdb->prepare(
454 $campaign_sql['sql'], // phpcs:ignore
455 $campaign_sql['start_date'],
456 $campaign_sql['end_date']
457 )
458 );
459
460 $donation_sql = (array) $this->get_activity_donation_sql( $args );
461 $donation_results = $wpdb->get_results( // phpcs:ignore
462 $wpdb->prepare(
463 $donation_sql['sql'], // phpcs:ignore
464 $donation_sql['start_date'],
465 $donation_sql['end_date']
466 )
467 );
468
469 $sql_results = array(
470 'campaign_activities' => $campaign_results,
471 'donation_activities' => $donation_results,
472 );
473
474 } else {
475
476 foreach ( $db_types as $db_type ) {
477
478 switch ( $db_type ) {
479 case 'campaign':
480 $campaign_sql = $this->get_activity_campaign_sql( $args, $db_actions['campaign'] );
481 $campaign_results = $wpdb->get_results( // phpcs:ignore
482 $wpdb->prepare(
483 $campaign_sql['sql'], // phpcs:ignore
484 $campaign_sql['start_date'],
485 $campaign_sql['end_date']
486 )
487 );
488 break;
489 case 'donation':
490 $donation_sql = $this->get_activity_donation_sql( $args, $db_actions['donation'] );
491 $donation_results = $wpdb->get_results( // phpcs:ignore
492 $wpdb->prepare(
493 $donation_sql['sql'], // phpcs:ignore
494 $donation_sql['start_date'],
495 $donation_sql['end_date']
496 )
497 );
498 break;
499 default:
500 // code...
501 break;
502 }
503
504 }
505
506 $sql_results = array(
507 'campaign_activities' => $campaign_results,
508 'donation_activities' => $donation_results,
509 );
510
511 }
512
513 return $sql_results;
514 }
515
516 /**
517 * Merge activities.
518 *
519 * @since 1.8.1
520 *
521 * @param array $activities The activities to merge. This is an array of arrays. Each array is a different type of activity.
522 *
523 * @return array
524 */
525 public function merge_activities( $activities = array() ) {
526
527 // merge the two associative arrays keeping the keys.
528 $activities = array_merge_recursive( $activities['campaign_activities'], $activities['donation_activities'] );
529
530 return $activities;
531 }
532
533 /**
534 * Sort activities.
535 * Mostly a placeholder until more sorting functions are added.
536 *
537 * @since 1.8.1
538 *
539 * @param array $activities The activities to sort.
540 * @param array $args The arguments which contain potentially a 'sort_by' and 'order'.
541 *
542 * @return array
543 */
544 public function sort_activities( $activities = array(), $args = array() ) {
545
546 if ( empty( $activities ) ) {
547 return $activities;
548 }
549
550 $activities = $this->sort_activities_by_datetime( $activities, $args );
551
552 return $this->sort_activities_by_datetime( $activities, $args );
553 }
554
555 /**
556 * Sort activities by mysql date time stamp (or date in general).
557 *
558 * @since 1.8.1
559 *
560 * @param array $activities The activities to sort.
561 * @param array $args The arguments which contain potentially a 'sort_by' and 'order'.
562 *
563 * @return array
564 */
565 public function sort_activities_by_datetime( $activities = array(), $args = array() ) {
566
567 if ( empty( $activities ) ) {
568 return $activities;
569 }
570
571 $sort_by = ! empty( $args['sort_by'] ) ? $args['sort_by'] : 'date_to_sort';
572 $order = ! empty( $args['order'] ) ? $args['order'] : 'DESC';
573
574 usort(
575 $activities,
576 function ( $a, $b ) use ( $sort_by, $order ) {
577 if ( $order === 'ASC' ) {
578 return strtotime( $a->$sort_by ) - strtotime( $b->$sort_by );
579 } else {
580 return strtotime( $b->$sort_by ) - strtotime( $a->$sort_by );
581 }
582 }
583 );
584
585 return $activities;
586 }
587
588 /**
589 * Get the activity report data.
590 *
591 * @since 1.8.1
592 *
593 * @param array $activities The activities to get the report data for.
594 *
595 * @return array
596 */
597 public function get_activity_report_data( $activities = array() ) {
598
599 $report_activities = array();
600
601 // if $activities isn't an array, convert it to an array.
602 $activities = (array) $activities;
603
604 if ( ! is_array( $activities ) || empty( $activities ) ) {
605 return;
606 }
607
608 foreach ( $activities as $index => $activity ) {
609
610 if ( empty( $activity->sample ) && ( empty( $activity->activity_id ) || 0 === $activity->activity_id || empty( $activity->primary_action ) ) ) {
611 continue;
612 }
613
614 // Make sure certain fields are set.
615 $activity->secondary_action = ! empty( $activity->secondary_action ) ? $activity->secondary_action : false;
616
617 if ( $activity->type === 'donation' && $activity->primary_action === 'charitable-completed' && ! empty( $activity->donation_date ) ) {
618 // paid, but need to record the date of donation not the time it was added in the stream (especially for manual donations).
619 $donation_recorded = ( $activity->donation_date );
620 } else {
621 $donation_recorded = ( $activity->date_recorded );
622 }
623
624 $donor = false;
625 $wp_user_name = false;
626
627 if ( ! empty( $activity->donor_id ) ) {
628 $donor = new Charitable_Donor( intval( $activity->donor_id ) );
629 }
630 if ( ! empty( $activity->created_by ) ) {
631 $wp_user = new WP_User( intval( $activity->created_by ) );
632 if ( $wp_user ) {
633 $wp_user_name = $wp_user->user_login;
634 }
635 }
636
637 // For campaigns, the item_id is the campaign id, not the donation id (or anything else).
638 $campaign_id = ( $activity->type === 'campaign' || $activity->type === 'donation' ) && ! empty( $activity->campaign_id ) ? $activity->campaign_id : false;
639
640 $report_activities[ $index ] = new stdClass();
641 $report_activities[ $index ]->type = $activity->type;
642 $report_activities[ $index ]->amount = $activity->amount;
643 $report_activities[ $index ]->date_recorded = $donation_recorded;
644 $report_activities[ $index ]->date_to_sort = ! empty( $activity->date_to_sort ) ? esc_html( $activity->date_to_sort ) : false;
645 $report_activities[ $index ]->campaign_id = ! empty( $campaign_id ) ? intval( $campaign_id ) : false;
646 $report_activities[ $index ]->campaign_title = ! empty( $campaign_id ) ? get_the_title( $campaign_id ) : false;
647 $report_activities[ $index ]->created_by = ! empty( $activity->created_by ) ? intval( $activity->created_by ) : false;
648 $report_activities[ $index ]->created_by_name = ! empty( $wp_user_name ) ? $wp_user_name : false;
649 $report_activities[ $index ]->donor_id = ! empty( $activity->donor_id ) ? intval( $activity->donor_id ) : false;
650 $report_activities[ $index ]->donor_name = ! empty( $donor ) ? $donor->get_name() : false;
651 $report_activities[ $index ]->status = $this->get_status_slug( $activity->type, $activity->primary_action, $activity->secondary_action );
652 $report_activities[ $index ]->status_label = $this->get_status_label( $activity->type, $activity->primary_action, $activity->secondary_action );
653 $report_activities[ $index ]->action_label = $this->get_action_label( $activity->type, $activity->primary_action, $activity->secondary_action );
654 $report_activities[ $index ]->icon = $this->get_icon( $activity->type, $activity->primary_action, $activity->secondary_action );
655 $report_activities[ $index ]->item_id = ! empty( $activity->donation_id ) ? intval( $activity->donation_id ) : false;
656 $report_activities[ $index ]->item_id_prefix = '#';
657 $report_activities[ $index ]->primary_action = ! empty( $activity->primary_action ) ? esc_attr( $activity->primary_action ) : false;
658 $report_activities[ $index ]->secondary_action = ! empty( $activity->secondary_action ) ? esc_attr( $activity->secondary_action ) : false;
659
660 }
661
662 return $report_activities;
663 }
664
665 /**
666 * Get the status "slug" from a string.
667 *
668 * @since 1.8.1
669 *
670 * @param string $activity_type The type of activity. Accepts 'donation', 'campaign', etc.
671 * @param string $primary_action The primary action. Accepts 'charitable-completed', 'charitable-refunded', etc.
672 * @param string $secondary_action The secondary action. Accepts 'add_donation', 'make_donation', etc.
673 *
674 * @return string
675 */
676 public function get_status_slug( $activity_type = 'donation', $primary_action = false, $secondary_action = false ) {
677
678 // if the primary action is charitable-completed, charitable-refunded, etc. then the slug is the status.
679 $primary_action_slug = strpos( $primary_action, 'charitable-', 0 ) !== false ? str_replace( 'charitable-', '', strtolower( $primary_action ) ) : $primary_action;
680
681 $status_slug = esc_attr( $primary_action_slug );
682
683 switch ( $activity_type ) {
684 case 'donation':
685 switch ( $status_slug ) {
686 case 'completed':
687 $status_slug = 'paid';
688 break;
689
690 default:
691 $status_slug = esc_attr( $primary_action_slug );
692 break;
693 }
694 break;
695 case 'campaign':
696 switch ( $status_slug ) {
697 case 'update':
698 $status_slug = 'published';
699 break;
700
701 default:
702 $status_slug = false;
703 break;
704 }
705 break;
706 default:
707 $status_slug = esc_attr( $primary_action_slug );
708 break;
709 }
710
711 return apply_filters( 'charitable_activities_status_slug', $status_slug, $activity_type, $primary_action, $secondary_action );
712 }
713
714 /**
715 * Get the status label from a status string.
716 *
717 * @since 1.8.1
718 *
719 * @param string $activity_type The type of activity. Accepts 'donation', 'campaign', etc.
720 * @param string $primary_action The primary action. Accepts 'charitable-completed', 'charitable-refunded', etc.
721 * @param string $secondary_action The secondary action. Accepts 'add_donation', 'make_donation', etc.
722 *
723 * @return string
724 */
725 public function get_status_label( $activity_type = 'donation', $primary_action = false, $secondary_action = false ) {
726
727 // if the primary action is charitable-completed, charitable-refunded, etc. then the slug is the status.
728 $primary_action_slug = strpos( $primary_action, 'charitable-', 0 ) !== false ? str_replace( 'charitable-', '', strtolower( $primary_action ) ) : $primary_action;
729
730 switch ( $activity_type ) {
731 case 'donation':
732 switch ( $primary_action_slug ) {
733 case 'completed':
734 $status_label = 'Paid';
735 break;
736
737 default:
738 $status_label = ucwords( $primary_action_slug );
739 break;
740 }
741 break;
742 case 'campaign':
743 switch ( $primary_action_slug ) {
744 case 'update':
745 $status_label = 'Published';
746 break;
747
748 default:
749 $status_label = false;
750 break;
751 }
752 break;
753 default:
754 $status_label = ucwords( 'Unknown' );
755 break;
756 }
757
758 return apply_filters( 'charitable_activities_status_label', $status_label, $activity_type, $primary_action, $secondary_action );
759 }
760
761 /**
762 * Get the action label from a status string.
763 *
764 * @since 1.8.1
765 *
766 * @param string $activity_type The type of activity. Accepts 'donation', 'campaign', etc.
767 * @param string $primary_action The primary action. Accepts 'charitable-completed', 'charitable-refunded', etc.
768 * @param string $secondary_action The secondary action. Accepts 'add_donation', 'make_donation', etc.
769 *
770 * @return string
771 */
772 public function get_action_label( $activity_type = 'donation', $primary_action = false, $secondary_action = false ) {
773
774 // if the primary action is charitable-completed, charitable-refunded, etc. then the slug is the status.
775 $primary_action_slug = strpos( $primary_action, 'charitable-', 0 ) !== false ? str_replace( 'charitable-', '', strtolower( $primary_action ) ) : $primary_action;
776
777 $action_label = '';
778
779 switch ( $activity_type ) {
780 case 'donation':
781 if ( in_array( $secondary_action, array( 'add_donation', 'make_donation' ), true ) ) {
782 $action_label = 'Donation %id Made';
783 } elseif ( in_array( $secondary_action, array( 'update_donation', 'change_status' ), true ) && 'refunded' === $primary_action_slug ) {
784 $action_label = 'Refund %id';
785 } elseif ( in_array( $secondary_action, array( 'update_donation', 'change_status' ), true ) ) {
786 $action_label = 'Donation %id Updated';
787 }
788 break;
789 case 'campaign':
790 if ( in_array( $primary_action, array( 'update' ), true ) && in_array( $secondary_action, array( 'publish' ), true ) ) {
791 $action_label = 'Campaign %id';
792 }
793 break;
794 default:
795 $action_label = ucwords( 'Unknown' );
796 break;
797 }
798
799 return apply_filters( 'charitable_activities_action_label', $action_label, $activity_type, $primary_action, $secondary_action );
800 }
801
802
803 /**
804 * Get the icon from a status string.
805 *
806 * @since 1.8.1
807 *
808 * @param string $activity_type The type of activity. Accepts 'donation', 'campaign', etc.
809 * @param string $primary_action The primary action. Accepts 'charitable-completed', 'charitable-refunded', etc.
810 * @param string $secondary_action The secondary action. Accepts 'add_donation', 'make_donation', etc.
811 *
812 * @return string
813 */
814 public function get_icon( $activity_type = 'donation', $primary_action = false, $secondary_action = false ) {
815
816 // Set the default.
817 $icon = $activity_type;
818
819 $status_slug = $this->get_status_slug( $activity_type, $primary_action, $secondary_action );
820
821 switch ( $activity_type ) {
822 case 'donation':
823 if ( in_array( $secondary_action, array( 'add_donation', 'make_donation' ), true ) ) { // phpcs:ignore
824
825 } elseif ( in_array( $secondary_action, array( 'update_donation', 'change_status' ), true ) && 'refunded' === $status_slug ) {
826 $icon = 'refund';
827 } elseif ( in_array( $secondary_action, array( 'update_donation', 'change_status' ), true ) ) { // phpcs:ignore
828
829 }
830 break;
831 default:
832 break;
833 }
834
835 return apply_filters( 'charitable_activities_action_icon', $icon, $activity_type, $primary_action, $secondary_action );
836 }
837
838
839 /**
840 * Insert a new row
841 *
842 * @since 1.8.1
843 * @since 1.8.1,3 Added check.
844 *
845 * @param array $data The data to insert.
846 * @param string $table_name The name of the table to insert into.
847 * @param string $type The type of activity.
848 *
849 * @return int
850 */
851 public function insert( $data, $table_name = '', $type = 'donation' ) {
852 global $wpdb;
853
854 if ( empty( $data ) || '' === $table_name ) {
855 return 0;
856 }
857
858 /* Set default values */
859 $data = wp_parse_args( $data, $this->get_column_defaults() );
860
861 /**
862 * Filter the activity data.
863 *
864 * @since 1.8.1
865 *
866 * @param array $data Core data.
867 * @param Charitable_Admin_Activities $this This object.
868 */
869 $data = apply_filters( 'charitable_activity_admin_insert_data', $data, $this );
870
871 do_action( 'charitable_activity_pre_insert_' . $type, $data );
872
873 /* Initialise column format array */
874 $column_formats = $this->get_columns();
875
876 /* Force fields to lower case */
877 $data = array_change_key_case( $data );
878
879 /* White list columns */
880 $data = array_intersect_key( $data, $column_formats );
881
882 /* Reorder $column_formats to match the order of columns given in $data */
883 $data_keys = array_keys( $data );
884 $column_formats = array_merge( array_flip( $data_keys ), $column_formats );
885
886 $inserted = $wpdb->insert( $table_name, $data, $column_formats ); // phpcs:ignore
887
888 /* If the insert failed, return 0 */
889 if ( false === $inserted ) {
890 return 0;
891 }
892
893 do_action( 'charitable_activity_post_insert_' . $type, $wpdb->insert_id, $data );
894
895 return $wpdb->insert_id;
896 }
897
898 /**
899 * Whitelist of columns.
900 *
901 * @since 1.0.0
902 *
903 * @return array
904 */
905 public function get_columns() {
906 $columns = array(
907 'activity_id' => '%d',
908 'donation_id' => '%d',
909 'campaign_id' => '%d',
910 'donor_id' => '%d',
911 'type' => '%s',
912 'primary_action' => '%s',
913 'secondary_action' => '%s',
914 'amount' => '%s',
915 'created_by' => '%d',
916 'date_recorded' => '%s',
917 );
918
919 return $columns;
920 }
921
922 /**
923 * Default column values.
924 *
925 * @since 1.0.0
926 *
927 * @return array
928 */
929 public function get_column_defaults() {
930 $defaults = array(
931 'created_by' => 0,
932 'date_recorded' => gmdate( 'Y-m-d H:i:s' ),
933 );
934
935 return $defaults;
936 }
937
938 /**
939 * Return sample data for the example tables.
940 *
941 * @since 1.8.1
942 *
943 * @param string $report_type The type of report.
944 * @param string $data_type The type of data to return. Either 'object' or 'array'.
945 * @param int $number_of_records The number of records to return.
946 *
947 * @return array
948 */
949 public function get_activity_data_sample( $report_type = 'donation', $data_type = 'object', $number_of_records = 10 ) {
950
951 if ( false === $report_type ) {
952 $report_type = 'donation';
953 }
954
955 if ( $data_type === 'object' ) {
956 $data = new stdClass();
957 } else {
958 $data = array();
959 }
960
961 for ( $x = 0; $x < $number_of_records; $x++ ) {
962
963 $random_price = wp_rand( 50, 1000 ) / 10;
964 $random_date = gmdate( 'Y-m-d H:i:s', wp_rand( strtotime( '-37 days' ), strtotime( 'now' ) ) );
965
966 if ( $data_type === 'object' ) {
967
968 $data->{$x} = new stdClass();
969
970 $data->{$x}->activity_id = 0;
971 $data->{$x}->donation_id = 0;
972 $data->{$x}->campaign_id = 0;
973 $data->{$x}->donor_id = 0;
974 $data->{$x}->type = 'donation';
975 $data->{$x}->primary_action = 'charitable-completed';
976 $data->{$x}->amount = $random_price;
977 $data->{$x}->created_by = 1;
978 $data->{$x}->date_recorded = $random_date;
979 $data->{$x}->donation_date = $random_date;
980 $data->{$x}->date_to_sort = $random_date;
981 $data->{$x}->sample = true;
982
983 } else {
984
985 $data[] = array(
986
987 'activity_id' => 0,
988 'donation_id' => 0,
989 'campaign_id' => 0,
990 'donor_id' => 0,
991 'type' => 'donation',
992 'primary_action' => 'charitable-completed',
993 'amount' => $random_price,
994 'created_by' => 1,
995 'date_recorded' => $random_date,
996 'donation_date' => $random_date,
997 'date_to_sort' => $random_date,
998 'sample' => true,
999
1000 );
1001 }
1002 }
1003
1004 // sort the sample data.
1005 if ( $data_type === 'object' && ! empty( $data ) ) {
1006
1007 // convert object having objects to array of objects.
1008 $_data = (array) $data;
1009
1010 function cmp( $a, $b ) { // phpcs:ignore
1011 return strcmp( $a->date_to_sort, $b->date_to_sort );
1012 }
1013
1014 usort( $_data, 'cmp' );
1015
1016 // convert array of objects back to object.
1017 $data = (object) $_data;
1018
1019 }
1020
1021 $data = apply_filters(
1022 'charitable_report_activity_sample_data',
1023 $data,
1024 $report_type,
1025 $data_type,
1026 $number_of_records
1027 );
1028
1029 return $data;
1030 }
1031
1032 /**
1033 * Log a donation form error.
1034 *
1035 * @since 1.8.9.2
1036 *
1037 * @param string $error_type The type of error (validation_failure, ajax_failure, security_failure, email_failure).
1038 * @param string $error_details Specific error details or code.
1039 * @param array $context Additional context data for the error.
1040 *
1041 * @return int|false The activity ID on success, false on failure.
1042 */
1043 public function log_form_error( $error_type, $error_details, $context = array() ) {
1044 global $wpdb;
1045
1046 // Check if error logging is enabled
1047 if ( ! apply_filters( 'charitable_form_error_logging_enabled', true ) ) {
1048 return false;
1049 }
1050
1051 // Prepare the data for insertion
1052 $data = array(
1053 'donation_id' => isset( $context['donation_id'] ) ? intval( $context['donation_id'] ) : null,
1054 'campaign_id' => isset( $context['campaign_id'] ) ? intval( $context['campaign_id'] ) : null,
1055 'donor_id' => isset( $context['donor_id'] ) ? intval( $context['donor_id'] ) : null,
1056 'type' => 'form_error',
1057 'primary_action' => sanitize_text_field( $error_type ),
1058 'secondary_action' => sanitize_text_field( $error_details ),
1059 'amount' => isset( $context['amount'] ) ? floatval( $context['amount'] ) : null,
1060 'created_by' => get_current_user_id(),
1061 'date_recorded' => gmdate( 'Y-m-d H:i:s' ),
1062 );
1063
1064 // Get table name
1065 $table_name = $wpdb->prefix . 'charitable_donation_activities';
1066
1067 // Insert the error log
1068 $activity_id = $this->insert( $data, $table_name, 'form_error' );
1069
1070 // Store additional context data as meta if needed
1071 if ( $activity_id && ! empty( $context ) ) {
1072 $meta_data = $this->prepare_error_meta_data( $context );
1073 if ( ! empty( $meta_data ) ) {
1074 // Store meta data using WordPress options temporarily
1075 // In a full implementation, you might want a dedicated meta table
1076 update_option( 'charitable_form_error_meta_' . $activity_id, $meta_data, false );
1077 }
1078 }
1079
1080 /**
1081 * Action fired after a form error is logged.
1082 *
1083 * @since 1.8.9.2
1084 *
1085 * @param int $activity_id The activity ID.
1086 * @param string $error_type The error type.
1087 * @param string $error_details The error details.
1088 * @param array $context The error context.
1089 */
1090 do_action( 'charitable_form_error_logged', $activity_id, $error_type, $error_details, $context );
1091
1092 return $activity_id;
1093 }
1094
1095 /**
1096 * Prepare error meta data for storage.
1097 *
1098 * @since 1.8.9.2
1099 * @since 1.8.9.8 Handle array recipients to prevent fatal TypeError in sanitize_email().
1100 *
1101 * @param array $context The error context.
1102 *
1103 * @return array The prepared meta data.
1104 */
1105 private function prepare_error_meta_data( $context ) {
1106 $meta_data = array();
1107
1108 // Store essential data only for privacy
1109 if ( isset( $context['url'] ) ) {
1110 $meta_data['url'] = esc_url_raw( $context['url'] );
1111 }
1112
1113 if ( isset( $context['user_agent'] ) ) {
1114 $meta_data['user_agent'] = sanitize_text_field( $context['user_agent'] );
1115 }
1116
1117 if ( isset( $context['ip_address'] ) ) {
1118 $meta_data['ip_address'] = sanitize_text_field( $context['ip_address'] );
1119 }
1120
1121 if ( isset( $context['gateway'] ) ) {
1122 $meta_data['gateway'] = sanitize_text_field( $context['gateway'] );
1123 }
1124
1125 // Store error message for email and other failures — critical for debugging.
1126 if ( isset( $context['error_message'] ) ) {
1127 $meta_data['error_message'] = sanitize_text_field( substr( $context['error_message'], 0, 500 ) );
1128 }
1129
1130 // Store email class for email failures.
1131 if ( isset( $context['email_class'] ) ) {
1132 $meta_data['email_class'] = sanitize_text_field( $context['email_class'] );
1133 }
1134
1135 // Store recipient for email failures (helps debug delivery issues).
1136 if ( isset( $context['recipient'] ) ) {
1137 $recipient = $context['recipient'];
1138 if ( is_array( $recipient ) ) {
1139 $meta_data['recipient'] = implode( ', ', array_map( 'sanitize_email', $recipient ) );
1140 } else {
1141 $meta_data['recipient'] = sanitize_email( $recipient );
1142 }
1143 }
1144
1145 // Apply privacy filter to allow customization
1146 $meta_data = apply_filters( 'charitable_form_error_log_data', $meta_data, $context );
1147
1148 return $meta_data;
1149 }
1150
1151 /**
1152 * Get recent form errors for display.
1153 *
1154 * @since 1.8.9.2
1155 *
1156 * @param int $limit The number of errors to retrieve.
1157 *
1158 * @return array Array of error records.
1159 */
1160 public function get_recent_form_errors( $limit = 50 ) {
1161 global $wpdb;
1162
1163 $table_name = $wpdb->prefix . 'charitable_donation_activities';
1164
1165 $sql = $wpdb->prepare(
1166 "SELECT * FROM {$table_name}
1167 WHERE type = 'form_error'
1168 ORDER BY date_recorded DESC
1169 LIMIT %d",
1170 $limit
1171 );
1172
1173 $errors = $wpdb->get_results( $sql ); // phpcs:ignore
1174
1175 // Enhance errors with meta data
1176 foreach ( $errors as &$error ) {
1177 $meta_data = get_option( 'charitable_form_error_meta_' . $error->activity_id, array() );
1178 $error->meta_data = $meta_data;
1179 }
1180
1181 return $errors;
1182 }
1183
1184 /**
1185 * Get form error statistics.
1186 *
1187 * @since 1.8.9.2
1188 *
1189 * @param int $days Number of days to look back.
1190 *
1191 * @return array Statistics array.
1192 */
1193 public function get_form_error_stats( $days = 7 ) {
1194 global $wpdb;
1195
1196 $table_name = $wpdb->prefix . 'charitable_donation_activities';
1197 $date_threshold = gmdate( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) );
1198
1199 $sql = $wpdb->prepare(
1200 "SELECT
1201 primary_action,
1202 secondary_action,
1203 COUNT(*) as error_count,
1204 MAX(date_recorded) as last_occurrence
1205 FROM {$table_name}
1206 WHERE type = 'form_error'
1207 AND date_recorded >= %s
1208 GROUP BY primary_action, secondary_action
1209 ORDER BY error_count DESC",
1210 $date_threshold
1211 );
1212
1213 return $wpdb->get_results( $sql ); // phpcs:ignore
1214 }
1215
1216 /**
1217 * Clean up old form error logs.
1218 *
1219 * @since 1.8.9.2
1220 *
1221 * @param int $days Number of days to retain.
1222 *
1223 * @return int Number of records deleted.
1224 */
1225 public function cleanup_old_form_errors( $days = 7 ) {
1226 global $wpdb;
1227
1228 $table_name = $wpdb->prefix . 'charitable_donation_activities';
1229 $date_threshold = gmdate( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) );
1230
1231 // Get IDs of records to delete for meta cleanup
1232 $sql = $wpdb->prepare(
1233 "SELECT activity_id FROM {$table_name}
1234 WHERE type = 'form_error'
1235 AND date_recorded < %s",
1236 $date_threshold
1237 );
1238
1239 $old_error_ids = $wpdb->get_col( $sql ); // phpcs:ignore
1240
1241 // Delete meta data
1242 foreach ( $old_error_ids as $activity_id ) {
1243 delete_option( 'charitable_form_error_meta_' . $activity_id );
1244 }
1245
1246 // Delete the activity records
1247 $sql = $wpdb->prepare(
1248 "DELETE FROM {$table_name}
1249 WHERE type = 'form_error'
1250 AND date_recorded < %s",
1251 $date_threshold
1252 );
1253
1254 return $wpdb->query( $sql ); // phpcs:ignore
1255 }
1256
1257 /**
1258 * Returns and/or create the single instance of this class.
1259 *
1260 * @since 1.8.1
1261 *
1262 * @return Charitable_Admin_Activities
1263 */
1264 public static function get_instance() {
1265 if ( is_null( self::$instance ) ) {
1266 self::$instance = new self();
1267 }
1268
1269 return self::$instance;
1270 }
1271 }
1272
1273 endif;
1274