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/admin/notifications/class-charitable-notifications.php +708 -179 1.7.0.7 → 1.8.13 View file →
@@ -1,14 +1,14 @@
1 1 <?php
2 2 /**
3 - * Admin form model class.
3 + * Charitable Admin Plugin Notifications.
4 4 *
5 5 * @package Charitable/Classes/Charitable_Admin_Form
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 - * @since 1.5.0
10 - * @version 1.5.0
9 + * @since 1.8.3
10 + * @version 1.8.3
11 11 */
12 12
13 13 // Exit if accessed directly.
14 14 if ( ! defined( 'ABSPATH' ) ) {
@@ -17,9 +17,10 @@
17 17
18 18 /**
19 19 * Notifications.
20 20 *
21 - * @since 1.7.5
21 + * @since 1.8.3
22 + * @version 1.8.3
22 23 */
23 24 class Charitable_Notifications {
24 25
25 26 /**
@@ -31,27 +32,27 @@
31 32
32 33 /**
33 34 * Source of notifications content.
34 35 *
35 - * @since 1.7.5
36 + * @since 1.8.3
36 37 *
37 38 * @var string
38 39 */
39 - const SOURCE_URL = 'https://plugin.wpcharitable.com/wp-content/notifications.json';
40 + const SOURCE_URL = 'https://plugin.wpcharitable.com/wp-content/charitable-notifications.json';
40 41
41 42 /**
42 43 * Array of license types, that are considered being Elite level.
43 44 *
44 - * @since 1.7.5
45 + * @since 1.8.3
45 46 *
46 47 * @var array
47 48 */
48 - const LICENSES_ELITE = [ 'agency', 'pro' ];
49 + const LICENSES_ELITE = [ 'agency', 'pro', 'elite' ];
49 50
50 51 /**
51 52 * Option value.
52 53 *
53 - * @since 1.7.5
54 + * @since 1.8.3
54 55 *
55 56 * @var bool|array
56 57 */
57 58 public $option = false;
@@ -58,9 +59,9 @@
58 59
59 60 /**
60 61 * Current license type.
61 62 *
62 - * @since 1.7.5
63 + * @since 1.8.3
63 64 *
64 65 * @var string
65 66 */
66 67 private $license_type;
@@ -65,8 +66,17 @@
65 66 */
66 67 private $license_type;
67 68
68 69 /**
70 + * Cached result of has_alert_notifications() for the current request.
71 + *
72 + * @since 1.8.12
73 + *
74 + * @var bool|null
75 + */
76 + private $has_alerts_cache = null;
77 +
78 + /**
69 79 * Returns and/or create the single instance of this class.
70 80 *
71 81 * @since 1.2.0
72 82 *
@@ -79,18 +89,24 @@
79 89
80 90 return self::$instance;
81 91 }
82 92
93 + /**
94 + * Constructor.
95 + *
96 + * @since 1.8.3
97 + */
83 98 public function __construct() {
84 99
85 - $this->init();
86 -
100 + if ( ! defined( 'CHARITABLE_DISABLE_NOTIFICATIONS' ) || ! CHARITABLE_DISABLE_NOTIFICATIONS ) {
101 + $this->init();
102 + }
87 103 }
88 104
89 105 /**
90 106 * Initialize class.
91 107 *
92 - * @since 1.7.5
108 + * @since 1.8.3
93 109 */
94 110 public function init() {
95 111
96 112 $this->hooks();
@@ -98,29 +114,21 @@
98 114
99 115 /**
100 116 * Register hooks.
101 117 *
102 - * @since 1.7.5
118 + * @since 1.8.3
103 119 */
104 120 public function hooks() {
105 -
106 - add_action( 'after_charitable_admin_enqueue_scripts', [ $this, 'enqueues' ], 10, 3 );
107 -
108 - // add_action( 'charitable_admin_overview_before_table', [ $this, 'output' ] ); // where the notifications comes out in the admin
109 - add_action( 'admin_notices', [ $this, 'output_notices' ], 99 ); // where the notifications comes out in the admin
110 - add_action( 'charitable_maybe_show_notification', [ $this, 'output' ], 99 ); // where the notifications comes out in the admin
111 -
121 + add_action( 'charitable_maybe_show_plugin_notifications', [ $this, 'output' ], 99 ); // where the notifications comes out in the admin.
112 122 add_action( 'charitable_admin_notifications_update', [ $this, 'update' ] );
113 -
114 123 add_action( 'deactivate_plugin', [ $this, 'delete' ], 10, 2 );
115 -
116 - add_action( 'wp_ajax_charitable_notification_dismiss', [ $this, 'dismiss' ] );
124 + add_filter( 'charitable_notifications_get', [ $this, 'priority_sort' ], 99 );
117 125 }
118 126
119 127 /**
120 128 * Check if user has access and is enabled.
121 129 *
122 - * @since 1.7.5
130 + * @since 1.8.3
123 131 *
124 132 * @return bool
125 133 */
126 134 public function has_access() {
@@ -137,11 +145,64 @@
137 145 return (bool) apply_filters( 'charitable_admin_notifications_has_access', $access );
138 146 }
139 147
140 148 /**
149 + * Add notifications to the WordPress admin menu for Charitable.
150 + *
151 + * @since 1.8.3
152 + *
153 + * @param array $submenu Array of menu items.
154 + * @return bool
155 + */
156 + public function add_notifications_to_menu( $submenu = array() ) {
157 +
158 + if ( ( defined( 'CHARITABLE_NOTIFICATIONS_FORCE_OFF' ) && CHARITABLE_NOTIFICATIONS_FORCE_OFF ) ) {
159 + return $submenu;
160 + }
161 +
162 + $notifications_menu_item = array(
163 + 'page_title' => __( 'Notifications', 'charitable' ),
164 + 'menu_title' => __( 'Notifications', 'charitable' ),
165 + 'menu_slug' => 'charitable-notifications',
166 + 'function' => array( $this, 'render_plugin_notifications_page' ),
167 + 'capability' => 'manage_charitable_settings',
168 + );
169 +
170 + // if there are new notifications add the indicator to the menu title.
171 + if ( $this->get_new_notifications_count() ) {
172 + $dot_type = $this->has_alert_notifications() ? 'alert' : 'info';
173 + $notifications_menu_item['menu_title'] .= '<div class="charitable-menu-notification-indicator ' . $dot_type . '"></div>';
174 + }
175 +
176 + // If the checklist exists in the submenu, add this item after it.
177 + $checklist_key = array_search( 'charitable-setup-checklist', array_column( $submenu, 'menu_slug' ), true );
178 + if ( false !== $checklist_key ) {
179 + $submenu = array_merge(
180 + array_slice( $submenu, 0, $checklist_key + 1 ),
181 + array( $notifications_menu_item ),
182 + array_slice( $submenu, $checklist_key + 1 )
183 + );
184 + } else {
185 + array_unshift( $submenu, $notifications_menu_item );
186 + }
187 +
188 + return $submenu;
189 + }
190 +
191 + /**
192 + * Technically this should render the plugin notifications page.
193 + * However, notifications aren't rendering with this method. There should be a redirect, but the existance of this function prevents fatal errors.
194 + *
195 + * @since 1.8.3
196 + */
197 + public function render_plugin_notifications_page() {
198 + $this->output();
199 + }
200 +
201 + /**
141 202 * Get option value.
142 203 *
143 - * @since 1.7.5
204 + * @since 1.8.3
144 205 *
145 206 * @param bool $cache Reference property cache if available.
146 207 *
147 208 * @return array
@@ -154,12 +215,16 @@
154 215
155 216 $option = (array) get_option( 'charitable_notifications', [] );
156 217
157 218 $this->option = [
158 - 'update' => ! empty( $option['update'] ) ? (int) $option['update'] : 0,
159 - 'feed' => ! empty( $option['feed'] ) ? (array) $option['feed'] : [],
160 - 'events' => ! empty( $option['events'] ) ? (array) $option['events'] : [],
161 - 'dismissed' => ! empty( $option['dismissed'] ) ? (array) $option['dismissed'] : [],
219 + 'update' => ! empty( $option['update'] ) ? (int) $option['update'] : 0,
220 + 'feed' => ! empty( $option['feed'] ) ? (array) $option['feed'] : [],
221 + 'events' => ! empty( $option['events'] ) ? (array) $option['events'] : [],
222 + 'dismissed' => ! empty( $option['dismissed'] ) ? (array) $option['dismissed'] : [],
223 + 'local' => ! empty( $option['local'] ) ? (array) $option['local'] : [],
224 + 'new_feed' => ! empty( $option['new_feed'] ) ? (array) $option['new_feed'] : [],
225 + 'dismissed_times' => ! empty( $option['dismissed_times'] ) ? (array) $option['dismissed_times'] : [],
226 + 'settings' => ! empty( $option['settings'] ) ? (array) $option['settings'] : [],
162 227 ];
163 228
164 229 return $this->option;
165 230 }
@@ -166,20 +231,23 @@
166 231
167 232 /**
168 233 * Fetch notifications from feed.
169 234 *
170 - * @since 1.7.5
235 + * @since 1.8.3
171 236 *
172 237 * @return array
173 238 */
174 239 public function fetch_feed() {
175 240
241 + $args = [
242 + 'timeout' => 10,
243 + 'sslverify' => false,
244 + 'user-agent' => charitable_get_default_user_agent(),
245 + ];
246 +
176 247 $response = wp_remote_get(
177 248 self::SOURCE_URL,
178 - [
179 - 'timeout' => 10,
180 - 'user-agent' => charitable_get_default_user_agent(),
181 - ]
249 + $args
182 250 );
183 251
184 252 if ( is_wp_error( $response ) ) {
185 253 return [];
@@ -196,9 +264,9 @@
196 264
197 265 /**
198 266 * Verify notification data before it is saved.
199 267 *
200 - * @since 1.7.5
268 + * @since 1.8.3
201 269 *
202 270 * @param array $notifications Array of notifications items to verify.
203 271 *
204 272 * @return array
@@ -218,9 +286,9 @@
218 286 // 1. notification message is empty.
219 287 // 2. license type does not match.
220 288 // 3. notification is expired.
221 289 // 4. notification has already been dismissed.
222 - // 5. notification existed before installing WPCharitable.
290 + // 5. notification existed before installing Charitable.
223 291 // (Prevents bombarding the user with notifications after activation).
224 292 if (
225 293 empty( $notification['content'] ) ||
226 294 ! $this->is_license_type_match( $notification ) ||
@@ -239,9 +307,9 @@
239 307
240 308 /**
241 309 * Verify saved notification data for active notifications.
242 310 *
243 - * @since 1.7.5
311 + * @since 1.8.3
244 312 *
245 313 * @param array $notifications Array of notifications items to verify.
246 314 *
247 315 * @return array
@@ -269,14 +337,19 @@
269 337
270 338 /**
271 339 * Get notification data.
272 340 *
273 - * @since 1.7.5
341 + * @since 1.8.3
342 + * @version 1.8.12
274 343 *
275 344 * @return array
276 345 */
277 346 public function get() {
278 347
348 + if ( defined( 'CHARITABLE_DISABLE_NOTIFICATIONS' ) && CHARITABLE_DISABLE_NOTIFICATIONS ) {
349 + return [];
350 + }
351 +
279 352 if ( ! $this->has_access() ) {
280 353 return [];
281 354 }
282 355
@@ -283,44 +356,169 @@
283 356 $option = $this->get_option();
284 357
285 358 // Update notifications using async task.
286 359 if ( empty( $option['update'] ) || time() > $option['update'] + DAY_IN_SECONDS ) {
360 + $option = $this->update();
361 + } elseif ( empty( $option['feed'] ) && empty( $option['events'] ) ) {
362 + // regardless of the update time, if there are no notifications, update attempt.
363 + $option = $this->update();
364 + }
287 365
288 - $this->update();
366 + $legacy_enabled = ! class_exists( 'Charitable_Notification_Settings' ) ||
367 + Charitable_Notification_Settings::get_instance()->is_category_enabled( 'legacy_feed' );
289 368
290 - // $tasks = charitable()->get( 'tasks' );
369 + $feed = ( $legacy_enabled && ! empty( $option['feed'] ) ) ? $this->verify_active( $option['feed'] ) : [];
370 + $events = ( $legacy_enabled && ! empty( $option['events'] ) ) ? $this->verify_active( $option['events'] ) : [];
291 371
292 - // if ( ! $tasks->is_scheduled( 'charitable_admin_notifications_update' ) !== false ) {
293 - // $tasks
294 - // ->create( 'charitable_admin_notifications_update' )
295 - // ->async()
296 - // ->params()
297 - // ->register();
298 - // }
372 + $notifications = array_merge( $feed, $events );
373 +
374 + /**
375 + * Filter the final notifications array before output.
376 + *
377 + * Used by addons (e.g. Pro's Charitable_Dynamic_Notifications) to prepend license-state
378 + * notifications (expiring/expired) above the remote feed.
379 + *
380 + * @since 1.8.12
381 + *
382 + * @param array $notifications Merged feed + events notifications.
383 + */
384 + return (array) apply_filters( 'charitable_notifications_get', $notifications );
385 + }
386 +
387 + /**
388 + * Final sort — enforces spec priority order regardless of filter injection order.
389 + *
390 + * Order: new_feed (non-dismissible first) → license → health → legal →
391 + * lifecycle → milestone → nudge → everything else (legacy feed).
392 + *
393 + * @since 1.8.12
394 + *
395 + * @param array $notifications
396 + * @return array
397 + */
398 + public function priority_sort( array $notifications ) {
399 + $order = [
400 + 'new_feed_pinned' => [],
401 + 'new_feed' => [],
402 + 'license' => [],
403 + 'health' => [],
404 + 'legal' => [],
405 + 'lifecycle' => [],
406 + 'milestone' => [],
407 + 'nudge' => [],
408 + 'other' => [],
409 + ];
410 +
411 + $license_ids = [ 'license_expired', 'license_expiring' ];
412 +
413 + foreach ( $notifications as $n ) {
414 + $id = isset( $n['id'] ) ? (string) $n['id'] : '';
415 + $category = isset( $n['category'] ) ? $n['category'] : '';
416 +
417 + if ( 'new_feed' === $category ) {
418 + $dismissible = ! isset( $n['dismissible'] ) || (bool) $n['dismissible'];
419 + $order[ $dismissible ? 'new_feed' : 'new_feed_pinned' ][] = $n;
420 + } elseif ( in_array( $id, $license_ids, true ) ) {
421 + $order['license'][] = $n;
422 + } elseif ( isset( $order[ $category ] ) ) {
423 + $order[ $category ][] = $n;
424 + } else {
425 + $order['other'][] = $n;
426 + }
299 427 }
300 428
301 - $feed = ! empty( $option['feed'] ) ? $this->verify_active( $option['feed'] ) : [];
302 - $events = ! empty( $option['events'] ) ? $this->verify_active( $option['events'] ) : [];
429 + $result = [];
430 + foreach ( $order as $group ) {
431 + foreach ( $group as $item ) {
432 + $result[] = $item;
433 + }
434 + }
435 + return $result;
436 + }
303 437
304 - return array_merge( $feed, $events );
438 + /**
439 + * Get notification count in the feed, regardless if they are dismissed or not.
440 + *
441 + * @since 1.8.3
442 + *
443 + * @return int
444 + */
445 + public function get_count() {
446 + return count( $this->get() );
305 447 }
306 448
307 449 /**
308 - * Get notification count.
450 + * Get new notifications by comparing those in the feed vs dismissed.
309 451 *
310 - * @since 1.7.5
452 + * @since 1.8.3
311 453 *
312 454 * @return int
313 455 */
314 - public function get_count() {
456 + public function get_new_notifications_count() {
315 457
316 - return count( $this->get() );
458 + $option = $this->get_option();
459 + $dimissed_ids = ! empty( $option['dismissed'] ) ? $option['dismissed'] : [];
460 + $notification_feed = $this->get();
461 +
462 + // if $option is empty or if the feed is empty, there are no new notifications.
463 + if ( empty( $option ) || empty( $notification_feed ) ) {
464 + return 0;
465 + }
466 +
467 + $count = 0;
468 +
469 + foreach ( $notification_feed as $notification ) {
470 + // check the id of the notification against the dismissed ids.
471 + if ( ! in_array( (string) $notification['id'], $dimissed_ids, true ) ) {
472 + ++$count;
473 + }
474 + }
475 +
476 + return (int) $count;
317 477 }
318 478
319 479 /**
480 + * Check if any undismissed notifications are alerts (health or legal category).
481 + *
482 + * @since 1.8.12
483 + *
484 + * @return bool
485 + */
486 + public function has_alert_notifications() {
487 +
488 + if ( null !== $this->has_alerts_cache ) {
489 + return $this->has_alerts_cache;
490 + }
491 +
492 + $option = $this->get_option();
493 + $dismissed_ids = ! empty( $option['dismissed'] ) ? $option['dismissed'] : [];
494 + $notification_feed = $this->get();
495 +
496 + if ( empty( $notification_feed ) ) {
497 + $this->has_alerts_cache = false;
498 + return $this->has_alerts_cache;
499 + }
500 +
501 + $alert_categories = [ 'health', 'legal' ];
502 +
503 + foreach ( $notification_feed as $notification ) {
504 + if ( in_array( (string) $notification['id'], $dismissed_ids, true ) ) {
505 + continue;
506 + }
507 + if ( ! empty( $notification['category'] ) && in_array( $notification['category'], $alert_categories, true ) ) {
508 + $this->has_alerts_cache = true;
509 + return $this->has_alerts_cache;
510 + }
511 + }
512 +
513 + $this->has_alerts_cache = false;
514 + return $this->has_alerts_cache;
515 + }
516 +
517 + /**
320 518 * Add a new Event Driven notification.
321 519 *
322 - * @since 1.7.5
520 + * @since 1.8.3
323 521 *
324 522 * @param array $notification Notification data.
325 523 */
326 524 public function add( $notification ) {
@@ -349,9 +547,9 @@
349 547
350 548 /**
351 549 * Determine if notification data is valid.
352 550 *
353 - * @since 1.7.5
551 + * @since 1.8.3
354 552 *
355 553 * @param array $notification Notification data.
356 554 *
357 555 * @return bool
@@ -367,9 +565,9 @@
367 565
368 566 /**
369 567 * Determine if notification has already been dismissed.
370 568 *
371 - * @since 1.7.5
569 + * @since 1.8.3
372 570 *
373 571 * @param array $notification Notification data.
374 572 *
375 573 * @return bool
@@ -384,9 +582,9 @@
384 582
385 583 /**
386 584 * Determine if license type is match.
387 585 *
388 - * @since 1.7.5
586 + * @since 1.8.3
389 587 *
390 588 * @param array $notification Notification data.
391 589 *
392 590 * @return bool
@@ -403,9 +601,9 @@
403 601
404 602 /**
405 603 * Determine if notification is expired.
406 604 *
407 - * @since 1.7.5
605 + * @since 1.8.3
408 606 *
409 607 * @param array $notification Notification data.
410 608 *
411 609 * @return bool
@@ -410,16 +608,31 @@
410 608 *
411 609 * @return bool
412 610 */
413 611 private function is_expired( $notification ) {
612 + // Check if the notification 'end' parameter is set and not empty.
613 + if ( empty( $notification['end'] ) ) {
614 + return false;
615 + }
414 616
415 - return ! empty( $notification['end'] ) && time() > strtotime( $notification['end'] );
617 + // Get the WordPress timezone.
618 + $timezone = wp_timezone();
619 +
620 + // Convert the notification end date to a DateTime object in the WordPress timezone.
621 + $end_time = new DateTime( $notification['end'], $timezone );
622 +
623 + // Get the current server time as a DateTime object in the WordPress timezone.
624 + $current_time = new DateTime( 'now', $timezone );
625 +
626 + // Compare the current time with the notification end time.
627 + return $current_time > $end_time;
416 628 }
417 629
418 630 /**
419 - * Determine if notification existed before installing WPForms.
631 + * Determine if notification existed before installing Charitable.
632 + * If there is an "allow_on_activation" key, it will be used to determine to display even if the notification existed before.
420 633 *
421 - * @since 1.7.5
634 + * @since 1.8.3
422 635 *
423 636 * @param array $notification Notification data.
424 637 *
425 638 * @return bool
@@ -427,55 +640,83 @@
427 640 private function is_existed( $notification ) {
428 641
429 642 $activated = charitable_get_activated_timestamp();
430 643
431 - return ! empty( $activated ) &&
644 + if ( ! isset( $notification['allow_on_activation'] ) ) {
645 + return ! empty( $activated ) &&
432 646 ! empty( $notification['start'] ) &&
433 647 $activated > strtotime( $notification['start'] );
648 + } else {
649 + return ! empty( $activated ) &&
650 + ! empty( $notification['start'] ) &&
651 + false === $notification['allow_on_activation'];
652 + }
434 653 }
435 654
436 655 /**
437 656 * Update notification data from feed.
438 657 *
439 - * @since 1.7.5
658 + * @since 1.8.3
440 659 */
441 660 public function update() {
442 661
443 662 $option = $this->get_option();
444 - $data = [
445 - 'update' => time(),
446 - 'feed' => $this->fetch_feed(),
447 - 'events' => $option['events'],
448 - 'dismissed' => $option['dismissed'],
663 +
664 + $data = [
665 + 'update' => time(),
666 + 'feed' => $this->fetch_feed(),
667 + 'events' => $option['events'],
668 + 'dismissed' => $option['dismissed'],
669 + 'local' => $option['local'],
670 + 'new_feed' => $option['new_feed'],
671 + 'dismissed_times' => $option['dismissed_times'],
672 + 'settings' => $option['settings'],
449 673 ];
450 674
451 - // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
675 + // phpcs:disable Charitable.PHP.ValidateHooks.InvalidHookName
452 676 /**
453 677 * Allow changing notification data before it will be updated in database.
454 678 *
455 - * @since 1.7.5
679 + * @since 1.8.3
456 680 *
457 681 * @param array $data New notification data.
458 682 */
459 683 $data = (array) apply_filters( 'charitable_admin_notifications_update_data', $data );
460 - // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
684 + // phpcs:enable Charitable.PHP.ValidateHooks.InvalidHookName
461 685
462 686 update_option( 'charitable_notifications', $data );
687 +
688 + return $data;
463 689 }
464 690
465 691 /**
692 + * Save specific keys back to the option without triggering a full update.
693 + *
694 + * @since 1.8.12
695 + *
696 + * @param array $partial Associative array of keys to merge into the stored option.
697 + * Intended keys: 'local', 'new_feed', 'dismissed_times', 'settings'.
698 + * @return void
699 + */
700 + public function save_option_partial( array $partial ) {
701 + $option = $this->get_option( false ); // bypass cache
702 + $merged = array_merge( $option, $partial );
703 + update_option( 'charitable_notifications', $merged );
704 + $this->option = false; // invalidate cache
705 + }
706 +
707 + /**
466 708 * Remove notification data from database before a plugin is deactivated.
467 709 *
468 - * @since 1.7.5
710 + * @since 1.8.3
469 711 *
470 712 * @param string $plugin Path to the plugin file relative to the plugins directory.
471 713 * @param bool $network_deactivating Whether the plugin is deactivated for all sites in the network
472 714 * or just the current site. Multisite only. Default false.
473 715 */
474 - public function delete( $plugin, $network_deactivating ) {
716 + public function delete( $plugin, $network_deactivating ) { // phpcs:ignore
475 717
476 718 $charitable_plugins = [
477 - 'charitable-lite/charitable.php',
478 719 'charitable/charitable.php',
479 720 ];
480 721
481 722 if ( ! in_array( $plugin, $charitable_plugins, true ) ) {
@@ -485,57 +726,12 @@
485 726 delete_option( 'charitable_notifications' );
486 727 }
487 728
488 729 /**
489 - * Enqueue assets on Form Overview admin page.
730 + * Output notices.
490 731 *
491 - * @since 1.7.5
732 + * @since 1.8.3
492 733 */
493 - public function enqueues( $suffix, $version, $assets_dir ) {
494 -
495 - if ( ! $this->get_count() ) {
496 - return;
497 - }
498 -
499 - $suffix = false;
500 -
501 - wp_register_style(
502 - 'charitable-admin-notifications',
503 - $assets_dir . 'css/charitable-admin-notifications' . $suffix . '.css',
504 - array( 'charitable-lity' ),
505 - $version
506 - );
507 -
508 - wp_enqueue_style( 'charitable-admin-notifications' );
509 -
510 - wp_register_script(
511 - 'charitable-admin-notifications',
512 - $assets_dir . 'js/charitable-admin-notifications' . $suffix . '.js',
513 - array( 'jquery', 'charitable-lity' ),
514 - $version,
515 - true
516 - );
517 -
518 - wp_enqueue_script( 'charitable-admin-notifications' );
519 -
520 - // Lity.
521 - wp_enqueue_style(
522 - 'charitable-lity',
523 - $assets_dir . 'lib/lity/lity.min.css',
524 - [],
525 - $version
526 - );
527 -
528 - wp_enqueue_script(
529 - 'charitable-lity',
530 - $assets_dir . 'lib/lity/lity.min.js',
531 - [ 'jquery' ],
532 - $version,
533 - true
534 - );
535 -
536 - }
537 -
538 734 public function output_notices() {
539 735 return $this->output( 'notices' );
540 736 }
541 737
@@ -541,31 +737,39 @@
541 737
542 738 /**
543 739 * Output notifications on Form Overview admin area.
544 740 *
545 - * @since 1.7.5
741 + * @since 1.8.3
742 + * @version 1.8.12
743 + *
744 + * @param string $location Location where the notifications are output.
745 + * @param int $limit_active Limit of active notifications to show.
546 746 */
547 - public function output( $location = false ) {
747 + public function output( $location = false, $limit_active = 4 ) {
548 748
749 + if ( defined( 'CHARITABLE_DISABLE_NOTIFICATIONS' ) && CHARITABLE_DISABLE_NOTIFICATIONS ) {
750 + return;
751 + }
752 +
549 753 global $post;
550 754
551 - if ('notices' === $location && ! $this->show_notifications() ) {
755 + if ( 'notices' === $location && ! $this->show_notifications() ) {
552 756 return;
553 757 }
554 758
555 759 $notifications = $this->get();
556 760
557 - if ( empty( $notifications ) ) {
558 - return;
559 - }
560 -
561 - $notifications_html = '';
562 - $current_class = ' current';
563 - $content_allowed_tags = [
761 + $active_notifications_html = '';
762 + $dismissed_notifications_html = '';
763 + $active_notifications_count = 0;
764 + $dismissed_notifications_count = 0;
765 + $current_class = ' current';
766 + $content_allowed_tags = [
564 767 'em' => [],
565 768 'strong' => [],
566 769 'span' => [
567 770 'style' => [],
771 + 'class' => [],
568 772 ],
569 773 'a' => [
570 774 'href' => [],
571 775 'target' => [],
@@ -571,9 +775,29 @@
571 775 'target' => [],
572 776 'rel' => [],
573 777 ],
574 778 ];
779 + $template_location = $location === 'dashboard' ? '/admin/templates/notifications-dashboard' : '/admin/templates/notifications';
780 + $template_location = apply_filters( 'charitable_admin_notifications_template_location', $template_location, $location );
575 781
782 + if ( empty( $notifications ) ) {
783 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
784 + echo charitable_render(
785 + $template_location,
786 + [
787 + 'notifications' => [
788 + 'active_count' => $active_notifications_count,
789 + 'dismissed_count' => $dismissed_notifications_count,
790 + 'active_html' => $active_notifications_html,
791 + 'dismissed_html' => $dismissed_notifications_html,
792 + ],
793 + ],
794 + true
795 + );
796 + return;
797 + }
798 +
799 + // Get active notifications.
576 800 foreach ( $notifications as $notification ) {
577 801
578 802 // Prepare required arguments.
579 803 $notification = wp_parse_args(
@@ -592,45 +816,320 @@
592 816 if ( ! $title && ! $content ) {
593 817 continue;
594 818 }
595 819
596 - // Notification HTML.
597 - $notifications_html .= sprintf(
598 - '<div class="charitable-notifications-message%5$s" data-message-id="%4$s">
599 - <h3 class="charitable-notifications-title">%1$s%6$s</h3>
600 - <p class="charitable-notifications-content">%2$s</p>
601 - %3$s
602 - </div>',
603 - esc_html( $title ),
604 - wp_kses( $content, $content_allowed_tags ),
605 - $this->get_notification_buttons_html( $notification ),
606 - esc_attr( $notification['id'] ),
607 - esc_attr( $current_class ),
608 - $this->get_video_badge_html( $this->get_component_data( $notification['video'] ) )
609 - );
820 + // Non-dismissible notifications (e.g. expired license) always render as active. @since 1.8.12
821 + $is_dismissible = ! isset( $notification['dismissible'] ) || false !== $notification['dismissible'];
610 822
823 + // Dynamic notifications (those with an explicit 'dismissible' key) suppress the relative date. @since 1.8.12
824 + $is_dynamic = isset( $notification['dismissible'] );
825 + $date_string = ! empty( $notification['start'] ) ? $this->human_readable_date_from_mysql( $notification['start'] ) : '';
826 + $date_html = $date_string ? '<span class="charitable-notification-date">' . esc_html( $date_string ) . '</span>' : '';
827 +
828 + // If the notification ID is not a part of the dismissed notifications, add it to the list.
829 + if ( ! $is_dismissible || ! $this->is_dismissed( $notification ) ) {
830 +
831 + // Dismiss link is suppressed for non-dismissible notifications (e.g. expired license). @since 1.8.12
832 + $dismiss_link = $is_dismissible ? '<a href="#" class="dismiss charitable-notif-dismiss-x" aria-label="Dismiss"><svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" width="10" height="10"><path d="M11.8211 1.3415L10.6451 0.166504L5.98305 4.82484L1.32097 0.166504L0.14502 1.3415L4.80711 5.99984L0.14502 10.6582L1.32097 11.8332L5.98305 7.17484L10.6451 11.8332L11.8211 10.6582L7.159 5.99984L11.8211 1.3415Z" fill="currentColor"/></svg></a>' : '';
833 +
834 + if ( 'dashboard' === $location ) {
835 +
836 + // Notification HTML, slightyl different for dashboard.
837 + $active_notifications_html .= sprintf(
838 + '<div aria-expanded="true" class="charitable-notification charitable-notification-%5$s %8$s" data-notification-id="%6$s">
839 + <div>
840 + %7$s
841 + <div class="body">
842 + <div class="title dashboard-title">
843 + <div>%1$s</div>
844 + <div class="date">%3$s</div>
845 + </div>
846 + <div class="notification-content">%2$s</div>
847 + <div class="actions">
848 + %4$s
849 + %9$s
850 + </div>
851 + </div>
852 + </div>
853 + </div>',
854 + esc_html( $title ),
855 + wp_kses( $content, $content_allowed_tags ),
856 + $date_html,
857 + $this->get_notification_buttons_html( $notification ),
858 + esc_attr( $notification['id'] ),
859 + esc_attr( $notification['id'] ),
860 + $this->get_notification_icon_html( $notification ),
861 + esc_attr( $current_class ),
862 + $dismiss_link // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
863 + // $this->get_video_badge_html( $this->get_component_data( $notification['video'] ) )
864 + );
865 +
866 + } else {
867 +
868 + // Notification HTML.
869 + $badge_html = $this->get_notification_badge_html( $notification );
870 + $border_type = ! empty( $notification['badge_type'] ) ? sanitize_html_class( $notification['badge_type'] ) : '';
871 + $border_class = $border_type ? ' charitable-notif-border--' . $border_type : '';
872 +
873 + $active_notifications_html .= sprintf(
874 + '<div aria-expanded="true" class="charitable-notification charitable-notification-%5$s %8$s" data-notification-id="%6$s">
875 + <div class="charitable-notification-card-inner">
876 + %9$s
877 + <div class="notif-left">
878 + %10$s
879 + %7$s
880 + </div>
881 + <div class="body">
882 + %3$s
883 + <h4>%1$s</h4>
884 + <div class="notification-content">%2$s</div>
885 + <div class="actions">
886 + %4$s
887 + </div>
888 + </div>
889 + </div>
890 + </div>',
891 + esc_html( $title ),
892 + wp_kses( $content, $content_allowed_tags ),
893 + $date_html,
894 + $this->get_notification_buttons_html( $notification ),
895 + esc_attr( $notification['id'] ),
896 + esc_attr( $notification['id'] ),
897 + $this->get_notification_icon_html( $notification ),
898 + esc_attr( $current_class ),
899 + $dismiss_link, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
900 + $badge_html // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
901 + );
902 +
903 + }
904 +
905 + ++$active_notifications_count;
906 +
907 + } else {
908 +
909 + // Dismissed notification.
910 + $dismissed_notifications_html .= sprintf(
911 + '<div aria-expanded="true" class="charitable-notification charitable-notification-%5$s %8$s" data-notification-id="%6$s">
912 + <div>
913 + %7$s
914 + <div class="body">
915 + <div class="title">
916 + <div class="date">%3$s</div>
917 + <div>%1$s</div>
918 + </div>
919 + <div class="notification-content">%2$s</div>
920 + <div class="actions">
921 + %4$s
922 + </div>
923 + </div>
924 + </div>
925 + </div>',
926 + esc_html( $title ),
927 + wp_kses( $content, $content_allowed_tags ),
928 + $date_html,
929 + $this->get_notification_buttons_html( $notification ),
930 + esc_attr( $notification['id'] ),
931 + esc_attr( $notification['id'] ),
932 + $this->get_notification_icon_html( $notification ),
933 + esc_attr( $current_class )
934 + // $this->get_video_badge_html( $this->get_component_data( $notification['video'] ) )
935 + );
936 +
937 + ++$dismissed_notifications_count;
938 + }
939 +
611 940 // Only first notification is current.
612 941 $current_class = '';
942 +
943 + if ( 'dashboard' === $location && $active_notifications_count >= $limit_active ) {
944 + break;
945 + }
613 946 }
614 947
948 + // For dashboard, we need to show the actual count being displayed (limited), not the total count
949 + $display_count = $active_notifications_count;
950 + if ( 'dashboard' === $location ) {
951 + // Get the total count for comparison
952 + $total_count = $this->get_new_notifications_count();
953 +
954 + // If we have more than the limit, show the limit with a "+"
955 + if ( $total_count > $limit_active ) {
956 + $display_count = $limit_active . '+';
957 + }
958 + }
959 +
615 960 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
616 961 echo charitable_render(
617 - '/admin/templates/notifications',
962 + $template_location,
618 963 [
619 964 'notifications' => [
620 - 'count' => count( $notifications ),
621 - 'html' => $notifications_html,
965 + 'active_count' => $display_count,
966 + 'dismissed_count' => $dismissed_notifications_count,
967 + 'active_html' => $active_notifications_html,
968 + 'dismissed_html' => $dismissed_notifications_html,
622 969 ],
623 970 ],
624 971 true
625 972 );
973 + }
626 974
975 + /**
976 + * Get notificaiton icon html based on if the type is a success, warning, info.
977 + *
978 + * @since 1.8.3
979 + *
980 + * @param array $notification Notification data.
981 + *
982 + * @return string
983 + */
984 + private function get_notification_icon_html( $notification ) {
985 +
986 + $notification_type = ! empty( $notification['notification_type'] ) ? sanitize_text_field( $notification['notification_type'] ) : '';
987 + $category = ! empty( $notification['category'] ) ? sanitize_text_field( $notification['category'] ) : '';
988 +
989 + // Category-specific icons take priority over generic notification_type icons.
990 + if ( 'celebration' === $notification_type ) {
991 + // Trophy — campaign goal reached.
992 + return '<div class="icon">
993 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-celebration" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
994 + <path d="M8 21h8M12 17v4"/>
995 + <path d="M7 4H5a2 2 0 0 0-2 2v1a4 4 0 0 0 4 4h.5"/>
996 + <path d="M17 4h2a2 2 0 0 1 2 2v1a4 4 0 0 1-4 4h-.5"/>
997 + <path d="M7 4h10v7a5 5 0 0 1-10 0V4z"/>
998 + </svg>
999 + </div>';
1000 + }
1001 +
1002 + if ( 'legal' === $category ) {
1003 + // Same circle exclamation as health alert, orange.
1004 + return '<div class="icon">
1005 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-compliance">
1006 + <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>
1007 + </svg>
1008 + </div>';
1009 + }
1010 +
1011 + if ( 'milestone' === $category ) {
1012 + // Star — donor milestone.
1013 + return '<div class="icon">
1014 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-milestone" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1015 + <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
1016 + </svg>
1017 + </div>';
1018 + }
1019 +
1020 + if ( 'lifecycle' === $category ) {
1021 + // Flag — campaign live or ended.
1022 + return '<div class="icon">
1023 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-lifecycle" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1024 + <path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/>
1025 + <line x1="4" y1="22" x2="4" y2="15"/>
1026 + </svg>
1027 + </div>';
1028 + }
1029 +
1030 + if ( 'nudge' === $category ) {
1031 + // Lightbulb — engagement nudge.
1032 + return '<div class="icon">
1033 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-nudge" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1034 + <path d="M9 18h6M10 22h4M12 2a7 7 0 00-4 12.87V17h8v-2.13A7 7 0 0012 2z"/>
1035 + </svg>
1036 + </div>';
1037 + }
1038 +
1039 + if ( 'new_feed' === $category ) {
1040 + // Megaphone — announcement.
1041 + return '<div class="icon">
1042 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-announcement" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1043 + <path d="M19 3v18M5 8H3a1 1 0 00-1 1v4a1 1 0 001 1h2l4 3V5L5 8z"/>
1044 + <path d="M19 7l-10 1M19 17L9 16"/>
1045 + </svg>
1046 + </div>';
1047 + }
1048 +
1049 + // Generic fallback by notification_type.
1050 + switch ( $notification_type ) {
1051 + case 'warning':
1052 + // Circle exclamation — health alert (keep as-is).
1053 + $html = '<div class="icon">
1054 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-circle-exclamation warning">
1055 + <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>
1056 + </svg>
1057 + </div>';
1058 + break;
1059 + case 'success':
1060 + $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>';
1061 + break;
1062 + default:
1063 + $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>';
1064 + break;
1065 + }
1066 +
1067 + return $html;
627 1068 }
628 1069
629 1070 /**
1071 + * Get badge HTML for a notification card.
1072 + *
1073 + * Badge displays the category label (e.g. "HEALTH ALERT", "MILESTONE").
1074 + * Returns empty string if no badge_label is set.
1075 + *
1076 + * @since 1.8.12
1077 + *
1078 + * @param array $notification Notification data.
1079 + * @return string
1080 + */
1081 + private function get_notification_badge_html( $notification ) {
1082 + if ( empty( $notification['badge_label'] ) ) {
1083 + return '';
1084 + }
1085 +
1086 + $badge_type = ! empty( $notification['badge_type'] ) ? sanitize_html_class( $notification['badge_type'] ) : 'info';
1087 + $label = esc_html( strtoupper( $notification['badge_label'] ) );
1088 +
1089 + return sprintf(
1090 + '<span class="charitable-notif-badge charitable-notif-badge--%s">%s</span>',
1091 + $badge_type,
1092 + $label
1093 + );
1094 + }
1095 +
1096 + /**
1097 + * Get human readable date from MySQL timestamp.
1098 + *
1099 + * @since 1.8.3
1100 + *
1101 + * @param string $mysql_timestamp MySQL timestamp.
1102 + *
1103 + * @return bool
1104 + */
1105 + private function human_readable_date_from_mysql( $mysql_timestamp = false ) {
1106 +
1107 + // Convert the MySQL timestamp to a Unix timestamp.
1108 + $timestamp = strtotime( $mysql_timestamp );
1109 +
1110 + // Get the current time as a Unix timestamp.
1111 + $current_time = current_time( 'timestamp' ); // phpcs:ignore
1112 +
1113 + // Calculate the difference in seconds.
1114 + $time_diff = $current_time - $timestamp;
1115 +
1116 + // 30 days in seconds
1117 + $thirty_days = 30 * 24 * 60 * 60;
1118 +
1119 + if ( $time_diff < $thirty_days ) {
1120 + // If the timestamp is less than 30 days ago, return a 'time ago' format.
1121 + return human_time_diff( $timestamp, $current_time ) . ' ago';
1122 + } else {
1123 + // If the timestamp is 30 days or more in the past, use 'F j, Y' format.
1124 + return date( 'F j, Y', $timestamp ); // phpcs:ignore
1125 + }
1126 + }
1127 +
1128 + /**
630 1129 * Retrieve notification's buttons HTML.
631 1130 *
632 - * @since 1.7.5
1131 + * @since 1.8.3
633 1132 *
634 1133 * @param array $notification Notification data.
635 1134 *
636 1135 * @return string
@@ -669,9 +1168,9 @@
669 1168
670 1169 /**
671 1170 * Retrieve notification's component data by a license type.
672 1171 *
673 - * @since 1.7.5
1172 + * @since 1.8.3
674 1173 *
675 1174 * @param mixed $data Component data.
676 1175 *
677 1176 * @return false|mixed
@@ -693,9 +1192,9 @@
693 1192
694 1193 /**
695 1194 * Retrieve the current installation license type (always lowercase).
696 1195 *
697 - * @since 1.7.5
1196 + * @since 1.8.3
698 1197 *
699 1198 * @return string
700 1199 */
701 1200 private function get_license_type() {
@@ -708,28 +1207,20 @@
708 1207 return 'pro';
709 1208 } else {
710 1209 return 'lite';
711 1210 }
712 -
713 - // $this->license_type = charitable_get_license_type();
714 -
715 - // if ( ! $this->license_type ) {
716 - // $this->license_type = 'lite';
717 - // }
718 -
719 - // return $this->license_type;
720 1211 }
721 1212
722 1213 /**
723 1214 * Dismiss notification via AJAX.
724 1215 *
725 - * @since 1.7.5
1216 + * @since 1.8.3
726 1217 */
727 1218 public function dismiss() {
728 1219
729 1220 // Check for required param, security and access.
730 1221 if (
731 - empty( $_POST['id'] ) ||
1222 + empty( $_POST['notification_id'] ) ||
732 1223 ! check_ajax_referer( 'charitable-admin', 'nonce', false ) ||
733 1224 ! $this->has_access()
734 1225 ) {
735 1226 wp_send_json_error();
@@ -734,9 +1225,9 @@
734 1225 ) {
735 1226 wp_send_json_error();
736 1227 }
737 1228
738 - $id = sanitize_key( $_POST['id'] );
1229 + $id = sanitize_key( $_POST['notification_id'] );
739 1230 $type = is_numeric( $id ) ? 'feed' : 'events';
740 1231 $option = $this->get_option();
741 1232
742 1233 $option['dismissed'][] = $id;
@@ -741,19 +1232,42 @@
741 1232
742 1233 $option['dismissed'][] = $id;
743 1234 $option['dismissed'] = array_unique( $option['dismissed'] );
744 1235
745 - // Remove notification.
746 - if ( is_array( $option[ $type ] ) && ! empty( $option[ $type ] ) ) {
747 - foreach ( $option[ $type ] as $key => $notification ) {
748 - if ( (string) $notification['id'] === (string) $id ) {
749 - unset( $option[ $type ][ $key ] );
1236 + update_option( 'charitable_notifications', $option );
750 1237
751 - break;
752 - }
753 - }
1238 + wp_send_json_success();
1239 + }
1240 +
1241 + /**
1242 + * Dismiss multiple notifications via AJAX.
1243 + *
1244 + * @since 1.8.3
1245 + */
1246 + public function dismiss_multiple() {
1247 +
1248 + // Check for required param, security and access.
1249 + if (
1250 + empty( $_POST['notification_ids'] ) ||
1251 + ! check_ajax_referer( 'charitable-admin', 'nonce', false ) ||
1252 + ! $this->has_access()
1253 + ) {
1254 + wp_send_json_error();
754 1255 }
755 1256
1257 + if ( ! is_array( $_POST['notification_ids'] ) ) {
1258 + wp_send_json_error();
1259 + }
1260 +
1261 + $option = $this->get_option();
1262 + $notification_ids = array_map( 'sanitize_key', $_POST['notification_ids'] );
1263 + foreach ( $notification_ids as $id ) {
1264 + $type = is_numeric( $id ) ? 'feed' : 'events';
1265 + $option['dismissed'][] = $id;
1266 + $option['dismissed'] = array_unique( $option['dismissed'] );
1267 +
1268 + }
1269 +
756 1270 update_option( 'charitable_notifications', $option );
757 1271
758 1272 wp_send_json_success();
759 1273 }
@@ -760,9 +1274,9 @@
760 1274
761 1275 /**
762 1276 * Prepare button URL.
763 1277 *
764 - * @since 1.7.5
1278 + * @since 1.8.3
765 1279 *
766 1280 * @param array $btn Button data.
767 1281 *
768 1282 * @return string
@@ -782,9 +1296,9 @@
782 1296
783 1297 /**
784 1298 * Get the notification's video badge HTML.
785 1299 *
786 - * @since 1.7.5
1300 + * @since 1.8.3
787 1301 *
788 1302 * @param string $video_url Valid video URL.
789 1303 *
790 1304 * @return string
@@ -802,9 +1316,9 @@
802 1316 return sprintf(
803 1317 '<a class="charitable-notifications-badge" href="%1$s" %2$s>%3$s</a>',
804 1318 esc_url( $video_url ),
805 1319 esc_attr( $data_attr_lity ),
806 - esc_html__( 'Watch Video', 'charitable-lite' )
1320 + esc_html__( 'Watch Video', 'charitable' )
807 1321 );
808 1322 }
809 1323
810 1324 /**
@@ -814,16 +1328,31 @@
814 1328 *
815 1329 * @return boolean
816 1330 */
817 1331 public function show_notifications() {
818 - if ( isset( $_GET['taxonomy'] ) ) {
1332 + if ( isset( $_GET['taxonomy'] ) ) { // phpcs:ignore
819 1333 return false;
820 1334 }
821 - if ( isset( $_GET['post_type'] ) && $_GET['post_type'] == 'campaign' ) {
1335 + if ( isset( $_GET['post_type'] ) && sanitize_text_field( $_GET['post_type'] ) === 'campaign' ) { // phpcs:ignore
822 1336 return true;
823 1337 }
824 - if ( isset( $_GET['post_type'] ) && $_GET['post_type'] == 'donation' ) {
1338 + if ( isset( $_GET['post_type'] ) && sanitize_text_field( $_GET['post_type'] ) === 'donation' ) { // phpcs:ignore
825 1339 return true;
826 1340 }
827 1341 return false;
1342 + }
1343 +
1344 + /**
1345 + * This will incercept the request and redirect to the dashboard page.
1346 + *
1347 + * @since 1.8.3
1348 + * @version 1.8.3.1
1349 + */
1350 + public function maybe_redirect_from_notifications() {
1351 +
1352 + if ( ! empty( $_GET['page'] ) && 'charitable-notifications' === $_GET['page'] ) { // phpcs:ignore
1353 + delete_transient( 'charitable_autoshow_plugin_notifications' );
1354 + wp_safe_redirect( admin_url( 'admin.php?page=charitable-dashboard' ) );
1355 + exit;
1356 + }
828 1357 }
829 1358 }