| @@ -16,14 +16,14 @@ | ||
| 16 | 16 | * Option name to store notifications on. |
| 17 | 17 | * |
| 18 | 18 | * @var string |
| 19 | 19 | */ |
| 20 | - const STORAGE_KEY = 'yoast_notifications'; | |
| 20 | + public const STORAGE_KEY = 'yoast_notifications'; | |
| 21 | 21 | |
| 22 | 22 | /** |
| 23 | 23 | * The singleton instance of this object. |
| 24 | 24 | * |
| 25 | - * @var \Yoast_Notification_Center | |
| 25 | + * @var Yoast_Notification_Center|null | |
| 26 | 26 | */ |
| 27 | 27 | private static $instance = null; |
| 28 | 28 | |
| 29 | 29 | /** |
| @@ -28,9 +28,9 @@ | ||
| 28 | 28 | |
| 29 | 29 | /** |
| 30 | 30 | * Holds the notifications. |
| 31 | 31 | * |
| 32 | - * @var \Yoast_Notification[][] | |
| 32 | + * @var Yoast_Notification[][] | |
| 33 | 33 | */ |
| 34 | 34 | private $notifications = []; |
| 35 | 35 | |
| 36 | 36 | /** |
| @@ -89,11 +89,9 @@ | ||
| 89 | 89 | * @return Yoast_Notification_Center |
| 90 | 90 | */ |
| 91 | 91 | public static function get() { |
| 92 | 92 | |
| 93 | - if ( self::$instance === null ) { | |
| 94 | - self::$instance = new self(); | |
| 95 | - } | |
| 93 | + self::$instance ??= new self(); | |
| 96 | 94 | |
| 97 | 95 | return self::$instance; |
| 98 | 96 | } |
| 99 | 97 | |
| @@ -98,18 +96,29 @@ | ||
| 98 | 96 | } |
| 99 | 97 | |
| 100 | 98 | /** |
| 101 | 99 | * Dismiss a notification. |
| 100 | + * | |
| 101 | + * @return void | |
| 102 | 102 | */ |
| 103 | 103 | public static function ajax_dismiss_notification() { |
| 104 | + $notification_center = self::get(); | |
| 104 | 105 | |
| 105 | - $notification_center = self::get(); | |
| 106 | + if ( ! isset( $_POST['notification'] ) || ! is_string( $_POST['notification'] ) ) { | |
| 107 | + exit( '-1' ); | |
| 108 | + } | |
| 106 | 109 | |
| 107 | - $notification_id = filter_input( INPUT_POST, 'notification' ); | |
| 110 | + $notification_id = sanitize_text_field( wp_unslash( $_POST['notification'] ) ); | |
| 111 | + | |
| 108 | 112 | if ( empty( $notification_id ) ) { |
| 109 | - die( '-1' ); | |
| 113 | + exit( '-1' ); | |
| 110 | 114 | } |
| 111 | 115 | |
| 116 | + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are using the variable as a nonce. | |
| 117 | + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), $notification_id ) ) { | |
| 118 | + exit( '-1' ); | |
| 119 | + } | |
| 120 | + | |
| 112 | 121 | $notification = $notification_center->get_notification_by_id( $notification_id ); |
| 113 | 122 | if ( ( $notification instanceof Yoast_Notification ) === false ) { |
| 114 | 123 | |
| 115 | 124 | // Permit legacy. |
| @@ -120,12 +129,12 @@ | ||
| 120 | 129 | $notification = new Yoast_Notification( '', $options ); |
| 121 | 130 | } |
| 122 | 131 | |
| 123 | 132 | if ( self::maybe_dismiss_notification( $notification ) ) { |
| 124 | - die( '1' ); | |
| 133 | + exit( '1' ); | |
| 125 | 134 | } |
| 126 | 135 | |
| 127 | - die( '-1' ); | |
| 136 | + exit( '-1' ); | |
| 128 | 137 | } |
| 129 | 138 | |
| 130 | 139 | /** |
| 131 | 140 | * Check if the user has dismissed a notification. |
| @@ -294,12 +303,14 @@ | ||
| 294 | 303 | /** |
| 295 | 304 | * Add notification to the cookie. |
| 296 | 305 | * |
| 297 | 306 | * @param Yoast_Notification $notification Notification object instance. |
| 307 | + * | |
| 308 | + * @return void | |
| 298 | 309 | */ |
| 299 | 310 | public function add_notification( Yoast_Notification $notification ) { |
| 300 | 311 | |
| 301 | - $callback = [ $this, __METHOD__ ]; | |
| 312 | + $callback = [ $this, __FUNCTION__ ]; | |
| 302 | 313 | $args = func_get_args(); |
| 303 | 314 | if ( $this->queue_transaction( $callback, $args ) ) { |
| 304 | 315 | return; |
| 305 | 316 | } |
| @@ -316,13 +327,13 @@ | ||
| 316 | 327 | if ( $notification_id !== '' ) { |
| 317 | 328 | |
| 318 | 329 | // If notification ID exists in notifications, don't add again. |
| 319 | 330 | $present_notification = $this->get_notification_by_id( $notification_id, $user_id ); |
| 320 | - if ( ! is_null( $present_notification ) ) { | |
| 331 | + if ( $present_notification !== null ) { | |
| 321 | 332 | $this->remove_notification( $present_notification, false ); |
| 322 | 333 | } |
| 323 | 334 | |
| 324 | - if ( is_null( $present_notification ) ) { | |
| 335 | + if ( $present_notification === null ) { | |
| 325 | 336 | $this->new[] = $notification_id; |
| 326 | 337 | } |
| 327 | 338 | } |
| 328 | 339 | |
| @@ -401,12 +412,14 @@ | ||
| 401 | 412 | * Remove notification after it has been displayed. |
| 402 | 413 | * |
| 403 | 414 | * @param Yoast_Notification $notification Notification to remove. |
| 404 | 415 | * @param bool $resolve Resolve as fixed. |
| 416 | + * | |
| 417 | + * @return void | |
| 405 | 418 | */ |
| 406 | 419 | public function remove_notification( Yoast_Notification $notification, $resolve = true ) { |
| 407 | 420 | |
| 408 | - $callback = [ $this, __METHOD__ ]; | |
| 421 | + $callback = [ $this, __FUNCTION__ ]; | |
| 409 | 422 | $args = func_get_args(); |
| 410 | 423 | if ( $this->queue_transaction( $callback, $args ) ) { |
| 411 | 424 | return; |
| 412 | 425 | } |
| @@ -497,9 +510,9 @@ | ||
| 497 | 510 | |
| 498 | 511 | /** |
| 499 | 512 | * Return the notifications sorted on type and priority. |
| 500 | 513 | * |
| 501 | - * @return array|Yoast_Notification[] Sorted Notifications | |
| 514 | + * @return Yoast_Notification[] Sorted Notifications | |
| 502 | 515 | */ |
| 503 | 516 | public function get_sorted_notifications() { |
| 504 | 517 | $notifications = $this->get_notifications_for_user( get_current_user_id() ); |
| 505 | 518 | if ( empty( $notifications ) ) { |
| @@ -513,21 +526,30 @@ | ||
| 513 | 526 | } |
| 514 | 527 | |
| 515 | 528 | /** |
| 516 | 529 | * AJAX display notifications. |
| 530 | + * | |
| 531 | + * @return void | |
| 517 | 532 | */ |
| 518 | 533 | public function ajax_get_notifications() { |
| 519 | - $echo = filter_input( INPUT_POST, 'version' ) === '2'; | |
| 534 | + $echo = false; | |
| 535 | + // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form data. | |
| 536 | + if ( isset( $_POST['version'] ) && is_string( $_POST['version'] ) ) { | |
| 537 | + // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are only comparing the variable in a condition. | |
| 538 | + $echo = wp_unslash( $_POST['version'] ) === '2'; | |
| 539 | + } | |
| 520 | 540 | |
| 521 | 541 | // Display the notices. |
| 522 | 542 | $this->display_notifications( $echo ); |
| 523 | 543 | |
| 524 | 544 | // AJAX die. |
| 525 | - exit; | |
| 545 | + exit(); | |
| 526 | 546 | } |
| 527 | 547 | |
| 528 | 548 | /** |
| 529 | 549 | * Remove storage when the plugin is deactivated. |
| 550 | + * | |
| 551 | + * @return void | |
| 530 | 552 | */ |
| 531 | 553 | public function deactivate_hook() { |
| 532 | 554 | |
| 533 | 555 | $this->clear_notifications(); |
| @@ -553,9 +575,9 @@ | ||
| 553 | 575 | * |
| 554 | 576 | * In other terms, it returns an associative array, |
| 555 | 577 | * mapping user ID to a list of notifications for this user. |
| 556 | 578 | * |
| 557 | - * @param array|Yoast_Notification[] $notifications The notifications to split. | |
| 579 | + * @param Yoast_Notification[] $notifications The notifications to split. | |
| 558 | 580 | * |
| 559 | 581 | * @return array The notifications, split on user ID. |
| 560 | 582 | */ |
| 561 | 583 | private function split_on_user_id( $notifications ) { |
| @@ -575,8 +597,14 @@ | ||
| 575 | 597 | * |
| 576 | 598 | * @return void |
| 577 | 599 | */ |
| 578 | 600 | public function update_storage() { |
| 601 | + /** | |
| 602 | + * Plugins might exit on the plugins_loaded hook. | |
| 603 | + * This prevents the pluggable.php file from loading, as it's loaded after the plugins_loaded hook. | |
| 604 | + * As we need functions defined in pluggable.php, make sure it's loaded. | |
| 605 | + */ | |
| 606 | + require_once ABSPATH . WPINC . '/pluggable.php'; | |
| 579 | 607 | |
| 580 | 608 | $notifications = $this->notifications; |
| 581 | 609 | |
| 582 | 610 | /** |
| @@ -591,9 +619,9 @@ | ||
| 591 | 619 | |
| 592 | 620 | /** |
| 593 | 621 | * Filter: 'yoast_notifications_before_storage' - Allows developer to filter notifications before saving them. |
| 594 | 622 | * |
| 595 | - * @api Yoast_Notification[] $notifications | |
| 623 | + * @param Yoast_Notification[] $notifications | |
| 596 | 624 | */ |
| 597 | 625 | $filtered_merged_notifications = apply_filters( 'yoast_notifications_before_storage', $merged_notifications ); |
| 598 | 626 | |
| 599 | 627 | // The notifications were filtered and therefore need to be stored. |
| @@ -619,10 +647,10 @@ | ||
| 619 | 647 | |
| 620 | 648 | /** |
| 621 | 649 | * Stores the notifications to its respective user's storage. |
| 622 | 650 | * |
| 623 | - * @param array|Yoast_Notification[] $notifications The notifications to store. | |
| 624 | - * @param int $user_id The ID of the user for which to store the notifications. | |
| 651 | + * @param Yoast_Notification[] $notifications The notifications to store. | |
| 652 | + * @param int $user_id The ID of the user for which to store the notifications. | |
| 625 | 653 | * |
| 626 | 654 | * @return void |
| 627 | 655 | */ |
| 628 | 656 | private function store_notifications_for_user( $notifications, $user_id ) { |
| @@ -632,9 +660,9 @@ | ||
| 632 | 660 | |
| 633 | 661 | /** |
| 634 | 662 | * Provide a way to verify present notifications. |
| 635 | 663 | * |
| 636 | - * @return array|Yoast_Notification[] Registered notifications. | |
| 664 | + * @return Yoast_Notification[] Registered notifications. | |
| 637 | 665 | */ |
| 638 | 666 | public function get_notifications() { |
| 639 | 667 | if ( ! $this->notifications ) { |
| 640 | 668 | return []; |
| @@ -668,22 +696,29 @@ | ||
| 668 | 696 | |
| 669 | 697 | /** |
| 670 | 698 | * Get information from the User input. |
| 671 | 699 | * |
| 700 | + * Note that this function does not handle nonce verification. | |
| 701 | + * | |
| 672 | 702 | * @param string $key Key to retrieve. |
| 673 | 703 | * |
| 674 | - * @return mixed value of key if set. | |
| 704 | + * @return string non-sanitized value of key if set, an empty string otherwise. | |
| 675 | 705 | */ |
| 676 | 706 | private static function get_user_input( $key ) { |
| 677 | - | |
| 678 | - $filter_input_type = INPUT_GET; | |
| 679 | - $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? filter_var( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; | |
| 680 | - | |
| 681 | - if ( strtoupper( $request_method ) === 'POST' ) { | |
| 682 | - $filter_input_type = INPUT_POST; | |
| 707 | + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information and only using this variable in a comparison. | |
| 708 | + $request_method = isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; | |
| 709 | + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: This function does not sanitize variables. | |
| 710 | + // phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing -- Reason: This function does not verify a nonce. | |
| 711 | + if ( $request_method === 'POST' ) { | |
| 712 | + if ( isset( $_POST[ $key ] ) && is_string( $_POST[ $key ] ) ) { | |
| 713 | + return wp_unslash( $_POST[ $key ] ); | |
| 714 | + } | |
| 683 | 715 | } |
| 684 | - | |
| 685 | - return filter_input( $filter_input_type, $key ); | |
| 716 | + elseif ( isset( $_GET[ $key ] ) && is_string( $_GET[ $key ] ) ) { | |
| 717 | + return wp_unslash( $_GET[ $key ] ); | |
| 718 | + } | |
| 719 | + // phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized | |
| 720 | + return ''; | |
| 686 | 721 | } |
| 687 | 722 | |
| 688 | 723 | /** |
| 689 | 724 | * Retrieve the notifications from storage and fill the relevant property. |
| @@ -692,9 +727,8 @@ | ||
| 692 | 727 | * |
| 693 | 728 | * @return void |
| 694 | 729 | */ |
| 695 | 730 | private function retrieve_notifications_from_storage( $user_id ) { |
| 696 | - | |
| 697 | 731 | if ( $this->notifications_retrieved ) { |
| 698 | 732 | return; |
| 699 | 733 | } |
| 700 | 734 | |
| @@ -708,8 +742,9 @@ | ||
| 708 | 742 | } |
| 709 | 743 | |
| 710 | 744 | if ( is_array( $stored_notifications ) ) { |
| 711 | 745 | $notifications = array_map( [ $this, 'array_to_notification' ], $stored_notifications ); |
| 746 | + | |
| 712 | 747 | // Apply array_values to ensure we get a 0-indexed array. |
| 713 | 748 | $notifications = array_values( array_filter( $notifications, [ $this, 'filter_notification_current_user' ] ) ); |
| 714 | 749 | |
| 715 | 750 | $this->notifications[ $user_id ] = $notifications; |
| @@ -745,8 +780,10 @@ | ||
| 745 | 780 | } |
| 746 | 781 | |
| 747 | 782 | /** |
| 748 | 783 | * Clear local stored notifications. |
| 784 | + * | |
| 785 | + * @return void | |
| 749 | 786 | */ |
| 750 | 787 | private function clear_notifications() { |
| 751 | 788 | |
| 752 | 789 | $this->notifications = []; |
| @@ -812,16 +849,23 @@ | ||
| 812 | 849 | unset( $notification_data['options']['nonce'] ); |
| 813 | 850 | } |
| 814 | 851 | |
| 815 | 852 | if ( isset( $notification_data['message'] ) |
| 816 | - && \is_subclass_of( $notification_data['message'], Abstract_Presenter::class, false ) | |
| 853 | + && is_subclass_of( $notification_data['message'], Abstract_Presenter::class, false ) | |
| 817 | 854 | ) { |
| 818 | 855 | $notification_data['message'] = $notification_data['message']->present(); |
| 819 | 856 | } |
| 820 | 857 | |
| 858 | + if ( isset( $notification_data['options']['user'] ) ) { | |
| 859 | + $notification_data['options']['user_id'] = $notification_data['options']['user']->ID; | |
| 860 | + unset( $notification_data['options']['user'] ); | |
| 861 | + | |
| 862 | + $this->notifications_need_storage = true; | |
| 863 | + } | |
| 864 | + | |
| 821 | 865 | return new Yoast_Notification( |
| 822 | 866 | $notification_data['message'], |
| 823 | - $notification_data['options'] | |
| 867 | + $notification_data['options'], | |
| 824 | 868 | ); |
| 825 | 869 | } |
| 826 | 870 | |
| 827 | 871 | /** |
| @@ -868,8 +912,10 @@ | ||
| 868 | 912 | * Adds a notification transaction to the queue for later execution. |
| 869 | 913 | * |
| 870 | 914 | * @param callable $callback Callback that performs the transaction. |
| 871 | 915 | * @param array $args Arguments to pass to the callback. |
| 916 | + * | |
| 917 | + * @return void | |
| 872 | 918 | */ |
| 873 | 919 | private function add_transaction_to_queue( $callback, $args ) { |
| 874 | 920 | $this->queued_transactions[] = [ $callback, $args ]; |
| 875 | 921 | } |