PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
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 1.7.0.9 All 211 releases
← All changes | includes/data/class-charitable-campaign-donations-db.php +804 -33 1.7.0 → 1.8.13 View file →
@@ -3,9 +3,9 @@
3 3 * Charitable Campaign Donations DB class.
4 4 *
5 5 * @package Charitable/Classes/Charitable_Campaign_Donations_DB
6 6 * @author David Bisset
7 - * @copyright Copyright (c) 2022, WP Charitable LLC
7 + * @copyright Copyright (c) 2023, WP Charitable LLC
8 8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 9 * @since 1.0.0
10 10 * @version 1.6.57
11 11 */
@@ -172,20 +172,34 @@
172 172
173 173 /**
174 174 * Delete all campaign donation records for a given donation.
175 175 *
176 - * @since 1.2.0
176 + * @since 1.2.0
177 + * @version 1.8.8.1
177 178 *
178 179 * @param int $donation_id The donation ID.
179 180 * @return boolean
180 181 */
181 182 public static function delete_donation_records( $donation_id ) {
183 + // Only act on Charitable donation post type.
184 + $post_type = get_post_type( (int) $donation_id );
185 + if ( empty( $post_type ) || ( defined( 'Charitable::DONATION_POST_TYPE' ) ? $post_type !== Charitable::DONATION_POST_TYPE : $post_type !== 'donation' ) ) { // Fallback to 'donation' slug if constant is unavailable.
186 + return false;
187 + }
188 +
182 189 $table = charitable_get_table( 'campaign_donations' );
190 + if ( empty( $table ) || ! is_object( $table ) || ! method_exists( $table, 'get_campaigns_for_donation' ) ) {
191 + return false;
192 + }
183 193
184 194 foreach ( $table->get_campaigns_for_donation( $donation_id ) as $campaign_id ) {
185 195 Charitable_Campaign::flush_donations_cache( $campaign_id );
186 196 }
187 197
198 + if ( ! method_exists( $table, 'delete_by' ) ) {
199 + return false;
200 + }
201 +
188 202 return $table->delete_by( 'donation_id', $donation_id );
189 203 }
190 204
191 205 /**
@@ -215,9 +229,9 @@
215 229 INNER JOIN $wpdb->posts p
216 230 ON p.ID = cd.donation_id
217 231 $where_sql";
218 232
219 - $total = $wpdb->get_var( $wpdb->prepare( $sql, $parameters ) );
233 + $total = $wpdb->get_var( $wpdb->prepare( $sql, $parameters ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
220 234
221 235 if ( $this->is_comma_decimal() ) {
222 236 $total = Charitable_Currency::get_instance()->sanitize_database_amount( $total );
223 237 }
@@ -246,9 +260,9 @@
246 260 $sql = "SELECT *
247 261 FROM $this->table_name
248 262 WHERE $column IN ( $in );";
249 263
250 - $records = $wpdb->get_results( $wpdb->prepare( $sql, $parameters ), OBJECT_K );
264 + $records = $wpdb->get_results( $wpdb->prepare( $sql, $parameters ), OBJECT_K ); // phpcs:ignore
251 265
252 266 if ( $this->is_comma_decimal() ) {
253 267 $records = array_map( array( $this, 'sanitize_amounts' ), $records );
254 268 }
@@ -278,9 +292,9 @@
278 292 $sql = "SELECT DISTINCT $select_column
279 293 FROM $this->table_name
280 294 WHERE $where_column IN ( $in );";
281 295
282 - return $wpdb->get_col( $wpdb->prepare( $sql, $parameters ) );
296 + return $wpdb->get_col( $wpdb->prepare( $sql, $parameters ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
283 297 }
284 298
285 299 /**
286 300 * Get an object of all campaign donations associated with one or more donations.
@@ -325,9 +339,9 @@
325 339 $sql = "SELECT SUM(amount)
326 340 FROM $this->table_name
327 341 WHERE $where_clause;";
328 342
329 - $total = $wpdb->get_var( $wpdb->prepare( $sql, $parameters ) );
343 + $total = $wpdb->get_var( $wpdb->prepare( $sql, $parameters ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
330 344
331 345 if ( $this->is_comma_decimal() ) {
332 346 $total = Charitable_Currency::get_instance()->sanitize_database_amount( $total );
333 347 }
@@ -339,9 +353,9 @@
339 353 * Get the total amount donated in a single donation.
340 354 *
341 355 * @since 1.0.0
342 356 *
343 - * @param int $donation_id
357 + * @param int $donation_id Donation ID.
344 358 * @return decimal
345 359 */
346 360 public function get_donation_total_amount( $donation_id ) {
347 361 return $this->get_donation_amount( $donation_id );
@@ -347,13 +361,146 @@
347 361 return $this->get_donation_amount( $donation_id );
348 362 }
349 363
350 364 /**
365 + * Return the total donated on each of several donations, grouped by
366 + * donation id, in a single query.
367 + *
368 + * The donation-side counterpart to get_donated_amounts_by_campaign().
369 + * Added because no batched per-donation accessor existed: listing a page
370 + * of donations and calling get_donation_total_amount() per row issues one
371 + * query per donation, which is the same N+1 that had to be fixed out of
372 + * the campaign list.
373 + *
374 + * No status clause: the donation ids are already selected by status by
375 + * the caller, and a per-donation total must not be filtered by the
376 + * donation's own status — the donation IS the status.
377 + *
378 + * The donor id comes back alongside the total, and that is the point of
379 + * it rather than a convenience. Charitable_Donation::get_donor_id()
380 + * resolves it through get_campaign_donations(), which issues its own
381 + * `SELECT * FROM campaign_donations WHERE donation_id IN (N)` per
382 + * donation — so a page of 20 donations paid 20 extra queries to learn
383 + * something this one grouped query already had in hand. Measured on the
384 + * development site: dropping that call took charitable/list-donations
385 + * from 76 queries to 36.
386 + *
387 + * MIN( cd.donor_id ) rather than any aggregate cleverness: every row of
388 + * a single donation carries the same donor_id, because the donor is a
389 + * property of the donation and not of the split across campaigns.
390 + *
391 + * Ported from Charitable Pro, with one change: Pro's version reads
392 + * COALESCE( cd.base_amount, cd.amount ) because Pro's multi-currency
393 + * feature adds a base_amount column to this table. Lite has no
394 + * base_amount column and no multi-currency feature, so this sums
395 + * cd.amount directly — the same substitution every other aggregate
396 + * method in this Lite file already carries (see
397 + * get_donations_summary_by_day_range() below for the same rationale).
398 + *
399 + * @since 1.8.13
400 + *
401 + * @global WPDB $wpdb
402 + * @param int[] $donation_ids Donation IDs.
403 + * @return array Keyed by donation_id (int), each value an array with
404 + * 'amount' (float) and 'donor_id' (int). A donation with no
405 + * campaign_donations rows is simply absent — callers should
406 + * treat a missing key as zero.
407 + */
408 + public function get_donated_amounts_by_donation( $donation_ids ) {
409 + global $wpdb;
410 +
411 + list( $donations_in, $parameters ) = $this->get_in_clause_params( $donation_ids );
412 +
413 + if ( empty( $donations_in ) || empty( $parameters ) ) {
414 + return array();
415 + }
416 +
417 + $sql = "SELECT cd.donation_id,
418 + COALESCE( SUM( cd.amount ), 0 ) as total,
419 + MIN( cd.donor_id ) as donor_id
420 + FROM $this->table_name cd
421 + WHERE cd.donation_id IN ( $donations_in )
422 + GROUP BY cd.donation_id";
423 +
424 + $results = $wpdb->get_results( $wpdb->prepare( $sql, $parameters ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
425 +
426 + $totals = array();
427 +
428 + foreach ( $results as $row ) {
429 + $totals[ (int) $row->donation_id ] = array(
430 + 'amount' => (float) $row->total,
431 + 'donor_id' => (int) $row->donor_id,
432 + );
433 + }
434 +
435 + return $totals;
436 + }
437 +
438 + /**
439 + * Return the per-campaign rows for MANY donations in one query.
440 + *
441 + * The batched counterpart to get_campaigns_for_donation() below. A
442 + * listing that wants to show what each gift was split across needs this
443 + * shape or it pays a query per row — the same reason
444 + * get_donated_amounts_by_donation() above exists, and that method is the
445 + * model for this one.
446 + *
447 + * Ordered by id so a split donation's campaigns come back in the order
448 + * they were recorded, which is the order the donation form collected
449 + * them.
450 + *
451 + * Ported from Charitable Pro, with one change: Pro's version reads
452 + * COALESCE( cd.base_amount, cd.amount ) because Pro's multi-currency
453 + * feature adds a base_amount column to this table. Lite has no
454 + * base_amount column and no multi-currency feature, so this reads
455 + * cd.amount directly — the same substitution get_donated_amounts_by_donation()
456 + * above carries.
457 + *
458 + * @since 1.8.13
459 + *
460 + * @global wpdb $wpdb
461 + * @param int[] $donation_ids The donation IDs.
462 + * @return array donation_id => array of { campaign_id, campaign_name, amount }
463 + */
464 + public function get_campaign_rows_by_donation( $donation_ids ) {
465 + global $wpdb;
466 +
467 + list( $donations_in, $parameters ) = $this->get_in_clause_params( $donation_ids );
468 +
469 + if ( empty( $donations_in ) || empty( $parameters ) ) {
470 + return array();
471 + }
472 +
473 + $sql = "SELECT cd.donation_id,
474 + cd.campaign_id,
475 + cd.campaign_name,
476 + cd.amount
477 + FROM $this->table_name cd
478 + WHERE cd.donation_id IN ( $donations_in )
479 + ORDER BY cd.donation_id ASC, cd.campaign_donation_id ASC";
480 +
481 + $results = $wpdb->get_results( $wpdb->prepare( $sql, $parameters ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
482 +
483 + $rows = array();
484 +
485 + foreach ( (array) $results as $row ) {
486 + $rows[ (int) $row->donation_id ][] = array(
487 + 'campaign_id' => (int) $row->campaign_id,
488 + 'campaign_name' => (string) $row->campaign_name,
489 + 'amount' => (float) $row->amount,
490 + );
491 + }
492 +
493 + return $rows;
494 + }
495 +
496 + /**
351 497 * Return an array of campaigns donated to in a single donation.
352 498 *
353 499 * @since 1.2.0
354 500 *
355 - * @global wpdb $wpdb
501 + * @global wpdb $wpdb WPDB.
502 + * @param int $donation_id Donation ID.
356 503 * @return object
357 504 */
358 505 public function get_campaigns_for_donation( $donation_id ) {
359 506 global $wpdb;
@@ -361,9 +508,9 @@
361 508 $sql = "SELECT DISTINCT campaign_id
362 509 FROM $this->table_name
363 510 WHERE donation_id = %d;";
364 511
365 - return $wpdb->get_col( $wpdb->prepare( $sql, intval( $donation_id ) ) );
512 + return $wpdb->get_col( $wpdb->prepare( $sql, intval( $donation_id ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
366 513 }
367 514
368 515 /**
369 516 * Get an object of all donations on a campaign.
@@ -371,9 +518,9 @@
371 518 * @uses Charitable_Campaign_Donations_DB::get_campaign_donations_by()
372 519 *
373 520 * @since 1.0.0
374 521 *
375 - * @param int|int[] $campaign_id
522 + * @param int|int[] $campaign_id Campaign ID.
376 523 * @return object
377 524 */
378 525 public function get_donations_on_campaign( $campaign_id ) {
379 526 return $this->get_campaign_donations_by( 'campaign_id', $campaign_id );
@@ -383,9 +530,9 @@
383 530 * Get total amount donated to a campaign.
384 531 *
385 532 * @since 1.0.0
386 533 *
387 - * @global wpdb $wpdb
534 + * @global wpdb $wpdb WPDB.
388 535 * @param int|int[] $campaigns A campaign ID. Optionally, you can pass an array of campaign IDs to get the total of all put together.
389 536 * @param boolean $include_all Whether donations with non-approved statuses should be included.
390 537 * @param boolean $sanitize Whether to sanitize the amount if we're using commas for decimals.
391 538 * @return string
@@ -397,12 +544,162 @@
397 544 'campaigns' => $campaigns,
398 545 ),
399 546 $sanitize
400 547 );
548 + }
401 549
550 + /**
551 + * Return the total amount donated to each of several campaigns, grouped
552 + * by campaign id, in a single query.
553 + *
554 + * Exists so a caller iterating N campaigns (e.g. the charitable/list-campaigns
555 + * ability) can resolve every row's donated amount in one query instead of
556 + * N calls to get_campaign_donated_amount() / Charitable_Campaign::get_donated_amount() —
557 + * one query per campaign multiplied by a page size is the exact N+1 shape
558 + * that made the long-range Reports Overview timeout ~18,000 queries before
559 + * it was batched the same way.
560 + *
561 + * Ported from Charitable Pro, with one change: Pro's version reads
562 + * COALESCE( cd.base_amount, cd.amount ) because Pro's multi-currency
563 + * feature adds a base_amount column to this table. Lite has no
564 + * base_amount column and no multi-currency feature, so this reads
565 + * cd.amount directly — the same column every other aggregate method in
566 + * this Lite file already uses (see get_amount_by_campaign_report()).
567 + *
568 + * Deliberately mirrors get_campaign_donated_amount( $campaigns, false, false )'s
569 + * raw, un-locale-converted output (no sanitize_database_amount() pass) so a
570 + * batched row and a get_campaign_donated_amount() call for the same campaign
571 + * return numerically identical values — callers that need the site's
572 + * comma/dot display convention should convert the same way that call site
573 + * already does.
574 + *
575 + * This is a raw aggregate accessor: it does NOT apply the
576 + * `charitable_campaign_donated_amount` filter that
577 + * Charitable_Campaign::get_donated_amount() applies on every call. A
578 + * caller that wants a figure consistent with the rest of the product —
579 + * not just a faster one — must apply that filter itself, per
580 + * campaign_id, with the same arguments get_donated_amount() uses:
581 + * `apply_filters( 'charitable_campaign_donated_amount', $amount,
582 + * $campaign, $sanitize )`. See
583 + * Charitable_Campaign_Abilities::campaign_summary() for a worked example.
584 + *
585 + * @since 1.8.13
586 + *
587 + * @global WPDB $wpdb
588 + * @param int[] $campaign_ids Campaign IDs.
589 + * @param boolean $include_all Whether to include all donations (true), or only approved (false).
590 + * @return array Keyed by campaign_id (int), each value a float total. A
591 + * campaign with no matching donations is simply absent from
592 + * the array — callers should treat a missing key as 0.
593 + */
594 + public function get_donated_amounts_by_campaign( $campaign_ids, $include_all = false ) {
595 + global $wpdb;
596 +
597 + list( $campaigns_in, $campaigns_parameters ) = $this->get_in_clause_params( $campaign_ids );
598 +
599 + if ( empty( $campaigns_in ) || empty( $campaigns_parameters ) ) {
600 + return array();
601 + }
602 +
603 + $statuses = $include_all ? array() : charitable_get_approval_statuses();
604 +
605 + list( $status_clause, $status_parameters ) = $this->get_donation_status_clause( $statuses );
606 +
607 + $sql_where_clauses = array( "cd.campaign_id IN ( $campaigns_in )" );
608 +
609 + if ( ! empty( $status_clause ) ) {
610 + $sql_where_clauses[] = $status_clause;
611 + }
612 +
613 + $sql_where = 'WHERE ' . implode( ' AND ', $sql_where_clauses );
614 + $parameters = array_merge( $campaigns_parameters, $status_parameters );
615 +
616 + $sql = "SELECT cd.campaign_id, COALESCE( SUM( cd.amount ), 0 ) as total
617 + FROM $this->table_name cd
618 + INNER JOIN $wpdb->posts p ON p.ID = cd.donation_id
619 + $sql_where
620 + GROUP BY cd.campaign_id";
621 +
622 + $results = $wpdb->get_results( $wpdb->prepare( $sql, $parameters ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
623 +
624 + $totals = array();
625 +
626 + foreach ( $results as $row ) {
627 + $totals[ (int) $row->campaign_id ] = (float) $row->total;
628 + }
629 +
630 + return $totals;
402 631 }
403 632
404 633 /**
634 + * Return the number of distinct donors for each of several campaigns,
635 + * grouped by campaign id, in a single query.
636 + *
637 + * Same rationale as get_donated_amounts_by_campaign(): count_campaign_donors()
638 + * has no caching layer at all (unlike the donated-amount path, which is at
639 + * least transient-cached), so calling it once per row in a list is the
640 + * more expensive half of that ability's N+1.
641 + *
642 + * Ported from Charitable Pro unchanged: this query never touches
643 + * base_amount, so Lite's lack of that column does not affect it.
644 + *
645 + * This is a raw aggregate accessor: it does NOT apply the
646 + * `charitable_campaign_donor_count` filter that
647 + * Charitable_Campaign::get_donor_count() applies on every call. A caller
648 + * that wants a figure consistent with the rest of the product must apply
649 + * that filter itself, per campaign_id: `apply_filters(
650 + * 'charitable_campaign_donor_count', $count, $campaign )`. See
651 + * Charitable_Campaign_Abilities::campaign_summary() for a worked example.
652 + *
653 + * @since 1.8.13
654 + *
655 + * @global WPDB $wpdb
656 + * @param int[] $campaign_ids Campaign IDs.
657 + * @param boolean $include_all Whether to include all donations (true), or only approved (false).
658 + * @return array Keyed by campaign_id (int), each value an int count. A
659 + * campaign with no matching donors is simply absent from the
660 + * array — callers should treat a missing key as 0.
661 + */
662 + public function count_campaign_donors_by_campaign( $campaign_ids, $include_all = false ) {
663 + global $wpdb;
664 +
665 + list( $campaigns_in, $campaigns_parameters ) = $this->get_in_clause_params( $campaign_ids );
666 +
667 + if ( empty( $campaigns_in ) || empty( $campaigns_parameters ) ) {
668 + return array();
669 + }
670 +
671 + $statuses = $include_all ? array() : charitable_get_approval_statuses();
672 +
673 + list( $status_clause, $status_parameters ) = $this->get_donation_status_clause( $statuses );
674 +
675 + $sql_where_clauses = array( "cd.campaign_id IN ( $campaigns_in )" );
676 +
677 + if ( ! empty( $status_clause ) ) {
678 + $sql_where_clauses[] = $status_clause;
679 + }
680 +
681 + $sql_where = 'WHERE ' . implode( ' AND ', $sql_where_clauses );
682 + $parameters = array_merge( $campaigns_parameters, $status_parameters );
683 +
684 + $sql = "SELECT cd.campaign_id, COUNT( DISTINCT cd.donor_id ) as donor_count
685 + FROM $this->table_name cd
686 + INNER JOIN $wpdb->posts p ON p.ID = cd.donation_id
687 + $sql_where
688 + GROUP BY cd.campaign_id";
689 +
690 + $results = $wpdb->get_results( $wpdb->prepare( $sql, $parameters ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
691 +
692 + $counts = array();
693 +
694 + foreach ( $results as $row ) {
695 + $counts[ (int) $row->campaign_id ] = (int) $row->donor_count;
696 + }
697 +
698 + return $counts;
699 + }
700 +
701 + /**
405 702 * Get an array of all donation ids for a campaign.
406 703 *
407 704 * @uses Charitable_Campaign_Donations_DB::get_distinct_ids()
408 705 *
@@ -433,9 +730,9 @@
433 730 * Return the number of users who have donated to the given campaign.
434 731 *
435 732 * @since 1.0.0
436 733 *
437 - * @global wpdb $wpdb
734 + * @global wpdb $wpdb WPDB.
438 735 * @param int|int[] $campaign The campaign ID, or list of campaign IDs.
439 736 * @param boolean $include_all Whether to include all donations (true), or only include approved donations (false).
440 737 * @return int
441 738 */
@@ -457,17 +754,65 @@
457 754 FROM $this->table_name cd
458 755 INNER JOIN $wpdb->posts p ON p.ID = cd.donation_id
459 756 $sql_where;";
460 757
461 - return $wpdb->get_var( $wpdb->prepare( $sql, $parameters ) );
758 + return $wpdb->get_var( $wpdb->prepare( $sql, $parameters ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
462 759 }
463 760
464 761 /**
762 + * Count distinct donations (transactions) for a campaign.
763 + *
764 + * Ported from Charitable Pro unchanged: a pure COUNT query, it never
765 + * touches base_amount, so Lite's lack of that column does not affect it.
766 + * Backs Charitable_Campaign::get_donation_count(), which the
767 + * charitable/get-campaign-performance ability calls.
768 + *
769 + * @since 1.8.13
770 + *
771 + * @global wpdb $wpdb
772 + * @param int|int[] $campaign The campaign ID, or list of campaign IDs.
773 + * @param boolean $include_all Whether to include all donations (true), or only approved (false).
774 + * @return int
775 + */
776 + public function count_campaign_donations( $campaign, $include_all = false ) {
777 + global $wpdb;
778 +
779 + $statuses = $include_all ? array() : charitable_get_approval_statuses();
780 +
781 + list( $status_clause, $status_parameters ) = $this->get_donation_status_clause( $statuses );
782 + list( $campaigns_in, $campaigns_parameters ) = $this->get_in_clause_params( $campaign );
783 +
784 + $sql_where_clauses = array();
785 +
786 + if ( ! empty( $campaigns_in ) && ! empty( $campaigns_parameters ) ) {
787 + $sql_where_clauses[] = "cd.campaign_id IN ( $campaigns_in )";
788 + }
789 +
790 + if ( ! empty( $status_clause ) ) {
791 + $sql_where_clauses[] = $status_clause;
792 + }
793 +
794 + if ( empty( $sql_where_clauses ) ) {
795 + return 0;
796 + }
797 +
798 + $sql_where = 'WHERE ' . implode( ' AND ', $sql_where_clauses );
799 + $parameters = array_merge( $campaigns_parameters, $status_parameters );
800 +
801 + $sql = "SELECT COUNT( DISTINCT cd.donation_id )
802 + FROM $this->table_name cd
803 + INNER JOIN $wpdb->posts p ON p.ID = cd.donation_id
804 + $sql_where;";
805 +
806 + return (int) $wpdb->get_var( $wpdb->prepare( $sql, $parameters ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
807 + }
808 +
809 + /**
465 810 * Return all donations made by a donor.
466 811 *
467 812 * @since 1.0.0
468 813 *
469 - * @global wpdb $wpdb
814 + * @global wpdb $wpdb WPDB.
470 815 * @param int $donor_id The donor ID.
471 816 * @param boolean $distinct_donations Whether to get distinct donations.
472 817 * @return object[]
473 818 */
@@ -483,9 +828,9 @@
483 828 $sql = "SELECT $select_fields
484 829 FROM $this->table_name cd
485 830 WHERE cd.donor_id = %d;";
486 831
487 - $results = $wpdb->get_results( $wpdb->prepare( $sql, $donor_id ), OBJECT_K );
832 + $results = $wpdb->get_results( $wpdb->prepare( $sql, $donor_id ), OBJECT_K ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
488 833
489 834 if ( $this->is_comma_decimal() ) {
490 835 $results = array_map( array( $this, 'sanitize_amounts' ), $results );
491 836 }
@@ -508,9 +853,9 @@
508 853 $sql = "SELECT COALESCE( SUM(cd.amount), 0 )
509 854 FROM $this->table_name cd
510 855 WHERE cd.donor_id = %d;";
511 856
512 - $total = $wpdb->get_var( $wpdb->prepare( $sql, $donor_id ) );
857 + $total = $wpdb->get_var( $wpdb->prepare( $sql, $donor_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
513 858
514 859 if ( $this->is_comma_decimal() ) {
515 860 $total = Charitable_Currency::get_instance()->sanitize_database_amount( $total );
516 861 }
@@ -522,9 +867,9 @@
522 867 * Count the number of donations made by the donor.
523 868 *
524 869 * @since 1.0.0
525 870 *
526 - * @global wpdb $wpdb
871 + * @global wpdb $wpdb WPDB.
527 872 * @param int $donor_id The donor ID.
528 873 * @param boolean $distinct_donations If true, will only count unique donations.
529 874 * @return int
530 875 */
@@ -536,9 +881,9 @@
536 881 $sql = "SELECT COUNT( $count )
537 882 FROM $this->table_name
538 883 WHERE donor_id = %d;";
539 884
540 - return $wpdb->get_var( $wpdb->prepare( $sql, $donor_id ) );
885 + return $wpdb->get_var( $wpdb->prepare( $sql, $donor_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
541 886 }
542 887
543 888 /**
544 889 * Count the number of campaigns that the donor has supported.
@@ -544,9 +889,9 @@
544 889 * Count the number of campaigns that the donor has supported.
545 890 *
546 891 * @since 1.0.0
547 892 *
548 - * @global wpdb $wpdb
893 + * @global wpdb $wpdb WPDB.
549 894 * @param int $donor_id The donor to retrieve campaigns for.
550 895 * @return int
551 896 */
552 897 public function count_campaigns_supported_by_donor( $donor_id ) {
@@ -555,9 +900,9 @@
555 900 $sql = "SELECT COUNT( DISTINCT campaign_id )
556 901 FROM $this->table_name
557 902 WHERE donor_id = %d;";
558 903
559 - return $wpdb->get_var( $wpdb->prepare( $sql, $donor_id ) );
904 + return $wpdb->get_var( $wpdb->prepare( $sql, $donor_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
560 905 }
561 906
562 907 /**
563 908 * Return a set of donations, filtered by the provided arguments.
@@ -562,8 +907,9 @@
562 907 /**
563 908 * Return a set of donations, filtered by the provided arguments.
564 909 *
565 910 * @since 1.0.0
911 + * @since 1.8.1.8 Added filter.
566 912 *
567 913 * @global WPDB $wpdb
568 914 * @param array $args Query arguments.
569 915 * @return array
@@ -572,8 +918,9 @@
572 918 global $wpdb;
573 919
574 920 $sql_order = $this->get_orderby_clause( $args, 'ORDER BY p.post_date ASC' );
575 921 list( $sql_where, $parameters ) = $this->get_report_sql_where_clause( $args );
922 + $limit = $this->get_limit_clause( $args );
576 923
577 924 /* This is our base SQL query */
578 925 $sql = "SELECT cd.donation_id, cd.campaign_id, cd.campaign_name, cd.amount, d.email, d.first_name, d.last_name, p.post_date, p.post_content, p.post_status
579 926 FROM $this->table_name cd
@@ -581,15 +928,18 @@
581 928 ON d.donor_id = cd.donor_id
582 929 INNER JOIN $wpdb->posts p
583 930 ON p.ID = cd.donation_id
584 931 $sql_where
585 - $sql_order";
932 + $sql_order
933 + $limit";
586 934
935 + $sql = apply_filters( 'charitable_campaign_donations_db_sql', $sql, $args, $sql_where, $sql_order, $parameters );
936 +
587 937 if ( ! empty( $parameters ) ) {
588 - $sql = $wpdb->prepare( $sql, $parameters );
938 + $sql = $wpdb->prepare( $sql, $parameters ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
589 939 }
590 940
591 - $results = $wpdb->get_results( $sql );
941 + $results = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
592 942
593 943 if ( $this->is_comma_decimal() ) {
594 944 $results = array_map( array( $this, 'sanitize_amounts' ), $results );
595 945 }
@@ -619,12 +969,12 @@
619 969 ON p.ID = cd.donation_id
620 970 $sql_where";
621 971
622 972 if ( ! empty( $parameters ) ) {
623 - $sql = $wpdb->prepare( $sql, $parameters );
973 + $sql = $wpdb->prepare( $sql, $parameters ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
624 974 }
625 975
626 - $total = $wpdb->get_var( $sql );
976 + $total = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
627 977
628 978 if ( $this->is_comma_decimal() && $sanitize ) {
629 979 $total = Charitable_Currency::get_instance()->sanitize_database_amount( $total );
630 980 }
@@ -657,12 +1007,12 @@
657 1007 $sql_where
658 1008 GROUP BY cd.campaign_id, cd.campaign_name";
659 1009
660 1010 if ( ! empty( $parameters ) ) {
661 - $sql = $wpdb->prepare( $sql, $parameters );
1011 + $sql = $wpdb->prepare( $sql, $parameters ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
662 1012 }
663 1013
664 - $results = $wpdb->get_results( $sql, 'ARRAY_N' );
1014 + $results = $wpdb->get_results( $sql, 'ARRAY_N' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
665 1015
666 1016 if ( $this->is_comma_decimal() && $sanitize ) {
667 1017 $results = array_map( array( $this, 'sanitize_grouped_amounts' ), $results );
668 1018 }
@@ -670,8 +1020,301 @@
670 1020 return $results;
671 1021 }
672 1022
673 1023 /**
1024 + * Count the distinct donations matching a report query.
1025 + *
1026 + * Counts DISTINCT donation_id, so a donation split across three
1027 + * campaigns counts once rather than three times — unlike
1028 + * count_donations_by_status(), which counts campaign_donations ROWS and
1029 + * cannot be date-ranged.
1030 + *
1031 + * Exists because the alternative is loading every matching donation id
1032 + * into PHP just to call count() on it. Filtering reuses
1033 + * get_report_sql_where_clause(), so campaigns, status and the date range
1034 + * behave exactly as they do for get_amount_report() — which means a
1035 + * total and a count taken from the same $args are always coherent, and
1036 + * average = total / count cannot disagree with itself.
1037 + *
1038 + * THE INNER JOIN IS DELIBERATE, and it is a behaviour decision worth
1039 + * stating. A donation POST with no campaign_donations row is not
1040 + * counted, because such a record carries no amount and so contributes
1041 + * nothing to any money figure. Counting it would report more donations
1042 + * than the money accounts for: on the development database, 18 donation
1043 + * posts (abandoned importer and gateway-test records) have no rows at
1044 + * all, and counting donation posts against a campaign_donations total
1045 + * produced an all-time average gift of 9.49 when the real average of
1046 + * the donations that carry money is 14.02 — an average no actual
1047 + * donation resembles. Taking the count from the same table as the total
1048 + * is what makes the reported average true.
1049 + *
1050 + * @since 1.8.13
1051 + *
1052 + * @global WPDB $wpdb
1053 + * @param array $args Query arguments: campaigns, status, start_date, end_date, donor_id.
1054 + * @return int
1055 + */
1056 + public function count_donations_report( $args = array() ) {
1057 + global $wpdb;
1058 +
1059 + list( $sql_where, $parameters ) = $this->get_report_sql_where_clause( $args );
1060 +
1061 + $sql = "SELECT COUNT( DISTINCT cd.donation_id )
1062 + FROM $this->table_name cd
1063 + INNER JOIN $wpdb->posts p
1064 + ON p.ID = cd.donation_id
1065 + $sql_where";
1066 +
1067 + if ( ! empty( $parameters ) ) {
1068 + $sql = $wpdb->prepare( $sql, $parameters ); // phpcs:ignore
1069 + }
1070 +
1071 + return (int) $wpdb->get_var( $sql ); // phpcs:ignore
1072 + }
1073 +
1074 + /**
1075 + * Count the distinct donors matching a report query.
1076 + *
1077 + * A donor_id of 0 is excluded: a row can carry it when the donation was
1078 + * recorded without a resolvable donor, and counting those as one donor
1079 + * would understate a site's supporter count while counting them
1080 + * individually would overstate it. Excluding is the only honest option.
1081 + *
1082 + * @since 1.8.13
1083 + *
1084 + * @global WPDB $wpdb
1085 + * @param array $args Query arguments: campaigns, status, start_date, end_date.
1086 + * @return int
1087 + */
1088 + public function count_donors_report( $args = array() ) {
1089 + global $wpdb;
1090 +
1091 + list( $sql_where, $parameters ) = $this->get_report_sql_where_clause( $args );
1092 +
1093 + $sql_where = '' !== $sql_where
1094 + ? $sql_where . ' AND cd.donor_id != 0'
1095 + : 'WHERE cd.donor_id != 0';
1096 +
1097 + $sql = "SELECT COUNT( DISTINCT cd.donor_id )
1098 + FROM $this->table_name cd
1099 + INNER JOIN $wpdb->posts p
1100 + ON p.ID = cd.donation_id
1101 + $sql_where";
1102 +
1103 + if ( ! empty( $parameters ) ) {
1104 + $sql = $wpdb->prepare( $sql, $parameters ); // phpcs:ignore
1105 + }
1106 +
1107 + return (int) $wpdb->get_var( $sql ); // phpcs:ignore
1108 + }
1109 +
1110 + /**
1111 + * Count the distinct donations in a report query, grouped by status.
1112 + *
1113 + * ONE query for every status, rather than one query per status. Pass no
1114 + * `status` argument: get_report_sql_where_clause() then constrains to
1115 + * all valid donation statuses, which is exactly the set to group over.
1116 + *
1117 + * Ported from Charitable Pro unchanged: this query never touches
1118 + * base_amount, so Lite's lack of that column does not affect it.
1119 + *
1120 + * @since 1.8.13
1121 + *
1122 + * @global WPDB $wpdb
1123 + * @param array $args Query arguments: campaigns, start_date, end_date, donor_id.
1124 + * @return array Keyed by post status, each value an int count. A status with
1125 + * no donations is absent — callers should treat a missing key
1126 + * as 0.
1127 + */
1128 + public function count_donations_by_status_report( $args = array() ) {
1129 + global $wpdb;
1130 +
1131 + unset( $args['status'] );
1132 +
1133 + list( $sql_where, $parameters ) = $this->get_report_sql_where_clause( $args );
1134 +
1135 + $sql = "SELECT p.post_status, COUNT( DISTINCT cd.donation_id ) as donation_count
1136 + FROM $this->table_name cd
1137 + INNER JOIN $wpdb->posts p
1138 + ON p.ID = cd.donation_id
1139 + $sql_where
1140 + GROUP BY p.post_status";
1141 +
1142 + if ( ! empty( $parameters ) ) {
1143 + $sql = $wpdb->prepare( $sql, $parameters ); // phpcs:ignore
1144 + }
1145 +
1146 + $results = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
1147 +
1148 + $counts = array();
1149 +
1150 + foreach ( (array) $results as $row ) {
1151 + $counts[ (string) $row->post_status ] = (int) $row->donation_count;
1152 + }
1153 +
1154 + return $counts;
1155 + }
1156 +
1157 + /**
1158 + * Return campaigns ranked by amount raised over a date range.
1159 + *
1160 + * The get_amount_by_campaign_report() accessor is the closest existing one
1161 + * and is correctly converted, but it selects `campaign_name` WITHOUT
1162 + * campaign_id and applies no ordering or limit. A caller that needs to
1163 + * link to the campaign, or that wants the top N, therefore cannot use
1164 + * it: names are not unique, and they drift when a campaign is renamed
1165 + * after the donation was recorded.
1166 + *
1167 + * Filtering reuses get_report_sql_where_clause(), so campaigns, status,
1168 + * start_date, end_date and donor_id behave exactly as they do for every
1169 + * other report query, including its default of all valid donation
1170 + * statuses when none is given.
1171 + *
1172 + * Sums cd.amount rather than Pro's COALESCE( cd.base_amount, cd.amount ):
1173 + * base_amount belongs to the multi-currency schema and does not exist on
1174 + * this table in Lite (see get_donations_summary_by_day_range() below for
1175 + * the same substitution and rationale).
1176 + *
1177 + * @since 1.8.13
1178 + *
1179 + * @global WPDB $wpdb
1180 + * @param array $args Query arguments: campaigns, status, start_date,
1181 + * end_date, donor_id, limit.
1182 + * @return object[] Rows of campaign_id, campaign_name, total, donation_count.
1183 + */
1184 + public function get_top_campaigns_report( $args = array() ) {
1185 + global $wpdb;
1186 +
1187 + list( $sql_where, $parameters ) = $this->get_report_sql_where_clause( $args );
1188 +
1189 + $limit = isset( $args['limit'] ) ? absint( $args['limit'] ) : 10;
1190 + $limit = $limit > 0 ? $limit : 10;
1191 +
1192 + $sql = "SELECT cd.campaign_id,
1193 + cd.campaign_name,
1194 + COALESCE( SUM( cd.amount ), 0 ) as total,
1195 + COUNT( DISTINCT cd.donation_id ) as donation_count
1196 + FROM $this->table_name cd
1197 + INNER JOIN $wpdb->posts p
1198 + ON p.ID = cd.donation_id
1199 + $sql_where
1200 + GROUP BY cd.campaign_id, cd.campaign_name
1201 + ORDER BY total DESC
1202 + LIMIT %d";
1203 +
1204 + $parameters[] = $limit;
1205 +
1206 + $results = $wpdb->get_results( $wpdb->prepare( $sql, $parameters ) ); // phpcs:ignore
1207 +
1208 + if ( $this->is_comma_decimal() ) {
1209 + $results = array_map( array( $this, 'sanitize_amounts' ), $results );
1210 + }
1211 +
1212 + return $results;
1213 + }
1214 +
1215 + /**
1216 + * Return donors ranked by amount given over a date range.
1217 + *
1218 + * Charitable_Reports::get_top_donors_overview() is the existing
1219 + * equivalent, and is not usable here: it requires the whole donation set
1220 + * to have been loaded into PHP first via init_with_array() plus
1221 + * get_donations(), sorts it in PHP, and calls charitable_get_donation()
1222 + * once per row. This is one grouped query.
1223 + *
1224 + * Ranked on cd.amount rather than Pro's COALESCE( base_amount, amount ):
1225 + * base_amount belongs to the multi-currency schema and does not exist on
1226 + * this table in Lite (see get_donations_summary_by_day_range() below for
1227 + * the same substitution and rationale).
1228 + *
1229 + * @since 1.8.13
1230 + *
1231 + * @global WPDB $wpdb
1232 + * @param array $args Query arguments: campaigns, status, start_date,
1233 + * end_date, limit.
1234 + * @return object[] Rows of donor_id, first_name, last_name, email, total,
1235 + * donation_count.
1236 + */
1237 + public function get_top_donors_report( $args = array() ) {
1238 + global $wpdb;
1239 +
1240 + list( $sql_where, $parameters ) = $this->get_report_sql_where_clause( $args );
1241 +
1242 + $limit = isset( $args['limit'] ) ? absint( $args['limit'] ) : 10;
1243 + $limit = $limit > 0 ? $limit : 10;
1244 +
1245 + /*
1246 + * donor_id 0 must not be grouped: a campaign_donations row can carry
1247 + * it when the donation was recorded without a resolvable donor, and
1248 + * grouping those would present the sum of several unrelated gifts as
1249 + * one large anonymous top donor.
1250 + *
1251 + * The INNER JOIN on the donors table below is what actually excludes
1252 + * them, because no donors row has donor_id 0 — the column is
1253 + * AUTO_INCREMENT and starts at 1. This clause is therefore SECONDARY
1254 + * and, on any normal install, redundant. It is kept because it states
1255 + * the intent at the point the grouping happens, so a future change
1256 + * from INNER to LEFT JOIN (to include gifts whose donor record
1257 + * was erased, say) does not silently reintroduce the anonymous-group
1258 + * bug.
1259 + *
1260 + * Honest note on coverage: this clause is NOT independently testable.
1261 + * Removing it leaves every test green, because the join already
1262 + * covers the case, and a donors row with donor_id 0 cannot be created
1263 + * to exercise it — MySQL treats an inserted 0 in an AUTO_INCREMENT
1264 + * column as a request for the next value unless NO_AUTO_VALUE_ON_ZERO
1265 + * is set on the session. Verified by mutation rather than assumed.
1266 + */
1267 + $sql_where = '' !== $sql_where
1268 + ? $sql_where . ' AND cd.donor_id != 0'
1269 + : 'WHERE cd.donor_id != 0';
1270 +
1271 + $sql = "SELECT cd.donor_id,
1272 + d.first_name,
1273 + d.last_name,
1274 + d.email,
1275 + COALESCE( SUM( cd.amount ), 0 ) as total,
1276 + COUNT( DISTINCT cd.donation_id ) as donation_count
1277 + FROM $this->table_name cd
1278 + INNER JOIN $wpdb->posts p
1279 + ON p.ID = cd.donation_id
1280 + INNER JOIN {$wpdb->prefix}charitable_donors d
1281 + ON d.donor_id = cd.donor_id
1282 + $sql_where
1283 + GROUP BY cd.donor_id, d.first_name, d.last_name, d.email
1284 + ORDER BY total DESC
1285 + LIMIT %d";
1286 +
1287 + $parameters[] = $limit;
1288 +
1289 + $results = $wpdb->get_results( $wpdb->prepare( $sql, $parameters ) ); // phpcs:ignore
1290 +
1291 + if ( $this->is_comma_decimal() ) {
1292 + $results = array_map( array( $this, 'sanitize_amounts' ), $results );
1293 + }
1294 +
1295 + return $results;
1296 + }
1297 +
1298 + /**
1299 + * Return an SQL limit for the report
1300 + *
1301 + * @since 1.8.1.10
1302 + *
1303 + * @param array $args Query arguments.
1304 + * @return array
1305 + */
1306 + public function get_limit_clause( $args ) {
1307 + $limit = '';
1308 +
1309 + if ( isset( $args['limit'] ) ) {
1310 + $limit = 'LIMIT ' . $args['limit'];
1311 + }
1312 +
1313 + return $limit;
1314 + }
1315 +
1316 + /**
674 1317 * Return an SQL where clause for the report
675 1318 *
676 1319 * @since 1.6.57
677 1320 *
@@ -722,8 +1365,41 @@
722 1365 $sql_where_clauses[] = 'p.post_date <= %s';
723 1366 $parameters[] = $args['end_date'];
724 1367 }
725 1368
1369 + // Added in 1.8.1.8.
1370 + if ( isset( $args['post_parent'] ) ) {
1371 + $sql_where_clauses[] = 'p.post_parent = %d';
1372 + $parameters[] = $args['post_parent'];
1373 + }
1374 +
1375 + /*
1376 + * `author` narrows to one donation author, which for a donation is
1377 + * the donor's WordPress user id
1378 + * (Charitable_Donation_Processor::parse_donation_data() sets
1379 + * post_author from `user_id`). Added in 1.8.13 for the ownership
1380 + * scoping on charitable/get-donation-summary: a report caller without
1381 + * `edit_others_donations` must see only their own donations, which is
1382 + * the rule wp-admin's donation list already applies
1383 + * (class-charitable-donation-list-table.php:783) via WP_Query's own
1384 + * `author` argument. The report accessors do not go through WP_Query,
1385 + * so the equivalent has to exist here.
1386 + *
1387 + * isset(), not ! empty(): author 0 is a real value — guest and
1388 + * manually recorded donations carry post_author 0 — so treating it as
1389 + * "no filter" would be fail-OPEN, returning every donation to a
1390 + * caller who asked to be scoped. No caller passes 0 today, because
1391 + * the only one that sets this key is gated behind `edit_donations`
1392 + * and so is always a logged-in user; isset() is what keeps that true
1393 + * if a future caller is not.
1394 + *
1395 + * @since 1.8.13
1396 + */
1397 + if ( isset( $args['author'] ) ) {
1398 + $sql_where_clauses[] = 'p.post_author = %d';
1399 + $parameters[] = (int) $args['author'];
1400 + }
1401 +
726 1402 if ( ! empty( $sql_where_clauses ) ) {
727 1403 $sql_where = 'WHERE ' . implode( ' AND ', $sql_where_clauses );
728 1404 }
729 1405
@@ -759,9 +1435,9 @@
759 1435 FROM {$wpdb->prefix}charitable_campaign_donations cd
760 1436 INNER JOIN $wpdb->posts p ON p.ID = cd.donation_id
761 1437 $sql_where";
762 1438
763 - $results = $wpdb->get_results( $wpdb->prepare( $sql, $parameters ) );
1439 + $results = $wpdb->get_results( $wpdb->prepare( $sql, $parameters ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
764 1440
765 1441 if ( empty( $results ) ) {
766 1442 return (object) array(
767 1443 'amount' => 0.0000,
@@ -778,8 +1454,103 @@
778 1454 return $result;
779 1455 }
780 1456
781 1457 /**
1458 + * Return a count and sum of donations for every day in a date range, keyed by day.
1459 + *
1460 + * Callers that need a per-day series must use this instead of calling
1461 + * get_donations_summary_by_period() once per calendar day. That pattern issued one
1462 + * uncached query per day, so the query count scaled with the LENGTH of the range
1463 + * rather than the amount of data, and a multi-year range timed out. It also wrapped
1464 + * post_date in DATE_FORMAT(), which prevented any index being used, making every
1465 + * one of those queries a full scan of the join.
1466 + *
1467 + * Days with no donations are absent from the return value; callers should treat a
1468 + * missing key as zero.
1469 + *
1470 + * @since 1.8.12.2
1471 + * @since 1.8.13 Added $campaign_id parameter.
1472 + *
1473 + * @global WPDB $wpdb
1474 + * @param string $start_date Start of the range, Y-m-d. Inclusive.
1475 + * @param string $end_date End of the range, Y-m-d. Inclusive of the whole day.
1476 + * @param string[] $statuses List of statuses.
1477 + * @param int $campaign_id Optional. Restrict to donations that include this
1478 + * campaign. 0, the default, means every campaign —
1479 + * which is what existing callers want.
1480 + * @return array Keyed by Y-m-d, each value an object with 'amount' and 'count'.
1481 + */
1482 + public function get_donations_summary_by_day_range( $start_date, $end_date, $statuses = array(), $campaign_id = 0 ) {
1483 + global $wpdb;
1484 +
1485 + if ( empty( $start_date ) || empty( $end_date ) ) {
1486 + return array();
1487 + }
1488 +
1489 + if ( empty( $statuses ) ) {
1490 + $statuses = charitable_get_approval_statuses();
1491 + }
1492 +
1493 + list( $status_clause, $status_parameters ) = $this->get_donation_status_clause( $statuses );
1494 +
1495 + // Every status was filtered out as invalid, so nothing could match. Without
1496 + // this the IN clause would be emitted empty and the query would error.
1497 + if ( empty( $status_parameters ) ) {
1498 + return array();
1499 + }
1500 +
1501 + $parameters = array_merge(
1502 + array( $start_date . ' 00:00:00', $end_date . ' 23:59:59' ),
1503 + $status_parameters
1504 + );
1505 +
1506 + /*
1507 + * Appended AFTER the status clause, so the extra parameter lands last
1508 + * in $parameters and the placeholder order still lines up. Filtering
1509 + * on cd.campaign_id rather than joining anything: a donation split
1510 + * across campaigns has one row per campaign in this table, and the
1511 + * per-campaign row is exactly what a per-campaign figure means.
1512 + */
1513 + $campaign_clause = '';
1514 + $campaign_id = (int) $campaign_id;
1515 +
1516 + if ( $campaign_id > 0 ) {
1517 + $campaign_clause = ' AND cd.campaign_id = %d';
1518 + $parameters[] = $campaign_id;
1519 + }
1520 +
1521 + // Sums cd.amount, matching get_donations_summary_by_period() above. The
1522 + // base_amount column belongs to the multi-currency schema and does not exist
1523 + // on this table in Lite, so it must not appear here.
1524 + $sql = "SELECT DATE( p.post_date ) as day,
1525 + COALESCE( SUM( cd.amount ), 0 ) as amount,
1526 + COUNT( cd.donation_id ) as count
1527 + FROM {$wpdb->prefix}charitable_campaign_donations cd
1528 + INNER JOIN $wpdb->posts p ON p.ID = cd.donation_id
1529 + WHERE p.post_date >= %s AND p.post_date <= %s AND {$status_clause}{$campaign_clause}
1530 + GROUP BY DATE( p.post_date )";
1531 +
1532 + $results = $wpdb->get_results( $wpdb->prepare( $sql, $parameters ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
1533 +
1534 + if ( empty( $results ) ) {
1535 + return array();
1536 + }
1537 +
1538 + $comma_decimal = $this->is_comma_decimal();
1539 + $summary = array();
1540 +
1541 + foreach ( $results as $result ) {
1542 + if ( $comma_decimal ) {
1543 + $result = $this->sanitize_amounts( $result );
1544 + }
1545 +
1546 + $summary[ $result->day ] = $result;
1547 + }
1548 +
1549 + return $summary;
1550 + }
1551 +
1552 + /**
782 1553 * Returns the orderby clause.
783 1554 *
784 1555 * @since 1.3.4
785 1556 *
@@ -786,15 +1557,15 @@
786 1557 * @param array $args Query arguments.
787 1558 * @param string $default Default.
788 1559 * @return string
789 1560 */
790 - public function get_orderby_clause( $args, $default = '' ) {
1561 + public function get_orderby_clause( $args, $default = '' ) { // phpcs:ignore
791 1562 if ( ! isset( $args['orderby'] ) && ! isset( $args['order'] ) ) {
792 1563 return $default;
793 1564 }
794 1565
795 1566 $orderby = isset( $args['orderby'] ) ? $args['orderby'] : 'ID';
796 - $order = isset( $args['order'] ) && in_array( $args['order'], array( 'ASC', 'DESC' ) ) ? $args['order'] : 'ASC';
1567 + $order = isset( $args['order'] ) && in_array( $args['order'], array( 'ASC', 'DESC' ) ) ? $args['order'] : 'ASC'; // phpcs:ignore
797 1568
798 1569 switch ( $orderby ) {
799 1570 case 'date':
800 1571 $ret = 'ORDER BY p.post_date ';
@@ -833,9 +1604,9 @@
833 1604 FROM {$wpdb->prefix}charitable_campaign_donations cd
834 1605 INNER JOIN $wpdb->posts p ON p.ID = cd.donation_id
835 1606 $sql_where;";
836 1607
837 - return $wpdb->get_var( $wpdb->prepare( $sql, $parameters ) );
1608 + return $wpdb->get_var( $wpdb->prepare( $sql, $parameters ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
838 1609 }
839 1610
840 1611 /**
841 1612 * Returns the donation status clause.
@@ -864,9 +1635,9 @@
864 1635 *
865 1636 * @param int|int[] $list List of elements.
866 1637 * @return array
867 1638 */
868 - private function get_in_clause_params( $list ) {
1639 + private function get_in_clause_params( $list ) { // phpcs:ignore
869 1640 if ( ! is_array( $list ) ) {
870 1641 $list = array( $list );
871 1642 }
872 1643
@@ -909,9 +1680,9 @@
909 1680 * Sanitize amounts for grouped queries
910 1681 *
911 1682 * @since 1.6.57
912 1683 *
913 - * @param array $item[0] in array is a label, item[1] is the amount. We need to sanitize the amount.
1684 + * @param array $item in array is a label, item[1] is the amount. We need to sanitize the amount.
914 1685 * @return object
915 1686 */
916 1687 private function sanitize_grouped_amounts( $item ) {
917 1688 $item[1] = Charitable_Currency::get_instance()->sanitize_database_amount( $item[1] );