/home/awneajlw/www/wp-content/plugins/formidable/classes/models/FrmSalesApi.php
<?php
if ( ! defined( 'ABSPATH' ) ) {
	die( 'You are not allowed to call this page directly.' );
}

/**
 * @since 6.17
 */
class FrmSalesApi extends FrmFormApi {

	/**
	 * @var FrmSalesApi|null
	 */
	private static $instance;

	protected $cache_key;

	/**
	 * All sales from API data.
	 *
	 * @var array|false
	 */
	private static $sales = false;

	/**
	 * Best sale from API data.
	 *
	 * @var array|false|null
	 */
	private static $best_sale;

	/**
	 * @since 6.25.1
	 *
	 * @var string|null
	 */
	private static $cross_sell_text;

	/**
	 * @since 6.25.1
	 *
	 * @var string|null
	 */
	private static $cross_sell_link;

	public function __construct() {
		$this->set_cache_key();

		if ( false === self::$sales ) {
			$this->set_sales();
		}
	}

	/**
	 * @since 6.17
	 *
	 * @return void
	 */
	protected function set_cache_key() {
		$this->cache_key = 'frm_sales_cache';
	}

	/**
	 * @since 6.17
	 *
	 * @return string
	 */
	protected function api_url() {
		return 'https://plapi.formidableforms.com/sales/';
	}

	/**
	 * @since 6.17
	 *
	 * @return void
	 */
	private function set_sales() {
		self::$sales = array();

		$api = $this->get_api_info();
		if ( empty( $api ) ) {
			return;
		}

		foreach ( $api as $sale ) {
			$this->add_sale( $sale );

			if ( is_array( $sale ) && isset( $sale['cross_sell_text'] ) ) {
				$this->set_cross_sell( $sale );
			}
		}
	}

	/**
	 * Check for a special array in the sales API array.
	 * Normally cross_sell_text and cross_sell_link are not set.
	 * But one array, that isn't actually a sale, contains cross sell data.
	 * This should be near the end of the array.
	 *
	 * @since 6.25.1
	 *
	 * @param array $data
	 * @return void
	 */
	private function set_cross_sell( $data ) {
		if ( ! self::cross_sell_is_valid( $data ) ) {
			return;
		}

		$cross_sell_text  = $data['cross_sell_text'];
		$cross_sell_links = $data['cross_sell_link'];
		$index            = self::determine_cross_sell_index( $cross_sell_text );

		self::$cross_sell_text = sanitize_text_field( $cross_sell_text[ $index ] );
		self::$cross_sell_link = esc_url_raw( $cross_sell_links[ $index ] );
	}

	/**
	 * Check that both cross_sell_text and cross_sell_link are set and are arrays of the same size.
	 *
	 * @since 6.25.1
	 *
	 * @param array $data
	 * @return bool
	 */
	private function cross_sell_is_valid( $data ) {
		if ( empty( $data['cross_sell_text'] ) || empty( $data['cross_sell_link'] ) ) {
			return false;
		}

		if ( ! is_array( $data['cross_sell_text'] ) || ! is_array( $data['cross_sell_link'] ) ) {
			return false;
		}

		return count( $data['cross_sell_link'] ) === count( $data['cross_sell_text'] );
	}

	/**
	 * Determine which cross sell text to use.
	 * These are shown in order for 30 days before moving on to the next one.
	 *
	 * @since 6.25.1
	 *
	 * @param array $cross_sell_text
	 * @return int
	 */
	private static function determine_cross_sell_index( $cross_sell_text ) {
		$option_name         = 'frm_cross_sell_settings';
		$cross_sell_settings = get_option( $option_name );

		if ( ! is_array( $cross_sell_settings ) ) {
			$cross_sell_settings = array(
				reset( $cross_sell_text ) => time(),
			);
			update_option( $option_name, $cross_sell_settings );
			return 0;
		}

		foreach ( $cross_sell_text as $index => $current_text ) {
			if ( ! isset( $cross_sell_settings[ $current_text ] ) ) {
				$cross_sell_settings[ $current_text ] = time();
				update_option( $option_name, $cross_sell_settings );
				return $index;
			}

			$time_elapsed = time() - $cross_sell_settings[ $current_text ];
			if ( $time_elapsed < DAY_IN_SECONDS * 30 ) {
				return $index;
			}
		}

		// If all options are expired, reset the option.
		delete_option( $option_name );
		return 0;
	}

	/**
	 * @param array|string $sale
	 *
	 * @return void
	 */
	private function add_sale( $sale ) {
		if ( ! is_array( self::$sales ) ) {
			// This gets set in the constructor.
			// This check is just here for Psalm analysis.
			return;
		}

		if ( ! is_array( $sale ) || ! isset( $sale['key'] ) ) {
			// if the API response is invalid, $sale may not be an array.
			// if there are no sales from the API, it is returning a "No Entries Found" item with no key, so check for a key as well.
			return;
		}

		if ( ! $this->sale_is_active( $sale ) ) {
			return;
		}

		self::$sales[ $sale['key'] ] = $this->fill_sale( $sale );
	}

	/**
	 * @param array $sale
	 * @return array
	 */
	private function fill_sale( $sale ) {
		$defaults = array(
			'key'                                  => '',
			'starts'                               => '',
			'expires'                              => '',
			// Use 'free', 'personal', 'business', 'elite', 'grandfathered'.
			'who'                                  => 'all',
			'discount_percent'                     => 0,
			'test_group'                           => '',
			'lite_banner_cta_link'                 => '',
			'lite_banner_cta_text'                 => '',
			'menu_cta_link'                        => '',
			'menu_cta_text'                        => '',
			'dashboard_license_cta_link'           => '',
			'dashboard_license_cta_text'           => '',
			'global_settings_license_cta_link'     => '',
			'global_settings_license_cta_text'     => '',
			'global_settings_unlock_more_cta_link' => '',
			'global_settings_unlock_more_cta_text' => '',
			'global_settings_upgrade_cta_link'     => '',
			'builder_sidebar_cta_link'             => '',
			'builder_sidebar_cta_text'             => '',
			'banner_title'                         => '',
			'banner_body'                          => '',
			'banner_icon'                          => '',
			'banner_text_color'                    => '',
			'banner_bg_color'                      => '',
			'banner_cta_link'                      => '',
			'banner_cta_text'                      => '',
			'banner_cta_text_color'                => '',
			'banner_cta_bg_color'                  => '',
		);

		return array_merge( $defaults, $sale );
	}

	/**
	 * Check if a sale is within the active period.
	 *
	 * @since 6.17
	 *
	 * @param array $sale
	 * @return bool
	 */
	private function sale_is_active( $sale ) {
		$starts  = $sale['starts'];
		$expires = $sale['expires'] + DAY_IN_SECONDS;
		$date    = new DateTime( 'now', new DateTimeZone( 'America/New_York' ) );
		$today   = $date->getTimestamp();
		return $today >= $starts && $today <= $expires;
	}

	/**
	 * @since 6.17
	 *
	 * @return array|false
	 */
	public function get_best_sale() {
		if ( ! self::$sales ) {
			return false;
		}

		if ( isset( self::$best_sale ) ) {
			return self::$best_sale;
		}

		$best_sale = false;
		foreach ( self::$sales as $sale ) {
			if ( ! FrmApiHelper::is_for_user( $sale ) ) {
				continue;
			}

			if ( ! $this->matches_ab_group( $sale ) ) {
				continue;
			}

			if ( ! $best_sale || $sale['discount_percent'] > $best_sale['discount_percent'] ) {
				$best_sale = $sale;
			}
		}

		self::$best_sale = $best_sale;
		return self::$best_sale;
	}

	/**
	 * Get text for best sale if applicable.
	 *
	 * @since 6.17
	 *
	 * @param string $key
	 * @return false|string False if no sale is active.
	 */
	public static function get_best_sale_value( $key ) {
		if ( ! isset( self::$instance ) ) {
			self::$instance = new FrmSalesApi();
		}

		$sale = self::$instance->get_best_sale();

		return is_array( $sale ) && ! empty( $sale[ $key ] ) ? $sale[ $key ] : false;
	}

	/**
	 * @since 6.17
	 *
	 * @param array $sale
	 * @return bool True if the sale is a match for the applicable group (if one is defined).
	 */
	private function matches_ab_group( $sale ) {
		if ( ! is_numeric( $sale['test_group'] ) ) {
			// No test group, so return true.
			return true;
		}

		$ab_group = $this->get_ab_group_for_current_site();
		return $ab_group === $sale['test_group'];
	}

	/**
	 * @since 6.17
	 *
	 * @return int 1 or 0.
	 */
	private function get_ab_group_for_current_site() {
		$option = get_option( 'frm_sale_ab_group' );
		if ( ! is_numeric( $option ) ) {
			// Generate either 0 or 1.
			$option = mt_rand( 0, 1 );
			update_option( 'frm_sale_ab_group', $option, false );
		}
		return (int) $option;
	}

	/**
	 * Maybe show banner for the best sale.
	 *
	 * @since 6.21
	 *
	 * @return bool
	 */
	public static function maybe_show_banner() {
		if ( ! current_user_can( 'frm_change_settings' ) ) {
			return false;
		}

		if ( ! isset( self::$instance ) ) {
			self::$instance = new FrmSalesApi();
		}

		$sale = self::$instance->get_best_sale();
		if ( ! $sale || ! is_array( $sale ) ) {
			return false;
		}

		$banner_title = ! empty( $sale['banner_title'] ) ? $sale['banner_title'] : false;
		$banner_body  = ! empty( $sale['banner_body'] ) ? $sale['banner_body'] : false;

		if ( false === $banner_title || false === $banner_body ) {
			return false;
		}

		if ( self::is_banner_dismissed( $sale['key'] ) ) {
			return false;
		}

		$banner_icon       = ! empty( $sale['banner_icon'] ) ? $sale['banner_icon'] : 'generic';
		$banner_bg_color   = ! empty( $sale['banner_bg_color'] ) ? $sale['banner_bg_color'] : false;
		$banner_text_color = ! empty( $sale['banner_text_color'] ) ? $sale['banner_text_color'] : false;
		$banner_cta_link   = ! empty( $sale['banner_cta_link'] ) ? $sale['banner_cta_link'] : false;

		// translators: %s is the discount percentage.
		$banner_cta_text       = ! empty( $sale['banner_cta_text'] ) ? $sale['banner_cta_text'] : sprintf( __( 'GET %s OFF NOW', 'formidable' ), $sale['discount_percent'] . '%' );
		$banner_cta_text_color = ! empty( $sale['banner_cta_text_color'] ) ? $sale['banner_cta_text_color'] : false;
		$banner_cta_bg_color   = ! empty( $sale['banner_cta_bg_color'] ) ? $sale['banner_cta_bg_color'] : false;

		if ( false === $banner_cta_link ) {
			$banner_cta_link = FrmAppHelper::admin_upgrade_link(
				array(
					'medium'  => 'sales-api-banner',
					'content' => $sale['key'],
				)
			);
		}

		$banner_attrs = array(
			'id'       => 'frm_sale_banner',
			'data-url' => $banner_cta_link,
		);

		if ( false === $banner_bg_color || 'gradient' === $banner_bg_color ) {
			$banner_attrs['class'] = 'frm-gradient';
		} else {
			$banner_attrs['style'] = 'background-color: ' . esc_attr( $banner_bg_color ) . ';';
		}

		$cta_attrs = array(
			'href'  => '#',
			'style' => '',
		);
		if ( false !== $banner_cta_text_color ) {
			$cta_attrs['style'] .= 'color: ' . esc_attr( $banner_cta_text_color ) . ';';
		}
		if ( false !== $banner_cta_bg_color ) {
			$cta_attrs['style'] .= 'background-color: ' . esc_attr( $banner_cta_bg_color ) . ';';
		}

		$dismiss_attrs = array(
			'href'  => '#',
			'class' => 'dismiss',
		);

		$content_attrs = array();

		if ( false !== $banner_text_color ) {
			$content_attrs['style'] = 'color: ' . esc_attr( $banner_text_color ) . ';';
			$dismiss_attrs['style'] = 'color: ' . esc_attr( $banner_text_color ) . ';';
		}

		?>
		<div <?php FrmAppHelper::array_to_html_params( $banner_attrs, true ); ?>>
			<div>
				<img src="<?php echo esc_url( FrmAppHelper::plugin_url() . '/images/sales/' . $banner_icon . '.svg' ); ?>" alt="<?php echo esc_attr( $banner_title ); ?>" />
			</div>
			<div <?php FrmAppHelper::array_to_html_params( $content_attrs, true ); ?>>
				<div class="frm-text-md frm-font-semibold">
					<?php echo esc_html( $banner_title ); ?>
				</div>
				<div>
					<?php echo esc_html( $banner_body ); ?>
				</div>
			</div>
			<div>
				<a <?php FrmAppHelper::array_to_html_params( $cta_attrs, true ); ?>>
					<?php echo esc_html( $banner_cta_text ); ?>
				</a>
			</div>
			<a <?php FrmAppHelper::array_to_html_params( $dismiss_attrs, true ); ?>><?php FrmAppHelper::icon_by_class( 'frm_icon_font frm_close_icon' ); ?></a>
		</div>
		<?php

		return true;
	}

	/**
	 * Dismiss a banner via AJAX hook.
	 *
	 * @since 6.21
	 */
	public static function dismiss_banner() {
		FrmAppHelper::permission_check( 'frm_view_forms' );
		check_ajax_referer( 'frm_ajax', 'nonce' );

		if ( ! isset( self::$instance ) ) {
			self::$instance = new FrmSalesApi();
		}

		$sale = self::$instance->get_best_sale();
		if ( ! $sale || ! is_array( $sale ) ) {
			wp_send_json_error();
		}

		$dismissed_sales = get_user_option( 'frm_dismissed_sales', get_current_user_id() );
		if ( ! is_array( $dismissed_sales ) ) {
			$dismissed_sales = array();
		}

		$dismissed_sales[] = $sale['key'];
		update_user_option( get_current_user_id(), 'frm_dismissed_sales', $dismissed_sales );

		wp_send_json_success();
	}

	/**
	 * @param string $key
	 * @return bool
	 */
	private static function is_banner_dismissed( $key ) {
		$dismissed_sales = get_user_option( 'frm_dismissed_sales', get_current_user_id() );
		return is_array( $dismissed_sales ) && in_array( $key, $dismissed_sales, true );
	}

	public static function menu() {
		if ( false === self::$sales ) {
			new self();
		}

		if ( ! self::$cross_sell_text || ! self::$cross_sell_link ) {
			return;
		}

		add_submenu_page(
			'formidable',
			esc_html( self::$cross_sell_text ) . ' | Formidable',
			esc_html( self::$cross_sell_text ),
			'activate_plugins',
			'frm-sales-api-cross-sell',
			function () {
				// There is no page. The redirect logic is handled below, before this callback is triggered.
			}
		);

		add_action(
			'admin_init',
			function () {
				if ( ! current_user_can( 'activate_plugins' ) ) {
					return;
				}

				if ( 'frm-sales-api-cross-sell' === FrmAppHelper::simple_get( 'page' ) && ! empty( self::$cross_sell_link ) ) {
					wp_redirect( self::$cross_sell_link );
					exit;
				}
			}
		);
	}
}