<?php

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

if ( ! class_exists( 'ACF_Updates' ) ) :

	class ACF_Updates {

		/** @var string The ACF_Updates version */
		var $version = '2.4';

		/** @var array The array of registered plugins */
		var $plugins = array();

		/** @var int Counts the number of plugin update checks */
		var $checked = 0;

		/*
		*  __construct
		*
		*  Sets up the class functionality.
		*
		*  @date    5/03/2014
		*  @since   5.0.0
		*
		*  @param   void
		*  @return  void
		*/

		function __construct() {

			// append update information to transient
			add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'modify_plugins_transient' ), 10, 1 );

			// modify plugin data visible in the 'View details' popup
			add_filter( 'plugins_api', array( $this, 'modify_plugin_details' ), 10, 3 );
		}

		/*
		*  add_plugin
		*
		*  Registeres a plugin for updates.
		*
		*  @date    8/4/17
		*  @since   5.5.10
		*
		*  @param   array $plugin The plugin array.
		*  @return  void
		*/

		function add_plugin( $plugin ) {

			// validate
			$plugin = wp_parse_args(
				$plugin,
				array(
					'id'       => '',
					'key'      => '',
					'slug'     => '',
					'basename' => '',
					'version'  => '',
				)
			);

			// Check if is_plugin_active() function exists. This is required on the front end of the
			// site, since it is in a file that is normally only loaded in the admin.
			if ( ! function_exists( 'is_plugin_active' ) ) {
				require_once ABSPATH . 'wp-admin/includes/plugin.php';
			}

			// add if is active plugin (not included in theme)
			if ( is_plugin_active( $plugin['basename'] ) ) {
				$this->plugins[ $plugin['basename'] ] = $plugin;
			}
		}

		/**
		 *  get_plugin_by
		 *
		 *  Returns a registered plugin for the give key and value.
		 *
		 *  @date    3/8/18
		 *  @since   5.7.2
		 *
		 *  @param   string $key The array key to compare
		 *  @param   string $value The value to compare against
		 *  @return  array|false
		 */

		function get_plugin_by( $key = '', $value = null ) {
			foreach ( $this->plugins as $plugin ) {
				if ( $plugin[ $key ] === $value ) {
					return $plugin;
				}
			}
			return false;
		}

		/*
		 * request
		 *
		 * Makes a request to the ACF connect server.
		 *
		 * @date    8/4/17
		 * @since   5.5.10
		 *
		 * @param   string $endpoint The API endpoint.
		 * @param   array $body The body to post.
		 * @return  (array|string|WP_Error)
		 */
		function request( $endpoint = '', $body = null ) {

			// Determine URL.
			$url = "https://connect.advancedcustomfields.com/$endpoint";

			// Staging environment.
			if ( defined( 'ACF_DEV_API' ) && ACF_DEV_API ) {
				$url = trailingslashit( ACF_DEV_API ) . $endpoint;
				acf_log( $url, $body );
			}

			// Make request.
			$raw_response = wp_remote_post(
				$url,
				array(
					'timeout' => 10,
					'body'    => $body,
				)
			);

			// Handle response error.
			if ( is_wp_error( $raw_response ) ) {
				return $raw_response;

				// Handle http error.
			} elseif ( wp_remote_retrieve_response_code( $raw_response ) != 200 ) {
				return new WP_Error( 'server_error', wp_remote_retrieve_response_message( $raw_response ) );
			}

			// Decode JSON response.
			$json = json_decode( wp_remote_retrieve_body( $raw_response ), true );

			// Allow non json value.
			if ( $json === null ) {
				return wp_remote_retrieve_body( $raw_response );
			}

			// return
			return $json;
		}

		/**
		 *  Returns update information for the given plugin id.
		 *
		 *  @since   5.5.10
		 *
		 *  @param   string  $id The plugin id such as 'pro'.
		 *  @param   boolean $force_check Bypasses cached result. Defaults to false.
		 *  @return  array|WP_Error
		 */
		public function get_plugin_info( $id = '', $force_check = false ) {
			$transient_name = 'acf_plugin_info_' . $id;

			// check cache but allow for $force_check override.
			if ( ! $force_check ) {
				$transient = get_transient( $transient_name );
				if ( $transient !== false ) {
					return $transient;
				}
			}

			$response = $this->request( 'v2/plugins/get-info?p=' . $id );

			// convert string (misc error) to WP_Error object.
			if ( is_string( $response ) ) {
				$response = new WP_Error( 'server_error', esc_html( $response ) );
			}

			// allow json to include expiration but force minimum and max for safety.
			$expiration = $this->get_expiration( $response, DAY_IN_SECONDS, MONTH_IN_SECONDS );

			// update transient.
			set_transient( $transient_name, $response, $expiration );

			return $response;
		}

		/**
		 *  get_plugin_update
		 *
		 *  Returns specific data from the 'update-check' response.
		 *
		 *  @date    3/8/18
		 *  @since   5.7.2
		 *
		 *  @param   string  $basename The plugin basename.
		 *  @param   boolean $force_check Bypasses cached result. Defaults to false
		 *  @return  array
		 */

		function get_plugin_update( $basename = '', $force_check = false ) {

			// get updates
			$updates = $this->get_plugin_updates( $force_check );

			// check for and return update
			if ( is_array( $updates ) && isset( $updates['plugins'][ $basename ] ) ) {
				return $updates['plugins'][ $basename ];
			}
			return false;
		}


		/**
		 *  get_plugin_updates
		 *
		 *  Checks for plugin updates.
		 *
		 *  @date    8/7/18
		 *  @since   5.6.9
		 *  @since   5.7.2 Added 'checked' comparison
		 *
		 *  @param   boolean $force_check Bypasses cached result. Defaults to false.
		 *  @return  array|WP_Error.
		 */

		function get_plugin_updates( $force_check = false ) {

			// var
			$transient_name = 'acf_plugin_updates';

			// construct array of 'checked' plugins
			// sort by key to avoid detecting change due to "include order"
			$checked = array();
			foreach ( $this->plugins as $basename => $plugin ) {
				$checked[ $basename ] = $plugin['version'];
			}
			ksort( $checked );

			// $force_check prevents transient lookup
			if ( ! $force_check ) {
				$transient = get_transient( $transient_name );

				// if cached response was found, compare $transient['checked'] against $checked and ignore if they don't match (plugins/versions have changed)
				if ( is_array( $transient ) ) {
					$transient_checked = isset( $transient['checked'] ) ? $transient['checked'] : array();
					if ( wp_json_encode( $checked ) !== wp_json_encode( $transient_checked ) ) {
						$transient = false;
					}
				}
				if ( $transient !== false ) {
					return $transient;
				}
			}

			// vars
			$post = array(
				'plugins' => wp_json_encode( $this->plugins ),
				'wp'      => wp_json_encode(
					array(
						'wp_name'     => get_bloginfo( 'name' ),
						'wp_url'      => acf_get_home_url(),
						'wp_version'  => get_bloginfo( 'version' ),
						'wp_language' => get_bloginfo( 'language' ),
						'wp_timezone' => get_option( 'timezone_string' ),
						'php_version' => PHP_VERSION,
					)
				),
				'acf'     => wp_json_encode(
					array(
						'acf_version' => get_option( 'acf_version' ),
						'acf_pro'     => ( defined( 'ACF_PRO' ) && ACF_PRO ),
						'block_count' => acf_pro_get_registered_block_count(),
					)
				),
			);

			// request
			$response = $this->request( 'v2/plugins/update-check', $post );

			// append checked reference
			if ( is_array( $response ) ) {
				$response['checked'] = $checked;
			}

			// allow json to include expiration but force minimum and max for safety
			$expiration = $this->get_expiration( $response, DAY_IN_SECONDS, MONTH_IN_SECONDS );

			// update transient
			set_transient( $transient_name, $response, $expiration );

			// return
			return $response;
		}

		/**
		 *  This function safely gets the expiration value from a response.
		 *
		 *  @since   5.6.9
		 *
		 *  @param   mixed $response The response from the server. Default false.
		 *  @param   int   $min The minimum expiration limit. Default 0.
		 *  @param   int   $max The maximum expiration limit. Default 0.
		 *  @return  int
		 */
		public function get_expiration( $response = false, $min = 0, $max = 0 ) {
			$expiration = 0;

			// check possible error conditions.
			if ( is_wp_error( $response ) || is_string( $response ) ) {
				return 5 * MINUTE_IN_SECONDS;
			}

			// use the server requested expiration if present.
			if ( is_array( $response ) && isset( $response['expiration'] ) ) {
				$expiration = (int) $response['expiration'];
			}

			// use the minimum if neither check matches, or ensure the server expiration isn't lower than our minimum.
			if ( $expiration < $min ) {
				return $min;
			}

			// ensure the server expiration isn't higher than our max.
			if ( $expiration > $max ) {
				return $max;
			}

			return $expiration;
		}

		/*
		*  refresh_plugins_transient
		*
		*  Deletes transients and allows a fresh lookup.
		*
		*  @date    11/4/17
		*  @since   5.5.10
		*
		*  @param   void
		*  @return  void
		*/

		function refresh_plugins_transient() {
			delete_site_transient( 'update_plugins' );
			delete_transient( 'acf_plugin_updates' );
		}

		/*
		*  modify_plugins_transient
		*
		*  Called when WP updates the 'update_plugins' site transient. Used to inject ACF plugin update info.
		*
		*  @date    16/01/2014
		*  @since   5.0.0
		*
		*  @param   object $transient
		*  @return  $transient
		*/

		function modify_plugins_transient( $transient ) {

			// bail early if no response (error)
			if ( ! isset( $transient->response ) ) {
				return $transient;
			}

			// force-check (only once)
			$force_check = ( $this->checked == 0 ) ? ! empty( $_GET['force-check'] ) : false; // phpcs:ignore -- False positive, value not used.

			// fetch updates (this filter is called multiple times during a single page load)
			$updates = $this->get_plugin_updates( $force_check );

			// append
			if ( is_array( $updates ) ) {
				foreach ( $updates['plugins'] as $basename => $update ) {
					$transient->response[ $basename ] = (object) $update;
				}
			}

			// increase
			$this->checked++;

			// return
			return $transient;
		}

		/*
		*  modify_plugin_details
		*
		*  Returns the plugin data visible in the 'View details' popup
		*
		*  @date    17/01/2014
		*  @since   5.0.0
		*
		*  @param   object $result
		*  @param   string $action
		*  @param   object $args
		*  @return  $result
		*/

		function modify_plugin_details( $result, $action = null, $args = null ) {

			// vars
			$plugin = false;

			// only for 'plugin_information' action
			if ( $action !== 'plugin_information' ) {
				return $result;
			}

			// find plugin via slug
			$plugin = $this->get_plugin_by( 'slug', $args->slug );
			if ( ! $plugin ) {
				return $result;
			}

			// connect
			$response = $this->get_plugin_info( $plugin['id'] );

			// bail early if no response
			if ( ! is_array( $response ) ) {
				return $result;
			}

			// remove tags (different context)
			unset( $response['tags'] );

			// convert to object
			$response = (object) $response;

			// sections
			$sections = array(
				'description'    => '',
				'installation'   => '',
				'changelog'      => '',
				'upgrade_notice' => '',
			);
			foreach ( $sections as $k => $v ) {
				$sections[ $k ] = $response->$k;
			}
			$response->sections = $sections;

			// return
			return $response;
		}
	}


	/*
	*  acf_updates
	*
	*  The main function responsible for returning the one true acf_updates instance to functions everywhere.
	*  Use this function like you would a global variable, except without needing to declare the global.
	*
	*  Example: <?php $acf_updates = acf_updates(); ?>
	*
	*  @date    9/4/17
	*  @since   5.5.12
	*
	*  @param   void
	*  @return  object
	*/

	function acf_updates() {
		global $acf_updates;
		if ( ! isset( $acf_updates ) ) {
			$acf_updates = new ACF_Updates();
		}
		return $acf_updates;
	}


	/*
	*  acf_register_plugin_update
	*
	*  Alias of acf_updates()->add_plugin().
	*
	*  @type    function
	*  @date    12/4/17
	*  @since   5.5.10
	*
	*  @param   array $plugin
	*  @return  void
	*/

	function acf_register_plugin_update( $plugin ) {
		acf_updates()->add_plugin( $plugin );
	}

endif; // class_exists check


