<?php

if ( ! interface_exists( 'WPDesk_Requirement_Checker' ) ) {
	require_once __DIR__ . '/Requirement_Checker.php';
}

if ( ! class_exists( 'WPDesk_Basic_Requirement_Checker' ) ) {
	/**
	 * Checks requirements for plugin
	 * have to be compatible with PHP 5.3.x
	 */
	class WPDesk_Basic_Requirement_Checker implements WPDesk_Requirement_Checker {

		const EXTENSION_NAME_OPENSSL = 'openssl';

		const HOOK_ADMIN_NOTICES_ACTION = 'admin_notices';

		const HOOK_PLUGIN_DEACTIVATED_ACTION = 'deactivated_plugin';

		const HOOK_PLUGIN_ACTIVATED_ACTION = 'activated_plugin';

		const PLUGIN_INFO_KEY_NICE_NAME = 'nice_name';

		const PLUGIN_INFO_KEY_NAME = 'name';

		const PLUGIN_INFO_VERSION = 'version';

		const PLUGIN_INFO_FAKE_REQUIRED_MINIMUM_VERSION = '0.0';

		const PLUGIN_INFO_APPEND_PLUGIN_DATA = 'required_version';

		const PLUGIN_INFO_TRANSIENT_NAME = 'wpdesk_requirements_plugins_data';

		const CACHE_TIME = 16;

		const EXPIRATION_TIME = 'expiration_time';

		const PLUGINS = 'plugins';

		/** @var string */
		protected $plugin_name;

		/** @var string */
		private $plugin_file;

		/** @var string */
		private $min_php_version;

		/** @var string */
		private $min_wp_version;

		/** @var string|null */
		private $min_wc_version = null;

		/** @var int|null */
		private $min_openssl_version = null;

		/** @var array */
		protected $plugin_require;

		/** @var array */
		protected $class_require;

		/** @var bool */
		protected $should_check_plugin_versions = false;

		/** @var array */
		private $module_require;

		/** @var array */
		private $setting_require;

		/** @var array */
		protected $notices;

		/** @var @string */
		private $text_domain;
		/**
		 * @var mixed|true
		 */
		private $use_transients;

		/**
		 * @param string $plugin_file
		 * @param string $plugin_name
		 * @param string $text_domain
		 * @param string $php_version
		 * @param string $wp_version
		 * @param bool   $use_transients
		 */
		public function __construct( $plugin_file, $plugin_name, $text_domain, $php_version, $wp_version, $use_transients = true ) {
			$this->plugin_file    = $plugin_file;
			$this->plugin_name    = $plugin_name;
			$this->text_domain    = $text_domain;
			$this->use_transients = $use_transients;

			$this->set_min_php_require( $php_version );
			$this->set_min_wp_require( $wp_version );

			$this->plugin_require  = array();
			$this->module_require  = array();
			$this->setting_require = array();
			$this->notices         = array();
			$this->class_require   = array();
		}

		/**
		 * @param string $version
		 *
		 * @return $this
		 */
		public function set_min_php_require( $version ) {
			$this->min_php_version = $version;

			return $this;
		}

		/**
		 * @param string $version
		 *
		 * @return $this
		 */
		public function set_min_wp_require( $version ) {
			$this->min_wp_version = $version;

			return $this;
		}

		/**
		 * @param string $version
		 *
		 * @return $this
		 */
		public function set_min_wc_require( $version ) {
			$this->min_wc_version = $version;

			return $this;
		}

		/**
		 * @param $version
		 *
		 * @return $this
		 */
		public function set_min_openssl_require( $version ) {
			$this->min_openssl_version = $version;

			return $this;
		}

		/**
		 * @param string $plugin_name Name in wp format dir/file.php
		 * @param string $nice_plugin_name Nice plugin name for better looks in notice
		 * @param string $plugin_require_version required plugin minimum version
		 *
		 * @return $this
		 */
		public function add_plugin_require( $plugin_name, $nice_plugin_name = null, $plugin_require_version = null ) {
			if ( $plugin_require_version ) {
				$this->should_check_plugin_versions = true;
			}
			$this->plugin_require[ $plugin_name ] = array(
				self::PLUGIN_INFO_KEY_NAME      => $plugin_name,
				self::PLUGIN_INFO_KEY_NICE_NAME => $nice_plugin_name === null ? $plugin_name : $nice_plugin_name,
				self::PLUGIN_INFO_VERSION       => $plugin_require_version === null ?
					self::PLUGIN_INFO_FAKE_REQUIRED_MINIMUM_VERSION : $plugin_require_version,
			);

			return $this;
		}

		/**
		 * Add plugin to require list. Plugin is from repository so we can ask for installation.
		 *
		 * @param string $plugin_name Name in wp format dir/file.php
		 * @param string $version Required version of the plugin.
		 * @param string $nice_plugin_name Nice plugin name for better looks in notice
		 *
		 * @return $this
		 */
		public function add_plugin_repository_require( $plugin_name, $version, $nice_plugin_name = null ) {
			$this->plugin_require[ $plugin_name ] = array(
				self::PLUGIN_INFO_KEY_NAME      => $plugin_name,
				self::PLUGIN_INFO_VERSION       => $version,
				'repository_url'                => 'http://downloads.wordpress.org/plugin/' . dirname( $plugin_name ) . '.latest-stable.zip',
				self::PLUGIN_INFO_KEY_NICE_NAME => $nice_plugin_name === null ? $plugin_name : $nice_plugin_name
			);

			return $this;
		}

		/**
		 * Add a class to require list.
		 *
		 * @param string $class_name       Name of the class.
		 * @param string $plugin_nice_name Nice plugin name for better looks in notice.
		 *
		 * @return $this
		 *
		 */
		public function add_class_require( $class_name, $plugin_nice_name ) {
			$this->class_require[$class_name] = $plugin_nice_name;

			return $this;
		}

		/**
		 * @param string $module_name
		 * @param string $nice_name Nice module name for better looks in notice
		 *
		 * @return $this
		 */
		public function add_php_module_require( $module_name, $nice_name = null ) {
			if ( $nice_name === null ) {
				$this->module_require[ $module_name ] = $module_name;
			} else {
				$this->module_require[ $module_name ] = $nice_name;
			}

			return $this;
		}

		/**
		 * @param string $setting
		 * @param mixed $value
		 *
		 * @return $this
		 */
		public function add_php_setting_require( $setting, $value ) {
			$this->setting_require[ $setting ] = $value;

			return $this;
		}

		/**
		 * Returns true if are requirements are met.
		 *
		 * @return bool
		 */
		public function are_requirements_met() {
			$this->notices = $this->prepare_requirement_notices();

			return count( $this->notices ) === 0;
		}

		/**
		 * @return array
		 */
		private function prepare_requirement_notices() {
			$notices = array();
			if ( ! self::is_php_at_least( $this->min_php_version ) ) {
				$notices[] = $this->prepare_notice_message( sprintf( __( 'The &#8220;%s&#8221; plugin cannot run on PHP versions older than %s. Please contact your host and ask them to upgrade.',
					'wp-basic-requirements' ), esc_html( $this->plugin_name ), $this->min_php_version ) );
			}
			if ( ! self::is_wp_at_least( $this->min_wp_version ) ) {
				$notices[] = $this->prepare_notice_message( sprintf( __( 'The &#8220;%s&#8221; plugin cannot run on WordPress versions older than %s. Please update WordPress.',
					'wp-basic-requirements' ), esc_html( $this->plugin_name ), $this->min_wp_version ) );
			}
			if ( $this->min_wc_version !== null && $this->can_check_plugin_version() && ! self::is_wc_at_least( $this->min_wc_version ) ) {
				$notices[] = $this->prepare_notice_message( sprintf( __( 'The &#8220;%s&#8221; plugin cannot run on WooCommerce versions older than %s. Please update WooCommerce.',
					'wp-basic-requirements' ), esc_html( $this->plugin_name ), $this->min_wc_version ) );
			}
			if ( $this->min_openssl_version !== null && ! self::is_open_ssl_at_least( $this->min_openssl_version ) ) {
				$notices[] = $this->prepare_notice_message( sprintf( __( 'The &#8220;%s&#8221; plugin cannot run without OpenSSL module version at least %s. Please update OpenSSL module.',
					'wp-basic-requirements' ), esc_html( $this->plugin_name ),
					'0x' . dechex( $this->min_openssl_version ) ) );
			}

			$notices = $this->append_plugin_require_notices( $notices );
			$notices = $this->append_module_require_notices( $notices );
			$notices = $this->append_settings_require_notices( $notices );
			$notices = $this->append_class_require_notices( $notices );
			if ( $this->should_check_plugin_versions ) {
				$notices = $this->check_minimum_require_plugins_version_and_append_notices( $notices );
			}

			return $notices;
		}

		/**
		 * @param $min_version
		 *
		 * @return mixed
		 */
		public static function is_php_at_least( $min_version ) {
			return version_compare( PHP_VERSION, $min_version, '>=' );
		}

		/**
		 * Prepares message in html format
		 *
		 * @param string $message
		 *
		 * @return string
		 */
		protected function prepare_notice_message( $message ) {
			return '<div class="error"><p>' . $message . '</p></div>';
		}

		public function get_text_domain() {
			return $this->text_domain;
		}

		/**
		 * @param string $min_version
		 *
		 * @return bool
		 */
		public static function is_wp_at_least( $min_version ) {
			return version_compare( get_bloginfo( 'version' ), $min_version, '>=' );
		}

		/**
		 * Are plugins loaded so we can check the version
		 *
		 * @return bool
		 */
		private function can_check_plugin_version() {
			return did_action( 'plugins_loaded' ) > 0;
		}

		/**
		 * Checks if plugin is active and have designated version. Needs to be enabled in deferred way.
		 *
		 * @param string $min_version
		 *
		 * @return bool
		 */
		public static function is_wc_at_least( $min_version ) {
			return defined( 'WC_VERSION' ) &&
			       version_compare( WC_VERSION, $min_version, '>=' );
		}

		/**
		 * Checks if ssl version is valid
		 *
		 * @param int $required_version Version in hex. Version 9.6 is 0x000906000
		 *
		 * @return bool
		 * @see https://www.openssl.org/docs/man1.1.0/crypto/OPENSSL_VERSION_NUMBER.html
		 *
		 */
		public static function is_open_ssl_at_least( $required_version ) {
			return defined( 'OPENSSL_VERSION_NUMBER' ) && OPENSSL_VERSION_NUMBER > (int) $required_version;
		}

		/**
		 * @param $notices array
		 *
		 * @return array
		 */
		private function check_minimum_require_plugins_version_and_append_notices( $notices ) {
			$required_plugins = $this->retrieve_required_plugins_data();
			if ( count( $required_plugins ) > 0 ) {
				foreach ( $required_plugins as $plugin ) {
					if ( isset( $plugin['Version'] ) && version_compare( $plugin['Version'], $plugin[ self::PLUGIN_INFO_APPEND_PLUGIN_DATA ], '<=' ) ) {
						$notices[] = $this->prepare_notice_message(
							sprintf(
								__( 'The &#8220;%1$s&#8221; plugin requires at least %2$s version of %3$s to work correctly. Please update it to its latest release.', 'wp-basic-requirements' ),
								esc_html( $this->plugin_name ),
								$plugin[ self::PLUGIN_INFO_APPEND_PLUGIN_DATA ],
								$plugin['Name']
							)
						);
					}
				}
			}

			return $notices;
		}

		/**
		 * Check the plugins directory and retrieve all plugin files with plugin data.
		 *
		 * @param bool $use_transients
		 *
		 * @return array In format [ 'plugindir/pluginfile.php' => ['Name' => 'Plugin Name', 'Version' => '1.0.1', ...],  ]
		 */
		private static function retrieve_plugins_data_in_transient( $use_transients = true ) {
			$current_time = time();
			$plugins = self::get_plugins_data_from_cache( $use_transients, $current_time );

			if ( $plugins === false ) {
				static $never_executed = true;
				if ( $never_executed ) {
					$never_executed = false;
					/** Required when WC starts later and these data should be in cache */
					add_filter( 'extra_plugin_headers', function( $headers = array() ) {
						$headers[] = 'WC tested up to';
						$headers[] = 'WC requires at least';
						$headers[] = 'Woo';

						return array_unique( $headers );
					} );
				}

				if ( ! function_exists( 'get_plugins' ) ) {
					require_once ABSPATH . '/wp-admin/includes/plugin.php';
				}

				$plugins = function_exists( 'get_plugins' ) ? get_plugins() : array();

				self::update_plugins_data_in_cache( $plugins, $use_transients, $current_time );
			}

			return $plugins;
		}

		/**
		 * @param bool $use_transients
		 * @param int $current_time
		 *
		 * @return array|false
		 */
		private static function get_plugins_data_from_cache( $use_transients, $current_time ) {
			if ( $use_transients ) {
				return get_transient( self::PLUGIN_INFO_TRANSIENT_NAME );
			} else {
				$plugins_option_value = get_option( self::PLUGIN_INFO_TRANSIENT_NAME );
				if ( is_array( $plugins_option_value )
				     && isset( $plugins_option_value[ self::EXPIRATION_TIME ], $plugins_option_value[ self::PLUGINS ] )
				     && (int) $plugins_option_value[ self::EXPIRATION_TIME ] > $current_time
				) {
					return $plugins_option_value[ self::PLUGINS ];
				}
			}

			return false;
		}

		/**
		 * @param array $plugins
		 * @param bool $use_transients
		 * @param int $current_time
		 */
		private static function update_plugins_data_in_cache( $plugins, $use_transients, $current_time ) {
			if ( $use_transients ) {
				set_transient( self::PLUGIN_INFO_TRANSIENT_NAME, $plugins, self::CACHE_TIME );
			} else {
				update_option( self::PLUGIN_INFO_TRANSIENT_NAME, array(
					self::EXPIRATION_TIME => $current_time + self::CACHE_TIME,
					self::PLUGINS         => $plugins,
				) );
			}
		}

		/**
		 * Check the plugins directory and retrieve all required plugin files with plugin data.
		 *
		 * @return array In format [ 'plugindir/pluginfile.php' => ['Name' => 'Plugin Name', 'Version' => '1.0.1', 'required_version' => '1.0.2']...  ]
		 */
		private function retrieve_required_plugins_data() {
			$require_plugins = array();
			$plugins         = self::retrieve_plugins_data_in_transient( $this->use_transients );
			if ( is_array( $plugins ) ) {
				if ( count( $plugins ) > 0 ) {
					if ( ! empty( $this->plugin_require ) ) {
						foreach ( $this->plugin_require as $plugin ) {
							$plugin_file_name = $plugin[ self::PLUGIN_INFO_KEY_NAME ];
							$plugin_version   = $plugin[ self::PLUGIN_INFO_VERSION ];

							if ( self::is_wp_plugin_active( $plugin_file_name ) ) {
								$require_plugins[ $plugin_file_name ]                                         = $plugins[ $plugin_file_name ];
								$require_plugins[ $plugin_file_name ][ self::PLUGIN_INFO_APPEND_PLUGIN_DATA ] = $plugin_version;
							}
						}
					}
				}
			}

			return $require_plugins;
		}

		/**
		 * @param array $notices
		 *
		 * @return array
		 */
		private function append_plugin_require_notices( $notices ) {
			if ( count( $this->plugin_require ) > 0 ) {
				foreach ( $this->plugin_require as $plugin_name => $plugin_info ) {
					$notice = null;
					if ( isset( $plugin_info['repository_url'] ) ) {
						$notice = $this->prepare_plugin_repository_require_notice( $plugin_info );
					} elseif ( ! self::is_wp_plugin_active( $plugin_name ) ) {
						$notice = $this->prepare_notice_message( sprintf( __( 'The &#8220;%s&#8221; plugin cannot run without %s active. Please install and activate %s plugin.',
							'wp-basic-requirements' ), esc_html( $this->plugin_name ),
							esc_html( basename( $plugin_info[ self::PLUGIN_INFO_KEY_NICE_NAME ] ) ),
							esc_html( basename( $plugin_info[ self::PLUGIN_INFO_KEY_NICE_NAME ] ) ) ) );
					}

					if ( $notice !== null ) {
						$notices[] = $notice;
					}
				}
			}

			return $notices;
		}

		/**
		 * @param array $notices
		 *
		 * @return array
		 */
		private function append_class_require_notices( $notices ) {
			foreach ( $this->class_require as $class_name => $plugin_nice_name ) {
				if ( class_exists( $class_name ) ) {
					continue;
				}

				$notices[] = $this->prepare_notice_message(
					sprintf(
						'The &#8220;%s&#8221; plugin cannot run without %s active. Please install and activate %s plugin.',
						esc_html( $this->plugin_name ),
						esc_html( $plugin_nice_name ),
						esc_html( $plugin_nice_name )
					)
				);
			}

			return $notices;
		}

		/**
		 * Prepares WP install url and injects info about plugin to the WP update engine.
		 *
		 * @param array $plugin_info
		 *
		 * @return string
		 */
		private function prepare_plugin_repository_install_url( $plugin_info ) {
			$slug        = basename( explode( '/', $plugin_info[ self::PLUGIN_INFO_KEY_NAME ] )[0] );
			$install_url = self_admin_url( 'update.php?action=install-plugin&plugin=' . $slug );
			if ( function_exists( 'wp_nonce_url' ) && function_exists( 'wp_create_nonce' ) ) {
				$install_url = wp_nonce_url( $install_url, 'install-plugin_' . $slug );
			}

			return $install_url;
        }

		/**
		 * @param array $plugin_info Internal required plugin info data.
		 *
		 * @return string|null Return null if no notice is needed.
		 */
		private function prepare_plugin_repository_require_notice( $plugin_info ) {
			$name      = $plugin_info[ self::PLUGIN_INFO_KEY_NAME ];
			$nice_name = $plugin_info[ self::PLUGIN_INFO_KEY_NICE_NAME ];

			if ( ! self::is_wp_plugin_active( $name ) ) {
				if ( ! self::is_wp_plugin_installed( $name, $this->use_transients ) ) {
					$install_url = $this->prepare_plugin_repository_install_url( $plugin_info );

					return $this->prepare_notice_message( sprintf( wp_kses( __( 'The &#8220;%s&#8221; plugin requires free %s plugin. <a href="%s">Install %s</a>',
						'wp-basic-requirements' ), array( 'a' => array( 'href' => array() ) ) ),
						$this->plugin_name, $nice_name, esc_url( $install_url ), $nice_name ) );
				}
				$activate_url = 'plugins.php?action=activate&plugin=' . urlencode( $plugin_info[ self::PLUGIN_INFO_KEY_NAME ] ) . '&plugin_status=all&paged=1&s';
				if ( function_exists( 'wp_create_nonce' ) ) {
					$activate_url .= '&_wpnonce=' . urlencode( wp_create_nonce( 'activate-plugin_' . $name ) );
				}

				return $this->prepare_notice_message( sprintf( wp_kses( __( 'The &#8220;%s&#8221; plugin requires activating %s plugin. <a href="%s">Activate %s</a>',
					'wp-basic-requirements' ), array( 'a' => array( 'href' => array() ) ) ),
					$this->plugin_name, $nice_name, esc_url( admin_url( $activate_url ) ), $nice_name ) );
			}

			return null;
		}

		/**
		 * Checks if plugin is active. Needs to be used in deferred way.
		 *
		 * @param string $plugin_file
		 *
		 * @return bool
		 */
		public static function is_wp_plugin_active( $plugin_file ) {
			$active_plugins = (array) get_option( 'active_plugins', array() );

			if ( is_multisite() ) {
				$active_plugins = array_merge( $active_plugins, get_site_option( 'active_sitewide_plugins', array() ) );
			}

			return in_array( $plugin_file, $active_plugins ) || array_key_exists( $plugin_file, $active_plugins );
		}

		/**
		 * Checks if plugin is installed. Needs to be enabled in deferred way.
		 *
		 * @param string $plugin_file
		 * @param bool   $use_transients
		 *
		 * @return bool
		 */
		public static function is_wp_plugin_installed( $plugin_file, $use_transients = false ) {
			$plugins_data = self::retrieve_plugins_data_in_transient( $use_transients );

			return array_key_exists( $plugin_file, (array) $plugins_data );
		}

		/**
		 * @param array $notices
		 *
		 * @return array
		 */
		private function append_module_require_notices( $notices ) {
			if ( count( $this->module_require ) > 0 ) {
				foreach ( $this->module_require as $module_name => $nice_module_name ) {
					if ( ! self::is_module_active( $module_name ) ) {
						$notices[] = $this->prepare_notice_message( sprintf( __( 'The &#8220;%s&#8221; plugin cannot run without %s PHP module installed. Please contact your host and ask them to install %s.',
							'wp-basic-requirements' ), esc_html( $this->plugin_name ),
							esc_html( basename( $nice_module_name ) ), esc_html( basename( $nice_module_name ) ) ) );
					}
				}
			}

			return $notices;
		}

		/**
		 * @param string $name
		 *
		 * @return bool
		 */
		public static function is_module_active( $name ) {
			return extension_loaded( $name );
		}

		/**
		 * @param array $notices
		 *
		 * @return array
		 */
		private function append_settings_require_notices( $notices ) {
			if ( count( $this->setting_require ) > 0 ) {
				foreach ( $this->setting_require as $setting => $value ) {
					if ( ! self::is_setting_set( $setting, $value ) ) {
						$notices[] = $this->prepare_notice_message( sprintf( __( 'The &#8220;%s&#8221; plugin cannot run without %s PHP setting set to %s. Please contact your host and ask them to set %s.',
							'wp-basic-requirements' ), esc_html( $this->plugin_name ),
							esc_html( basename( $setting ) ),
							esc_html( basename( $value ) ), esc_html( basename( $setting ) ) ) );
					}
				}
			}

			return $notices;
		}

		/**
		 * @param string $name
		 * @param mixed $value
		 *
		 * @return bool
		 */
		public static function is_setting_set( $name, $value ) {
			return ini_get( $name ) === (string) $value;
		}

		/**
		 * @return void
		 *
		 * @deprecated use render_notices or disable_plugin
		 */
		public function disable_plugin_render_notice() {
			add_action( self::HOOK_ADMIN_NOTICES_ACTION, array(
				$this,
				'handle_render_notices_action'
			) );
		}

		/**
		 * Renders requirement notices in admin panel
		 *
		 * @return void
		 */
		public function render_notices() {
			add_action( self::HOOK_ADMIN_NOTICES_ACTION, array(
				$this,
				'handle_render_notices_action'
			) );
		}

		/**
		 * Renders requirement notices in admin panel
		 *
		 * @return void
		 */
		public function disable_plugin() {
			add_action( self::HOOK_ADMIN_NOTICES_ACTION, array(
				$this,
				'handle_deactivate_action'
			) );
		}

		/**
		 * @return void
		 * @internal Do not use as public. Public only for wp action.
		 *
		 */
		public function handle_deactivate_action() {
			if ( isset( $this->plugin_file ) ) {
				deactivate_plugins( plugin_basename( $this->plugin_file ) );

				$this->clear_plugin_info_data();
			}
		}

		/**
		 * Triggers the transient delete after plugin deactivated
		 *
		 * @return void
		 */
		public function transient_delete_on_plugin_version_changed() {
			add_action( self::HOOK_PLUGIN_DEACTIVATED_ACTION, array(
				$this,
				'clear_plugin_info_data'
			) );
			add_action( self::HOOK_PLUGIN_ACTIVATED_ACTION, array(
				$this,
				'clear_plugin_info_data'
			) );
		}

		/**
		 * Handles the transient delete
		 *
		 * @return void
		 */
		public function clear_plugin_info_data() {
			delete_transient( self::PLUGIN_INFO_TRANSIENT_NAME );
			delete_option( self::PLUGIN_INFO_TRANSIENT_NAME );
		}

		/**
		 * Should be called as WordPress action
		 *
		 * @return void
		 * @internal Do not use as public. Public only for wp action.
		 *
		 */
		public function handle_render_notices_action() {
			foreach ( $this->notices as $notice ) {
				echo wp_kses_post( $notice );
			}
		}

	}
}
