Source: includes/class-sensei-notices.php

<?php
/**
 * All functionality pertaining to displaying of various notices on the frontend.
 *
 * @package Core
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Sensei Notices Class
 *
 * All functionality pertaining to displaying of various notices on the frontend.
 *
 * @package Core
 * @author Automattic
 *
 * @since 1.6.3
 */
class Sensei_Notices {
	/**
	 * The key to use for storing the notices as user meta
	 */
	private const USER_META_KEY = 'sensei_notices';

	/**
	 * Notices.
	 *
	 *  @var $notices
	 */
	protected $notices;

	/**
	 * Has Printed.
	 *
	 * @var boolean $has_printed
	 */
	protected $has_printed;

	/**
	 * The HTML allowed for message boxes.
	 *
	 * @var array The HTML allowed for message boxes.
	 */
	protected $allowed_html;

	/**
	 * Constructor
	 */
	public function __construct() {
		// initialize the notices variable.
		$this->notices      = array();
		$this->has_printed  = false;
		$this->allowed_html = array_merge(
			wp_kses_allowed_html( 'post' ),
			array(
				'embed'  => array(),
				'iframe' => array(
					'width'           => array(),
					'height'          => array(),
					'src'             => array(),
					'frameborder'     => array(),
					'allowfullscreen' => array(),
				),
			)
		);

		add_action( 'template_redirect', [ $this, 'setup_block_notices' ] );
		add_action( 'shutdown', [ $this, 'maybe_persist_notices' ] );
	}

	/**
	 *  Add a notice to the array of notices for display at a later stage.
	 *
	 * @param string $content Content.
	 * @param string $type    Defaults to alert options( alert, tick , download , info   ).
	 * @param string $key     Notices with the same key will be overwritten.
	 *
	 * @return void
	 */
	public function add_notice( string $content, string $type = 'alert', string $key = null ) {
		$notice = [
			'content' => $content,
			'type'    => $type,
			'key'     => $key,
		];

		/**
		 * Allows to modify a sensei notice that will be shown to the user.
		 *
		 * @since 4.7.0
		 *
		 * @hook sensei_notice
		 *
		 * @param {array} $notice The notice data.
		 * @return {array|null} The notice data. Or return null if you want to prevent the notice showing up.
		 */
		$notice = apply_filters( 'sensei_notice', $notice );

		if ( empty( $notice ) ) {
			return;
		}

		// append the new notice.
		if ( empty( $notice['key'] ) ) {
			$this->notices[] = $notice;
		} else {
			$this->notices[ $notice['key'] ] = $notice;
		}

		// if a notice is added after we've printed, print it immediately.
		if ( $this->has_printed ) {
			$this->maybe_print_notices();
		}
	}

	/**
	 * Output all notices added
	 *
	 * @return void
	 */
	public function maybe_print_notices() {
		$this->maybe_load_notices();
		if ( ! empty( $this->notices ) ) {
			foreach ( $this->notices  as  $notice ) {
				// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in generate_notice
				echo $this->generate_notice( $notice['type'], $notice['content'] );
			}
			// empty the notice queue to avoid reprinting the same notices.
			$this->clear_notices();

		}

		// set this to print immediately if notices are added after the notices were printed.
		$this->has_printed = true;
	}

	/**
	 * Load the notices from the user meta, if the user is logged in, and delete them.
	 *
	 * @return void
	 */
	public function maybe_load_notices() {
		if ( is_user_logged_in() && Sensei_Utils::is_frontend_request() ) {
			$user_id = get_current_user_id();
			$values  = get_user_meta( $user_id, self::USER_META_KEY );

			$this->notices = array_merge( $this->notices, ...$values );
			foreach ( $values as $value ) {
				delete_user_meta( $user_id, self::USER_META_KEY, $value );
			}
		}
	}

	/**
	 * If the user is logged in and there's notices to print, persist the saved notices as user meta, and clear the
	 * notice list.
	 *
	 * @return void
	 */
	public function maybe_persist_notices() {
		if ( ! empty( $this->notices ) && is_user_logged_in() && Sensei_Utils::is_frontend_request() ) {
			update_user_meta( get_current_user_id(), self::USER_META_KEY, $this->notices );
			$this->clear_notices();
		}
	}

	/**
	 * Adds a filter to the content to add notices added by blocks.
	 */
	public function setup_block_notices() {
		if ( is_singular( 'course' ) && Sensei()->course->has_sensei_blocks( get_post() ) ) {
			add_filter( 'the_content', [ $this, 'prepend_notices_to_content' ] );
		}
	}

	/**
	 * Adds the notices before main content.
	 *
	 * @param string $content The post content.
	 *
	 * @access private
	 *
	 * @return string The modified content.
	 */
	public function prepend_notices_to_content( $content ) {
		if ( in_the_loop() && is_main_query() ) {

			ob_start();
			$this->maybe_print_notices();
			$notice = ob_get_clean();

			return $notice . $content;
		}

		return $content;
	}

	/**
	 * Helper method which generates the HTML for a notice.
	 *
	 * @param string $type    Notice type.
	 * @param string $content Notice content.
	 *
	 * @return string The HTML
	 */
	public function generate_notice( string $type, string $content ) : string {
		$classes = 'sensei-message ' . $type;

		return '<div class="' . esc_attr( $classes ) . '">' . wp_kses( $content, $this->allowed_html ) . '</div>';
	}

	/**
	 *  Clear all notices
	 *
	 * @return void
	 */
	public function clear_notices() {
		// assign an empty array to clear all existing notices.
		$this->notices = array();
	}
}

/**
 * Class Woothemes_Sensei_Notices
 *
 * @ignore only for backward compatibility
 * @since 1.9.0
 */
class Woothemes_Sensei_Notices extends Sensei_Notices{}