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 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
← All changes | includes/campaigns/class-charitable-campaign.php +568 -31 1.6.6 → 1.8.13 View file →
@@ -1,18 +1,20 @@
1 1 <?php
2 2 /**
3 - * Campaign model
3 + * Campaign model.
4 4 *
5 5 * @package Charitable/Classes/Charitable_Campaign
6 - * @author Eric Daams
7 - * @copyright Copyright (c) 2018, Studio 164a
6 + * @author David Bisset
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 - * @version 1.0.0
10 + * @version 1.6.54
11 11 */
12 12
13 13 // Exit if accessed directly.
14 -if ( ! defined( 'ABSPATH' ) ) { exit; }
14 +if ( ! defined( 'ABSPATH' ) ) {
15 + exit;
16 +}
15 17
16 18 if ( ! class_exists( 'Charitable_Campaign' ) ) :
17 19
18 20 /**
@@ -68,8 +70,15 @@
68 70 */
69 71 private $goal;
70 72
71 73 /**
74 + * The fundraising goal for the campaign.
75 + *
76 + * @var string|false
77 + */
78 + private $min_donation_amount;
79 +
80 + /**
72 81 * The donations made to this campaign.
73 82 *
74 83 * @var WP_Query|false
75 84 */
@@ -109,12 +118,65 @@
109 118 if ( ! $post instanceof WP_Post ) {
110 119 $post = get_post( $post );
111 120 }
112 121
122 + add_filter( 'charitable_campaign_description_template_content', array( $this, 'description_content' ), 10, 2 );
123 +
113 124 $this->post = $post;
114 125 }
115 126
116 127 /**
128 + * Return description content.
129 + *
130 + * @since 1.7.0.4
131 + *
132 + * @param string $content The content.
133 + * @param object $campaign The campaign object.
134 + * @return mixed
135 + */
136 + public function description_content( $content = false, $campaign = false ) {
137 +
138 + // search for YouTube and Vimeo video urls by default and attempt to convert them to oembed.
139 +
140 + preg_match_all( '#(?<=v=|v\/|vi=|vi\/|youtu.be\/)[a-zA-Z0-9_-]{11}#', $content, $matches );
141 +
142 + if ( $matches ) :
143 + foreach ( $matches as $video_id ) {
144 + if ( ! empty( $video_id ) ) :
145 + $video_embed_args = apply_filters( 'charitable_campaign_description_template_video', array(), 'youtube', $video_id, $content );
146 + $embed = wp_oembed_get( 'https://youtube.com/watch?v=' . $video_id[0], $video_embed_args );
147 +
148 + $content = str_replace( trim( 'https://youtube.com/watch?v=' . $video_id[0] ), $embed, $content );
149 + $content = str_replace( trim( 'http://youtube.com/watch?v=' . $video_id[0] ), $embed, $content );
150 + $content = str_replace( trim( 'https://www.youtube.com/watch?v=' . $video_id[0] ), $embed, $content );
151 + $content = str_replace( trim( 'http://www.youtube.com/watch?v=' . $video_id[0] ), $embed, $content );
152 + endif;
153 + }
154 + endif;
155 +
156 + preg_match( '/https?:\/\/(?:www\.)?vimeo\.com\/\d{8}/', $content, $matches );
157 +
158 + if ( $matches ) :
159 + foreach ( $matches as $video_url ) {
160 + if ( ! empty( $video_id ) ) :
161 + $video_embed_args = apply_filters( 'charitable_campaign_description_template_video', array(), 'vimeo', $video_id, $content, $campaign );
162 + $embed = wp_oembed_get( $video_url, $video_embed_args );
163 +
164 + $content = str_replace( trim( $video_url ), $embed, $content );
165 + endif;
166 + }
167 + endif;
168 +
169 + // allow developers and plugins to hook into content.
170 + $content = apply_filters( 'charitable_campaign_description_content', $content, $campaign );
171 +
172 + // apply WP's the_content filter, which other plugins can hook into.
173 + $content = apply_filters( 'the_content', $content ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- 'the_content' is a WordPress core filter hook.
174 +
175 + return $content;
176 + }
177 +
178 + /**
117 179 * Magic getter.
118 180 *
119 181 * @since 1.0.0
120 182 *
@@ -121,9 +183,9 @@
121 183 * @param string $key The key of the field to get.
122 184 * @return mixed
123 185 */
124 186 public function __get( $key ) {
125 - if ( property_exists( $this->post, $key ) ) {
187 + if ( null !== $this->post && property_exists( $this->post, $key ) ) {
126 188 return $this->post->$key;
127 189 }
128 190
129 191 return $this->get( $key );
@@ -129,8 +191,26 @@
129 191 return $this->get( $key );
130 192 }
131 193
132 194 /**
195 + * Limit properties to be serialized.
196 + *
197 + * @since 1.6.18
198 + *
199 + * @return array
200 + */
201 + public function __sleep() {
202 + return array(
203 + 'post',
204 + 'end_time',
205 + 'goal',
206 + 'donations',
207 + 'donated_amount',
208 + 'donation_form',
209 + );
210 + }
211 +
212 + /**
133 213 * Returns the campaign's post_meta values. _campaign_ is automatically prepended to the meta key.
134 214 *
135 215 * @see get_post_meta
136 216 *
@@ -165,8 +245,13 @@
165 245 * @return mixed This will return an array if single is false. If it's true, the value of the
166 246 * meta_value field will be returned.
167 247 */
168 248 public function get_meta( $meta_name, $single = true ) {
249 +
250 + if ( empty( $this->post->ID ) ) {
251 + return;
252 + }
253 +
169 254 /**
170 255 * Filter the meta value of a particular campaign field.
171 256 *
172 257 * @since 1.0.0
@@ -194,8 +279,20 @@
194 279 return $this->fields;
195 280 }
196 281
197 282 /**
283 + * Returns the id of the campaign (post).
284 + *
285 + * @since 1.7.0.9
286 + * @version 1.8.0
287 + *
288 + * @return integer
289 + */
290 + public function get_campaign_id() {
291 + return ( isset( $this->post->ID ) ) ? intval( $this->post->ID ) : false;
292 + }
293 +
294 + /**
198 295 * Returns whether the campaign is endless (i.e. no end date has been set).
199 296 *
200 297 * @since 1.0.0
201 298 *
@@ -205,8 +302,19 @@
205 302 return 0 == $this->get_meta( '_campaign_end_date' );
206 303 }
207 304
208 305 /**
306 + * Return whether the campaign supports custom donation amounts.
307 + *
308 + * @since 1.6.53
309 + *
310 + * @return boolean
311 + */
312 + public function get_allow_custom_donations() {
313 + return (int) get_post_meta( $this->post->ID, '_campaign_allow_custom_donations', true );
314 + }
315 +
316 + /**
209 317 * Return the suggested amounts, or an empty array if there are none.
210 318 *
211 319 * @since 1.0.0
212 320 *
@@ -222,8 +330,25 @@
222 330 return apply_filters( 'charitable_campaign_suggested_donations', $value, $this );
223 331 }
224 332
225 333 /**
334 + * Return the suggested amounts, or an empty array if there are none.
335 + *
336 + * @since 1.0.0
337 + *
338 + * @return array
339 + */
340 + public function get_suggested_donations_default() {
341 + $value = get_post_meta( $this->post->ID, '_campaign_suggested_donations_default', true );
342 +
343 + if ( is_array( $value ) && isset( $value[0] ) ) {
344 + $value = $value[0];
345 + }
346 +
347 + return apply_filters( 'charitable_campaign_suggested_donations_default', $value, $this );
348 + }
349 +
350 + /**
226 351 * Returns the end date in your preferred format.
227 352 *
228 353 * If a format is not provided, the user-defined date_format in WordPress settings is used.
229 354 *
@@ -255,14 +380,14 @@
255 380 /**
256 381 * Returns the timetamp of the end date.
257 382 *
258 383 * @since 1.0.0
384 + * @since 1.8.1.10
259 385 *
260 386 * @return int|false Int if campaign has an end date. False if campaign has no end date.
261 387 */
262 388 public function get_end_time() {
263 389 if ( ! isset( $this->end_time ) ) {
264 -
265 390 $end_date = $this->get_meta( '_campaign_end_date' );
266 391
267 392 if ( ! $end_date ) {
268 393 return false;
@@ -272,8 +397,13 @@
272 397 $date_time = explode( ' ', $end_date );
273 398 $date = explode( '-', $date_time[0] );
274 399 $time = explode( ':', $date_time[1] );
275 400
401 + // Check for undefined array keys. If any of these are not defined, return false.
402 + if ( ! isset( $date[0] ) || ! isset( $date[1] ) || ! isset( $date[2] ) || ! isset( $time[0] ) || ! isset( $time[1] ) || ! isset( $time[2] ) ) {
403 + return false;
404 + }
405 +
276 406 $this->end_time = mktime( $time[0], $time[1], $time[2], $date[1], $date[2], $date[0] );
277 407 }
278 408
279 409 return $this->end_time;
@@ -329,9 +459,11 @@
329 459
330 460 /* Condition 2: There is less than an hour left. */
331 461
332 462 $minutes_remaining = ceil( $seconds_left / 60 );
333 - $time_left = apply_filters( 'charitabile_campaign_minutes_left',
463 + $time_left = apply_filters(
464 + 'charitabile_campaign_minutes_left', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- This is a legacy hook used by other Charitable code. Changing it would break existing functionality.
465 + // translators: %s is the number of minutes left.
334 466 sprintf( _n( '%s Minute Left', '%s Minutes Left', $minutes_remaining, 'charitable' ), '<span class="amount time-left minutes-left">' . $minutes_remaining . '</span>' ),
335 467 $this
336 468 );
337 469
@@ -339,9 +471,11 @@
339 471
340 472 /* Condition 3: There is less than a day left. */
341 473
342 474 $hours_remaining = floor( $seconds_left / 3600 );
343 - $time_left = apply_filters( 'charitabile_campaign_hours_left',
475 + $time_left = apply_filters(
476 + 'charitabile_campaign_hours_left', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- This is a legacy hook used by other Charitable code. Changing it would break existing functionality.
477 + // translators: %s is the number of hours left.
344 478 sprintf( _n( '%s Hour Left', '%s Hours Left', $hours_remaining, 'charitable' ), '<span class="amount time-left hours-left">' . $hours_remaining . '</span>' ),
345 479 $this
346 480 );
347 481
@@ -349,14 +483,16 @@
349 483
350 484 /* Condition 4: There is more than a day left. */
351 485
352 486 $days_remaining = floor( $seconds_left / 86400 );
353 - $time_left = apply_filters( 'charitabile_campaign_days_left',
487 + $time_left = apply_filters(
488 + 'charitabile_campaign_days_left', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- This is a legacy hook used by other Charitable code. Changing it would break existing functionality.
489 + // translators: %s is the number of days left.
354 490 sprintf( _n( '%s Day Left', '%s Days Left', $days_remaining, 'charitable' ), '<span class="amount time-left days-left">' . $days_remaining . '</span>' ),
355 491 $this
356 492 );
357 493
358 - }//end if
494 + } //end if
359 495
360 496 return apply_filters( 'charitable_campaign_time_left', $time_left, $this );
361 497 }
362 498
@@ -404,12 +540,15 @@
404 540 return '';
405 541 }
406 542
407 543 if ( ! $this->has_goal() ) {
544 + // translators: %s is the time since the campaign ended.
408 545 $message = __( 'This campaign ended %s ago', 'charitable' );
409 546 } elseif ( $this->has_achieved_goal() ) {
547 + // translators: %s is the time since the campaign ended.
410 548 $message = __( 'This campaign successfully reached its funding goal and ended %s ago', 'charitable' );
411 549 } else {
550 + // translators: %s is the time since the campaign ended.
412 551 $message = __( 'This campaign failed to reach its funding goal %s ago', 'charitable' );
413 552 }
414 553
415 554 return apply_filters( 'charitable_campaign_finished_notice', sprintf( $message, '<span class="time-ago">' . human_time_diff( $this->get_end_time() ) . '</span>' ), $this );
@@ -445,8 +584,24 @@
445 584 return $this->goal;
446 585 }
447 586
448 587 /**
588 + * Returns the minimum donation amount of the campaign.
589 + *
590 + * @since 1.0.0
591 + *
592 + * @return string|false Amount if goal is set. False otherwise.
593 + */
594 + public function get_min_donation_amount() {
595 + if ( ! isset( $this->min_donation_amount ) ) {
596 + $this->min_donation_amount = $this->get_meta( '_campaign_min_donation_amount' ) ? $this->get_meta( '_campaign_min_donation_amount' ) : false;
597 + }
598 +
599 + return $this->min_donation_amount;
600 + }
601 +
602 +
603 + /**
449 604 * Returns whether a goal has been set (anything greater than $0 is a goal).
450 605 *
451 606 * @since 1.0.0
452 607 *
@@ -533,10 +688,11 @@
533 688 *
534 689 * @return string
535 690 */
536 691 public function get_status() {
537 - $status = $this->post->post_status;
538 692
693 + $status = $post_status = isset( $this->post ) && ! empty( $this->post->post_status ) ? $this->post->post_status : false; // phpcs:ignore
694 +
539 695 if ( 'publish' == $status ) {
540 696 $status = $this->has_ended() ? 'finished' : 'active';
541 697 }
542 698
@@ -548,9 +704,9 @@
548 704 * @param string $status The campaign status.
549 705 * @param string $post_status The post_status of the campaign.
550 706 * @param Charitable_Campaign $campaign This instance of `Charitable_Campaign`.
551 707 */
552 - return apply_filters( 'charitable_campaign_status', $status, $this->post->post_status, $this );
708 + return apply_filters( 'charitable_campaign_status', $status, $post_status, $this );
553 709 }
554 710
555 711 /**
556 712 * Return a status key for the campaign.
@@ -606,15 +762,24 @@
606 762 * Return the campaign status tag.
607 763 *
608 764 * @since 1.0.0
609 765 *
766 + * @param boolean $show_achievement Whether to show the achievement status tag.
767 + * @param boolean $show_active_tag Whether to show the active status tag.
768 + *
610 769 * @return string
611 770 */
612 - public function get_status_tag() {
613 - $key = $this->get_status_key();
614 - $show_achievement = apply_filters( 'charitable_campaign_show_achievement_status_tag', true );
615 - $show_active_tag = apply_filters( 'charitable_campaign_show_active_status_tag', false );
771 + public function get_status_tag( $show_achievement = null, $show_active_tag = null ) {
772 + $key = $this->get_status_key();
616 773
774 + if ( is_null( $show_achievement ) ) {
775 + $show_achievement = apply_filters( 'charitable_campaign_show_achievement_status_tag', true );
776 + }
777 +
778 + if ( is_null( $show_active_tag ) ) {
779 + $show_active_tag = apply_filters( 'charitable_campaign_show_active_status_tag', false );
780 + }
781 +
617 782 switch ( $key ) {
618 783 case 'ended':
619 784 $tag = __( 'Ended', 'charitable' );
620 785 break;
@@ -631,11 +796,31 @@
631 796 $tag = __( 'Ending Soon', 'charitable' );
632 797 break;
633 798
634 799 case 'active':
800 + case 'publish':
635 801 $tag = $show_active_tag ? __( 'Active', 'charitable' ) : '';
636 802 break;
637 803
804 + case 'inactive':
805 + switch ( $this->post->post_status ) {
806 + case 'pending':
807 + $tag = __( 'Pending', 'charitable' );
808 + break;
809 +
810 + case 'draft':
811 + $tag = __( 'Draft', 'charitable' );
812 + break;
813 +
814 + case 'future':
815 + $tag = __( 'Scheduled', 'charitable' );
816 + break;
817 +
818 + default:
819 + $tag = '';
820 + }
821 + break;
822 +
638 823 default:
639 824 $tag = '';
640 825
641 826 }//end switch
@@ -720,8 +905,9 @@
720 905 }
721 906
722 907 $amount = $this->donated_amount;
723 908
909 + /* If returning a string, return it in the site number format. */
724 910 if ( ! $sanitize ) {
725 911 $amount = Charitable_Currency::get_instance()->sanitize_database_amount( $amount );
726 912 }
727 913
@@ -760,14 +946,18 @@
760 946 $amount = $this->get_donated_amount();
761 947 $goal = $this->get_meta( '_campaign_goal' );
762 948
763 949 if ( $goal ) {
764 - $ret = sprintf( _x( '%s donated of %s goal', 'amount donated of goal', 'charitable' ),
950 + $ret = sprintf(
951 + // translators: %1$s is the amount donated, %2$s is the goal amount.
952 + _x( '%1$s donated of %2$s goal', 'amount donated of goal', 'charitable' ),
765 953 '<span class="amount">' . $currency_helper->get_monetary_amount( $amount ) . '</span>',
766 954 '<span class="goal-amount">' . $currency_helper->get_monetary_amount( $goal ) . '</span>'
767 955 );
768 956 } else {
769 - $ret = sprintf( _x( '%s donated', 'amount donated', 'charitable' ),
957 + $ret = sprintf(
958 + // translators: %s is the amount donated.
959 + _x( '%s donated', 'amount donated', 'charitable' ),
770 960 '<span class="amount">' . $currency_helper->get_monetary_amount( $amount ) . '</span>'
771 961 );
772 962 }
773 963
@@ -786,13 +976,44 @@
786 976 return apply_filters( 'charitable_donation_summary', $ret, $this, $amount, $goal );
787 977 }
788 978
789 979 /**
980 + * Return a string especially for the campaign builder describing the campaign's donation summary.
981 + *
982 + * @since 1.8.0
983 + *
984 + * @return string
985 + */
986 + public function get_builder_donation_summary() {
987 + $currency_helper = charitable_get_currency_helper();
988 + $amount = $this->get_donated_amount();
989 + $goal = $this->get_meta( '_campaign_goal' );
990 +
991 + $ret = sprintf(
992 + // translators: %s is the amount donated.
993 + _x( '%s donated', 'amount donated', 'charitable' ),
994 + '<span class="amount">' . $currency_helper->get_monetary_amount( $amount ) . '</span>'
995 + );
996 +
997 + /**
998 + * Filter the returned string if you want to display the donation summary in a different way.
999 + *
1000 + * @since 1.8.0
1001 + *
1002 + * @param string $ret The summary.
1003 + * @param Charitable_Campaign $this This campaign object.
1004 + * @param float $amount The amount donated.
1005 + * @param int $goal The campaign goal.
1006 + */
1007 + return apply_filters( 'charitable_builder_donation_summary', $ret, $this, $amount, $goal );
1008 + }
1009 +
1010 + /**
790 1011 * Return the percentage donated. Use this if you want a formatted string.
791 1012 *
792 1013 * @since 1.0.0
793 1014 *
794 - * @return string|false String if campaign has a goal. False if no goal is set.
1015 + * @return string|false String if campaign has a goal. False if no goal is set.
795 1016 */
796 1017 public function get_percent_donated() {
797 1018 $percent = $this->get_percent_donated_raw();
798 1019
@@ -807,10 +1028,43 @@
807 1028
808 1029 /**
809 1030 * Returns the percentage donated as a number.
810 1031 *
811 - * @since 1.0.0
1032 + * Ported from Charitable Pro, 1.8.13, THEN deliberately changed —
1033 + * Lite deviates from Pro here on purpose. Pro's own version (and my
1034 + * first port of it) unconditionally strips the thousands separator
1035 + * and swaps the decimal separator, which is correct for a
1036 + * DISPLAY-locale-formatted goal like "1,000.00" but wrong for the
1037 + * CANONICAL dot-decimal form sanitize_campaign_goal() actually
1038 + * stores: that method runs every saved goal through
1039 + * Charitable_Currency::sanitize_monetary_amount(), which returns
1040 + * floatval() — always dot-decimal, regardless of site locale. On a
1041 + * comma-decimal / dot-thousands site (Settings > General; both
1042 + * separators are user-settable), the unconditional strip reads an
1043 + * already-canonical "1000.5" as "10005" — a campaign that raised 300
1044 + * of a 1000.5 goal reported 3% instead of 30%.
812 1045 *
1046 + * is_numeric() distinguishes the two shapes: a canonical dot-decimal
1047 + * value passes it and is used via a plain floatval(), untouched; a
1048 + * display-formatted value ("1,000.00", "1.000,50") does not, and
1049 + * still gets the separator treatment below. This is what makes both
1050 + * the locale-formatted-input case (list-campaigns/get-campaign's own
1051 + * "1,000.00" trap) and the canonical-storage case correct at once.
1052 + *
1053 + * Callers reach well beyond the abilities surface: the front-end
1054 + * progress bar (templates/campaign/progress-bar.php, both the bar
1055 + * width and the aria-valuenow a screen reader announces), the
1056 + * dashboard and reports classes, and get_percent_donated() below all
1057 + * go through this method. Pro carries the same bug on the same
1058 + * front-end path; Charitable_Campaign_Abilities::percent_raised()
1059 + * being correct would not have been enough to fix what a donor's
1060 + * screen reader announces. The verbatim-fidelity rule for this port
1061 + * covers the abilities registrars; it does not require shipping a
1062 + * known locale regression on a shared front-end template.
1063 + *
1064 + * @since 1.0.0
1065 + * @version 1.8.13
1066 + *
813 1067 * @return int
814 1068 */
815 1069 public function get_percent_donated_raw() {
816 1070 if ( ! $this->has_goal() ) {
@@ -816,9 +1070,30 @@
816 1070 if ( ! $this->has_goal() ) {
817 1071 return false;
818 1072 }
819 1073
820 - return ( $this->get_donated_amount( true ) / $this->get_goal() ) * 100;
1074 + $donated = floatval( $this->get_donated_amount( true ) );
1075 +
1076 + $goal_meta = $this->get_meta( '_campaign_goal' );
1077 + $currency = Charitable_Currency::get_instance();
1078 + $decimal_separator = $currency->get_decimal_separator();
1079 + $thousands_separator = $currency->get_thousands_separator();
1080 +
1081 + /*
1082 + * Already-canonical dot-decimal values (the shape every saved
1083 + * goal is actually stored in) pass through untouched; anything
1084 + * else is a display-formatted string and gets the thousands
1085 + * strip + decimal-separator swap.
1086 + */
1087 + $goal = is_numeric( $goal_meta )
1088 + ? floatval( $goal_meta )
1089 + : floatval( str_replace( $decimal_separator, '.', str_replace( $thousands_separator, '', $goal_meta ) ) );
1090 +
1091 + if ( $goal <= 0 ) {
1092 + return 0;
1093 + }
1094 +
1095 + return ( $donated / $goal ) * 100;
821 1096 }
822 1097
823 1098 /**
824 1099 * Return the number of people who have donated to the campaign.
@@ -835,12 +1110,44 @@
835 1110 *
836 1111 * @param int $count Number of donors.
837 1112 * @param Charitable_Campaign $this This campaign object.
838 1113 */
839 - return apply_filters( 'charitable_campaign_donor_count', charitable_get_table( 'campaign_donations' )->count_campaign_donors( $this->ID ), $this );
1114 + return apply_filters(
1115 + 'charitable_campaign_donor_count',
1116 + (int) charitable_get_table( 'campaign_donations' )->count_campaign_donors( $this->ID ),
1117 + $this
1118 + );
840 1119 }
841 1120
842 1121 /**
1122 + * Return the number of donations (transactions) for the campaign.
1123 + *
1124 + * Ported from Charitable Pro unchanged. Backs the
1125 + * charitable/get-campaign-performance ability's donation_count field —
1126 + * a campaign with two donations from the same donor has a donor_count
1127 + * of 1 but a get_donation_count() of 2.
1128 + *
1129 + * @since 1.8.13
1130 + *
1131 + * @return int
1132 + */
1133 + public function get_donation_count() {
1134 + /**
1135 + * Filter the number of donations (transactions) for the campaign.
1136 + *
1137 + * @since 1.8.13
1138 + *
1139 + * @param int $count Number of donations.
1140 + * @param Charitable_Campaign $this This campaign object.
1141 + */
1142 + return apply_filters(
1143 + 'charitable_campaign_donation_count',
1144 + (int) charitable_get_table( 'campaign_donations' )->count_campaign_donations( $this->ID ),
1145 + $this
1146 + );
1147 + }
1148 +
1149 + /**
843 1150 * Returns the donation form object.
844 1151 *
845 1152 * @since 1.0.0
846 1153 *
@@ -886,8 +1193,82 @@
886 1193 return apply_filters( 'charitable_session_donation_amount', $amount, $this );
887 1194 }
888 1195
889 1196 /**
1197 + * Returns the donation period for the amount in session.
1198 + *
1199 + * @since 1.6.25
1200 + *
1201 + * @return false|string
1202 + */
1203 + public function get_donation_period_in_session() {
1204 + $donation = charitable_get_session()->get_donation_by_campaign( $this->ID );
1205 +
1206 + if ( ! $donation ) {
1207 + return false;
1208 + }
1209 +
1210 + if ( ! is_array( $donation ) || ! array_key_exists( 'donation_period', $donation ) ) {
1211 + return 'once';
1212 + }
1213 +
1214 + return $donation['donation_period'];
1215 + }
1216 +
1217 + /**
1218 + * Return the initial/default donation period when
1219 + *
1220 + * @since 1.6.32
1221 + *
1222 + * @return string
1223 + */
1224 + public function get_initial_donation_period() {
1225 + $period = $this->get_donation_period_in_session();
1226 +
1227 + if ( false !== $period ) {
1228 + return $period;
1229 + }
1230 +
1231 + if ( ! class_exists( 'Charitable_Recurring' ) ) {
1232 + return 'once';
1233 + }
1234 +
1235 + switch ( $this->get( 'recurring_donation_mode' ) ) {
1236 + case 'advanced':
1237 + $period = 'one-time' === $this->get( 'recurring_default_tab' ) ? 'once' : 'recurring';
1238 + break;
1239 +
1240 + default:
1241 + $period = 'once';
1242 + }
1243 +
1244 + return $period;
1245 + }
1246 +
1247 + /**
1248 + * Get the default donation amount for this campaign.
1249 + *
1250 + * @since 1.6.32
1251 + *
1252 + * @return int
1253 + */
1254 + public function get_default_donation_amount() {
1255 + /**
1256 + * Filter the default donation amount.
1257 + *
1258 + * Note, the donation amount should be expressed in a format where
1259 + * a period (.) is used as the decimal separator. i.e. 2.50 is
1260 + * two dollars and fifty cents.
1261 + *
1262 + * @since 1.5.6
1263 + *
1264 + * @param float|int $amount The amount to be filtered. $0 by default.
1265 + * @param Charitable_Campaign $campaign The instance of `Charitable_Campaign`.
1266 + */
1267 + return apply_filters( 'charitable_default_donation_amount', 0, $this );
1268 + }
1269 +
1270 + /**
890 1271 * Renders the donate button template.
891 1272 *
892 1273 * @since 1.0.0
893 1274 *
@@ -934,18 +1315,19 @@
934 1315
935 1316 $display_option = charitable_get_option( 'donation_form_display', 'separate_page' );
936 1317
937 1318 switch ( $display_option ) {
938 - case 'modal' :
1319 + case 'modal':
939 1320 $template_name = 'campaign-loop/donate-modal.php';
940 1321 break;
941 1322
942 - default :
1323 + default:
943 1324 $template_name = apply_filters( 'charitable_donate_button_loop_template', 'campaign-loop/donate-link.php', $this );
944 1325 }
945 1326
946 1327 charitable_template( $template_name, array( 'campaign' => $this ) );
947 1328 }
1329 +
948 1330 /**
949 1331 * Returns the campaign creator.
950 1332 *
951 1333 * By default, this just returns the user from the post_author field, but
@@ -997,13 +1379,29 @@
997 1379 return charitable_get_currency_helper()->sanitize_monetary_amount( $value );
998 1380 }
999 1381
1000 1382 /**
1383 + * Sanitize the campaign minimum amount.
1384 + *
1385 + * @since 1.7.0.3
1386 + *
1387 + * @param string $value Current value of min amount.
1388 + * @return string|int
1389 + */
1390 + public static function sanitize_min_donation_amount( $value ) {
1391 + if ( empty( $value ) || ! $value ) {
1392 + return 0;
1393 + }
1394 +
1395 + return charitable_get_currency_helper()->sanitize_monetary_amount( $value );
1396 + }
1397 +
1398 + /**
1001 1399 * Sanitize the campaign end date.
1002 1400 *
1003 1401 * We use WP_Locale to parse the month that the user has set.
1004 1402 *
1005 - * @global WP_Locale $wp_locale
1403 + * @global WP_Locale $wp_locale
1006 1404 *
1007 1405 * @since 1.0.0
1008 1406 *
1009 1407 * @param string $value Current end date value.
@@ -1011,10 +1409,15 @@
1011 1409 * @return string|int
1012 1410 */
1013 1411 public static function sanitize_campaign_end_date( $value, $submitted = array() ) {
1014 1412 $end_time = array_key_exists( '_campaign_end_time', $submitted ) ? $submitted['_campaign_end_time'] : '23:59:59';
1015 - $end_date = charitable_sanitize_date( $value, 'Y-m-d ' . $end_time );
1016 1413
1414 + if ( charitable()->registry()->get( 'i18n' )->decline_months() ) {
1415 + $end_date = $value . ' ' . $end_time;
1416 + } else {
1417 + $end_date = charitable_sanitize_date( $value, 'Y-m-d ' . $end_time );
1418 + }
1419 +
1017 1420 if ( ! $end_date ) {
1018 1421 $end_date = 0;
1019 1422 }
1020 1423
@@ -1028,9 +1431,24 @@
1028 1431 *
1029 1432 * @param array $value Current suggested donations value.
1030 1433 * @return array
1031 1434 */
1032 - public static function sanitize_campaign_suggested_donations( $value ) {
1435 + public static function sanitize_campaign_suggested_donations_default( $value ) {
1436 +
1437 + return $value; // this is likely an array.
1438 + }
1439 +
1440 + /**
1441 + * Sanitize the campaign suggested donations.
1442 + *
1443 + * @since 1.0.0
1444 + *
1445 + * @param array $value Current suggested donations value.
1446 + * @param bool $submitted Whether the value was submitted.
1447 + * @param int $campaign_id The campaign ID.
1448 + * @return array
1449 + */
1450 + public static function sanitize_campaign_suggested_donations( $value, $submitted = false, $campaign_id = false ) { // phpcs:ignore
1033 1451 if ( ! is_array( $value ) ) {
1034 1452 return array();
1035 1453 }
1036 1454
@@ -1041,8 +1459,33 @@
1041 1459 }
1042 1460
1043 1461 foreach ( $value as $key => $suggestion ) {
1044 1462 $value[ $key ]['amount'] = charitable_sanitize_amount( (string) $suggestion['amount'] );
1463 +
1464 + /*
1465 + * A description is OPTIONAL. filter_suggested_donation() above keeps any entry that has
1466 + * a non-empty 'amount', so array( 'amount' => 25 ) is a perfectly valid suggestion with
1467 + * no 'description' key at all - which is the normal shape when a campaign offers bare
1468 + * amounts. Reading it unguarded emitted an "Undefined array key" warning on every such
1469 + * sanitize pass. Defaulted rather than skipped so the key is always present in the
1470 + * returned array, which is what callers and the stored meta already assume.
1471 + */
1472 + $description = isset( $suggestion['description'] ) ? $suggestion['description'] : '';
1473 +
1474 + /**
1475 + * Sanitize the description field.
1476 + *
1477 + * @since 1.6.51
1478 + *
1479 + * @param boolean $sanitize Whether to sanitize the suggested amount description.
1480 + * Returning false will allow HTML to be used, though it will
1481 + * still be filtered using wp_kses_post.
1482 + */
1483 + if ( apply_filters( 'charitable_sanitize_suggested_amount_description', true ) ) {
1484 + $value[ $key ]['description'] = sanitize_text_field( $description );
1485 + } else {
1486 + $value[ $key ]['description'] = wp_kses_post( $description );
1487 + }
1045 1488 }
1046 1489
1047 1490 return $value;
1048 1491 }
@@ -1077,21 +1520,96 @@
1077 1520
1078 1521 /**
1079 1522 * Sanitize the campaign description.
1080 1523 *
1081 - * @since 1.0.0
1524 + * @since 1.0.0
1525 + * @updated 1.7.0.4
1082 1526 *
1083 1527 * @param string $value Current description value.
1084 1528 * @return string
1085 1529 */
1086 1530 public static function sanitize_campaign_description( $value ) {
1087 - return sanitize_text_field( $value );
1531 +
1532 + /**
1533 + * Allowed HTML in description.
1534 + */
1535 + $wp_kses_allowed_html = array(
1536 + 'a' => array(
1537 + 'href' => array(),
1538 + 'target' => array(),
1539 + 'class' => array(),
1540 + 'title' => array(),
1541 + ),
1542 + 'br' => array(),
1543 + 'img' => array(
1544 + 'src' => array(),
1545 + 'class' => array(),
1546 + 'alt' => array(),
1547 + ),
1548 + 'h1' => array(
1549 + 'class' => array(),
1550 + ),
1551 + 'h2' => array(
1552 + 'class' => array(),
1553 + ),
1554 + 'h3' => array(
1555 + 'class' => array(),
1556 + ),
1557 + 'h4' => array(
1558 + 'class' => array(),
1559 + ),
1560 + 'h5' => array(
1561 + 'class' => array(),
1562 + ),
1563 + 'h6' => array(
1564 + 'class' => array(),
1565 + ),
1566 + 'div' => array(
1567 + 'class' => array(),
1568 + ),
1569 + 'p' => array(
1570 + 'style' => array(),
1571 + 'class' => array(),
1572 + ),
1573 + 'ul' => array(
1574 + 'class' => array(),
1575 + ),
1576 + 'li' => array(
1577 + 'id' => array(),
1578 + 'class' => array(),
1579 + ),
1580 + 'em' => array(),
1581 + 'span' => array(
1582 + 'class' => array(),
1583 + ),
1584 + 'strong' => array(),
1585 + );
1586 +
1587 + if ( apply_filters( 'charitable_sanitize_campaign_description', false ) ) {
1588 + return sanitize_text_field( $value );
1589 + } else {
1590 + return wp_kses( apply_filters( 'charitable_campaign_description', $value ), apply_filters( 'charitable_sanitize_campaign_description_allowed_html', $wp_kses_allowed_html ) );
1591 + }
1088 1592 }
1089 1593
1090 1594 /**
1595 + * Sanitize the campaign donation button text..
1596 + *
1597 + * @since 1.7.0.9
1598 + *
1599 + * @param string $value Current text value.
1600 + * @return string
1601 + */
1602 + public static function sanitize_campaign_donate_button_text( $value = false ) {
1603 +
1604 + return wp_strip_all_tags( $value );
1605 + }
1606 +
1607 + /**
1091 1608 * Sanitize the value provided for custom donations.
1092 1609 *
1093 - * @since 1.3.6
1610 + * @since 1.3.6
1611 + * @version 1.8.0
1094 1612 *
1095 1613 * @param mixed $value Current custom donations value.
1096 1614 * @param array $submitted All posted values.
1097 1615 * @return boolean
@@ -1100,8 +1618,27 @@
1100 1618 $checked = self::sanitize_checkbox( $value );
1101 1619
1102 1620 if ( $checked ) {
1103 1621 return $checked;
1622 + }
1623 +
1624 + /**
1625 + * Filter whether a campaign can be saved with custom donations disabled
1626 + * and no suggested donations.
1627 + *
1628 + * @since 1.6.14
1629 + *
1630 + * @param boolean $permitted Whether a campaign is permitted to be saved without
1631 + * suggested donations or custom donations.
1632 + * @param array $submitted The submitted values.
1633 + */
1634 + if ( apply_filters( 'charitable_campaign_permitted_without_custom_or_suggested', false, $submitted ) ) {
1635 + return $checked;
1636 + }
1637 +
1638 + // see if literally "0" was passed.
1639 + if ( is_string( $value ) && '0' === $value ) {
1640 + return 0;
1104 1641 }
1105 1642
1106 1643 /* If suggested donations are not set, custom donations needs to be enabled. */
1107 1644 if ( ! isset( $submitted['_campaign_suggested_donations'] ) ) {