| 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 |
|