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

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

1,113 lines 28.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Admin Plugin Notifications.
4 *
5 * @package Charitable/Classes/Charitable_Admin_Form
6 * @author David Bisset
7 * @copyright Copyright (c) 2023, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.8.3
10 * @version 1.8.3
11 */
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 /**
19 * Notifications.
20 *
21 * @since 1.8.3
22 * @version 1.8.3
23 */
24 class Charitable_Notifications {
25
26 /**
27 * The single instance of this class.
28 *
29 * @var Charitable_Notifications|null
30 */
31 private static $instance = null;
32
33 /**
34 * Source of notifications content.
35 *
36 * @since 1.8.3
37 *
38 * @var string
39 */
40 const SOURCE_URL = 'https://plugin.wpcharitable.com/wp-content/charitable-notifications.json';
41
42 /**
43 * Array of license types, that are considered being Elite level.
44 *
45 * @since 1.8.3
46 *
47 * @var array
48 */
49 const LICENSES_ELITE = [ 'agency', 'pro', 'elite' ];
50
51 /**
52 * Option value.
53 *
54 * @since 1.8.3
55 *
56 * @var bool|array
57 */
58 public $option = false;
59
60 /**
61 * Current license type.
62 *
63 * @since 1.8.3
64 *
65 * @var string
66 */
67 private $license_type;
68
69 /**
70 * Returns and/or create the single instance of this class.
71 *
72 * @since 1.2.0
73 *
74 * @return Charitable_User_Dashboard
75 */
76 public static function get_instance() {
77 if ( is_null( self::$instance ) ) {
78 self::$instance = new self();
79 }
80
81 return self::$instance;
82 }
83
84 /**
85 * Constructor.
86 *
87 * @since 1.8.3
88 */
89 public function __construct() {
90
91 if ( ! defined( 'CHARITABLE_DISABLE_NOTIFICATIONS' ) || ! CHARITABLE_DISABLE_NOTIFICATIONS ) {
92 $this->init();
93 }
94 }
95
96 /**
97 * Initialize class.
98 *
99 * @since 1.8.3
100 */
101 public function init() {
102
103 $this->hooks();
104 }
105
106 /**
107 * Register hooks.
108 *
109 * @since 1.8.3
110 */
111 public function hooks() {
112 add_action( 'charitable_maybe_show_plugin_notifications', [ $this, 'output' ], 99 ); // where the notifications comes out in the admin.
113 add_action( 'charitable_admin_notifications_update', [ $this, 'update' ] );
114 add_action( 'deactivate_plugin', [ $this, 'delete' ], 10, 2 );
115 }
116
117 /**
118 * Check if user has access and is enabled.
119 *
120 * @since 1.8.3
121 *
122 * @return bool
123 */
124 public function has_access() {
125
126 $access = charitable_current_user_can( 'administrator' ) && ! charitable_get_option( 'hide-announcements' );
127
128 /**
129 * Allow modifying state if a user has access.
130 *
131 * @since 1.7.0.3
132 *
133 * @param bool $access True if user has access.
134 */
135 return (bool) apply_filters( 'charitable_admin_notifications_has_access', $access );
136 }
137
138 /**
139 * Add notifications to the WordPress admin menu for Charitable.
140 *
141 * @since 1.8.3
142 *
143 * @param array $submenu Array of menu items.
144 * @return bool
145 */
146 public function add_notifications_to_menu( $submenu = array() ) {
147
148 if ( ( defined( 'CHARITABLE_NOTIFICATIONS_FORCE_OFF' ) && CHARITABLE_NOTIFICATIONS_FORCE_OFF ) ) {
149 return $submenu;
150 }
151
152 $notifications_menu_item = array(
153 'page_title' => __( 'Notifications', 'charitable' ),
154 'menu_title' => __( 'Notifications', 'charitable' ),
155 'menu_slug' => 'charitable-notifications',
156 'function' => array( $this, 'render_plugin_notifications_page' ),
157 'capability' => 'manage_charitable_settings',
158 );
159
160 // if there are new notifications add the indicator to the menu title.
161 if ( $this->get_new_notifications_count() ) {
162 $notifications_menu_item['menu_title'] .= '<div class="charitable-menu-notification-indicator"></div>';
163 }
164
165 // If the checklist exists in the submenu, add this item after it.
166 $checklist_key = array_search( 'charitable-setup-checklist', array_column( $submenu, 'menu_slug' ), true );
167 if ( false !== $checklist_key ) {
168 $submenu = array_merge(
169 array_slice( $submenu, 0, $checklist_key + 1 ),
170 array( $notifications_menu_item ),
171 array_slice( $submenu, $checklist_key + 1 )
172 );
173 } else {
174 array_unshift( $submenu, $notifications_menu_item );
175 }
176
177 return $submenu;
178 }
179
180 /**
181 * Technically this should render the plugin notifications page.
182 * However, notifications aren't rendering with this method. There should be a redirect, but the existance of this function prevents fatal errors.
183 *
184 * @since 1.8.3
185 */
186 public function render_plugin_notifications_page() {
187 $this->output();
188 }
189
190 /**
191 * Get option value.
192 *
193 * @since 1.8.3
194 *
195 * @param bool $cache Reference property cache if available.
196 *
197 * @return array
198 */
199 public function get_option( $cache = true ) {
200
201 if ( $this->option && $cache ) {
202 return $this->option;
203 }
204
205 $option = (array) get_option( 'charitable_notifications', [] );
206 // $option = [];
207
208 $this->option = [
209 'update' => ! empty( $option['update'] ) ? (int) $option['update'] : 0,
210 'feed' => ! empty( $option['feed'] ) ? (array) $option['feed'] : [],
211 'events' => ! empty( $option['events'] ) ? (array) $option['events'] : [],
212 'dismissed' => ! empty( $option['dismissed'] ) ? (array) $option['dismissed'] : [],
213 ];
214
215 return $this->option;
216 }
217
218 /**
219 * Fetch notifications from feed.
220 *
221 * @since 1.8.3
222 *
223 * @return array
224 */
225 public function fetch_feed() {
226
227 $args = [
228 'timeout' => 10,
229 'sslverify' => false,
230 'user-agent' => charitable_get_default_user_agent(),
231 ];
232
233 $response = wp_remote_get(
234 self::SOURCE_URL,
235 $args
236 );
237
238 if ( is_wp_error( $response ) ) {
239 return [];
240 }
241
242 $body = wp_remote_retrieve_body( $response );
243
244 if ( empty( $body ) ) {
245 return [];
246 }
247
248 return $this->verify( json_decode( $body, true ) );
249 }
250
251 /**
252 * Verify notification data before it is saved.
253 *
254 * @since 1.8.3
255 *
256 * @param array $notifications Array of notifications items to verify.
257 *
258 * @return array
259 */
260 public function verify( $notifications ) {
261
262 $data = [];
263
264 if ( ! is_array( $notifications ) || empty( $notifications ) ) {
265 return $data;
266 }
267
268 foreach ( $notifications as $notification ) {
269
270 // Ignore if one of the conditional checks is true:
271 //
272 // 1. notification message is empty.
273 // 2. license type does not match.
274 // 3. notification is expired.
275 // 4. notification has already been dismissed.
276 // 5. notification existed before installing Charitable.
277 // (Prevents bombarding the user with notifications after activation).
278 if (
279 empty( $notification['content'] ) ||
280 ! $this->is_license_type_match( $notification ) ||
281 $this->is_expired( $notification ) ||
282 $this->is_dismissed( $notification ) ||
283 $this->is_existed( $notification )
284 ) {
285 continue;
286 }
287
288 $data[] = $notification;
289 }
290
291 return $data;
292 }
293
294 /**
295 * Verify saved notification data for active notifications.
296 *
297 * @since 1.8.3
298 *
299 * @param array $notifications Array of notifications items to verify.
300 *
301 * @return array
302 */
303 public function verify_active( $notifications ) {
304
305 if ( ! is_array( $notifications ) || empty( $notifications ) ) {
306 return [];
307 }
308
309 $current_timestamp = time();
310
311 // Remove notifications that are not active.
312 foreach ( $notifications as $key => $notification ) {
313 if (
314 ( ! empty( $notification['start'] ) && $current_timestamp < strtotime( $notification['start'] ) ) ||
315 ( ! empty( $notification['end'] ) && $current_timestamp > strtotime( $notification['end'] ) )
316 ) {
317 unset( $notifications[ $key ] );
318 }
319 }
320
321 return $notifications;
322 }
323
324 /**
325 * Get notification data.
326 *
327 * @since 1.8.3
328 *
329 * @return array
330 */
331 public function get() {
332
333 if ( defined( 'CHARITABLE_DISABLE_NOTIFICATIONS' ) && CHARITABLE_DISABLE_NOTIFICATIONS ) {
334 return [];
335 }
336
337 if ( ! $this->has_access() ) {
338 return [];
339 }
340
341 $option = $this->get_option();
342
343 // Update notifications using async task.
344 if ( empty( $option['update'] ) || time() > $option['update'] + DAY_IN_SECONDS ) {
345 $option = $this->update();
346 } elseif ( empty( $option['feed'] ) && empty( $option['events'] ) ) {
347 // regardless of the update time, if there are no notifications, update attempt.
348 $option = $this->update();
349 }
350
351 $feed = ! empty( $option['feed'] ) ? $this->verify_active( $option['feed'] ) : [];
352 $events = ! empty( $option['events'] ) ? $this->verify_active( $option['events'] ) : [];
353
354 return array_merge( $feed, $events );
355 }
356
357 /**
358 * Get notification count in the feed, regardless if they are dismissed or not.
359 *
360 * @since 1.8.3
361 *
362 * @return int
363 */
364 public function get_count() {
365 return count( $this->get() );
366 }
367
368 /**
369 * Get new notifications by comparing those in the feed vs dismissed.
370 *
371 * @since 1.8.3
372 *
373 * @return int
374 */
375 public function get_new_notifications_count() {
376
377 $option = $this->get_option();
378 $dimissed_ids = ! empty( $option['dismissed'] ) ? $option['dismissed'] : [];
379 $notification_feed = $this->get();
380
381 // if $option is empty or if the feed is empty, there are no new notifications.
382 if ( empty( $option ) || empty( $notification_feed ) ) {
383 return 0;
384 }
385
386 $count = 0;
387
388 foreach ( $notification_feed as $notification ) {
389 // check the id of the notification against the dismissed ids.
390 if ( ! in_array( (string) $notification['id'], $dimissed_ids, true ) ) {
391 ++$count;
392 }
393 }
394
395 return (int) $count;
396 }
397
398 /**
399 * Add a new Event Driven notification.
400 *
401 * @since 1.8.3
402 *
403 * @param array $notification Notification data.
404 */
405 public function add( $notification ) {
406
407 if ( ! $this->is_valid( $notification ) ) {
408 return;
409 }
410
411 $option = $this->get_option();
412
413 // Notification ID already exists.
414 if ( ! empty( $option['events'][ $notification['id'] ] ) ) {
415 return;
416 }
417
418 update_option(
419 'charitable_notifications',
420 [
421 'update' => $option['update'],
422 'feed' => $option['feed'],
423 'events' => array_merge( $notification, $option['events'] ),
424 'dismissed' => $option['dismissed'],
425 ]
426 );
427 }
428
429 /**
430 * Determine if notification data is valid.
431 *
432 * @since 1.8.3
433 *
434 * @param array $notification Notification data.
435 *
436 * @return bool
437 */
438 public function is_valid( $notification ) {
439
440 if ( empty( $notification['id'] ) ) {
441 return false;
442 }
443
444 return ! empty( $this->verify( [ $notification ] ) );
445 }
446
447 /**
448 * Determine if notification has already been dismissed.
449 *
450 * @since 1.8.3
451 *
452 * @param array $notification Notification data.
453 *
454 * @return bool
455 */
456 private function is_dismissed( $notification ) {
457
458 $option = $this->get_option();
459
460 // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
461 return ! empty( $option['dismissed'] ) && in_array( $notification['id'], $option['dismissed'] );
462 }
463
464 /**
465 * Determine if license type is match.
466 *
467 * @since 1.8.3
468 *
469 * @param array $notification Notification data.
470 *
471 * @return bool
472 */
473 private function is_license_type_match( $notification ) {
474
475 // A specific license type is not required.
476 if ( empty( $notification['type'] ) ) {
477 return true;
478 }
479
480 return in_array( $this->get_license_type(), (array) $notification['type'], true );
481 }
482
483 /**
484 * Determine if notification is expired.
485 *
486 * @since 1.8.3
487 *
488 * @param array $notification Notification data.
489 *
490 * @return bool
491 */
492 private function is_expired( $notification ) {
493 // Check if the notification 'end' parameter is set and not empty.
494 if ( empty( $notification['end'] ) ) {
495 return false;
496 }
497
498 // Get the WordPress timezone.
499 $timezone = wp_timezone();
500
501 // Convert the notification end date to a DateTime object in the WordPress timezone.
502 $end_time = new DateTime( $notification['end'], $timezone );
503
504 // Get the current server time as a DateTime object in the WordPress timezone.
505 $current_time = new DateTime( 'now', $timezone );
506
507 // Compare the current time with the notification end time.
508 return $current_time > $end_time;
509 }
510
511 /**
512 * Determine if notification existed before installing Charitable.
513 * If there is an "allow_on_activation" key, it will be used to determine to display even if the notification existed before.
514 *
515 * @since 1.8.3
516 *
517 * @param array $notification Notification data.
518 *
519 * @return bool
520 */
521 private function is_existed( $notification ) {
522
523 $activated = charitable_get_activated_timestamp();
524
525 if ( ! isset( $notification['allow_on_activation'] ) ) {
526 return ! empty( $activated ) &&
527 ! empty( $notification['start'] ) &&
528 $activated > strtotime( $notification['start'] );
529 } else {
530 return ! empty( $activated ) &&
531 ! empty( $notification['start'] ) &&
532 false === $notification['allow_on_activation'];
533 }
534 }
535
536 /**
537 * Update notification data from feed.
538 *
539 * @since 1.8.3
540 */
541 public function update() {
542
543 $option = $this->get_option();
544
545 $data = [
546 'update' => time(),
547 'feed' => $this->fetch_feed(),
548 'events' => $option['events'],
549 'dismissed' => $option['dismissed'],
550 ];
551
552 // phpcs:disable Charitable.PHP.ValidateHooks.InvalidHookName
553 /**
554 * Allow changing notification data before it will be updated in database.
555 *
556 * @since 1.8.3
557 *
558 * @param array $data New notification data.
559 */
560 $data = (array) apply_filters( 'charitable_admin_notifications_update_data', $data );
561 // phpcs:enable Charitable.PHP.ValidateHooks.InvalidHookName
562
563 update_option( 'charitable_notifications', $data );
564
565 return $data;
566 }
567
568 /**
569 * Remove notification data from database before a plugin is deactivated.
570 *
571 * @since 1.8.3
572 *
573 * @param string $plugin Path to the plugin file relative to the plugins directory.
574 * @param bool $network_deactivating Whether the plugin is deactivated for all sites in the network
575 * or just the current site. Multisite only. Default false.
576 */
577 public function delete( $plugin, $network_deactivating ) { // phpcs:ignore
578
579 $charitable_plugins = [
580 'charitable/charitable.php',
581 ];
582
583 if ( ! in_array( $plugin, $charitable_plugins, true ) ) {
584 return;
585 }
586
587 delete_option( 'charitable_notifications' );
588 }
589
590 /**
591 * Output notices.
592 *
593 * @since 1.8.3
594 */
595 public function output_notices() {
596 return $this->output( 'notices' );
597 }
598
599 /**
600 * Output notifications on Form Overview admin area.
601 *
602 * @since 1.8.3
603 *
604 * @param string $location Location where the notifications are output.
605 * @param int $limit_active Limit of active notifications to show.
606 */
607 public function output( $location = false, $limit_active = 4 ) {
608
609 if ( defined( 'CHARITABLE_DISABLE_NOTIFICATIONS' ) && CHARITABLE_DISABLE_NOTIFICATIONS ) {
610 return;
611 }
612
613 global $post;
614
615 if ( 'notices' === $location && ! $this->show_notifications() ) {
616 return;
617 }
618
619 $notifications = $this->get();
620
621 $active_notifications_html = '';
622 $dismissed_notifications_html = '';
623 $active_notifications_count = 0;
624 $dismissed_notifications_count = 0;
625 $current_class = ' current';
626 $content_allowed_tags = [
627 'em' => [],
628 'strong' => [],
629 'span' => [
630 'style' => [],
631 ],
632 'a' => [
633 'href' => [],
634 'target' => [],
635 'rel' => [],
636 ],
637 ];
638 $template_location = $location === 'dashboard' ? '/admin/templates/notifications-dashboard' : '/admin/templates/notifications';
639 $template_location = apply_filters( 'charitable_admin_notifications_template_location', $template_location, $location );
640
641 if ( empty( $notifications ) ) {
642 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
643 echo charitable_render(
644 $template_location,
645 [
646 'notifications' => [
647 'active_count' => $active_notifications_count,
648 'dismissed_count' => $dismissed_notifications_count,
649 'active_html' => $active_notifications_html,
650 'dismissed_html' => $dismissed_notifications_html,
651 ],
652 ],
653 true
654 );
655 return;
656 }
657
658 // Get active notifications.
659 foreach ( $notifications as $notification ) {
660
661 // Prepare required arguments.
662 $notification = wp_parse_args(
663 $notification,
664 [
665 'id' => 0,
666 'title' => '',
667 'content' => '',
668 'video' => '',
669 ]
670 );
671
672 $title = $this->get_component_data( $notification['title'] );
673 $content = $this->get_component_data( $notification['content'] );
674
675 if ( ! $title && ! $content ) {
676 continue;
677 }
678
679 // If the notification ID is not a part of the dismissed notifications, add it to the list.
680 if ( ! $this->is_dismissed( $notification ) ) {
681
682 if ( 'dashboard' === $location ) {
683
684 // Notification HTML, slightyl different for dashboard.
685 $active_notifications_html .= sprintf(
686 '<div aria-expanded="true" class="charitable-notification charitable-notification-%5$s %8$s" data-notification-id="%6$s">
687 <div>
688 %7$s
689 <div class="body">
690 <div class="title dashboard-title">
691 <div>%1$s</div>
692 <div class="date">%3$s</div>
693 </div>
694 <div class="notification-content">%2$s</div>
695 <div class="actions">
696 %4$s
697 <!----><a href="#" class="dismiss">Dismiss</a>
698 </div>
699 </div>
700 </div>
701 </div>',
702 esc_html( $title ),
703 wp_kses( $content, $content_allowed_tags ),
704 ! empty( $notification['start'] ) ? $this->human_readable_date_from_mysql( $notification['start'] ) : '',
705 $this->get_notification_buttons_html( $notification ),
706 esc_attr( $notification['id'] ),
707 esc_attr( $notification['id'] ),
708 $this->get_notification_icon_html( $notification ),
709 esc_attr( $current_class )
710 // $this->get_video_badge_html( $this->get_component_data( $notification['video'] ) )
711 );
712
713 } else {
714
715 // Notification HTML.
716 $active_notifications_html .= sprintf(
717 '<div aria-expanded="true" class="charitable-notification charitable-notification-%5$s %8$s" data-notification-id="%6$s">
718 <div>
719 %7$s
720 <div class="body">
721 <div class="title">
722 <div class="date">%3$s</div>
723 <div>%1$s</div>
724 </div>
725 <div class="notification-content">%2$s</div>
726 <div class="actions">
727 %4$s
728 <!----><a href="#" class="dismiss">Dismiss</a>
729 </div>
730 </div>
731 </div>
732 </div>',
733 esc_html( $title ),
734 wp_kses( $content, $content_allowed_tags ),
735 ! empty( $notification['start'] ) ? $this->human_readable_date_from_mysql( $notification['start'] ) : '',
736 $this->get_notification_buttons_html( $notification ),
737 esc_attr( $notification['id'] ),
738 esc_attr( $notification['id'] ),
739 $this->get_notification_icon_html( $notification ),
740 esc_attr( $current_class )
741 // $this->get_video_badge_html( $this->get_component_data( $notification['video'] ) )
742 );
743
744 }
745
746 ++$active_notifications_count;
747
748 } else {
749
750 // Dismissed notification.
751 $dismissed_notifications_html .= sprintf(
752 '<div aria-expanded="true" class="charitable-notification charitable-notification-%5$s %8$s" data-notification-id="%6$s">
753 <div>
754 %7$s
755 <div class="body">
756 <div class="title">
757 <div class="date">%3$s</div>
758 <div>%1$s</div>
759 </div>
760 <div class="notification-content">%2$s</div>
761 <div class="actions">
762 %4$s
763 </div>
764 </div>
765 </div>
766 </div>',
767 esc_html( $title ),
768 wp_kses( $content, $content_allowed_tags ),
769 ! empty( $notification['start'] ) ? $this->human_readable_date_from_mysql( $notification['start'] ) : '',
770 $this->get_notification_buttons_html( $notification ),
771 esc_attr( $notification['id'] ),
772 esc_attr( $notification['id'] ),
773 $this->get_notification_icon_html( $notification ),
774 esc_attr( $current_class )
775 // $this->get_video_badge_html( $this->get_component_data( $notification['video'] ) )
776 );
777
778 ++$dismissed_notifications_count;
779 }
780
781 // Only first notification is current.
782 $current_class = '';
783
784 if ( 'dashboard' === $location && $active_notifications_count >= $limit_active ) {
785 break;
786 }
787 }
788
789 // For dashboard, we need to show the actual count being displayed (limited), not the total count
790 $display_count = $active_notifications_count;
791 if ( 'dashboard' === $location ) {
792 // Get the total count for comparison
793 $total_count = $this->get_new_notifications_count();
794
795 // If we have more than the limit, show the limit with a "+"
796 if ( $total_count > $limit_active ) {
797 $display_count = $limit_active . '+';
798 }
799 }
800
801 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
802 echo charitable_render(
803 $template_location,
804 [
805 'notifications' => [
806 'active_count' => $display_count,
807 'dismissed_count' => $dismissed_notifications_count,
808 'active_html' => $active_notifications_html,
809 'dismissed_html' => $dismissed_notifications_html,
810 ],
811 ],
812 true
813 );
814 }
815
816 /**
817 * Get notificaiton icon html based on if the type is a success, warning, info.
818 *
819 * @since 1.8.3
820 *
821 * @param array $notification Notification data.
822 *
823 * @return string
824 */
825 private function get_notification_icon_html( $notification ) {
826
827 $icon = '';
828
829 $notification_type = ! empty( $notification['notification_type'] ) ? sanitize_text_field( $notification['notification_type'] ) : '';
830
831 switch ( $notification_type ) {
832 case 'warning':
833 $html = '<div class="icon">
834 <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-circle-exclamation warning">
835 <path fill-rule="evenodd" clip-rule="evenodd" d="M11.99 2.00012C6.47 2.00012 2 6.48012 2 12.0001C2 17.5201 6.47 22.0001 11.99 22.0001C17.52 22.0001 22 17.5201 22 12.0001C22 6.48012 17.52 2.00012 11.99 2.00012ZM13 13.0001V7.00012H11V13.0001H13ZM13 17.0001V15.0001H11V17.0001H13ZM4 12.0001C4 16.4201 7.58 20.0001 12 20.0001C16.42 20.0001 20 16.4201 20 12.0001C20 7.58012 16.42 4.00012 12 4.00012C7.58 4.00012 4 7.58012 4 12.0001Z" fill="currentColor"></path>
836 </svg>
837 </div>';
838 break;
839 case 'success':
840 $html = '<div class="icon"><svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-circle-check success""><path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path></svg></div>';
841 break;
842 default:
843 $html = '<div class="icon"><svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-circle-check success""><path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path></svg></div>';
844 break;
845 }
846
847 return $html;
848 }
849
850 /**
851 * Get human readable date from MySQL timestamp.
852 *
853 * @since 1.8.3
854 *
855 * @param string $mysql_timestamp MySQL timestamp.
856 *
857 * @return bool
858 */
859 private function human_readable_date_from_mysql( $mysql_timestamp = false ) {
860
861 // Convert the MySQL timestamp to a Unix timestamp.
862 $timestamp = strtotime( $mysql_timestamp );
863
864 // Get the current time as a Unix timestamp.
865 $current_time = current_time( 'timestamp' ); // phpcs:ignore
866
867 // Calculate the difference in seconds.
868 $time_diff = $current_time - $timestamp;
869
870 // 30 days in seconds
871 $thirty_days = 30 * 24 * 60 * 60;
872
873 if ( $time_diff < $thirty_days ) {
874 // If the timestamp is less than 30 days ago, return a 'time ago' format.
875 return human_time_diff( $timestamp, $current_time ) . ' ago';
876 } else {
877 // If the timestamp is 30 days or more in the past, use 'F j, Y' format.
878 return date( 'F j, Y', $timestamp ); // phpcs:ignore
879 }
880 }
881
882 /**
883 * Retrieve notification's buttons HTML.
884 *
885 * @since 1.8.3
886 *
887 * @param array $notification Notification data.
888 *
889 * @return string
890 */
891 private function get_notification_buttons_html( $notification ) {
892
893 $html = '';
894
895 if ( empty( $notification['btns'] ) || ! is_array( $notification['btns'] ) ) {
896 return $html;
897 }
898
899 foreach ( $notification['btns'] as $btn_type => $btn ) {
900
901 $btn = $this->get_component_data( $btn );
902
903 if ( ! $btn ) {
904 continue;
905 }
906
907 $url = $this->prepare_btn_url( $btn );
908 $target = ! empty( $btn['target'] ) ? $btn['target'] : '_blank';
909 $target = ! empty( $url ) && strpos( $url, home_url() ) === 0 ? '_self' : $target;
910
911 $html .= sprintf(
912 '<a href="%1$s" class="button button-%2$s"%3$s>%4$s</a>',
913 esc_url( $url ),
914 $btn_type === 'main' ? 'primary' : 'secondary',
915 $target === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '',
916 ! empty( $btn['text'] ) ? esc_html( $btn['text'] ) : ''
917 );
918 }
919
920 return ! empty( $html ) ? sprintf( '<div class="charitable-notifications-buttons">%s</div>', $html ) : '';
921 }
922
923 /**
924 * Retrieve notification's component data by a license type.
925 *
926 * @since 1.8.3
927 *
928 * @param mixed $data Component data.
929 *
930 * @return false|mixed
931 */
932 private function get_component_data( $data ) {
933
934 if ( empty( $data['license'] ) ) {
935 return $data;
936 }
937
938 $license_type = $this->get_license_type();
939
940 if ( in_array( $license_type, self::LICENSES_ELITE, true ) ) {
941 $license_type = 'elite';
942 }
943
944 return ! empty( $data['license'][ $license_type ] ) ? $data['license'][ $license_type ] : false;
945 }
946
947 /**
948 * Retrieve the current installation license type (always lowercase).
949 *
950 * @since 1.8.3
951 *
952 * @return string
953 */
954 private function get_license_type() {
955
956 if ( $this->license_type ) {
957 return $this->license_type;
958 }
959
960 if ( charitable_is_pro() ) {
961 return 'pro';
962 } else {
963 return 'lite';
964 }
965 }
966
967 /**
968 * Dismiss notification via AJAX.
969 *
970 * @since 1.8.3
971 */
972 public function dismiss() {
973
974 // Check for required param, security and access.
975 if (
976 empty( $_POST['notification_id'] ) ||
977 ! check_ajax_referer( 'charitable-admin', 'nonce', false ) ||
978 ! $this->has_access()
979 ) {
980 wp_send_json_error();
981 }
982
983 $id = sanitize_key( $_POST['notification_id'] );
984 $type = is_numeric( $id ) ? 'feed' : 'events';
985 $option = $this->get_option();
986
987 $option['dismissed'][] = $id;
988 $option['dismissed'] = array_unique( $option['dismissed'] );
989
990 update_option( 'charitable_notifications', $option );
991
992 wp_send_json_success();
993 }
994
995 /**
996 * Dismiss multiple notifications via AJAX.
997 *
998 * @since 1.8.3
999 */
1000 public function dismiss_multiple() {
1001
1002 // Check for required param, security and access.
1003 if (
1004 empty( $_POST['notification_ids'] ) ||
1005 ! check_ajax_referer( 'charitable-admin', 'nonce', false ) ||
1006 ! $this->has_access()
1007 ) {
1008 wp_send_json_error();
1009 }
1010
1011 if ( ! is_array( $_POST['notification_ids'] ) ) {
1012 wp_send_json_error();
1013 }
1014
1015 $option = $this->get_option();
1016 $notification_ids = array_map( 'sanitize_key', $_POST['notification_ids'] );
1017 foreach ( $notification_ids as $id ) {
1018 $type = is_numeric( $id ) ? 'feed' : 'events';
1019 $option['dismissed'][] = $id;
1020 $option['dismissed'] = array_unique( $option['dismissed'] );
1021
1022 }
1023
1024 update_option( 'charitable_notifications', $option );
1025
1026 wp_send_json_success();
1027 }
1028
1029 /**
1030 * Prepare button URL.
1031 *
1032 * @since 1.8.3
1033 *
1034 * @param array $btn Button data.
1035 *
1036 * @return string
1037 */
1038 private function prepare_btn_url( $btn ) {
1039
1040 if ( empty( $btn['url'] ) ) {
1041 return '';
1042 }
1043
1044 $replace_tags = [
1045 '{admin_url}' => admin_url(),
1046 ];
1047
1048 return str_replace( array_keys( $replace_tags ), array_values( $replace_tags ), $btn['url'] );
1049 }
1050
1051 /**
1052 * Get the notification's video badge HTML.
1053 *
1054 * @since 1.8.3
1055 *
1056 * @param string $video_url Valid video URL.
1057 *
1058 * @return string
1059 */
1060 private function get_video_badge_html( $video_url ) {
1061
1062 $video_url = wp_http_validate_url( $video_url );
1063
1064 if ( empty( $video_url ) ) {
1065 return '';
1066 }
1067
1068 $data_attr_lity = wp_is_mobile() ? '' : 'data-lity';
1069
1070 return sprintf(
1071 '<a class="charitable-notifications-badge" href="%1$s" %2$s>%3$s</a>',
1072 esc_url( $video_url ),
1073 esc_attr( $data_attr_lity ),
1074 esc_html__( 'Watch Video', 'charitable' )
1075 );
1076 }
1077
1078 /**
1079 * Determine to show notifications on this page or not.
1080 *
1081 * @since 1.7.0.3
1082 *
1083 * @return boolean
1084 */
1085 public function show_notifications() {
1086 if ( isset( $_GET['taxonomy'] ) ) { // phpcs:ignore
1087 return false;
1088 }
1089 if ( isset( $_GET['post_type'] ) && sanitize_text_field( $_GET['post_type'] ) === 'campaign' ) { // phpcs:ignore
1090 return true;
1091 }
1092 if ( isset( $_GET['post_type'] ) && sanitize_text_field( $_GET['post_type'] ) === 'donation' ) { // phpcs:ignore
1093 return true;
1094 }
1095 return false;
1096 }
1097
1098 /**
1099 * This will incercept the request and redirect to the dashboard page.
1100 *
1101 * @since 1.8.3
1102 * @version 1.8.3.1
1103 */
1104 public function maybe_redirect_from_notifications() {
1105
1106 if ( ! empty( $_GET['page'] ) && 'charitable-notifications' === $_GET['page'] ) { // phpcs:ignore
1107 delete_transient( 'charitable_autoshow_plugin_notifications' );
1108 wp_safe_redirect( admin_url( 'admin.php?page=charitable-dashboard' ) );
1109 exit;
1110 }
1111 }
1112 }
1113