D exists in notifications, don't add again. $present_notification = $this->get_notification_by_id( $notification_id ); if ( ! is_null( $present_notification ) ) { $this->remove_notification( $present_notification, false ); } if ( is_null( $present_notification ) ) { $this->new[] = $notification_id; } } // Add to list. $this->notifications[] = $notification; } /** * Get the notification by ID. * * @param string $notification_id The ID of the notification to search for. * * @return null|Yoast_Notification */ public function get_notification_by_id( $notification_id ) { foreach ( $this->notifications as & $notification ) { if ( $notification_id === $notification->get_id() ) { return $notification; } } return null; } /** * Display the notifications. * * @param bool $echo_as_json True when notifications should be printed directly. * * @return void */ public function display_notifications( $echo_as_json = false ) { // Never display notifications for network admin. if ( function_exists( 'is_network_admin' ) && is_network_admin() ) { return; } $sorted_notifications = $this->get_sorted_notifications(); $notifications = array_filter( $sorted_notifications, array( $this, 'is_notification_persistent' ) ); if ( empty( $notifications ) ) { return; } array_walk( $notifications, array( $this, 'remove_notification' ) ); $notifications = array_unique( $notifications ); if ( $echo_as_json ) { $notification_json = array(); 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 ); return; } 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. echo $notification; } } /** * 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 = array( $this, __METHOD__ ); $args = func_get_args(); if ( $this->queue_transaction( $callback, $args ) ) { return; } $index = false; // Match persistent Notifications by ID, non persistent by item in the array. if ( $notification->is_persistent() ) { foreach ( $this->notifications as $current_index => $present_notification ) { if ( $present_notification->get_id() === $notification->get_id() ) { $index = $current_index; break; } } } else { $index = array_search( $notification, $this->notifications, true ); } if ( false === $index ) { return; } if ( $notification->is_persistent() && $resolve ) { $this->resolved++; $this->clear_dismissal( $notification ); } unset( $this->notifications[ $index ] ); $this->notifications = array_values( $this->notifications ); } /** * 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 ); } /** * 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(); $notifications = array_filter( $notifications, array( $this, 'filter_persistent_notifications' ) ); if ( ! $dismissed ) { $notifications = array_filter( $notifications, array( $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(); if ( empty( $notifications ) ) { return array(); } // Sort by severity, error first. usort( $notifications, array( $this, 'sort_notifications' ) ); return $notifications; } /** * AJAX display notifications. */ public function ajax_get_notifications() { $echo = filter_input( INPUT_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(); } /** * 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->get_notifications(); /** * Filter: 'yoast_notifications_before_storage' - Allows developer to filter notifications before saving them. * * @api Yoast_Notification[] $notifications */ $notifications = apply_filters( 'yoast_notifications_before_storage', $notifications ); // No notifications to store, clear storage if it was previously present. if ( empty( $notifications ) ) { $this->remove_storage(); return; } $notifications = array_map( array( $this, 'notification_to_array' ), $notifications ); // Save the notifications to the storage. update_user_option( get_current_user_id(), self::STORAGE_KEY, $notifications ); } /** * Provide a way to verify present notifications. * * @return array|Yoast_Notification[] Registered notifications. */ public function get_notifications() { return $this->notifications; } /** * Get newly added notifications. * * @return array */ public function get_new_notifications() { return array_map( array( $this, 'get_notification_by_id' ), $this->new ); } /** * Get information from the User input. * * @param string $key Key to retrieve. * * @return mixed value of key if set. */ private static function get_user_input( $key ) { $filter_input_type = INPUT_GET; if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === strtoupper( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) { $filter_input_type = INPUT_POST; } return filter_input( $filter_input_type, $key ); } /** * Retrieve the notifications from storage. * * @return array|void Yoast_Notification[] Notifications. */ private function retrieve_notifications_from_storage() { if ( $this->notifications_retrieved ) { return; } $this->notifications_retrieved = true; $stored_notifications = get_user_option( self::STORAGE_KEY, get_current_user_id() ); // Check if notifications are stored. if ( empty( $stored_notifications ) ) { return; } if ( is_array( $stored_notifications ) ) { $notifications = array_map( array( $this, 'array_to_notification' ), $stored_notifications ); // Apply array_values to ensure we get a 0-indexed array. $notifications = array_values( array_filter( $notifications, array( $this, 'filter_notification_current_user' ) ) ); $this->notifications = $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 ( 'error' === $a_type ) { return -1; } if ( 'error' === $b_type ) { return 1; } return 0; } /** * Clear local stored notifications. */ private function clear_notifications() { $this->notifications = array(); $this->notifications_retrieved = false; } /** * Filter out non-persistent notifications. * * @param Yoast_Notification $notification Notification to test for persistent. * * @since 3.2 * * @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 ! $this->maybe_dismiss_notification( $notification ); } /** * Convert Notification to array representation. * * @param Yoast_Notification $notification Notification to convert. * * @since 3.2 * * @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'] ); } 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[] = array( $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() ); } }