PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.18.0
GiveWP – Donation Plugin and Fundraising Platform v4.18.0
4.18.0 4.17.0 4.16.9 4.16.8.1 4.16.8 4.16.7.2 4.16.7.1 4.16.7 4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 All 257 releases
give / includes / donors / class-give-donor-wall.php

class-give-donor-wall.php in GiveWP – Donation Plugin and Fundraising Platform 4.18.0, at includes/donors/class-give-donor-wall.php

676 lines 19.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Donor Wall
4 *
5 * @package Give
6 * @subpackage Classes/Give_Donor_Wall
7 * @copyright Copyright (c) 2020, GiveWP
8 * @license https://opensource.org/licenses/gpl-license GNU Public License
9 * @since 1.0
10 */
11
12 // Exit if accessed directly.
13 use Give\Donations\ValueObjects\DonationMetaKeys;
14
15 if (!defined('ABSPATH')) {
16 exit;
17 }
18
19
20 /**
21 * Give_Donor_Wall Class
22 *
23 * This class handles donors.
24 *
25 * @since 2.2.0
26 */
27 class Give_Donor_Wall {
28
29 /**
30 * Instance.
31 *
32 * @since 2.2.0
33 * @access private
34 * @var Give_Donor_Wall
35 */
36 private static $instance;
37
38 /**
39 * Singleton pattern.
40 *
41 * @since 2.2.0
42 * @access private
43 */
44 private function __construct() {
45 }
46
47
48 /**
49 * Get instance.
50 *
51 * @since 2.2.0
52 * @access public
53 * @return Give_Donor_Wall
54 */
55 public static function get_instance() {
56 if ( null === static::$instance ) {
57 self::$instance = new static();
58
59 self::$instance->setup_actions();
60 }
61
62 return self::$instance;
63 }
64
65 /**
66 * Setup the default hooks and actions
67 *
68 * @since 2.2.0
69 *
70 * @return void
71 */
72 public function setup_actions() {
73
74 add_shortcode( 'give_donor_wall', [ $this, 'render_shortcode' ] );
75
76 add_action( 'wp_ajax_give_get_donor_comments', [ $this, 'ajax_handler' ] );
77 add_action( 'wp_ajax_nopriv_give_get_donor_comments', [ $this, 'ajax_handler' ] );
78
79 }
80
81
82 /**
83 * Displays donors in a grid layout.
84 *
85 * @since 4.16.9 Added additional sanitization to donor output.
86 * @since 4.13.2 add strip_shortcodes to the html output
87 * @since 4.3.1 remove redundant _give_redirect_form_id() function.
88 * @since 3.7.0 Sanitize attributes
89 * @since 2.27.0 Moved AJAX nonce verification to ajax_handler method.
90 * @since 2.2.0
91 *
92 * @param array $atts {
93 * Optional. Attributes of the donor wall shortcode.
94 *
95 * @type int $donors_per_page Number of donors per page. Default '20'.
96 * @type int $form_id The donation form to filter donors by. Default is all forms (no filter).
97 * @type bool $paged Whether to paginate donors. Default 'true'.
98 * @type string $ids A comma-separated list of donor IDs to display. Default empty.
99 * @type string $columns Maximum columns to display. Default 'best-fit'.
100 * Accepts 'best-fit', '1', '2', '3', '4'.
101 * @type bool $show_avatar Whether to display the donor's gravatar image if available. Default 'true'.
102 * @type bool $show_name Whether to display the donor's full name, first and last. Default 'true'.
103 * @type bool $show_company_name Whether to display the donor's company name. Default 'false'.
104 * @type bool $show_total Whether to display the donor's donation amount. Default 'true'.
105 * @type bool $show_comments Whether to display the donor's comment if they left one. Default 'true'.
106 * @type int $comment_length The number of words to display for the comments before a "Read more" field
107 * @type int $only_comments Whether to display the donors only with comment. Default 'false'.
108 * @type bool $show_time Whether to display date of the last donation. Default 'true'.
109 *
110 * @type string $readmore_text Link label for modal in which donor can read full comment.
111 * @type string $loadmore_text Button label which will load more donor comments.
112 * @type int $avatar_size Avatar image size in pixels without the "px". Default "75"
113 * @type string $orderby The order in which you want the donations to appear.
114 * Currently we are using this attribute internally and, it will sort donations by created date.
115 * @type string $order The order in which you want the donors to appear. Accepts "ASC". "DESC".
116 *
117 * }
118 * @return string|bool The markup of the form grid or false.
119 */
120 public function render_shortcode( $atts ) {
121 $atts = give_clean($atts);
122
123 $give_settings = give_get_settings();
124
125 $atts = $this->parse_atts( $atts );
126
127 $donations = $this->get_donation_data( $atts );
128 $html = '';
129
130 if ( $donations ) {
131
132 ob_start();
133
134 foreach ( $donations as $donation ) {
135 $donor = new Give_Donor($donation['_give_payment_donor_id']);
136 // Give/templates/shortcode-donor-wall.php.
137 give_get_template(
138 'shortcode-donor-wall',
139 [
140 $donation,
141 $give_settings,
142 $atts,
143 $donor
144 ]
145 );
146 }
147
148 $html = ob_get_clean();
149
150 // Strip shortcodes to prevent execution of user-supplied shortcode syntax.
151 $html = give_strip_shortcodes_deep($html);
152
153 // Return only donor html.
154 if (
155 isset( $atts['only_donor_html'] )
156 && wp_doing_ajax()
157 && $atts['only_donor_html']
158 ) {
159 return $html;
160 }
161 }
162
163 $temp_atts = $atts;
164 $temp_atts['paged'] = $atts['paged'] + 1;
165
166 $more_btn_html = sprintf(
167 '<input type="hidden" class="give-donor-wall-shortcode-attrs" data-shortcode="%s" data-nonce="%s">',
168 rawurlencode( http_build_query( $atts ) ),
169 wp_create_nonce( 'givewp-donor-wall-more' )
170 );
171
172 if ( $this->has_donations( $temp_atts ) ) {
173 $more_btn_html .= sprintf(
174 '<button class="give-donor__load_more give-button-with-loader"><span class="give-loading-animation"></span>%1$s</button>',
175 $atts['loadmore_text']
176 );
177 }
178
179 $html = $html
180 ? sprintf(
181 '<div class="give-wrap give-grid-ie-utility"><div class="give-grid give-grid--%1$s">%2$s</div>%3$s</div>',
182 esc_attr( $atts['columns'] ),
183 $html,
184 $more_btn_html
185 )
186 : '';
187
188 return $html;
189 }
190
191 /**
192 * Parse shortcode attributes
193 *
194 * @since 2.30.0
195 * @since 2.2.0
196 * @access public
197 *
198 * @param array $atts Shortcode attributes.
199 *
200 * @return array
201 */
202 public function parse_atts( $atts ) {
203 $atts = shortcode_atts(
204 [
205 'donors_per_page' => 12,
206 'form_id' => 0,
207 'paged' => 1,
208 'ids' => '',
209 'cats' => '',
210 'tags' => '',
211 'columns' => '3',
212 'anonymous' => true,
213 'show_avatar' => true,
214 'show_name' => true,
215 'show_company_name' => false,
216 'show_form' => false,
217 'show_total' => true,
218 'show_comments' => true,
219 'show_tributes' => true,
220 'comment_length' => 140,
221 'only_comments' => false,
222 'readmore_text' => esc_html__( 'Read more', 'give' ),
223 'loadmore_text' => esc_html__( 'Load more', 'give' ),
224 'avatar_size' => 75,
225 'color' => "#219653",
226 'orderby' => 'post_date',
227 'order' => 'DESC',
228 'hide_empty' => true, // Deprecated in 2.3.0
229 'only_donor_html' => false, // Only for internal use.,
230 'show_time' => true,
231 ],
232 $atts,
233 'give_donor_wall'
234 );
235
236 // Validate boolean attributes.
237 $boolean_attributes = [
238 'anonymous',
239 'show_avatar',
240 'show_name',
241 'show_company_name',
242 'show_total',
243 'show_comments',
244 'show_tributes',
245 'hide_empty',
246 'only_comments',
247 'only_donor_html',
248 'show_time'
249 ];
250
251 foreach ( $boolean_attributes as $att ) {
252 // Convert numeric to boolean.
253 // It will prevent condition check against boolean value.
254 if ( is_numeric( $atts[ $att ] ) ) {
255 $atts[ $att ] = (bool) $atts[ $att ];
256 }
257
258 $atts[ $att ] = filter_var( $atts[ $att ], FILTER_VALIDATE_BOOLEAN );
259 }
260
261 // Validate numeric attributes.
262 $numeric_attributes = [
263 'donors_per_page',
264 'paged',
265 'comment_length',
266 'avatar_size',
267 ];
268
269 foreach ( $numeric_attributes as $att ) {
270 // It will prevent condition check against numeric value.
271 $atts[ $att ] = absint( $atts[ $att ] );
272 }
273
274 // Validate comma separated numeric attributes and keep original data format ( comma separated string).
275 if ( ! empty( $atts['ids'] ) ) {
276 $atts['ids'] = implode( ',', $this->split_string($atts['ids'], 'absint') );
277 }
278
279 // Validate Form IDs
280 if ( ! empty( $atts['form_id'] ) ) {
281 $atts['form_id'] = implode( ',', $this->split_string($atts['form_id'], 'absint') );
282 }
283
284 // Donation form categories
285 if ( ! empty( $atts['cats'] ) ) {
286 $atts['cats'] = $this->split_string($atts['cats']);
287 }
288
289 // Donation form tags
290 if ( ! empty( $atts['tags'] ) ) {
291 $atts['tags'] = $this->split_string($atts['tags']);
292 }
293
294 return $atts;
295 }
296
297 /**
298 * Get donors
299 *
300 * @since 2.2.0
301 * @access public
302 *
303 * @param array $donor_query Donor query.
304 *
305 * @return array
306 */
307 public function get_donors( $donor_query ) {
308 $donor_query = new Give_Donors_Query( $donor_query );
309
310 return $donor_query->get_donors();
311 }
312
313
314 /**
315 * This function should return donor comment for ajax request.
316 *
317 * @since 2.27.0 Check nonce for AJAX request to prevent scrapping, see https://github.com/impress-org/givewp/issues/6374.
318 * @since 2.2.0
319 * @access public
320 */
321 public function ajax_handler() {
322 $shortcode_atts = array_map( 'give_clean', wp_parse_args( rawurldecode( $_POST['data'] ) ) ); // @codingStandardsIgnoreLine
323
324 // Get next page donor comments.
325 $shortcode_atts['paged'] = $shortcode_atts['paged'] + 1;
326 $shortcode_atts['only_donor_html'] = true;
327
328 check_ajax_referer( 'givewp-donor-wall-more', 'nonce' );
329
330 $donors_comment_html = $this->render_shortcode( $shortcode_atts );
331
332 // Check if donor comment remaining.
333 $temp_atts = $shortcode_atts;
334 $temp_atts['paged'] = $shortcode_atts['paged'] + 1;
335 $has_donors = $this->has_donations( $temp_atts ) ? 1 : 0;
336
337 // Remove internal shortcode param.
338 unset( $shortcode_atts['only_donor_html'] );
339
340 wp_send_json(
341 [
342 'shortcode' => rawurlencode( http_build_query( $shortcode_atts ) ),
343 'html' => $donors_comment_html,
344 'remaining' => $has_donors,
345 ]
346 );
347 }
348
349 /**
350 * Get query params
351 *
352 * @since 2.24.1
353 * @since 2.3.0
354 *
355 * @param array $atts
356 *
357 * @return array
358 */
359 private function get_query_param( $atts = [] ) {
360 $valid_order = [ 'ASC', 'DESC' ];
361 $valid_orderby = [ 'post_date', 'donation_amount' ];
362
363 $query_atts = [];
364
365 $query_atts['order'] = in_array( $atts['order'], $valid_order ) ? $atts['order'] : 'DESC';
366 $query_atts['orderby'] = in_array( $atts['orderby'], $valid_orderby ) ? $atts['orderby'] : 'post_date';
367 $query_atts['limit'] = absint( $atts['donors_per_page'] );
368 $query_atts['offset'] = absint( $atts['donors_per_page'] * ( $atts['paged'] - 1 ) );
369 $query_atts['form_id'] = implode( '\',\'', array_map( 'absint', explode( ',', $atts['form_id'] ) ) );
370 $query_atts['ids'] = implode( '\',\'', array_map( 'absint', explode( ',', $atts['ids'] ) ) );
371 $query_atts['cats'] = $atts['cats'];
372 $query_atts['tags'] = $atts['tags'];
373 $query_atts['only_comments'] = ( true === $atts['only_comments'] );
374 $query_atts['anonymous'] = ( true === $atts['anonymous'] );
375
376
377
378 return $query_atts;
379 }
380
381 /**
382 * Get donation data.
383 *
384 * @since 4.18.0 Read meta values as stored instead of unserializing them, and read the donor
385 * comment from the fetched rows instead of a per-donation meta lookup.
386 * @since 4.16.7.2 Restrict unserialize to prevent object instantiation.
387 * @since 2.27.0 Change to read comment from donations meta table
388 * @since 2.3.0
389 *
390 * @param array $atts
391 *
392 * @return array
393 */
394 private function get_donation_data( $atts = [] ) {
395 global $wpdb;
396
397 // Bailout if donation does not exist.
398 if ( ! ( $donation_ids = $this->get_donations( $atts ) ) ) {
399 return [];
400 }
401
402 $donation_ids = ! empty( $donation_ids )
403 ? '\'' . implode( '\',\'', $donation_ids ) . '\''
404 : '';
405
406 // Backward compatibility
407 $donation_id_col = Give()->payment_meta->get_meta_type() . '_id';
408
409 $sql = "SELECT m1.*, p1.post_date as donation_date FROM {$wpdb->donationmeta} as m1
410 INNER JOIN {$wpdb->posts} as p1 ON (m1.{$donation_id_col}=p1.ID)
411 WHERE m1.{$donation_id_col} IN ( {$donation_ids} )
412 ORDER BY FIELD( p1.ID, {$donation_ids} )
413 ";
414
415 $results = (array) $wpdb->get_results( $sql );
416
417 if ( ! empty( $results ) ) {
418 $temp = [];
419
420 /* @var stdClass $result */
421 foreach ( $results as $result ) {
422 $temp[ $result->{$donation_id_col} ][ $result->meta_key ] = $result->meta_value;
423
424 // Set donation date.
425 if ( empty( $temp[ $result->{$donation_id_col} ]['donation_date'] ) ) {
426 $temp[ $result->{$donation_id_col} ]['donation_date'] = $result->donation_date;
427 }
428 }
429
430 if ( ! empty( $temp ) ) {
431 foreach ( $temp as $donation_id => $donation_data ) {
432 $temp[ $donation_id ]['donation_id'] = $donation_id;
433
434 $temp[ $donation_id ]['name_initial'] = give_get_name_initial(
435 [
436 'firstname' => $donation_data['_give_donor_billing_first_name'],
437 'lastname' => $donation_data['_give_donor_billing_last_name'],
438 ]
439 );
440
441 $temp[ $donation_id ]['donor_comment'] = $donation_data[ DonationMetaKeys::COMMENT ] ?? '';
442 }
443
444 $results = ! empty( $temp ) ? $temp : [];
445 }
446 }
447
448 return $results;
449 }
450
451 /**
452 * Get donation list for specific query
453 *
454 * @since 3.17.2 fix - filter by only_comments attr
455 * @since 2.3.0
456 *
457 * @param array $atts
458 *
459 * @return array
460 */
461 private function get_donations( $atts = [] ) {
462 global $wpdb;
463
464 // Backward compatibility
465 $donation_id_col = Give()->payment_meta->get_meta_type() . '_id';
466
467 $query_params = $this->get_query_param( $atts );
468
469 $sql = "SELECT p1.ID FROM {$wpdb->posts} as p1";
470 $where = " WHERE p1.post_status IN ('publish') AND p1.post_type = 'give_payment'";
471
472 // exclude donation with zero amount from result.
473 $sql .= " INNER JOIN {$wpdb->donationmeta} as m1 ON (p1.ID = m1.{$donation_id_col})";
474 $where .= " AND m1.meta_key='_give_payment_total' AND m1.meta_value>0";
475
476 if ( $query_params['form_id'] ) {
477 $sql .= " INNER JOIN {$wpdb->donationmeta} as m2 ON (p1.ID = m2.{$donation_id_col})";
478 $where .= " AND m2.meta_key='_give_payment_form_id' AND m2.meta_value IN ('{$query_params['form_id']}')";
479 }
480
481 // Get donations only from specific donors.
482 if ( $query_params['ids'] ) {
483 $sql .= " INNER JOIN {$wpdb->donationmeta} as m3 ON (p1.ID = m3.{$donation_id_col})";
484 $where .= " AND m3.meta_key='_give_payment_donor_id' AND m3.meta_value IN ('{$query_params['ids']}')";
485 }
486
487 // exclude donations which does not has donor comment.
488 if ( $query_params['only_comments'] ) {
489 $sql .= " INNER JOIN {$wpdb->donationmeta} as m4 ON (p1.ID = m4.{$donation_id_col})";
490 $where .= " AND m4.meta_key='_give_donation_comment'";
491 }
492
493 // exclude anonymous donation form query based on query parameters.
494 if (
495 ! $query_params['anonymous']
496 || $query_params['only_comments']
497 ) {
498 $where .= " AND p1.ID NOT IN ( SELECT DISTINCT({$donation_id_col}) FROM {$wpdb->donationmeta} WHERE meta_key='_give_anonymous_donation' AND meta_value='1')";
499 }
500
501 // Handle Taxonomy
502 $args = [
503 'post_type' => 'give_forms',
504 'posts_per_page' => -1,
505 'fields' => 'ids',
506 'tax_query' => [],
507 ];
508
509 // Categories
510 if ( is_array($atts['cats'])) {
511 $args['tax_query']['conditions'] = ['relation' => 'OR'];
512
513 foreach ($atts['cats'] as $category) {
514 $args['tax_query']['conditions'][] = [
515 'operator' => 'IN',
516 'taxonomy' => 'give_forms_category',
517 'field' => 'slug',
518 'terms' => $category,
519 ];
520 }
521 }
522
523 // Tags
524 if ( is_array($atts['tags'])) {
525 if (empty($args['tax_query'])) {
526 $args['tax_query']['conditions'] = ['relation' => 'OR'];
527 }
528
529 foreach($atts['tags'] as $tag) {
530 $args['tax_query']['conditions'][] = [
531 'operator' => 'IN',
532 'taxonomy' => 'give_forms_tag',
533 'field' => 'slug',
534 'terms' => $tag,
535 ];
536 }
537 }
538
539 if ( ! empty( $args['tax_query'] ) ) {
540 $query = new WP_Query( $args );
541
542 if ( ! empty($query->posts) ) {
543 $form_ids = implode("','", $query->posts );
544 $sql .= " INNER JOIN {$wpdb->donationmeta} as m4 ON (p1.ID = m4.{$donation_id_col})";
545 $where .= " AND m4.meta_key='_give_payment_form_id' AND m4.meta_value IN ('{$form_ids}')";
546 }
547 }
548
549 // order by query based on parameter.
550 if ( 'donation_amount' === $query_params['orderby'] ) {
551 $order = " ORDER BY m1.meta_value+0 {$query_params['order']}";
552 } else {
553 $order = " ORDER BY p1.{$query_params['orderby']} {$query_params['order']}, p1.ID {$query_params['order']}";
554 }
555
556 $limit = " LIMIT {$query_params['limit']}";
557 $offset = " OFFSET {$query_params['offset']}";
558
559 $sql .= $where . $order . $limit . $offset;
560
561 return $wpdb->get_col( $sql );
562 }
563
564 /**
565 * Get donor comments
566 *
567 * @since 2.3.0
568 *
569 * @param array $donations_data
570 *
571 * @return array
572 */
573 private function get_donor_comments( $donations_data = [] ) {
574 global $wpdb;
575 $comments = [];
576
577 // Bailout.
578 if ( empty( $donations_data ) ) {
579 return $comments;
580 }
581
582 // Backward compatibility.
583 if (
584 ! give_has_upgrade_completed( 'v230_move_donor_note' )
585 || ! give_has_upgrade_completed( 'v230_move_donation_note' )
586 ) {
587 foreach ( $donations_data as $id => $data ) {
588 $comment = give_get_donor_donation_comment( $id, $data['_give_payment_donor_id'] );
589 $comments[ $id ] = ! empty( $comment ) ? $comment->comment_content : '';
590 }
591
592 return $comments;
593 }
594
595 $sql = "SELECT c1.comment_parent as donation_id, c1.comment_content as comment FROM {$wpdb->give_comments} as c1";
596 $sql .= " INNER JOIN {$wpdb->give_commentmeta} as cm1 ON (c1.comment_ID=cm1.give_comment_id)";
597 $where = [];
598
599 foreach ( $donations_data as $id => $data ) {
600 // Do not fetch comment for anonymous donation.
601 if ( ! empty( $data['_give_anonymous_donation'] ) ) {
602 continue;
603 }
604
605 $where[] = "(c1.comment_parent={$id} AND cm1.meta_key='_give_donor_id' AND cm1.meta_value={$data['_give_payment_donor_id']})";
606 }
607
608 $where = ' WHERE ' . implode( ' OR ', $where );
609 $where .= " AND c1.comment_type='donor_donation'";
610
611 $sql = $sql . $where;
612
613 $comments = (array) $wpdb->get_results( $sql );
614
615 if ( ! empty( $comments ) ) {
616 $comments = array_combine(
617 wp_list_pluck( $comments, 'donation_id' ),
618 wp_list_pluck( $comments, 'comment' )
619 );
620 }
621
622 return $comments;
623 }
624
625 /**
626 * Check if donation exist or not for specific query
627 *
628 * @since 2.3.0
629 *
630 * @param array $atts
631 *
632 * @return bool
633 */
634 private function has_donations( $atts = [] ) {
635 return (bool) $this->get_donations( $atts );
636 }
637
638 /**
639 * @since 2.20.0
640 *
641 * @param string $string
642 * @param null|callable $filter
643 * @param string $separator
644 *
645 * @return array
646 */
647 private function split_string($string, $filter = null, $separator = ',') {
648 if ( false === strpos( $string, $separator ) ) {
649 $string = trim( $string );
650
651 if (is_callable($filter)) {
652 $string = $filter($string);
653 }
654
655 return [$string];
656 }
657
658 return array_filter(
659 array_map(
660 static function( $value ) use ($filter) {
661 $value = trim( $value );
662
663 if (is_callable($filter)) {
664 return $filter($value);
665 }
666 return $value;
667 },
668 explode( $separator, $string )
669 )
670 );
671 }
672 }
673
674 // Initialize shortcode.
675 Give_Donor_Wall::get_instance();
676