HEX
Server: Apache
System: Linux sg241.singhost.net 2.6.32-896.16.1.lve1.4.51.el6.x86_64 #1 SMP Wed Jan 17 13:19:23 EST 2018 x86_64
User: honghock (909)
PHP: 8.0.30
Disabled: passthru,system,shell_exec,show_source,exec,popen,proc_open
Upload Files
File: /home/honghock/www/wp-content/plugins/searchwp-live-ajax-search/includes/class-notifications.php
<?php

use SearchWP_Live_Search_Utils as Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class SearchWP_Live_Search_Notifications.
 *
 * The SearchWP Live Ajax Search settings.
 *
 * @since 1.7.3
 */
class SearchWP_Live_Search_Notifications {

	/**
	 * URL for fetching remote notifications.
	 *
	 * @since 1.7.3
	 */
	private const SOURCE_URL = 'https://plugin.searchwp.com/wp-content/notifications.json';

	/**
	 * Name of the WP option to save the notifications data to.
	 *
	 * @since 1.7.3
	 */
	private const OPTION_NAME = 'searchwp_lite_admin_notifications';

	/**
	 * Internal constant to populate the Notifications panel bypassing all checks.
	 * Change false to true to enable.
	 *
	 * @since 1.7.5
	 */
	private const TEST_MODE = false;

	/**
	 * Init.
	 *
	 * @since 1.7.3
	 */
	public static function init() {

		add_filter( 'searchwp_live_search_settings_defaults', [ __CLASS__, 'hide_opt_out_setting' ] );

        if ( Utils::is_searchwp_active() ) {
            return;
        }

		if ( ! self::has_access() ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ __CLASS__, 'assets_global' ] );
		add_action( 'admin_enqueue_scripts', [ __CLASS__, 'assets' ] );

		add_filter( 'searchwp_live_search_options_submenu_pages', [ __CLASS__, 'add_submenu_page' ] );

		add_action( 'searchwp_live_search_settings_header_actions', [ __CLASS__, 'output_header_button' ] );
		add_action( 'searchwp_live_search_settings_header_after', [ __CLASS__, 'output_panel' ] );

		add_action( 'wp_ajax_searchwp_live_search_notification_dismiss', [ __CLASS__, 'dismiss' ] );
    }

	/**
	 * Check if user has access.
	 *
	 * @since 1.7.3
	 *
	 * @return bool
	 */
	public static function has_access() {

        $settings_api = searchwp_live_search()->get( 'Settings_Api' );

		return current_user_can( $settings_api::get_capability() ) && ! $settings_api->get( 'hide-announcements' );
	}

	/**
	 * Register global assets.
	 *
	 * @since 1.7.3
	 */
	public static function assets_global() {

		if ( ! self::has_access() ) {
			return;
		}

		if ( empty( self::get_count() ) ) {
			return;
		}

		wp_enqueue_style(
			'searchwp-live-search-admin-notifications-global',
			SEARCHWP_LIVE_SEARCH_PLUGIN_URL . 'assets/styles/admin/notifications-global.css',
			[],
			SEARCHWP_LIVE_SEARCH_VERSION
		);
	}

	/**
	 * Register assets.
	 *
	 * @since 1.7.3
	 */
	public static function assets() {

		if ( ! self::has_access() ) {
			return;
		}

		if ( ! Utils::is_swp_live_search_admin_page() ) {
			return;
		}

		wp_enqueue_style(
			'searchwp-live-search-admin-notifications',
			SEARCHWP_LIVE_SEARCH_PLUGIN_URL . 'assets/styles/admin/notifications.css',
			[],
			SEARCHWP_LIVE_SEARCH_VERSION
		);

		wp_enqueue_script(
			'searchwp-live-search-admin-notifications',
			SEARCHWP_LIVE_SEARCH_PLUGIN_URL . 'assets/js/admin/notifications.js',
			[],
			SEARCHWP_LIVE_SEARCH_VERSION
		);

		Utils::localize_script( 'searchwp-live-search-admin-notifications' );
	}

	/**
	 * Add Notifications pseudo submenu item to the SearchWP admin menu.
	 *
	 * @since 1.7.3
	 *
	 * @param array $submenu_pages List of registered SearchWP submenu pages.
	 *
	 * @return array
	 */
	public static function add_submenu_page( $submenu_pages ) {

		if ( ! self::has_access() ) {
			return $submenu_pages;
		}

		if ( empty( self::get_count() ) ) {
			return $submenu_pages;
		}

		$settings = searchwp_live_search()->get( 'Settings' );

		$menu_slug = 'searchwp-' . $settings::$slug;

		$submenu_pages['notifications'] = [
			'menu_title' => esc_html__( 'Notifications', 'searchwp-live-ajax-search' ) . '<span style="margin-top: 6px;" class="searchwp-admin-menu-notification-indicator"></span>',
			'menu_slug'  => $menu_slug . '#notifications',
			'position'   => 0,
			'function'   => [ $settings, 'render_searchwp_disabled' ],
		];

		return $submenu_pages;
	}

	/**
	 * Hide a plugin setting to opt out of plugin notifications if SearchWP is active.
	 *
	 * @since 1.7.3
	 *
	 * @param array $settings List of registered plugin settings.
	 *
	 * @return array
	 */
	public static function hide_opt_out_setting( $settings ) {

		if ( Utils::is_searchwp_active() ) {
			unset( $settings['misc-heading'], $settings['hide-announcements'] );
		}

		return $settings;
    }

	/**
	 * Output header action button.
	 *
	 * @since 1.7.3
	 */
	public static function output_header_button() {

		$notifications = self::get();

		?>
        <div id="swp-notifications-page-header-button" class="swp-header-menu--item swp-relative" title="<?php esc_html_e( 'Notifications', 'searchwp-live-ajax-search' ); ?>">
	        <svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
		        <path fill-rule="evenodd" clip-rule="evenodd" d="M13.3333 0.5H1.66667C0.75 0.5 0 1.25 0 2.16667V13.8333C0 14.75 0.741667 15.5 1.66667 15.5H13.3333C14.25 15.5 15 14.75 15 13.8333V2.16667C15 1.25 14.25 0.5 13.3333 0.5ZM13.3333 13.8333H1.66667V11.3333H4.63333C5.20833 12.325 6.275 13 7.50833 13C8.74167 13 9.8 12.325 10.3833 11.3333H13.3333V13.8333ZM9.175 9.66667H13.3333V2.16667H1.66667V9.66667H5.84167C5.84167 10.5833 6.59167 11.3333 7.50833 11.3333C8.425 11.3333 9.175 10.5833 9.175 9.66667Z" fill="#0E2121" fill-opacity="0.6"/>
	        </svg>

	        <?php if ( ! empty( $notifications ) ) : ?>
		        <div class="swp-badge">
			        <span><?php echo count( $notifications ); ?></span>
		        </div>
	        <?php endif; ?>
        </div>
		<?php
	}

	/**
	 * Output main notifications panel.
	 *
	 * @since 1.7.3
	 */
	public static function output_panel() {

		$notifications = self::get();

		?>
        <div class="searchwp-notifications-panel-wrapper" style="display: none;">
            <div class="searchwp-notifications-panel components-animate__slide-in is-from-left">

                <div class="searchwp-notifications-panel__header">
                    <span><span><?php echo count( $notifications ); ?></span> Unread Notifications</span>
                    <button type="button" class="components-button has-icon searchwp-notifications-panel__close"
                            aria-label="Close notifications">
                        <svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
                            <path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path>
                        </svg>
                    </button>
                </div>

                <div class="searchwp-notifications-panel__notifications">
					<?php
					foreach ( $notifications as $notification ) {
						self::output_panel_notification_single( $notification );
					}
					?>
					<?php self::output_empty_notifications_message(); ?>
                </div>

            </div>
            <div class="searchwp-notifications-backdrop"></div>
        </div>
		<?php
	}

	/**
     * Output empty notifications message.
     *
     * @since 1.8.0
	 */
    private static function output_empty_notifications_message() {
        ?>
        <div class="swp-notifications--empty-notifications-message"<?php echo self::get_count() ? ' style="display: none;"' : ''; ?>>
            <img src="<?php echo esc_url( SEARCHWP_LIVE_SEARCH_PLUGIN_URL . 'assets/images/admin/searchwp-finnie.png' ); ?>" alt="SearchWP Finnie">
            <div class="swp-notifications--great"><?php esc_html_e( 'Fancy meeting you here!', 'searchwp-live-ajax-search' ); ?></div>
            <div class="swp-notifications--no-new-notifications"><?php esc_html_e( 'You have no new notifications.', 'searchwp-live-ajax-search' ); ?></div>
        </div>
        <?php
	}

	/**
	 * Output single notification in the main notifications panel.
	 *
	 * @since 1.7.3
	 *
	 * @param array $notification Single notification data.
	 */
	private static function output_panel_notification_single( $notification ) {

		?>
        <div class="searchwp-notifications-notification" style="background-color: transparent;">
            <div class="searchwp-notifications-notification__icon searchwp-notifications-notification__icon-success">
                <span class="dashicons dashicons-yes-alt"></span>
            </div>
            <div class="searchwp-notifications-notification__body">
                <div class="searchwp-notifications-notification__header">
                    <div class="searchwp-notifications-notification__title"><?php echo esc_html( $notification['title'] ); ?></div>
                    <div class="searchwp-notifications-notification__date"><?php echo esc_html( human_time_diff( strtotime( $notification['start'] ), strtotime( current_time( 'mysql' ) ) ) ); ?>
                        ago
                    </div>
                </div>
                <div class="searchwp-notifications-notification__content">
                    <p><?php echo wp_kses_post( $notification['content'] ); ?></p>
                </div>
                <div class="searchwp-notifications-notification__actions">
					<?php foreach ( $notification['actions'] as $notification_action ) : ?>
                        <a href="<?php echo esc_url( $notification_action['url'] ); ?>" target="_blank" class="components-button is-<?php echo esc_attr( $notification_action['type'] ); ?>">
							<?php echo esc_html( $notification_action['text'] ); ?>
                        </a>
					<?php endforeach; ?>
                    <button type="button" class="searchwp-notification-dismiss components-button is-link" data-id="<?php echo absint( $notification['remote_id'] ); ?>">
						<?php esc_html_e( 'Dismiss', 'searchwp-live-ajax-search' ); ?>
                    </button>
                </div>
            </div>
        </div>
		<?php
	}

	/**
	 * Get available notifications.
	 *
	 * @since 1.7.3
	 *
	 * @return array
	 */
	public static function get() {

		if ( ! self::has_access() ) {
			return [];
		}

		$option = self::get_option();

		// Fetch remote notifications every 12 hours.
		if ( self::TEST_MODE || empty( $option['updated_at'] ) || time() > $option['updated_at'] + ( 12 * HOUR_IN_SECONDS ) ) {
			self::save( self::fetch() );
			$option = self::get_option( [ 'cache' => false ] ); // Make sure the notifications are available right away.
		}

		return ! empty( $option['notifications'] ) ? self::filter_active( $option['notifications'] ) : [];
	}

	/**
	 * Get available notifications count.
	 *
	 * @since 1.7.3
	 *
	 * @return int
	 */
	public static function get_count() {

		if ( ! self::has_access() ) {
			return 0;
		}

		return count( self::get() );
	}

	/**
	 * Fetch notifications from the remote server.
	 *
	 * @since 1.7.3
	 *
	 * @return array
	 */
	private static function fetch() {

		$request = wp_remote_get(
			self::SOURCE_URL,
			[ 'sslverify' => false ]
		);

		if ( is_wp_error( $request ) ) {
			return [];
		}

		$response      = wp_remote_retrieve_body( $request );
		$notifications = ! empty( $response ) ? json_decode( $response, true ) : [];

		if ( ! is_array( $notifications ) ) {
			return [];
		}

		return self::filter_fetched( $notifications );
	}

	/**
	 * Parse single notification data.
	 *
	 * @since 1.7.3
	 *
	 * @param array $notification Raw data to parse.
	 *
	 * @return array
	 */
	private static function parse_notification( $notification ) {

		$remote_id  = ! empty( $notification['id'] ) ? $notification['id'] : '0';
		$type       = ! empty( $notification['notification_type'] ) ? $notification['notification_type'] : 'info';
		$title      = ! empty( $notification['title'] ) ? $notification['title'] : '';
		$slug       = ! empty( $notification['slug'] ) ? $notification['slug'] : $title;
		$content    = ! empty( $notification['content'] ) ? $notification['content'] : '';
		$buttons    = ! empty( $notification['btns'] ) && is_array( $notification['btns'] ) ? $notification['btns'] : [];
		$conditions = ! empty( $notification['type'] ) && is_array( $notification['type'] ) ? $notification['type'] : [];
		$start      = ! empty( $notification['start'] ) ? $notification['start'] : date( 'Y-m-d H:i:s' );
		$end        = ! empty( $notification['end'] ) ? $notification['end'] : date( 'Y-m-d H:i:s', time() + ( YEAR_IN_SECONDS * 1 ) );

		return [
			'remote_id'  => sanitize_text_field( $remote_id ),
			'type'       => sanitize_text_field( $type ),
			'title'      => esc_html( $title ),
			'slug'       => sanitize_title( $slug ),
			'content'    => wp_kses_post( $content ),
			'actions'    => self::parse_notification_actions( $buttons ),
			'conditions' => array_map( 'sanitize_text_field', $conditions ),
			'start'      => sanitize_text_field( $start ),
			'end'        => sanitize_text_field( $end ),
		];
	}

	/**
	 * Parse single notification actions data.
	 *
	 * @since 1.7.3
	 *
	 * @param array $buttons Raw data to parse.
	 *
	 * @return array
	 */
	private static function parse_notification_actions( $buttons ) {

		$actions = [];

		foreach ( $buttons as $type => $btn ) {

			$button_type = $type === 'main' ? 'primary' : 'secondary';

			$actions[] = [
				'type' => sanitize_text_field( $button_type ),
				'url'  => esc_url_raw( $btn['url'] ),
				'text' => esc_html( $btn['text'] ),
			];
		}

		return $actions;
	}

	/**
	 * Filter fetched notifications before saving.
	 *
	 * @since 1.7.3
	 *
	 * @param array $notifications Array of notifications items to verify.
	 *
	 * @return array
	 */
	private static function filter_fetched( $notifications ) {

		$data = [];

		if ( ! is_array( $notifications ) || empty( $notifications ) ) {
			return $data;
		}

		foreach ( $notifications as $notification ) {
			if ( self::verify_single( $notification ) ) {
				$data[] = self::parse_notification( $notification );
			}
		}

		return $data;
	}

	/**
	 * Filter active notifications and remove outdated ones.
	 *
	 * @since 1.7.3
	 *
	 * @param array $notifications Array of notifications items to filter.
	 *
	 * @return array
	 */
	private static function filter_active( $notifications ) {

		if ( ! is_array( $notifications ) || empty( $notifications ) ) {
			return [];
		}

		if ( self::TEST_MODE ) {
			return $notifications;
		}

		// Remove notifications that are not active.
		foreach ( $notifications as $key => $notification ) {
			if (
				( ! empty( $notification['start'] ) && time() < strtotime( $notification['start'] ) ) ||
				( ! empty( $notification['end'] ) && time() > strtotime( $notification['end'] ) )
			) {
				unset( $notifications[ $key ] );
			}
		}

		return $notifications;
	}

	/**
	 * Verify a single notification data.
	 *
	 * @since 1.7.3
	 *
	 * @param array $notification Notification data to verify.
	 *
	 * @return bool
	 */
	private static function verify_single( $notification ) {

		if ( self::TEST_MODE ) {
			return true;
		}

		$option = self::get_option();

		// The message and license should never be empty, if they are, ignore.
		if ( empty( $notification['content'] ) || empty( $notification['type'] ) ) {
			return false;
		}

		$license_type = 'lite';

		// Ignore if license type does not match.
		if ( ! in_array( $license_type, $notification['type'], true ) ) {
			return false;
		}

		// Ignore if expired.
		if ( ! empty( $notification['end'] ) && time() > strtotime( $notification['end'] ) ) {
			return false;
		}

		// Ignore if notification has already been dismissed.
		if ( ! empty( $option['dismissed_ids'] ) && in_array( $notification['id'], $option['dismissed_ids'] ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
			return false;
		}

		return true;
	}

	/**
	 * Get option value.
	 *
	 * @since 1.7.3
	 *
	 * @param array $args Method arguments.
	 *
	 * @return array
	 */
	private static function get_option( $args = [] ) {

		static $option_cache;

		if ( ! isset( $args['cache'] ) ) {
			$args['cache'] = true;
		}

		if ( $option_cache && ! empty( $args['cache'] ) ) {
			return $option_cache;
		}

		$option = get_option( self::OPTION_NAME, [] );

		if ( empty( $args['cache'] ) ) {
			return $option;
		}

		$option_cache = [
			'updated_at'    => ! empty( $option['updated_at'] ) ? $option['updated_at'] : 0,
			'dismissed_ids' => ! empty( $option['dismissed_ids'] ) ? $option['dismissed_ids'] : [],
			'notifications' => ! empty( $option['notifications'] ) ? $option['notifications'] : [],
		];

		return $option_cache;
	}

	/**
	 * Save notifications data in the database.
	 *
	 * @param array $notifications Array of notifications data to save.
	 *
	 * @since 1.7.3
	 */
	private static function save( $notifications ) {

		$option = self::get_option();

		update_option(
			self::OPTION_NAME,
			[
				'updated_at'    => time(),
				'dismissed_ids' => $option['dismissed_ids'],
				'notifications' => $notifications,
			]
		);
	}

	/**
	 * Dismiss notification via AJAX.
	 *
	 * @since 1.7.3
	 */
	public static function dismiss() {

		Utils::check_ajax_permissions();

		if ( searchwp_live_search()->get( 'Settings_Api' )->get( 'hide-announcements' ) ) {
			wp_send_json_error();
		}

		if ( empty( $_POST['id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			wp_send_json_error();
		}

		$id     = sanitize_text_field( wp_unslash( $_POST['id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
		$option = self::get_option();

		$option['dismissed_ids'][] = $id;
		$option['dismissed_ids']   = array_unique( $option['dismissed_ids'] );

		// Remove notification.
		if ( is_array( $option['notifications'] ) && ! empty( $option['notifications'] ) ) {
			foreach ( $option['notifications'] as $key => $notification ) {
				if ( (int) $notification['remote_id'] === (int) $id ) {
					unset( $option['notifications'][ $key ] );
					break;
				}
			}
		}

		update_option( self::OPTION_NAME, $option );

		wp_send_json_success();
	}
}