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.
*
* @return void
*/
public function remove_notification_by_id( $notification_id, $resolve = true ) {
$notification = $this->get_notification_by_id( $notification_id );
if ( $notification === null ) {
return;
}
$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' ] );
if ( ! $dismissed ) {
$notifications = array_filter( $notifications, [ $this, 'filter_dismissed_notifications' ] );
}
return count( $notifications );
}
/**
* Get the number of notifications resolved this execution.
*
* These notifications have been resolved and should be counted when active again.
*
* @return int
*/
public function get_resolved_notification_count() {
return $this->resolved;
}
/**
* Return the notifications sorted on type and priority.
*
* @return array|Yoast_Notification[] Sorted Notifications
*/
public function get_sorted_notifications() {
$notifications = $this->get_notifications_for_user( get_current_user_id() );
if ( empty( $notifications ) ) {
return [];
}
// Sort by severity, error first.
usort( $notifications, [ $this, 'sort_notifications' ] );
return $notifications;
}
/**
* AJAX display notifications.
*/
public function ajax_get_notifications() {
$echo = false;
// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form data.
if ( isset( $_POST['version'] ) && is_string( $_POST['version'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are only comparing the variable in a condition.
$echo = wp_unslash( $_POST['version'] ) === '2';
}
// Display the notices.
$this->display_notifications( $echo );
// AJAX die.
exit;
}
/**
* Remove storage when the plugin is deactivated.
*/
public function deactivate_hook() {
$this->clear_notifications();
}
/**
* Returns the given user ID if it exists.
* Otherwise, this function returns the ID of the current user.
*
* @param int $user_id The user ID to check.
*
* @return int The user ID to use.
*/
private static function get_user_id( $user_id ) {
if ( $user_id ) {
return $user_id;
}
return get_current_user_id();
}
/**
* Splits the notifications on user ID.
*
* In other terms, it returns an associative array,
* mapping user ID to a list of notifications for this user.
*
* @param array|Yoast_Notification[] $notifications The notifications to split.
*
* @return array The notifications, split on user ID.
*/
private function split_on_user_id( $notifications ) {
$split_notifications = [];
foreach ( $notifications as $notification ) {
$split_notifications[ $notification->get_user_id() ][] = $notification;
}
return $split_notifications;
}
/**
* Save persistent notifications to storage.
*
* We need to be able to retrieve these so they can be dismissed at any time during the execution.
*
* @since 3.2
*
* @return void
*/
public function update_storage() {
$notifications = $this->notifications;
/**
* One array of Yoast_Notifications, merged from multiple arrays.
*
* @var Yoast_Notification[] $merged_notifications
*/
$merged_notifications = [];
if ( ! empty( $notifications ) ) {
$merged_notifications = array_merge( ...$notifications );
}
/**
* Filter: 'yoast_notifications_before_storage' - Allows developer to filter notifications before saving them.
*
* @api Yoast_Notification[] $notifications
*/
$filtered_merged_notifications = apply_filters( 'yoast_notifications_before_storage', $merged_notifications );
// The notifications were filtered and therefore need to be stored.
if ( $merged_notifications !== $filtered_merged_notifications ) {
$merged_notifications = $filtered_merged_notifications;
$this->notifications_need_storage = true;
}
$notifications = $this->split_on_user_id( $merged_notifications );
// No notifications to store, clear storage if it was previously present.
if ( empty( $notifications ) ) {
$this->remove_storage();
return;
}
// Only store notifications if changes are made.
if ( $this->notifications_need_storage ) {
array_walk( $notifications, [ $this, 'store_notifications_for_user' ] );
}
}
/**
* Stores the notifications to its respective user's storage.
*
* @param array|Yoast_Notification[] $notifications The notifications to store.
* @param int $user_id The ID of the user for which to store the notifications.
*
* @return void
*/
private function store_notifications_for_user( $notifications, $user_id ) {
$notifications_as_arrays = array_map( [ $this, 'notification_to_array' ], $notifications );
update_user_option( $user_id, self::STORAGE_KEY, $notifications_as_arrays );
}
/**
* Provide a way to verify present notifications.
*
* @return array|Yoast_Notification[] Registered notifications.
*/
public function get_notifications() {
if ( ! $this->notifications ) {
return [];
}
return array_merge( ...$this->notifications );
}
/**
* Returns the notifications for the given user.
*
* @param int $user_id The id of the user to check.
*
* @return Yoast_Notification[] The notifications for the user with the given ID.
*/
public function get_notifications_for_user( $user_id ) {
if ( array_key_exists( $user_id, $this->notifications ) ) {
return $this->notifications[ $user_id ];
}
return [];
}
/**
* Get newly added notifications.
*
* @return array
*/
public function get_new_notifications() {
return array_map( [ $this, 'get_notification_by_id' ], $this->new );
}
/**
* Get information from the User input.
*
* Note that this function does not handle nonce verification.
*
* @param string $key Key to retrieve.
*
* @return string non-sanitized value of key if set, an empty string otherwise.
*/
private static function get_user_input( $key ) {
// 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.
$request_method = isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: This function does not sanitize variables.
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing -- Reason: This function does not verify a nonce.
if ( $request_method === 'POST' ) {
if ( isset( $_POST[ $key ] ) && is_string( $_POST[ $key ] ) ) {
return wp_unslash( $_POST[ $key ] );
}
}
else {
if ( isset( $_GET[ $key ] ) && is_string( $_GET[ $key ] ) ) {
return wp_unslash( $_GET[ $key ] );
}
}
// phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
return '';
}
/**
* Retrieve the notifications from storage and fill the relevant property.
*
* @param int $user_id The ID of the user to retrieve notifications for.
*
* @return void
*/
private function retrieve_notifications_from_storage( $user_id ) {
if ( $this->notifications_retrieved ) {
return;
}
$this->notifications_retrieved = true;
$stored_notifications = get_user_option( self::STORAGE_KEY, $user_id );
// Check if notifications are stored.
if ( empty( $stored_notifications ) ) {
return;
}
if ( is_array( $stored_notifications ) ) {
$notifications = array_map( [ $this, 'array_to_notification' ], $stored_notifications );
// Apply array_values to ensure we get a 0-indexed array.
$notifications = array_values( array_filter( $notifications, [ $this, 'filter_notification_current_user' ] ) );
$this->notifications[ $user_id ] = $notifications;
}
}
/**
* Sort on type then priority.
*
* @param Yoast_Notification $a Compare with B.
* @param Yoast_Notification $b Compare with A.
*
* @return int 1, 0 or -1 for sorting offset.
*/
private function sort_notifications( Yoast_Notification $a, Yoast_Notification $b ) {
$a_type = $a->get_type();
$b_type = $b->get_type();
if ( $a_type === $b_type ) {
return WPSEO_Utils::calc( $b->get_priority(), 'compare', $a->get_priority() );
}
if ( $a_type === 'error' ) {
return -1;
}
if ( $b_type === 'error' ) {
return 1;
}
return 0;
}
/**
* Clear local stored notifications.
*/
private function clear_notifications() {
$this->notifications = [];
$this->notifications_retrieved = false;
}
/**
* Filter out non-persistent notifications.
*
* @since 3.2
*
* @param Yoast_Notification $notification Notification to test for persistent.
*
* @return bool
*/
private function filter_persistent_notifications( Yoast_Notification $notification ) {
return $notification->is_persistent();
}
/**
* Filter out dismissed notifications.
*
* @param Yoast_Notification $notification Notification to check.
*
* @return bool
*/
private function filter_dismissed_notifications( Yoast_Notification $notification ) {
return ! self::maybe_dismiss_notification( $notification );
}
/**
* Convert Notification to array representation.
*
* @since 3.2
*
* @param Yoast_Notification $notification Notification to convert.
*
* @return array
*/
private function notification_to_array( Yoast_Notification $notification ) {
$notification_data = $notification->to_array();
if ( isset( $notification_data['nonce'] ) ) {
unset( $notification_data['nonce'] );
}
return $notification_data;
}
/**
* Convert stored array to Notification.
*
* @param array $notification_data Array to convert to Notification.
*
* @return Yoast_Notification
*/
private function array_to_notification( $notification_data ) {
if ( isset( $notification_data['options']['nonce'] ) ) {
unset( $notification_data['options']['nonce'] );
}
if ( isset( $notification_data['message'] )
&& \is_subclass_of( $notification_data['message'], Abstract_Presenter::class, false )
) {
$notification_data['message'] = $notification_data['message']->present();
}
return new Yoast_Notification(
$notification_data['message'],
$notification_data['options']
);
}
/**
* Filter notifications that should not be displayed for the current user.
*
* @param Yoast_Notification $notification Notification to test.
*
* @return bool
*/
private function filter_notification_current_user( Yoast_Notification $notification ) {
return $notification->display_for_current_user();
}
/**
* Checks if given notification is persistent.
*
* @param Yoast_Notification $notification The notification to check.
*
* @return bool True when notification is not persistent.
*/
private function is_notification_persistent( Yoast_Notification $notification ) {
return ! $notification->is_persistent();
}
/**
* Queues a notification transaction for later execution if notifications are not yet set up.
*
* @param callable $callback Callback that performs the transaction.
* @param array $args Arguments to pass to the callback.
*
* @return bool True if transaction was queued, false if it can be performed immediately.
*/
private function queue_transaction( $callback, $args ) {
if ( $this->notifications_retrieved ) {
return false;
}
$this->add_transaction_to_queue( $callback, $args );
return true;
}
/**
* Adds a notification transaction to the queue for later execution.
*
* @param callable $callback Callback that performs the transaction.
* @param array $args Arguments to pass to the callback.
*/
private function add_transaction_to_queue( $callback, $args ) {
$this->queued_transactions[] = [ $callback, $args ];
}
/**
* Removes all notifications from storage.
*
* @return bool True when notifications got removed.
*/
protected function remove_storage() {
if ( ! $this->has_stored_notifications() ) {
return false;
}
delete_user_option( get_current_user_id(), self::STORAGE_KEY );
return true;
}
/**
* Checks if there are stored notifications.
*
* @return bool True when there are stored notifications.
*/
protected function has_stored_notifications() {
$stored_notifications = $this->get_stored_notifications();
return ! empty( $stored_notifications );
}
/**
* Retrieves the stored notifications.
*
* @codeCoverageIgnore
*
* @return array|false Array with notifications or false when not set.
*/
protected function get_stored_notifications() {
return get_user_option( self::STORAGE_KEY, get_current_user_id() );
}
}