<?php
/**
 * Plugin Update Engine Class
 *
 * This is a direct port to Tribe Commons of the PUE classes contained
 * in The Events Calendar.
 *
 * @todo switch all plugins over to use the PUE utilities here in Commons
 */

use function TEC\Common\StellarWP\Uplink\get_resource;

// Don't load directly.
if ( ! defined( 'ABSPATH' ) ) {
	die( '-1' );
}

// phpcs:disable PEAR.NamingConventions.ValidClassName.Invalid
// phpcs:disable TEC.Classes.ValidClassName.NotSnakeCase

if ( ! class_exists( 'Tribe__PUE__Checker' ) ) {
	/**
	 * A custom plugin update checker.
	 *
	 * @since  1.7
	 */
	class Tribe__PUE__Checker {

		/**
		 * Plugin filename relative to the plugins directory.
		 *
		 * @var string
		 */
		private $plugin_file = '';

		/**
		 * Used to hold the plugin_name as set by the constructor.
		 *
		 * @var string
		 */
		private $plugin_name = '';

		/**
		 * The plugin slug (without the .php extension)
		 *
		 * @var string
		 */
		protected $plugin_slug;

		/**
		 * Plugin slug. (with .php extension)
		 *
		 * @var string
		 */
		private $slug = '';

		/**
		 * Current domain.
		 *
		 * @var string
		 */
		private static $domain = '';

		/**
		 * Used to hold the query variables for download checks
		 *
		 * @var array
		 */
		private $download_query = [];

		/**
		 * The context in which this license key is used. May be 'component'
		 * in the case of a downloadable set of files such as a plugin or
		 * theme or else 'service' if the license key is used to utilize a
		 * remote SaaS platform.
		 *
		 * @var string
		 */
		private $context = 'component';

		/**
		 * How often to check for updates (in hours).
		 *
		 * @var int
		 */
		public $check_period = 12;

		/**
		 * Where to store the update info.
		 *
		 * @var string
		 */
		public $pue_option_name = '';

		/**
		 * Where to store the temporary status info.
		 *
		 * @todo remove transient in a major feature release where we release all plugins.
		 *
		 * @since 4.14.14
		 *
		 * @var string
		 */
		public $pue_key_status_transient_name;

		/**
		 * Where to store the temporary status info.
		 *
		 * @since 4.14.9
		 *
		 * @var string
		 */
		public $pue_key_status_option_name;

		/**
		 * Used to hold the install_key if set (included here for addons that will extend PUE to use install key checks).
		 *
		 * @var bool
		 */
		public $install_key = false;

		/**
		 * For setting the dismiss upgrade option (per plugin).
		 *
		 * @var string
		 */
		public $dismiss_upgrade;

		/**
		 * We'll customize this later so each plugin can have it's own install key!
		 *
		 * @var string
		 */
		public $pue_install_key;

		/**
		 * Storing any `json_error` data that get's returned so we can display an admin notice.
		 * For backwards compatibility this will be kept in the code for 2 versions
		 *
		 * @var array|null
		 *
		 * @deprecated
		 * @todo  remove on 4.5
		 */
		public $json_error;

		/**
		 * Storing any `plugin_info` data that get's returned so we can display an admin notice.
		 *
		 * @var Tribe__PUE__Plugin_Info|array|null
		 */
		public $plugin_info;

		/**
		 * Storing the `plugin_notice` message.
		 *
		 * @var string
		 */
		public $plugin_notice;

		/**
		 * Stats
		 *
		 * @var array
		 */
		private static $stats = [];

		/**
		 * Full Stats
		 *
		 * @var array
		 */
		private static $stats_full = [];

		/**
		 * Validation array
		 *
		 * @since 5.1.4
		 *
		 * @var array
		 */
		private $validate_query = [];

		/**
		 * A unique list of instances of the PUE Checker that has been initialized.
		 *
		 * @since 6.3.2
		 *
		 * @var Tribe__PUE__Checker[] Instances of checkers that have been registered.
		 */
		protected static $instances = [];

		/**
		 * @var string The transient key.
		 */
		public const IS_ANY_LICENSE_VALID_TRANSIENT_KEY = 'TEC_IS_ANY_LICENSE_VALID_TRANSIENT';

		/**
		 * Class constructor.
		 *
		 * @param string $pue_update_url Deprecated. The URL of the plugin's metadata file.
		 * @param string $slug           The plugin's 'slug'.
		 * @param array  $options {
		 *        Contains any options that need to be set in the class initialization for construct.
		 *
		 *        @type integer $check_period     How often to check for updates (in hours). Defaults to checking every
		 *                                        12 hours. Set to 0 to disable automatic update checks.
		 *        @type string  $pue_option_name  Where to store book-keeping info about update checks. Defaults to
		 *                                        'external_updates-$slug'.
		 *        @type string  $apikey           Used to authorize download updates from developer server
		 *        @type string  $context          Defaults to 'component' which is expected for plugins (or themes).
		 *                                        If set to 'service' it will not hook into WP update checks.
		 *        @type string  $plugin_name      The plugin name, defaults to the name in the plugin file itself.
		 * }
		 * @param string $plugin_file    fully qualified path to the main plugin file.
		 */
		public function __construct( $pue_update_url, $slug = '', $options = [], $plugin_file = '' ) {
			$this->set_slug( $slug );
			$this->set_plugin_file( $plugin_file );
			$this->set_options( $options );
			$this->hooks();
			$this->set_key_status_name();
			// So we can reference our "registered" instances later.
			self::$instances[ $slug ] ??= $this;
		}

		/**
		 * Gets whether the license key is valid or not.
		 *
		 * @since 4.14.9
		 * @since 6.4.1 Added uplink resource check.
		 */
		public function is_key_valid() {
			$uplink_resource = get_resource( $this->get_slug() );

			if ( $uplink_resource ) {
				return $uplink_resource->has_valid_license();
			}

			// @todo remove transient in a major feature release where we release all plugins.
			$status = get_transient( $this->pue_key_status_transient_name );

			if ( empty( $status ) ) {
				$status = get_option( $this->pue_key_status_option_name, 'invalid' );
			}

			return 'valid' === $status;
		}

		/**
		 * Iterate on all the registered PUE Product Licenses we have and find if any are valid.
		 * Will revalidate the licenses if none are found to be valid.
		 *
		 * @todo In scenarios where a user goes from a Free license to an active license the transient may give a false positive.
		 *
		 * @since 6.3.2
		 *
		 * @return bool
		 */
		public static function is_any_license_valid(): bool {
			$valid_slug = 'valid';
			$has_valid  = false;

			// Check our transient.
			$transient_value = get_transient( self::IS_ANY_LICENSE_VALID_TRANSIENT_KEY );
			if ( ! empty( $transient_value ) ) {
				return $transient_value === $valid_slug;
			}

			// Check our local transient/cache first.
			foreach ( self::$instances as $checker ) {
				if ( $checker->is_key_valid() ) {
					set_transient( self::IS_ANY_LICENSE_VALID_TRANSIENT_KEY, $valid_slug, HOUR_IN_SECONDS );
					$has_valid = true;
					break;
				}
			}

			if ( ! $has_valid ) {
				// Revalidate if we haven't found a valid license yet.
				foreach ( self::$instances as $checker ) {
					$license  = get_option( $checker->get_license_option_key() );
					$response = $checker->validate_key( $license );
					// Is it valid?
					if ( ! empty( $response['status'] ) ) {
						set_transient( self::IS_ANY_LICENSE_VALID_TRANSIENT_KEY, $valid_slug, HOUR_IN_SECONDS );
						$has_valid = true;
						break;
					}
				}
			}

			// We found no valid licenses above.
			if ( ! $has_valid ) {
				set_transient( self::IS_ANY_LICENSE_VALID_TRANSIENT_KEY, 'invalid', HOUR_IN_SECONDS );
			}

			return get_transient( self::IS_ANY_LICENSE_VALID_TRANSIENT_KEY ) === $valid_slug;
		}

		/**
		 * Gets whether or not the PUE key validation check is expired.
		 *
		 * @since 4.14.9
		 */
		public function is_key_validation_expired() {
			// If we have a transient, then we're good. Not expired.
			// @todo remove transient in a major feature release where we release all plugins.
			if ( get_transient( $this->pue_key_status_transient_name ) ) {
				return false;
			}

			$option_expiration = get_option( "{$this->pue_key_status_option_name}_timeout", null );
			return is_null( $option_expiration ) || ( time() > $option_expiration );
		}

		/**
		 * Set the PUE key status property names.
		 *
		 * @since 4.14.9
		 */
		public function set_key_status_name() {
			$this->pue_key_status_option_name = 'pue_key_status_' . $this->get_slug() . '_' . $this->get_site_domain();

			// @todo remove transient in a major feature release where we release all plugins.
			$this->pue_key_status_transient_name = md5( $this->get_slug() . $this->get_site_domain() );
		}

		/**
		 * Creates a hash for the transient name that holds the current key status.
		 *
		 * @todo remove transient in a major feature release where we release all plugins.
		 *
		 * @since 4.14.14
		 */
		public function set_key_status_transient_name() {
			_deprecated_function( __METHOD__, '4.14.9', __CLASS__ . '::set_key_status_name()' );
		}

		/**
		 * Sets the key status based on the key validation check results.
		 *
		 * @since 4.14.9
		 *
		 * @param int $valid 0 for invalid, 1 or 2 for valid.
		 */
		public function set_key_status( $valid ) {
			$status = tribe_is_truthy( $valid ) ? 'valid' : 'invalid';
			update_option( $this->pue_key_status_option_name, $status );
			update_option( "{$this->pue_key_status_option_name}_timeout", $this->check_period * HOUR_IN_SECONDS );

			// We set a transient in addition to an option for compatibility reasons.
			// @todo remove transient in a major feature release where we release all plugins.
			set_transient( $this->pue_key_status_transient_name, $status, $this->check_period * HOUR_IN_SECONDS );
		}

		/**
		 * Sets the key status transient based on the key validation check results.
		 *
		 * @since 4.14.9
		 *
		 * @param int $valid 0 for invalid, 1 or 2 for valid.
		 */
		public function set_key_status_transient( $valid ) {
			_deprecated_function( __METHOD__, '4.14.9', __CLASS__ . '::set_key_status()' );
			$this->set_key_status( $valid );
		}

		/**
		 * Install the hooks required to run periodic update checks and inject update info
		 * into WP data structures.
		 * Also other hooks related to the automatic updates (such as checking agains API and what not (@from Darren)
		 */
		public function hooks() {
			// Override requests for plugin information.
			add_filter( 'plugins_api', [ $this, 'inject_info' ], 10, 3 );

			// Check for updates when the WP updates are checked and inject our update if needed.
			// Only add filter if the TRIBE_DISABLE_PUE constant is not set as true and where
			// the context is not 'service'.
			if ( ( ! defined( 'TRIBE_DISABLE_PUE' ) || true !== TRIBE_DISABLE_PUE ) && 'service' !== $this->context ) {
				add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_for_updates' ] );
			}

			add_filter( 'tribe_licensable_addons', [ $this, 'build_addon_list' ] );
			add_filter( 'tribe_license_fields', [ $this, 'do_license_key_fields' ] );
			add_action( 'tribe_settings_after_content_tab_licenses', [ $this, 'do_license_key_javascript' ] );
			add_action( 'tribe_settings_success_message', [ $this, 'do_license_key_success_message' ], 10, 2 );
			add_action( 'load-plugins.php', [ $this, 'remove_default_inline_update_msg' ], 50 );

			// Key validation.
			add_filter( 'tribe_settings_save_field_value', [ $this, 'check_for_api_key_error' ], 10, 3 );
			add_action( 'wp_ajax_pue-validate-key_' . $this->get_slug(), [ $this, 'ajax_validate_key' ] );
			add_filter( 'tribe-pue-install-keys', [ $this, 'return_install_key' ] );
			add_action( 'admin_enqueue_scripts', [ $this, 'maybe_display_json_error_on_plugins_page' ], 1 );
			add_action( 'admin_init', [ $this, 'general_notifications' ] );

			// Package name.
			add_filter( 'upgrader_pre_download', [ Tribe__PUE__Package_Handler::instance(), 'filter_upgrader_pre_download' ], 5, 3 );
		}



		/********************** Getter / Setter Functions **********************/

		/**
		 * Get the slug
		 *
		 * @return string
		 */
		public function get_slug() {
			return apply_filters( 'pue_get_slug', $this->slug );
		}

		/**
		 * Set the slug
		 *
		 * @param string $slug The plugin slug.
		 */
		private function set_slug( $slug = '' ) {
			$this->slug            = $slug;
			$clean_slug            = str_replace( '-', '_', $this->slug );
			$this->dismiss_upgrade = 'pu_dismissed_upgrade_' . $clean_slug;
			$this->pue_install_key = 'pue_install_key_' . $clean_slug;
		}

		/**
		 * Get the PUE update API endpoint url
		 *
		 * @return string
		 */
		public function get_pue_update_url() {
			$pue_update_url = 'https://pue.theeventscalendar.com';

			if ( defined( 'PUE_UPDATE_URL' ) ) {
				$pue_update_url = PUE_UPDATE_URL;
			}

			$pue_update_url = apply_filters( 'pue_get_update_url', $pue_update_url, $this->get_slug() );

			$pue_update_url = untrailingslashit( $pue_update_url );

			return $pue_update_url;
		}

		/**
		 * Get the plugin file path
		 *
		 * @return string
		 */
		public function get_plugin_file() {
			return apply_filters( 'pue_get_plugin_file', $this->plugin_file, $this->get_slug() );
		}

		/**
		 * Set the plugin file path
		 *
		 * @param string $plugin_file The plugin file path.
		 */
		private function set_plugin_file( $plugin_file = '' ) {

			if ( ! empty( $plugin_file ) ) {
				$this->plugin_file = $plugin_file;

				return;
			}

			$slug = $this->get_slug();
			if ( ! empty( $slug ) ) {
				$this->plugin_file = $slug . '/' . $slug . '.php';
			}
		}

		/**
		 * Set the plugin name
		 *
		 * @param string $plugin_name The plugin name.
		 */
		private function set_plugin_name( $plugin_name = '' ) {
			if ( ! empty( $plugin_name ) ) {
				$this->plugin_name = $plugin_name;
			} else {
				// Get name from plugin file itself.
				if ( ! function_exists( 'get_plugins' ) ) {
					require_once ABSPATH . 'wp-admin/includes/plugin.php';
				}

				// Prevents get_plugins from throwing a weird notice.
				if ( ! file_exists( WP_PLUGIN_DIR . '/' . $this->get_plugin_file() ) ) {
					return;
				}

				$plugin_details    = explode( '/', $this->get_plugin_file() );
				$plugin_folder     = get_plugins( '/' . $plugin_details[0] );
				$this->plugin_name = isset( $plugin_details[1] ) && isset( $plugin_folder[ $plugin_details[1] ] ) ? $plugin_folder[ $plugin_details[1] ]['Name'] : null;
			}
		}

		/**
		 * Get the plugin name
		 *
		 * @return string
		 */
		public function get_plugin_name() {
			if ( empty( $this->plugin_name ) ) {
				$this->set_plugin_name();
			}

			return apply_filters( 'pue_get_plugin_name', $this->plugin_name, $this->get_slug() );
		}

		/**
		 * Set all the PUE instantiation options.
		 *
		 * @param array $options Passed instantiation options.
		 */
		private function set_options( $options = [] ) {

			$options = wp_parse_args(
				$options,
				[
					'pue_option_name' => 'external_updates-' . $this->get_slug(),
					'apikey'          => '',
					'check_period'    => 12,
					'context'         => 'component',
					'plugin_name'     => '',
				]
			);

			$this->pue_option_name = $options['pue_option_name'];
			$this->check_period    = (int) $options['check_period'];
			$this->context         = $options['context'];
			$this->plugin_name     = $options['plugin_name'];
		}

		/**
		 * Set all the download query array
		 *
		 * @param array $download_query Passed download query.
		 */
		private function set_download_query( $download_query = [] ) {

			if ( ! empty( $download_query ) ) {
				$this->download_query = $download_query;

				return;
			}

			// Plugin slug.
			$this->download_query['plugin'] = sanitize_text_field( $this->get_slug() );

			// Include current version.
			$this->download_query['installed_version'] = sanitize_text_field( $this->get_installed_version() );

			$this->download_query['domain'] = sanitize_text_field( $this->get_domain() );

			// Get general stats.
			$stats = $this->get_stats();

			$this->download_query['multisite']         = $stats['network']['multisite'];
			$this->download_query['network_activated'] = $stats['network']['network_activated'];
			$this->download_query['active_sites']      = $stats['network']['active_sites'];
			$this->download_query['wp_version']        = $stats['versions']['wp'];

			// The following is for install key inclusion (will apply later with PUE addons).
			$this->download_query['key'] = sanitize_text_field( $this->get_key() );
			$this->download_query['dk']  = sanitize_text_field( $this->get_key( 'default' ) );
			$this->download_query['o']   = sanitize_text_field( $this->get_key( 'any', 'origin' ) );
		}

		/**
		 * Get the download_query args
		 *
		 * @return array
		 */
		public function get_download_query() {
			if ( empty( $this->download_query ) ) {
				$this->set_download_query();
			}

			return apply_filters( 'pue_get_download_query', $this->download_query, $this->get_slug() );
		}

		/**
		 * Set all the validate query array
		 *
		 * @param array $validate_query Passed validate query params.
		 */
		private function set_validate_query( $validate_query = [] ) {

			if ( ! empty( $validate_query ) ) {
				$this->validate_query = $validate_query;

				return;
			}

			// The following is for install key inclusion (will apply later with PUE addons).
			$this->validate_query['key'] = sanitize_text_field( $this->get_key() );

			// Include default key.
			$this->validate_query['default_key'] = sanitize_text_field( $this->get_key( 'default' ) );

			// Include license origin.
			$this->validate_query['license_origin'] = sanitize_text_field( $this->get_key( 'any', 'origin' ) );

			// Plugin slug.
			$this->validate_query['plugin'] = sanitize_text_field( $this->get_slug() );

			// Include current version.
			$this->validate_query['version'] = sanitize_text_field( $this->get_installed_version() );

			// Include current domain.
			$this->validate_query['domain'] = sanitize_text_field( $this->get_domain() );

			// Include plugin stats.
			$this->validate_query['stats'] = $this->get_stats();
		}

		/**
		 * Get the validate_query args
		 *
		 * @return array
		 */
		public function get_validate_query() {
			if ( empty( $this->validate_query ) ) {
				$this->set_validate_query();
			}

			return apply_filters( 'pue_get_validate_query', $this->validate_query, $this->get_slug() );
		}

		/**
		 * Get current domain
		 *
		 * @return string
		 */
		public function get_domain() {

			$domain = self::$domain;

			if ( empty( $domain ) ) {
				$url = wp_parse_url( get_option( 'siteurl' ) );
				if ( ! empty( $url ) && isset( $url['host'] ) ) {
					$domain = $url['host'];
				} elseif ( isset( $_SERVER['SERVER_NAME'] ) ) {
					$domain = $_SERVER['SERVER_NAME'];
				}

				if ( is_multisite() ) {
					// For multisite, return the network-level siteurl.
					$domain = $this->get_network_domain();
				}

				self::$domain = $domain;
			}

			return $domain;
		}


		/********************** General Functions **********************/

		/**
		 * Compile  a list of addons
		 *
		 * @param array $addons List of addons.
		 *
		 * @return array list of addons
		 */
		public function build_addon_list( $addons = [] ) {
			$addons[] = $this->get_plugin_name();

			return $addons;
		}

		/**
		 * Inserts license key fields on license key page
		 *
		 * @param array $fields List of fields.
		 *
		 * @return array Modified list of fields.
		 */
		public function do_license_key_fields( $fields ) {
			// Common fields whether licenses should be hidden or not.
			$to_insert = [
				$this->pue_install_key . '-heading' => [
					'type'  => 'heading',
					'class' => [
						'tec-settings-form__section-header',
						'tec-settings-form__section-header--sub',
					],
					'label' => $this->get_plugin_name(),
				],
			];

			$no_license_tooltip = esc_html__( 'A valid license key is required for support and updates', 'tribe-common' );
			if ( 'event-aggregator' === $this->get_slug() ) {
				$no_license_tooltip = sprintf(
					/* Translators: %1$s and %2$s are opening and closing <a> tags, respectively. */
					esc_html__( '%1$sBuy a license%2$s for the Event Aggregator service to access additional import features.', 'tribe-common' ),
					'<a href="https://evnt.is/196y" target="_blank">',
					'</a>'
				);
			}

			// We want to inject the following license settings at the end of the licenses tab.
			if ( $this->should_show_network_editable_license() ) {
				$to_insert[ $this->pue_install_key ] = [
					'type'            => 'license_key',
					'size'            => 'large',
					'validation_type' => 'license_key',
					'label'           => sprintf( esc_attr__( 'License Key', 'tribe-common' ) ),
					'default'         => $this->get_key( 'default' ),
					'tooltip'         => $no_license_tooltip,
					'parent_option'   => false,
					'network_option'  => true,
				];
			} elseif ( $this->should_show_subsite_editable_license() ) {
				$to_insert[ $this->pue_install_key ] = [
					'type'            => 'license_key',
					'size'            => 'large',
					'validation_type' => 'license_key',
					'label'           => sprintf( esc_attr__( 'License Key', 'tribe-common' ) ),
					'default'         => $this->get_key( 'default' ),
					'tooltip'         => $no_license_tooltip,
					'parent_option'   => false,
					'network_option'  => false,
				];
			} elseif ( $this->should_show_overrideable_license() ) {
				$to_insert[ $this->pue_install_key . '-state' ] = [
					'type'             => 'html',
					'label'            => sprintf( esc_attr__( 'License Key Status:', 'tribe-common' ) ),
					'label_attributes' => [ 'style' => 'width:auto;' ],
					'html'             => sprintf( '<p>%s</p>', $this->get_network_license_state_string() ),
				];

				$override_id = $this->pue_install_key . '-override';

				$to_insert[ $override_id ] = [
					'type'            => 'checkbox_bool',
					'label'           => esc_html__( 'Override network license key', 'tribe-common' ),
					'tooltip'         => esc_html__( 'Check this box if you wish to override the network license key with your own', 'tribe-common' ),
					'default'         => false,
					'validation_type' => 'boolean',
					'parent_option'   => false,
					'attributes'      => [ 'id' => $override_id . '-field' ],
				];

				$to_insert[ $this->pue_install_key ] = [
					'type'                => 'license_key',
					'size'                => 'large',
					'validation_type'     => 'license_key',
					'label'               => sprintf( esc_attr__( 'Site License Key', 'tribe-common' ) ),
					'tooltip'             => $no_license_tooltip,
					'parent_option'       => false,
					'network_option'      => false,
					'class'               => 'tribe-dependent',
					'fieldset_attributes' => [
						'data-depends'           => '#' . $override_id . '-field',
						'data-condition-checked' => true,
					],
				];
			} else {
				$to_insert[ $this->pue_install_key . '-state' ] = [
					'type'             => 'html',
					'label'            => sprintf( esc_attr__( 'License Key Status:', 'tribe-common' ) ),
					'label_attributes' => [ 'style' => 'width:auto;' ],
					'html'             => sprintf( '<p>%s</p>', $this->get_network_license_state_string() ),
				];
			}

			$to_insert = tribe( 'settings' )->wrap_section_content( $this->pue_install_key, $to_insert );

			$fields = self::array_insert_after_key( 'tribe-form-content-start', $fields, $to_insert );

			return $fields;
		}

		/**
		 * Inserts the javascript that makes the ajax checking
		 * work on the license key page
		 */
		public function do_license_key_javascript() {
			?>
			<script>
				jQuery(document).ready(function ($) {
					$( '.tribe-field-license_key' ).each( function() {
						var $el = $( this );
						var $field = $el.find( 'input' );

						if ( '' === $field.val().trim() ) {
							$el.find( '.license-test-results' ).hide();
						}
					} );

					$('#tribe-field-<?php echo esc_attr( $this->pue_install_key ); ?>').change(function () {
						<?php echo sanitize_html_class( $this->pue_install_key ); ?>_validateKey();
					});
					<?php echo sanitize_html_class( $this->pue_install_key ); ?>_validateKey();
				});

				function <?php echo sanitize_html_class( $this->pue_install_key ); ?>_validateKey() {
					var this_id       = '#tribe-field-<?php echo esc_attr( $this->pue_install_key ); ?>';
					var $validity_msg = jQuery(this_id + ' .key-validity');

					if (jQuery(this_id + ' input').val() != '') {
						jQuery( this_id + ' .license-test-results' ).show();
						jQuery(this_id + ' .tooltip').hide();
						jQuery(this_id + ' .ajax-loading-license').show();
						$validity_msg.hide();

						// Strip whitespace from key
						var <?php echo sanitize_html_class( $this->pue_install_key ); ?>_license_key = jQuery(this_id + ' input').val().replace(/^\s+|\s+$/g, "");
						jQuery(this_id + ' input').val(<?php echo sanitize_html_class( $this->pue_install_key ); ?>_license_key);

						var data = {
							action: 'pue-validate-key_<?php echo esc_attr( $this->get_slug() ); ?>',
							key: <?php echo sanitize_html_class( $this->pue_install_key ); ?>_license_key,
							_wpnonce: '<?php echo esc_attr( wp_create_nonce( 'pue-validate-key_' . $this->get_slug() ) ); ?>'
						};
						jQuery.post(ajaxurl, data, function (response) {
							var data          = jQuery.parseJSON(response);

							jQuery(this_id + ' .ajax-loading-license').hide();
							$validity_msg.show();
							$validity_msg.html(data.message);

							switch ( data.status ) {
								case 1: $validity_msg.addClass( 'valid-key' ).removeClass( 'invalid-key' ); break;
								case 2: $validity_msg.addClass( 'valid-key service-msg' ); break;
								default: $validity_msg.addClass( 'invalid-key' ).removeClass( 'valid-key' ); break;
							}
						});
					}
				}
			</script>
			<?php
		}

		/**
		 * Filter the success message on license key page
		 *
		 * @param string $message Success message.
		 * @param string $tab     Tab name.
		 *
		 * @return string
		 */
		public function do_license_key_success_message( $message, $tab ) {

			if ( 'licenses' !== $tab ) {
				return $message;
			}

			return '<div id="message" class="updated"><p><strong>' . esc_html__( 'License key(s) updated.', 'tribe-common' ) . '</strong></p></div>';
		}

		/**
		 * Build stats for endpoints
		 *
		 * @return array
		 */
		public function build_stats() {

			global $wpdb;

			$stats = [
				'versions' => [
					'wp' => sanitize_text_field( $GLOBALS['wp_version'] ),
				],
				'network'  => [
					'multisite'         => 0,
					'network_activated' => 0,
					'active_sites'      => 1,
				],
			];

			if ( is_multisite() ) {
				$sql_count = "
					SELECT COUNT( `blog_id` )
					FROM `{$wpdb->blogs}`
					WHERE
						`public` = '1'
						AND `archived` = '0'
						AND `spam` = '0'
						AND `deleted` = '0'
				";

				$stats['network']['multisite']         = 1;
				$stats['network']['network_activated'] = (int) $this->is_plugin_active_for_network();
				$stats['network']['active_sites']      = (int) $wpdb->get_var( $sql_count );
			}

			self::$stats = $stats;

			return $stats;
		}

		/**
		 * Build full stats for endpoints
		 *
		 * @param array $stats Initial stats.
		 *
		 * @return array
		 */
		public function build_full_stats( $stats ) {

			global $wpdb;

			$theme = wp_get_theme();

			$current_offset = (int) get_option( 'gmt_offset', 0 );
			$tzstring       = get_option( 'timezone_string' );

			// Remove old Etc mappings. Fallback to gmt_offset.
			if ( false !== strpos( $tzstring, 'Etc/GMT' ) ) {
				$timezone = '';
			}

			// Create a UTC+- zone if no timezone string exists.
			if ( empty( $tzstring ) ) {
				if ( 0 === $current_offset ) {
					$timezone = 'UTC+0';
				} elseif ( $current_offset < 0 ) {
					$timezone = 'UTC' . $current_offset;
				} else {
					$timezone = 'UTC+' . $current_offset;
				}
			}

			$stats['versions'] = [
				'wp'    => sanitize_text_field( $GLOBALS['wp_version'] ),
				'php'   => sanitize_text_field( phpversion() ),
				'mysql' => sanitize_text_field( $wpdb->db_version() ),
			];

			$stats['theme'] = [
				'name'       => sanitize_text_field( $theme->get( 'Name' ) ),
				'version'    => sanitize_text_field( $theme->get( 'Version' ) ),
				'stylesheet' => sanitize_text_field( $theme->get_stylesheet() ),
				'template'   => sanitize_text_field( $theme->get_template() ),
			];

			$stats['site_language'] = sanitize_text_field( get_locale() );
			$stats['user_language'] = sanitize_text_field( get_user_locale() );
			$stats['is_public']     = (int) get_option( 'blog_public', 0 );
			$stats['wp_debug']      = (int) ( defined( 'WP_DEBUG' ) && WP_DEBUG );
			$stats['site_timezone'] = sanitize_text_field( $timezone );

			$stats['totals'] = [
				'all_post_types'   => (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$wpdb->posts}`" ),
				'events'           => (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$wpdb->posts}` WHERE post_type = %s", 'tribe_events' ) ),
				'venues'           => (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$wpdb->posts}` WHERE post_type = %s", 'tribe_venue' ) ),
				'organizers'       => (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$wpdb->posts}` WHERE post_type = %s", 'tribe_organizer' ) ),
				'event_categories' => (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$wpdb->term_taxonomy}` WHERE taxonomy = %s", 'tribe_events_cat' ) ),
			];

			self::$stats_full = $stats;

			return $stats;
		}

		/**
		 * Build and get the stats
		 *
		 * @return array
		 */
		public function get_stats() {

			$stats = self::$stats;

			if ( empty( $stats ) ) {
				$stats = $this->build_stats();
			}

			/**
			 * Allow full stats data to be built and sent.
			 *
			 * @param boolean $use_full_stats Whether to send full stats
			 *
			 * @since 4.5.1
			 */
			$use_full_stats = apply_filters( 'pue_use_full_stats', false );

			if ( $use_full_stats ) {
				$stats_full = self::$stats_full;

				if ( empty( $stats_full ) ) {
					$stats = $this->build_full_stats( $stats );
				}
			}

			/**
			 * Filter stats and allow plugins to add their own stats
			 * for tracking specific points of data.
			 *
			 * @param array                $stats          Stats gathered by PUE Checker class
			 * @param boolean              $use_full_stats Whether to send full stats
			 * @param \Tribe__PUE__Checker $checker        PUE Checker class object
			 *
			 * @since 4.5.1
			 */
			$stats = apply_filters( 'pue_stats', $stats, $use_full_stats, $this );

			return $stats;
		}

		/**
		 * Get current license key, optionally of a specific type.
		 *
		 * @param string $type        The type of key to get (any, network, local, default).
		 * @param string $return_type The type of data to return (key, origin).
		 *
		 * @return string
		 */
		public function get_key( $type = 'any', $return_type = 'key' ) {

			$resource    = get_resource( $this->get_slug() );
			$license_key = $resource ? $resource->get_license_key( $type ) : false;
			if ( $license_key ) {
				return $license_key;
			}

			$license_key    = '';
			$license_origin = 'm';

			/*
			 * Even if we have a network key if the plugin is not active on the network then it should
			 * not be used.
			 */
			if (
				( 'network' === $type || 'any' === $type )
				&& is_multisite()
				&& $this->is_plugin_active_for_network()
			) {
				$license_key = get_network_option( null, $this->pue_install_key, '' );
			}

			if ( ( 'local' === $type || 'any' === $type ) && empty( $license_key ) ) {
				$license_key = get_option( $this->pue_install_key, '' );
			}

			if ( empty( $license_key ) && ( 'default' === $type || 'any' === $type ) ) {
				$autoloader = Tribe__Autoloader::instance();

				$class_name = $autoloader->get_prefix_by_slug( $this->get_slug() );

				if ( $class_name ) {
					$is_namespaced = false !== strpos( $class_name, '\\' );

					if ( $is_namespaced ) {
						// Handle class prefixes like Tribe\Plugin\.
						$class_name .= 'PUE\Helper';
					} else {
						// Handle class prefixes like Tribe__Plugin__.
						$class_name .= 'PUE__Helper';
					}

					if ( constant( $class_name . '::DATA' ) ) {
						$license_key = constant( $class_name . '::DATA' );

						$license_origin = 'e';
					}
				}
			}

			if ( 'origin' === $return_type ) {
				if ( 'm' === $license_origin ) {
					$default_key = $this->get_key( 'default' );

					if ( $license_key !== $default_key ) {
						$license_origin = 'o';
					}
				}

				return $license_origin;
			}

			return $license_key;
		}

		/**
		 * Update license key for specific type of license.
		 *
		 * @param string $license_key The new license key value.
		 * @param string $type        The type of key to update (network or local).
		 */
		public function update_key( $license_key, $type = 'local' ) {

			if ( 'network' === $type && is_multisite() ) {
				update_network_option( null, $this->pue_install_key, sanitize_text_field( $license_key ) );
			} elseif ( 'local' === $type ) {
				update_option( $this->pue_install_key, sanitize_text_field( $license_key ) );
			}
		}

		/**
		 * Checks for the license key status with MT servers.
		 *
		 * @param string $key     The license key to check.
		 * @param bool   $network Whether the key to check for is a network one or not.
		 *
		 * @return array An associative array containing the license status response.
		 */
		public function validate_key( $key, $network = false ) {
			$response           = [];
			$response['status'] = 0;

			$uplink_resource = get_resource( $this->get_slug() );

			if ( $uplink_resource ) {
				$key = $uplink_resource->get_license_key();
			}

			if ( ! $key ) {
				$response['message'] = sprintf(
					/* Translators: %1$s and %2$s are opening and closing <a> tags, respectively. */
					esc_html__(
						'Hmmm... something\'s wrong with this validator. Please contact %1$ssupport%2$s.',
						'tribe-common'
					),
					'<a href="https://evnt.is/1u">',
					'</a>'
				);
				return $response;
			}

			$query_args = $this->get_validate_query();

			$query_args['key'] = sanitize_text_field( $key );

			// This method is primarily used during when validating keys by ajax, before they are
			// formally committed or saved by the user: for that reason we call request_info()
			// rather than license_key_status() as at this stage invalid or missing keys should
			// not result in admin notices being generated.
			$plugin_info = $this->request_info( $query_args );
			$expiration = isset( $plugin_info->expiration ) ? $plugin_info->expiration : __( 'unknown date', 'tribe-common' );

			$pue_notices = tribe( 'pue.notices' );
			$plugin_name = $this->get_plugin_name();

			if ( empty( $plugin_info ) ) {
				$response['message'] = __( 'Sorry, key validation server is not available.', 'tribe-common' );
			} elseif ( isset( $plugin_info->api_expired ) && 1 === (int) $plugin_info->api_expired ) {
				$response['message'] = $this->get_license_expired_message();
				$response['api_expired'] = true;
			} elseif ( isset( $plugin_info->api_upgrade ) && 1 === (int) $plugin_info->api_upgrade ) {
				$response['message'] = $this->get_api_message( $plugin_info );
				$response['api_upgrade'] = true;
			} elseif ( isset( $plugin_info->api_invalid ) && 1 === (int) $plugin_info->api_invalid ) {
				$response['message'] = $this->get_api_message( $plugin_info );
				$response['api_invalid'] = true;
			} else {
				$key_type = 'local';

				if ( $network ) {
					$key_type = 'network';
				}

				$current_install_key = $this->get_key( $key_type );
				$replacement_key = $query_args['key'];

				if ( ! empty( $plugin_info->replacement_key ) ) {
					// The PUE service might send over a new key upon validation.
					$replacement_key = $plugin_info->replacement_key;
				}

				if ( $current_install_key && $current_install_key === $replacement_key ) {
					$default_success_msg = esc_html(
						sprintf(
							/* Translators: %s is the expiration date. */
							__( 'Valid Key! Expires on %s', 'tribe-common' ),
							$expiration
						)
					);
				} else {
					// Set the key.
					$this->update_key( $replacement_key, $key_type );

					$default_success_msg = esc_html(
						sprintf(
							/* Translators: %s is the expiration date. */
							__(
								'Thanks for setting up a valid key. It will expire on %s',
								'tribe-common'
							),
							$expiration
						)
					);

					// Set system info key on TEC.com after successful validation of license.
					$optin_key = get_option( 'tribe_systeminfo_optin' );
					if ( $optin_key ) {
						Tribe__Support::send_sysinfo_key( $optin_key, $query_args['domain'], false, true );
					}
				}

				$pue_notices->clear_notices( $plugin_name );

				$response['status']     = isset( $plugin_info->api_message ) ? 2 : 1;
				$response['message']    = $plugin_info->api_message ?? $default_success_msg;
				$response['expiration'] = esc_html( $expiration );

				if ( isset( $plugin_info->daily_limit ) ) {
					$response['daily_limit'] = intval( $plugin_info->daily_limit );
				}
			}

			$response['message'] = wp_kses( $response['message'], 'data' );

			$this->set_key_status( $response['status'] );

			return $response;
		}

		/**
		 * Get the message for an expired license.
		 */
		public function get_license_expired_message() {
			return '<a href="https://evnt.is/195y" target="_blank" class="button button-primary">' .
				__( 'Renew Your License Now', 'tribe-common' ) .
				'<span class="screen-reader-text">' .
				__( ' (opens in a new window)', 'tribe-common' ) .
				'</span></a>';
		}

		/**
		 * Echo JSON results for key validation
		 */
		public function ajax_validate_key() {
			$key   = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : null;
			$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : null;

			if (
				empty( $nonce )
				|| false === wp_verify_nonce( $nonce, 'pue-validate-key_' . $this->get_slug() )
			) {
				$response = [
					'status'  => 0,
					'message' => __( 'Please refresh the page and try your request again.', 'tribe-common' ),
				];
			} else {
				$response = $this->validate_key( $key );
			}

			echo json_encode( $response );
			exit;
		}

		/**
		 * Processes variable substitutions for server-side API message.
		 *
		 * @param Tribe__PUE__Plugin_Info $info
		 *
		 * @return string
		 */
		private function get_api_message( $info ) {
			// this default message should never show, but is here as a fallback just in case.
			$message = sprintf(
				/* Translators: %1$s is the plugin name. %2$s and %3$s are opening and closing <a> tags, respectively. */
				esc_html__(
					'There is an update for %1$s. You\'ll need to %2$scheck your license%3$s to have access to updates, downloads, and support.',
					'tribe-common'
				),
				$this->get_plugin_name(),
				'<a href="https://theeventscalendar.com/license-keys/">',
				'</a>'
			);

			if ( ! empty( $info->api_inline_invalid_message ) ) {
				$message = wp_kses( $info->api_inline_invalid_message, 'post' );
			}

			$message = str_replace( '%plugin_name%', $this->get_plugin_name(), $message );
			$message = str_replace( '%plugin_slug%', $this->get_slug(), $message );
			$message = str_replace( '%update_url%', $this->get_pue_update_url() . '/', $message );
			$message = str_replace( '%version%', $info->version, $message );
			$message = str_replace( '%changelog%', '<a class="thickbox" title="' . $this->get_plugin_name() . '" href="plugin-install.php?tab=plugin-information&plugin=' . $this->get_slug() . '&TB_iframe=true&width=640&height=808">what\'s new</a>', $message );

			return $message;
		}

		/**
		 * Whether the plugin is network activated and licensed or not.
		 *
		 * @return bool
		 */
		public function is_network_licensed() {
			$is_network_licensed = false;

			if ( ! is_network_admin() && $this->is_plugin_active_for_network() ) {
				$network_key = $this->get_key( 'network' );
				$local_key   = $this->get_key( 'local' );

				// Check whether the network is licensed and NOT overridden by local license.
				if ( $network_key && ( empty( $local_key ) || $local_key === $network_key ) ) {
					$is_network_licensed = true;
				}
			}

			return $is_network_licensed;
		}

		/**
		 * Returns tet name of the option that stores the license key.
		 *
		 * @return string
		 */
		public function get_license_option_key() {
			return $this->pue_install_key;
		}

		/**
		 * Get the API update message.
		 */
		private function get_api_update_message() {
			$plugin_info = $this->plugin_info;

			if ( ! isset( $plugin_info->api_invalid_message ) ) {
				return false;
			}

			return sprintf(
				/* Translators: %1$s is the plugin name. %2$s and %3$s are opening and closing <a> tags, respectively. */
				esc_html__(
					'There is an update for %1$s. %2$sRenew your license%3$s to get access to bug fixes, security updates, and new features.',
					'tribe-common'
				),
				$this->get_plugin_name(),
				'<a href="https://theeventscalendar.com/license-keys/">',
				'</a>'
			);
		}

		/**
		 * Displays a PUE message on the page if it is relevant
		 *
		 * @param string $page The current page.
		 */
		public function maybe_display_json_error_on_plugins_page( $page ) {
			if ( 'plugins.php' !== $page ) {
				return;
			}

			$state            = $this->get_state();
			$messages         = [];
			$plugin_updates   = get_plugin_updates();
			$update_available = isset( $plugin_updates[ $this->plugin_file ] );

			// Check to see if there is an licensing error or update message we should show.
			if ( ! empty( $state->update->license_error ) ) {
				$messages[] = $state->update->license_error;
			} elseif ( $update_available && current_user_can( 'update_plugins' ) ) {
				// A plugin update is available.
				$update_now = sprintf(
					/* Translators: %s is the plugin version number. */
					esc_html__(
						'Update now to version %s.',
						'tribe-common'
					),
					$state->update->version
				);

				$update_now_link = sprintf(
					' <a href="%1$s" class="update-link">%2$s</a>',
					wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $this->plugin_file, 'upgrade-plugin_' . $this->plugin_file ),
					$update_now
				);

				$update_message = sprintf(
					/* Translators: %1$s is the plugin name. %2$s is the update now link. */
					esc_html__( 'There is a new version of %1$s available. %2$s', 'tribe-common' ),
					$this->plugin_name,
					$update_now_link
				);

				$messages[] = sprintf(
					'<p>%s</p>',
					$update_message
				);
			}

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

			$message_row_html = '';

			foreach ( $messages as $message ) {
				$message_row_html .= sprintf(
					'<div class="update-message notice inline notice-warning notice-alt">%s</div>',
					$message
				);
			}

			$message_row_html = sprintf(
				'<tr class="plugin-update-tr active"><td colspan="3" class="plugin-update">%s</td></tr>',
				$message_row_html
			);

			$this->plugin_notice = [
				'slug'             => $this->plugin_file,
				'message_row_html' => $message_row_html,
			];

			add_filter( 'tribe_plugin_notices', [ $this, 'add_notice_to_plugin_notices' ] );
		}

		/**
		 * Adds the PUE message to the plugin notices array.
		 *
		 * @param array $notices The current plugin notices.
		 *
		 * @return array
		 */
		public function add_notice_to_plugin_notices( $notices ) {
			if ( ! $this->plugin_notice || $this->is_network_licensed() ) {
				return $notices;
			}

			$notices[ $this->plugin_notice['slug'] ] = $this->plugin_notice;

			return $notices;
		}

		/**
		 * Returns plugin/license key data based on the provided query arguments.
		 *
		 * Calling this method will also take care of setting up admin notices for any
		 * keys that are invalid or have expired, etc.
		 *
		 * @see Tribe__PUE__Checker::request_info()
		 *
		 * @param array $query_args Additional query arguments to append to the request.
		 *
		 * @return Tribe__PUE__Plugin_Info|null
		 */
		public function license_key_status( $query_args ) {
			$pue_notices = tribe( 'pue.notices' );
			$plugin_info = $this->request_info( $query_args );
			$plugin_name = empty( $this->plugin_name ) ? $this->get_plugin_name() : $this->plugin_name;

			if ( empty( $plugin_name ) ) {
				return $plugin_info;
			}

			$install_key = $this->get_key();

			// Check for expired keys.
			if ( ! empty( $plugin_info->api_expired ) ) {
				$pue_notices->add_notice( Tribe__PUE__Notices::EXPIRED_KEY, $plugin_name );
			} elseif ( ! empty( $plugin_info->api_upgrade ) ) {
				// Check for keys that are out of installs (*must* happen before the api_invalid test).
				$pue_notices->add_notice( Tribe__PUE__Notices::UPGRADE_KEY, $plugin_name );
			} elseif (
				// Check for invalid keys last of all (upgrades/empty keys will be flagged as invalid).
				! empty( $plugin_info->api_invalid )
				&& (
					'component' === $this->context
					|| (
						'service' === $this->context
						&& $install_key
					)
				)
			) {
				$pue_notices->add_notice( Tribe__PUE__Notices::INVALID_KEY, $plugin_name );
			} else {
				// If none of the above were satisfied we can assume the key is valid.
				$pue_notices->clear_notices( $plugin_name );
			}

			return $plugin_info;
		}

		/**
		 * Sets up and manages those license key notifications which don't depend on communicating with a remote
		 * PUE server, etc.
		 */
		public function general_notifications() {
			$plugin_name = empty( $this->plugin_name ) ? $this->get_plugin_name() : $this->plugin_name;

			// Register our plugin name for use in messages (thus if we're deactivated, any previously
			// added persistent messaging can be cleared).
			tribe( 'pue.notices' )->register_name( $plugin_name );

			// Detect and setup notices for missing keys.
			$install_key = $this->get_key();

			if ( empty( $install_key ) && 'service' !== $this->context ) {
				tribe( 'pue.notices' )->add_notice( Tribe__PUE__Notices::INVALID_KEY, $plugin_name );
			}
		}

		/**
		 * Retrieve plugin info from the configured API endpoint.
		 *
		 * In general, this method should not be called directly and it is preferable to call
		 * the license_key_status() method instead. That method returns the same result, but
		 * also analyses each response to set up appropriate license key notifications in the
		 * admin environment.
		 *
		 * @uses wp_remote_get()
		 * @see Tribe__PUE__Checker::license_key_status()
		 *
		 * @param array $query_args Additional query arguments to append to the request. Optional.
		 *
		 * @return Tribe__PUE__Plugin_Info|string|null $plugin_info
		 */
		public function request_info( $query_args = [] ) {
			$query_args = apply_filters( 'tribe_puc_request_info_query_args-' . $this->get_slug(), $query_args );

			// Cache the API call so it only needs to be made once per plugin per page load.
			static $plugin_info_cache;

			// Sort parameter keys.
			$hash_data = $query_args;

			ksort( $hash_data );

			// Flatten hashed data.
			$hash_data = json_encode( $hash_data );

			// Generate unique hash.
			$key = hash( 'sha256', $hash_data );

			if ( isset( $plugin_info_cache[ $key ] ) ) {
				return $plugin_info_cache[ $key ];
			}

			// Various options for the wp_remote_get() call. Plugins can filter these, too.
			$options = [
				'body'    => $query_args,
				'timeout' => 15, // In seconds.
				'headers' => [
					'Accept' => 'application/json',
				],
			];
			$options = apply_filters( 'tribe_puc_request_info_options-' . $this->get_slug(), $options );

			$url = sprintf( '%s/api/plugins/v2/license/validate', $this->get_pue_update_url() );

			$result = wp_remote_post(
				$url,
				$options
			);

			// Try to parse the response.
			$plugin_info = null;

			if ( ! is_wp_error( $result )
				&& isset( $result['response']['code'] )
				&& ( 200 === (int) $result['response']['code'] )
				&& ! empty( $result['body'] )
			) {
				$plugin_info = Tribe__PUE__Plugin_Info::from_json( $result['body'] );
			}

			$plugin_info = apply_filters( 'tribe_puc_request_info_result-' . $this->get_slug(), $plugin_info, $result );

			$plugin_info_cache[ $key ] = $plugin_info;

			return $plugin_info;
		}

		/**
		 * Returns the domain contained in the network's siteurl option (not the full URL).
		 *
		 * @return string
		 */
		public function get_network_domain() {
			$site_url = wp_parse_url( get_site_option( 'siteurl' ) );
			if ( ! $site_url || ! isset( $site_url['host'] ) ) {
				return '';
			} else {
				return strtolower( $site_url['host'] );
			}
		}

		/**
		 * Retrieve the latest update (if any) from the configured API endpoint.
		 *
		 * @uses Tribe__PUE__Checker::request_info()
		 *
		 * @return Tribe__PUE__Utility An instance of Tribe__PUE__Utility, or NULL when no updates are available.
		 */
		public function request_update() {
			// For the sake of simplicity, this function just calls request_info()
			// and transforms the result accordingly.
			$query_args = $this->get_validate_query();

			if ( ! empty( $_POST['key'] ) ) {
				$query_args['key'] = sanitize_text_field( $_POST['key'] );
			} elseif ( ! empty( $_POST[ $this->pue_install_key ] ) ) {
				$query_args['key'] = sanitize_text_field( $_POST[ $this->pue_install_key ] );
			}

			$this->plugin_info = $plugin_info = $this->license_key_status( $query_args );

			if ( null === $plugin_info ) {
				return null;
			}

			// admin display for if the update check reveals that there is a new version but the API key isn't valid.
			if ( isset( $plugin_info->api_invalid ) ) {
				$plugin_info = Tribe__PUE__Utility::from_plugin_info( $plugin_info );
				$plugin_info->license_error = $this->get_api_message( $plugin_info );

				return $plugin_info;
			}

			if ( ! empty( $plugin_info->new_install_key ) ) {
				$this->update_key( $plugin_info->new_install_key );
			}

			// Need to correct the download url so it contains the custom user data (e.g. api and any other parameters).
			$download_query = $this->get_download_query();

			if ( ! empty( $download_query ) ) {
				$plugin_info->download_url = esc_url_raw( add_query_arg( $download_query, $plugin_info->download_url ) );
			}

			// Add plugin dirname/file (this will be expected by WordPress when it builds the plugin list table).
			$plugin_info->plugin = $this->get_plugin_file();

			return Tribe__PUE__Utility::from_plugin_info( $plugin_info );
		}

		/**
		 * Display a changelog when the api key is missing.
		 */
		public function display_changelog() {
			// Contents of changelog display page when api-key is invalid or missing.  It will ONLY show the changelog (hook into existing thickbox?).
		}

		/**
		 * Get the currently installed version of the plugin.
		 *
		 * @return string Version number.
		 */
		public function get_installed_version() {
			if ( function_exists( 'get_plugins' ) ) {
				$all_plugins = get_plugins();
				if ( array_key_exists( $this->get_plugin_file(), $all_plugins ) && array_key_exists( 'Version', $all_plugins[ $this->get_plugin_file() ] ) ) {
					return $all_plugins[ $this->get_plugin_file() ]['Version'];
				}
			}
		}

		/**
		 * Get plugin update state
		 *
		 * @param boolean $force_recheck Whether to force a recheck of the update status.
		 *
		 * @return object
		 */
		public function get_state( $force_recheck = false ) {

			$state = null;

			if ( ! $force_recheck ) {
				$state = get_site_option( $this->pue_option_name, false );
			}

			if ( empty( $state ) ) {
				$state                 = new stdClass();
				$state->lastCheck      = 0;
				$state->checkedVersion = '';
				$state->update         = null;
			}

			return $state;
		}

		/**
		 * Update plugin update state
		 *
		 * @param object $value
		 */
		public function update_state( $value ) {

			update_site_option( $this->pue_option_name, $value );
		}

		/**
		 * Check for plugin updates.
		 *
		 * The results are stored in the DB option specified in $pue_option_name.
		 *
		 * @param array|object $updates       Existing updates.
		 * @param boolean      $force_recheck Whether to force a recheck of the update status.
		 *
		 * @return array|object
		 */
		public function check_for_updates( $updates = [], $force_recheck = false ) {
			$state = $this->get_state( $force_recheck );

			$state->lastCheck      = time();
			$state->checkedVersion = $this->get_installed_version();

			// Save before checking in case something goes wrong.
			$this->update_state( $state );

			$state->update = $this->request_update();

			// If a null update was returned, skip to the end of the function.
			if ( null !== $state->update ) {
				// We treat this like an object throughout so let's convert it here.
				$updates = (object) $updates;

				// Is there an update to insert?
				if ( version_compare( $state->update->version, $this->get_installed_version(), '>' ) ) {
					if ( ! isset( $updates->response ) ) {
						$updates->response = [];
					}

					$plugin_file = $this->get_plugin_file();

					$updates->response[ $plugin_file ] = $state->update->to_wp_format( $plugin_file );

					// Clear the no_update property if it exists.
					if ( isset( $updates->no_update[ $plugin_file ] ) ) {
						unset( $updates->no_update[ $plugin_file ] );
					}

					// If the key has expired we should register an appropriate admin notice.
					if ( $this->plugin_info->api_expired ) {
						tribe( 'pue.notices' )->add_notice( Tribe__PUE__Notices::EXPIRED_KEY, $this->plugin_name );
					}
				} else {
					$plugin_file = $this->get_plugin_file();

					// Clean up any stale update info.
					if ( isset( $updates->response[ $plugin_file ] ) ) {
						unset( $updates->response[ $plugin_file ] );
					}

					/**
					 * If the plugin is up to date, we need to add it to the `no_update` property so that enable auto updates can appear correctly in the UI.
					 *
					 * See this post for more information:
					 *
					 * @link https://make.wordpress.org/core/2020/07/30/recommended-usage-of-the-updates-api-to-support-the-auto-updates-ui-for-plugins-and-themes-in-wordpress-5-5/
					 */
					/** @var \stdClass $transient */
					if ( ! isset( $updates->no_update ) ) {
						$updates->no_update = [];
					}

					$updates->no_update[ $plugin_file ] = $state->update->to_wp_format( $plugin_file );
				}
			}

			$this->update_state( $state );

			return $updates;
		}

		/**
		 * Clears out the site external site option and re-checks the license key
		 *
		 * @param string $value           The value of the option.
		 * @param string $field_id        The ID of the field.
		 * @param object $validated_field The validated field.
		 *
		 * @return string
		 */
		public function check_for_api_key_error( $value, $field_id, $validated_field ) {

			// Only hook into our option.
			if ( $this->pue_install_key !== $field_id ) {
				return $value;
			}

			if ( 'service' !== $this->context ) {
				$this->check_for_updates( [], true );
			}

			$network_option = false;

			if ( ! empty( $validated_field->field['network_option'] ) ) {
				$network_option = (bool) $validated_field->field['network_option'];
			}

			$key_type = 'local';

			if ( $network_option ) {
				$key_type = 'network';
			}

			$current_key = $this->get_key( $key_type );

			// if we are saving this PUE key, we need to make sure we update the license key notices
			// appropriately. Otherwise, we could have an invalid license key in place but the notices
			// aren't being thrown globally.

			$query_args = $this->get_validate_query();

			$query_args['key'] = sanitize_text_field( $value );

			$this->license_key_status( $query_args );

			return $value;
		}

		/**
		 * Intercept plugins_api() calls that request information about our plugin and
		 * use the configured API endpoint to satisfy them.
		 *
		 * @see plugins_api()
		 *
		 * @param mixed        $result The result object or NULL.
		 * @param string       $action The type of information being requested from the Plugin Install API.
		 * @param array|object $args   Arguments used to query for installer pages from the Plugin Install API.
		 *
		 * @return mixed
		 */
		public function inject_info( $result, $action = null, $args = null ) {
			$relevant = ( 'plugin_information' === $action ) && isset( $args->slug ) && ( $args->slug === $this->slug );
			if ( ! $relevant ) {
				return $result;
			}

			$query_args = $this->get_validate_query();

			$plugin_info = $this->license_key_status( $query_args );

			if ( $plugin_info ) {
				return $plugin_info->to_wp_format();
			}

			return $result;
		}

		/**
		 * Register a callback for filtering query arguments.
		 *
		 * The callback function should take one argument - an associative array of query arguments.
		 * It should return a modified array of query arguments.
		 *
		 * @uses add_filter() This method is a convenience wrapper for add_filter().
		 *
		 * @param callback $callback The callback function.
		 */
		public function add_query_arg_filter( $callback ) {
			add_filter( 'tribe_puc_request_info_query_args-' . $this->get_slug(), $callback );
		}

		/**
		 * Register a callback for filtering arguments passed to wp_remote_get().
		 *
		 * The callback function should take one argument - an associative array of arguments -
		 * and return a modified array or arguments. See the WP documentation on wp_remote_get()
		 * for details on what arguments are available and how they work.
		 *
		 * @uses add_filter() This method is a convenience wrapper for add_filter().
		 *
		 * @param callback $callback The callback function.
		 */
		public function add_http_request_arg_filter( $callback ) {
			add_filter( 'tribe_puc_request_info_options-' . $this->get_slug(), $callback );
		}

		/**
		 * Register a callback for filtering the plugin info retrieved from the external API.
		 *
		 * The callback function should take two arguments. If the plugin info was retrieved
		 * successfully, the first argument passed will be an instance of Tribe__PUE__Plugin_Info. Otherwise,
		 * it will be NULL. The second argument will be the corresponding return value of
		 * wp_remote_get (see WP docs for details).
		 *
		 * The callback function should return a new or modified instance of Tribe__PUE__Plugin_Info or NULL.
		 *
		 * @uses add_filter() This method is a convenience wrapper for add_filter().
		 *
		 * @param callback $callback The callback function.
		 */
		public function add_result_filter( $callback ) {
			add_filter( 'tribe_puc_request_info_result-' . $this->get_slug(), $callback, 10, 2 );
		}

		/**
		 * Insert an array after a specified key within another array.
		 *
		 * @param string $key          The key to insert after.
		 * @param array  $source_array The array to insert into.
		 * @param array  $insert_array The array to insert.
		 *
		 * @return array
		 */
		public static function array_insert_after_key( $key, $source_array, $insert_array ) {
			if ( array_key_exists( $key, $source_array ) ) {
				$position     = array_search( $key, array_keys( $source_array ), true ) + 1;
				$source_array = array_slice( $source_array, 0, $position, true ) + $insert_array + array_slice( $source_array, $position, null, true );
			} else {
				// If no key is found, then add it to the end of the array.
				$source_array += $insert_array;
			}

			return $source_array;
		}

		/**
		 * Add this plugin key to the list of keys
		 *
		 * @param array $keys The list of keys.
		 *
		 * @return array $keys
		 */
		public function return_install_key( $keys = [] ) {
			$key = $this->get_key();

			if ( ! empty( $key ) ) {
				$keys[ $this->get_slug() ] = $key;
			}

			return $keys;
		}

		/**
		 * Prevent the default inline update-available messages from appearing, as we
		 * have implemented our own.
		 *
		 * @see resources/js/pue-notices.js
		 */
		public function remove_default_inline_update_msg() {
			remove_action( "after_plugin_row_{$this->plugin_file}", 'wp_plugin_update_row' );
		}

		/**
		 * Returns the domain of the single site installation
		 *
		 * Will try to read it from the $_SERVER['SERVER_NAME'] variable
		 * and fall back on the one contained in the siteurl option.
		 *
		 * @return string
		 */
		protected function get_site_domain() {
			if ( isset( $_SERVER['SERVER_NAME'] ) ) {
				return $_SERVER['SERVER_NAME'];
			}
			$site_url = wp_parse_url( get_option( 'siteurl' ) );
			if ( ! $site_url || ! isset( $site_url['host'] ) ) {
				return '';
			} else {
				return strtolower( $site_url['host'] );
			}
		}

		/**
		 * Check whether the current plugin is active for the network or not.
		 *
		 * @return boolean Whether the plugin is network activated
		 */
		protected function is_plugin_active_for_network() {

			if ( ! is_multisite() ) {
				return false;
			}

			$map = [
				'event-aggregator/event-aggregator.php' => 'the-events-calendar/the-events-calendar.php',
			];

			$plugin_file = $this->get_plugin_file();

			if ( isset( $map[ $this->plugin_file ] ) ) {
				$plugin_file = $map[ $this->plugin_file ];
			}

			if ( function_exists( 'is_plugin_active_for_network' ) ) {
				// If is_plugin_active_for_network() is available, let's use it!
				return is_plugin_active_for_network( $plugin_file );
			} else {
				// When this method is called sufficiently early in the request,
				// is_plugin_active_for_network() may not be available (#115826).
				$plugins = get_site_option( 'active_sitewide_plugins' );
				return isset( $plugins[ $plugin_file ] );
			}
		}

		/**
		 * Returns the localized string for a plugin or component license state.
		 *
		 * @return string The localized state string.
		 */
		protected function get_network_license_state_string() {
			$states = [
				'licensed'     => esc_html__( 'A valid license has been entered by your network administrator.', 'tribe-common' ),
				'not-licensed' => esc_html__( 'No license entered. Consult your network administrator.', 'tribe-common' ),
				'expired'      => esc_html__( 'Expired license. Consult your network administrator.', 'tribe-common' ),
			];

			$response = $this->validate_key( $this->get_key( 'network' ), true );

			if ( isset( $response['status'] ) && 1 === (int) $response['status'] ) {
				$state = 'licensed';
			} elseif ( isset( $response['api_expired'] ) && true === (bool) $response['api_expired'] ) {
				$state = 'expired';
			} else {
				$state = 'not-licensed';
			}

			return $states[ $state ];
		}

		/**
		 * Whether the user should be shown the fully editable subsite license field or not.
		 *
		 * This check will happen in the context of the plugin administration area; checks on the user
		 * capability to edit the plugin settings have been made before.
		 *
		 * @return bool
		 */
		public function should_show_subsite_editable_license() {
			if ( ! is_multisite() ) {
				return true;
			}

			if ( is_network_admin() ) {
				return false;
			}

			if ( $this->is_plugin_active_for_network() && ! is_super_admin() ) {
				return false;
			}

			return true;
		}

		/**
		 * Whether the user should be shown the override control to override the network license key or not.
		 *
		 * This check will happen in the context of the plugin administration area; checks on the user
		 * capability to edit the plugin settings have been made before.
		 *
		 * @return bool
		 */
		public function should_show_overrideable_license() {
			if ( is_network_admin() ) {
				return false;
			}

			if ( is_super_admin() ) {
				return false;
			}

			if ( ! $this->is_plugin_active_for_network() ) {
				return false;
			}

			return true;
		}

		/**
		 * Whether the user should be shown the fully editable network license field or not.
		 *
		 * This check will happen in the context of the network plugin administration area; checks on the user
		 * capability to edit the network plugin settings have been made before.
		 *
		 * @return bool
		 */
		public function should_show_network_editable_license() {
			return is_network_admin() && is_super_admin();
		}

		/**
		 * Determines if the value on the DB is the correct format.
		 *
		 * @since 4.15.0
		 *
		 * @return bool
		 */
		public function is_valid_key_format() {
			$license_opt = (string) get_option( $this->get_license_option_key() );
			if ( empty( $license_opt ) ) {
				return false;
			}

			if ( ! preg_match( '/([0-9a-z]+)/i', $license_opt, $matches ) ) {
				return false;
			}

			// Pull the matching string into a variable.
			$license = $matches[1];

			if ( 40 !== strlen( $license ) ) {
				return false;
			}

			return true;
		}
	}
}
