: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* @package WPSEO\Admin\Notifications
use Yoast\WP\SEO\Presenters\Abstract_Presenter;
* Handles notifications storage and display.
class Yoast_Notification_Center {
* Option name to store notifications on.
public const STORAGE_KEY = 'yoast_notifications';
* The singleton instance of this object.
* @var Yoast_Notification_Center
private static $instance = null;
* Holds the notifications.
* @var Yoast_Notification[][]
private $notifications = [];
* Notifications there are newly added.
* Notifications that were resolved this execution.
* Internal storage for transaction before notifications have been retrieved from storage.
private $queued_transactions = [];
* Internal flag for whether notifications have been retrieved from storage.
private $notifications_retrieved = false;
* Internal flag for whether notifications need to be updated in storage.
private $notifications_need_storage = false;
private function __construct() {
add_action( 'init', [ $this, 'setup_current_notifications' ], 1 );
add_action( 'all_admin_notices', [ $this, 'display_notifications' ] );
add_action( 'wp_ajax_yoast_get_notifications', [ $this, 'ajax_get_notifications' ] );
add_action( 'wpseo_deactivate', [ $this, 'deactivate_hook' ] );
add_action( 'shutdown', [ $this, 'update_storage' ] );
* @return Yoast_Notification_Center
public static function get() {
if ( self::$instance === null ) {
self::$instance = new self();
* Dismiss a notification.
public static function ajax_dismiss_notification() {
$notification_center = self::get();
if ( ! isset( $_POST['notification'] ) || ! is_string( $_POST['notification'] ) ) {
$notification_id = sanitize_text_field( wp_unslash( $_POST['notification'] ) );
if ( empty( $notification_id ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are using the variable as a nonce.
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), $notification_id ) ) {
$notification = $notification_center->get_notification_by_id( $notification_id );
if ( ( $notification instanceof Yoast_Notification ) === false ) {
'id' => $notification_id,
'dismissal_key' => $notification_id,
$notification = new Yoast_Notification( '', $options );
if ( self::maybe_dismiss_notification( $notification ) ) {
* Check if the user has dismissed a notification.
* @param Yoast_Notification $notification The notification to check for dismissal.
* @param int|null $user_id User ID to check on.
public static function is_notification_dismissed( Yoast_Notification $notification, $user_id = null ) {
$user_id = self::get_user_id( $user_id );
$dismissal_key = $notification->get_dismissal_key();
// This checks both the site-specific user option and the meta value.
$current_value = get_user_option( $dismissal_key, $user_id );
// Migrate old user meta to user option on-the-fly.
if ( ! empty( $current_value )
&& metadata_exists( 'user', $user_id, $dismissal_key )
&& update_user_option( $user_id, $dismissal_key, $current_value ) ) {
delete_user_meta( $user_id, $dismissal_key );
return ! empty( $current_value );
* Checks if the notification is being dismissed.
* @param Yoast_Notification $notification Notification to check dismissal of.
* @param string $meta_value Value to set the meta value to if dismissed.
* @return bool True if dismissed.
public static function maybe_dismiss_notification( Yoast_Notification $notification, $meta_value = 'seen' ) {
// Only persistent notifications are dismissible.
if ( ! $notification->is_persistent() ) {
// If notification is already dismissed, we're done.
if ( self::is_notification_dismissed( $notification ) ) {
$dismissal_key = $notification->get_dismissal_key();
$notification_id = $notification->get_id();
$is_dismissing = ( $dismissal_key === self::get_user_input( 'notification' ) );
if ( ! $is_dismissing ) {
$is_dismissing = ( $notification_id === self::get_user_input( 'notification' ) );
// Fallback to ?dismissal_key=1&nonce=bla when JavaScript fails.
if ( ! $is_dismissing ) {
$is_dismissing = ( self::get_user_input( $dismissal_key ) === '1' );
if ( ! $is_dismissing ) {
$user_nonce = self::get_user_input( 'nonce' );
if ( wp_verify_nonce( $user_nonce, $notification_id ) === false ) {
return self::dismiss_notification( $notification, $meta_value );
* Dismisses a notification.
* @param Yoast_Notification $notification Notification to dismiss.
* @param string $meta_value Value to save in the dismissal.
* @return bool True if dismissed, false otherwise.
public static function dismiss_notification( Yoast_Notification $notification, $meta_value = 'seen' ) {
return update_user_option( get_current_user_id(), $notification->get_dismissal_key(), $meta_value ) !== false;
* Restores a notification.
* @param Yoast_Notification $notification Notification to restore.
* @return bool True if restored, false otherwise.
public static function restore_notification( Yoast_Notification $notification ) {
$user_id = get_current_user_id();
$dismissal_key = $notification->get_dismissal_key();
$restored = delete_user_option( $user_id, $dismissal_key );
// Delete unprefixed user meta too for backward-compatibility.
if ( metadata_exists( 'user', $user_id, $dismissal_key ) ) {
$restored = delete_user_meta( $user_id, $dismissal_key ) && $restored;
* Clear dismissal information for the specified Notification.
* When a cause is resolved, the next time it is present we want to show
* @param string|Yoast_Notification $notification Notification to clear the dismissal of.
public function clear_dismissal( $notification ) {
if ( $notification instanceof Yoast_Notification ) {
$dismissal_key = $notification->get_dismissal_key();
if ( is_string( $notification ) ) {
$dismissal_key = $notification;
if ( empty( $dismissal_key ) ) {
// Remove notification dismissal for all users.
$deleted = delete_metadata( 'user', 0, $wpdb->get_blog_prefix() . $dismissal_key, '', true );
// Delete unprefixed user meta too for backward-compatibility.
$deleted = delete_metadata( 'user', 0, $dismissal_key, '', true ) || $deleted;
* Retrieves notifications from the storage and merges in previous notification changes.
* The current user in WordPress is not loaded shortly before the 'init' hook, but the plugin
* sometimes needs to add or remove notifications before that. In such cases, the transactions
* are not actually executed, but added to a queue. That queue is then handled in this method,
* after notifications for the current user have been set up.
public function setup_current_notifications() {
$this->retrieve_notifications_from_storage( get_current_user_id() );
foreach ( $this->queued_transactions as $transaction ) {
list( $callback, $args ) = $transaction;
call_user_func_array( $callback, $args );
$this->queued_transactions = [];
* Add notification to the cookie.
* @param Yoast_Notification $notification Notification object instance.
public function add_notification( Yoast_Notification $notification ) {
$callback = [ $this, __FUNCTION__ ];
if ( $this->queue_transaction( $callback, $args ) ) {
// Don't add if the user can't see it.
if ( ! $notification->display_for_current_user() ) {
$notification_id = $notification->get_id();
$user_id = $notification->get_user_id();
// Empty notifications are always added.
if ( $notification_id !== '' ) {
// If notification ID exists in notifications, don't add again.
$present_notification = $this->get_notification_by_id( $notification_id, $user_id );
if ( ! is_null( $present_notification ) ) {
$this->remove_notification( $present_notification, false );
if ( is_null( $present_notification ) ) {
$this->new[] = $notification_id;
$this->notifications[ $user_id ][] = $notification;
$this->notifications_need_storage = true;
* Get the notification by ID and user ID.
* @param string $notification_id The ID of the notification to search for.
* @param int|null $user_id The ID of the user.
* @return Yoast_Notification|null
public function get_notification_by_id( $notification_id, $user_id = null ) {
$user_id = self::get_user_id( $user_id );
$notifications = $this->get_notifications_for_user( $user_id );
foreach ( $notifications as $notification ) {
if ( $notification_id === $notification->get_id() ) {
* Display the notifications.
* @param bool $echo_as_json True when notifications should be printed directly.
public function display_notifications( $echo_as_json = false ) {
// Never display notifications for network admin.
if ( is_network_admin() ) {
$sorted_notifications = $this->get_sorted_notifications();
$notifications = array_filter( $sorted_notifications, [ $this, 'is_notification_persistent' ] );
if ( empty( $notifications ) ) {
array_walk( $notifications, [ $this, 'remove_notification' ] );
$notifications = array_unique( $notifications );
foreach ( $notifications as $notification ) {
$notification_json[] = $notification->render();
// phpcs:ignore WordPress.Security.EscapeOutput -- Reason: WPSEO_Utils::format_json_encode is safe.
echo WPSEO_Utils::format_json_encode( $notification_json );
foreach ( $notifications as $notification ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Temporarily disabled, see: https://github.com/Yoast/wordpress-seo-premium/issues/2510 and https://github.com/Yoast/wordpress-seo-premium/issues/2511.
* Remove notification after it has been displayed.
* @param Yoast_Notification $notification Notification to remove.
* @param bool $resolve Resolve as fixed.
public function remove_notification( Yoast_Notification $notification, $resolve = true ) {
$callback = [ $this, __FUNCTION__ ];
if ( $this->queue_transaction( $callback, $args ) ) {
// ID of the user to show the notification for, defaults to current user id.
$user_id = $notification->get_user_id();
$notifications = $this->get_notifications_for_user( $user_id );
// Match persistent Notifications by ID, non persistent by item in the array.
if ( $notification->is_persistent() ) {
foreach ( $notifications as $current_index => $present_notification ) {
if ( $present_notification->get_id() === $notification->get_id() ) {
$index = array_search( $notification, $notifications, true );
if ( $index === false ) {
if ( $notification->is_persistent() && $resolve ) {
$this->clear_dismissal( $notification );
unset( $notifications[ $index ] );
$this->notifications[ $user_id ] = array_values( $notifications );
$this->notifications_need_storage = true;
* Removes a notification by its ID.
* @param string $notification_id The notification id.
* @param bool $resolve Resolve as fixed.
public function remove_notification_by_id( $notification_id, $resolve = true ) {
$notification = $this->get_notification_by_id( $notification_id );
if ( $notification === null ) {
$this->remove_notification( $notification, $resolve );
$this->notifications_need_storage = true;
* Get the notification count.
* @param bool $dismissed Count dismissed notifications.
* @return int Number of notifications
public function get_notification_count( $dismissed = false ) {
$notifications = $this->get_notifications_for_user( get_current_user_id() );
$notifications = array_filter( $notifications, [ $this, 'filter_persistent_notifications' ] );
$notifications = array_filter( $notifications, [ $this, 'filter_dismissed_notifications' ] );
return count( $notifications );