<?php
/**
 * Plugin Name: WP Overnight Updater
 * Description: Automatic updates for WP Overnight plugins.
 * Version:     3.0.4
 * Author:      WP Overnight
 * Author URI:  https://www.wpovernight.com
 * Text Domain: wpo-updater
 */

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

if ( class_exists( 'WPO_Update_Helper' ) ) {
	return;
}

class WPO_Update_Helper {
	
	public string $version = '3.0.4';

	private string $wpo_update_api;
	private string $plugin_item_name;
	private string $plugin_file;
	private string $plugin_basename;
	private string $plugin_slug;
	private string $plugin_license_slug;
	private string $plugin_version;
	private string $plugin_author;
	private string $license_keys_option_name;
	private string $license_key;
	
	private ?\WPO\EDD_SL_Plugin_Updater $edd_updater = null;

	// Constant containing all WPO plugins that have built-in updater,
	// with the version when the updater was introduced and the upgrade IDs
	private const WPO_UPDATER_PLUGINS_WITH_UPDATER = array(
		'woocommerce-product-batch-numbers/woocommerce-product-batch-numbers.php' => array(
			'version'  => '1.1.5',
			'download' => 249266,
			'upgrades' => array(
				3  => 2,
				25 => 3,
			),
		),
		'woocommerce-pdf-ips-templates/woocommerce-pdf-ips-templates.php' => array(
			'version'  => '2.13.1',
			'download' => 1017,
			'upgrades' => array(
				3  => 5,
				25 => 6,
			),
		),
		'woocommerce-opening-hours/opening-hours.php' => array(
			'version'  => '1.15.25',
			'download' => 29974,
			'upgrades' => array(
				3  => 2,
				25 => 1,
			),
		),
		'woocommerce-printnode/print-orders.php' => array(
			'version'  => '1.7.3',
			'download' => 259681,
			'upgrades' => array(
				5  => 1,
				25 => 2,
			),
		),
		'woocommerce-customer-manager/woocommerce-customer-manager.php' => array(
			'version'  => '2.3.0',
			'download' => 19356,
			'upgrades' => array(
				3  => 1,
				25 => 2,
			),
		),
		'improved-external-products-pro/wc-improved-external-products-pro.php' => array(
			'version'  => '1.6.0',
			'download' => 36110,
			'upgrades' => array(
				3  => 1,
				25 => 2,
			),
		),
		'woocommerce-next-order-coupon/woocommerce-next-order-coupon.php' => array(
			'version'  => '1.7.2',
			'download' => 664,
			'upgrades' => array(
				3  => 1,
				25 => 2,
			),
		),
		'wp-menu-cart-pro/wp-menu-cart-pro.php' => array(
			'version'  => '3.5.0',
			'download' => 10,
			'upgrades' => array(
				3  => 1,
				25 => 2,
			),
		),
		'wc-bulk-order-form-pro/woocommerce-bulk-order-form-pro.php' => array(
			'version'  => '3.2.0',
			'download' => 879,
			'upgrades' => array(
				3  => 1,
				25 => 2,
			),
		),
		'woocommerce-pdf-ips-pro/woocommerce-pdf-ips-pro.php' => array(
			'version'  => '2.9.2',
			'download' => 15179,
			'upgrades' => array(
				3  => 1,
				25 => 2,
			),
		),
		'woocommerce-bulk-csv-stock-updater/woocommerce-bulk-stock-updater.php' => array(
			'version'  => '2.1.2',
			'download' => 53047,
			'upgrades' => array(
				25 => 2,
			),
		),
		'woocommerce-ultimate-barcodes/woocommerce-ultimate-barcodes.php' => array(
			'version'  => '1.0.0',
			'download' => 411406,
			'upgrades' => array(
				3  => 1,
				25 => 2,
			),
		),
		'woocommerce-address-labels/woocommerce-address-labels.php' => array(
			'version'  => '1.6.9',
			'download' => 2122,
			'upgrades' => array(
				3  => 1,
				25 => 2,
			),
		),
		'woocommerce-order-list/woocommerce-order-list.php' => array(
			'version'  => '1.6.5',
			'download' => 10774,
			'upgrades' => array(
				3  => 1,
				25 => 2,
			),
		),
		'woocommerce-eu-vat-compliance-premium/eu-vat-compliance-premium.php' => array(
			'version'  => '1.25.10',
			'download' => 32729,
			'upgrades' => array(
				3  => 1,
				25 => 2,
			),
		),
		'wc-postcode-checker/wc-postcode-checker.php' => array(
			'version'  => '2.5.6',
			'download' => 47303,
			'upgrades' => array(
				5  => 2,
				25 => 3,
			),
		),
		'wc-reminder-emails/wc-reminder-emails.php' => array(
			'version'  => '2.2.5',
			'download' => 51660,
			'upgrades' => array(
				5  => 1,
				25 => 2,
			),
		),
		'woocommerce-order-proposal/woocommerce-order-proposal.php' => array(
			'version'  => '1.7.19',
			'download' => 114886,
			'upgrades' => array(
				3  => 1,
				25 => 2,
			),
		),
		'wc-bulk-order-form-prepopulated/wc-bulk-order-form-prepopulated.php' => array(
			'version'  => '3.1.5',
			'download' => 16222,
			'upgrades' => array(
				3  => 1,
				25 => 2,
			),
		),
		'woocommerce-product-serialnumbers/woocommerce-product-serialnumbers.php' => array(
			'version'  => '1.1.16',
			'download' => 151970,
			'upgrades' => array(
				3  => 1,
				25 => 2,
			),
		),
		'woocommerce-manual-products/woocommerce-manual-products.php' => array(
			'version'  => '1.3.0',
			'download' => 44051,
			'upgrades' => array(
				3  => 1,
				25 => 2,
			),
		),
		'woocommerce-minima-and-maxima/woocommerce-minima-and-maxima.php' => array(
			'version'  => '1.7.4',
			'download' => 46620,
			'upgrades' => array(
				25 => 2,
			),
		),
		'wpo-ips-edocs-network-tab/wpo-ips-edocs-network-tab.php' => array(
			'version'  => '1.0.0',
			'download' => 772130,
			'upgrades' => array(),
		),
	);

	/**
	 * Constructor
	 * 
	 * @param string $_item_name
	 * @param string $_file
	 * @param string $_license_slug
	 * @param string $_version
	 * @param string $_author
	 */
	public function __construct( $_item_name, $_file, $_license_slug, $_version, $_author ) {
		$this->wpo_update_api           = apply_filters( 'wpovernight_api_url', 'https://wpovernight.com/license-api/' );
		$this->plugin_item_name         = $_item_name;
		$this->plugin_file              = $_file;
		$this->plugin_basename          = plugin_basename( $_file );
		$this->plugin_slug              = dirname( $this->plugin_basename );
		$this->plugin_license_slug      = $_license_slug;
		$this->plugin_version           = $_version;
		$this->plugin_author            = $_author;
		$this->license_keys_option_name = 'wpocore_settings';
		$this->license_key              = $this->get_license_key();

		if ( ! class_exists( '\\WPO\\EDD_SL_Plugin_Updater' ) ) {
			// load the EDD updater class
			include __DIR__ . '/EDD_SL_Plugin_Updater.php';
		}

		add_action( 'init', array( $this, 'load_textdomain' ) );
		
		add_filter( 'edd_sl_plugin_updater_api_params', array( $this, 'set_url_for_version_request' ), 10, 3 );

		$this->load_edd_updater();

		add_action( 'admin_init', array( $this, 'disable_subsite_update_notifications' ) );

		add_action( 'wp_ajax_wpo_updater_licence_key_action_' . $this->plugin_license_slug, array( $this, 'ajax_remote_license_actions' ) );
		add_filter( 'http_response', array( $this, 'unauthorized_response'), 10, 3 );

		add_filter( 'network_admin_plugin_action_links_' . $this->plugin_basename, array( $this, 'add_license_activation_link' ), 10, 4 );
		add_filter( 'plugin_action_links_' . $this->plugin_basename, array( $this, 'add_license_activation_link' ), 10, 4 );
		
		add_action( 'after_plugin_row_' . $this->plugin_basename, array( $this, 'plugin_license_field' ), 10, 3 );

		add_action( 'admin_enqueue_scripts', array( $this, 'scripts_styles' ) );
		
		add_filter( 'http_request_args', array( $this, 'exclude_plugin_from_wp_api' ), 10, 2 );
		
		add_filter( 'plugins_api', array( $this, 'override_plugins_api_info' ), 20, 3 );

		add_action( 'admin_notices', array( $this, 'maybe_show_sidekick_notice' ) );
	}

	/**
	 * Shows the Sidekick uninstall notice if needed.
	 *
	 * @return void
	 */
	public function maybe_show_sidekick_notice(): void {
		static $wpo_updater_sidekick_uninstall_persistent_notice_shown = false;

		if ( isset( get_plugins()['wpovernight-sidekick/wpovernight-sidekick.php'] ) && 0 === $this->plugins_with_lower_version_or_sidekick() ) {
			$message = sprintf(
				/* translators: %s: plugin name */
				__( 'The %s plugin is now redundant since all of your installed WP Overnight plugins have built-in updaters.', 'wpo-updater' ),
				'<strong>WP Overnight Sidekick</strong>'
			) . ' ';

			if ( class_exists( 'WPO_Updater' ) ) {
				deactivate_plugins( 'wpovernight-sidekick/wpovernight-sidekick.php' );
				$message .= sprintf(
					/* translators: %s: plugin name */
					__( "We've deactivated %s, and you can safely delete it.", 'wpo-updater' ),
					'<strong>Sidekick</strong>'
				) . ' ';
			}

			$message .= sprintf(
				/* translators: %s: here link. */
				__( 'More information on managing your license with the built-in updater is available %s.', 'wpo-updater' ), 
				'<a target="_blank" href="https://docs.wpovernight.com/general/installing-wp-overnight-plugins/#activating-your-license">' . __( 'here', 'wpo-updater' ) . '</a>'
			);

			if ( $wpo_updater_sidekick_uninstall_persistent_notice_shown ) {
				return;
			}
			
			if ( is_admin() && ! get_option( 'wpo_updater_sidekick_uninstall_persistent_notice' ) ) {
				?>
				<div class="notice notice-warning">
					<p><?php echo $message; ?></p>
					<p><a href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wpo_updater_sidekick_uninstall_persistent_notice', 'true' ), 'wpo_updater_sidekick_uninstall_persistent_notice_nonce' ) ); ?>"><?php _e( 'Hide this message', 'wpo-updater' ); ?></a></p>
				</div>
				<?php
			}

			$wpo_updater_sidekick_uninstall_persistent_notice_shown = true;
		}
		
		if ( isset( $_REQUEST['wpo_updater_sidekick_uninstall_persistent_notice'] ) && isset( $_REQUEST['_wpnonce'] ) ) {
			if ( wp_verify_nonce( $_REQUEST['_wpnonce'], 'wpo_updater_sidekick_uninstall_persistent_notice_nonce' ) ) {
				update_option( 'wpo_updater_sidekick_uninstall_persistent_notice', true );
			}
			wp_redirect( 'plugins.php' );
			exit;
		}
	}

	/**
	 * Returns the number of plugins that have activated version lower than defined in the array or the plugins that have Sidekick.
	 *
	 * @return int
	 */
	private function plugins_with_lower_version_or_sidekick(): int {
		$count       = 0;
		$all_plugins = get_plugins();
	
		foreach ( self::WPO_UPDATER_PLUGINS_WITH_UPDATER as $slug => $plugin ) {
			if ( isset( $all_plugins[ $slug ]['Version'] ) && version_compare( $all_plugins[ $slug ]['Version'], $plugin['version'], '<' ) ) {					
				$count++;
			}
		}
	
		return $count;
	}

	/**
	 * Load the plugin textdomain
	 * 
	 * @return void
	 */
	public function load_textdomain(): void {
		load_plugin_textdomain( 'wpo-updater', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
	}

	/**
	 * Enqueue scripts and styles.
	 *
	 * @param string|null $hook_suffix
	 * @return void
	 */
	public function scripts_styles( ?string $hook_suffix ): void {
		if ( ! is_admin() ) {
			return;
		}

		$is_plugins_page = ( 'plugins.php' === $hook_suffix );

		if ( ! $is_plugins_page && function_exists( 'get_current_screen' ) ) {
			$screen          = get_current_screen();
			$is_plugins_page = ( $screen && 'plugins' === $screen->id );
		}

		if ( ! $is_plugins_page ) {
			return;
		}

		if ( wp_script_is( 'wpo-update-helper', 'enqueued' ) ) {
			return;
		}

		wp_enqueue_style(
			'wpo-update-helper',
			plugin_dir_url( __FILE__ ) . 'assets/style.css',
			array(),
			$this->version
		);

		wp_enqueue_script(
			'wpo-update-helper',
			plugin_dir_url( __FILE__ ) . 'assets/script.js',
			array( 'jquery' ),
			$this->version,
			true
		);

		wp_localize_script(
			'wpo-update-helper',
			'wpo_update_helper',
			[
				'ajaxurl' => admin_url( 'admin-ajax.php' ),
				'nonce'   => wp_create_nonce( 'wpo_update_helper' ),
			]
		);
	}

	/**
	 * Add license activation link to plugin action links
	 * 
	 * @param array $actions
	 * @param string $plugin_file
	 * @param array|null $plugin_data
	 * @param string|null $context
	 * @return array
	 */
	public function add_license_activation_link( array $actions, string $plugin_file, ?array $plugin_data, ?string $context ): array {
		if ( $this->should_show_license_manager() ) {
			$actions['wpo_license'] = sprintf(
				'<a class="wpo-license-registration-toggle" data-edd_action="show_license" data-plugin_license_slug="%1$s" data-plugin_slug="%2$s" id="%2$s-manage-license">%3$s</a>',
				esc_attr( $this->plugin_license_slug ),
				esc_attr( $this->plugin_slug ),
				esc_html__( 'Manage license', 'wpo-updater' )
			);
		}
		return $actions;
	}

	/**
	 * Display the license field in the plugins list
	 * 
	 * @param string $plugin_file
	 * @param array $plugin_data
	 * @param string $status
	 * @return void
	 */
	public function plugin_license_field( string $plugin_file, array $plugin_data, string $status = '' ): void {
		if ( ! $this->should_show_license_manager() ) {
			return;
		}
		?>
		<tr class="plugin-update-tr active wpo-license-row-<?php echo esc_attr( $this->plugin_slug ); ?>">
			<td colspan="4" class="plugin-update colspanchange wpo-update-helper" data-plugin_license_slug="<?php echo esc_attr( $this->plugin_license_slug ); ?>">
				<?php wp_nonce_field( 'wpo_update_helper', 'wpo_update_helper_nonce' ); ?>
				<?php
					if ( ! $this->license_is_active() ) {
						$this->plugin_license_field_content();
					} else {
						?>
						<div class="license-data"></div>
						<?php
					}
				?>
			</td>
		</tr>
		<?php
	}

	/**
	 * Determine if the license manager should be shown
	 *
	 * @return bool
	 */
	public function should_show_license_manager(): bool {
		if ( is_plugin_active_for_network( $this->plugin_basename ) ) {
			// multisite & network active: show only on network admin
			$show = is_network_admin(); 
		} else {
			// single site OR multisite and not active on network: only show on the subsite
			$show = ! is_network_admin();
		}
		return apply_filters( 'wpo_updater_show_license_manager', $show, $this->plugin_slug );
	}

	/**
	 * Render the license field content
	 * 
	 * @param array $action_args
	 * @return void
	 */
	public function plugin_license_field_content( array $action_args = array() ): void {
		$name = "wpocore_settings[{$this->plugin_license_slug}]";
		if ( array_key_exists( 'license_key', $action_args ) ) {
			$this->license_key = $action_args['license_key'];
		}

		if ( empty( $action_args ) ) {
			if ( $this->license_is_active() ) {
				$action_args = array(
					'edd_action'  => 'check_license',
					'license_key' => trim( $this->license_key ),
				);
			} else {
				$action_args = array();
			}
		}

		$status = $this->remote_license_actions( $action_args );

		if ( is_object( $status ) && isset( $status->license ) ) {
			$activation_status = $status->license;
		} elseif ( is_string( $status ) ) {
			$activation_status = $status;
		} else {
			$activation_status = '';
		}
		
		$license_state_message = is_object( $status ) && isset( $status->license_state_message ) ? $status->license_state_message : '';
		
		if ( ! $this->license_is_active() ) {
			$notice_class = 'notice-warning notice-alt';
		} else {
			$notice_class = 'notice-alt';
		}
		
		// allow setting license field to password
		if ( apply_filters( 'wpo_update_helper_shield_license_field', false ) ) {
			$license_field_type = 'password';
		} else {
			$license_field_type = 'text';
		}
		?>
		<div class="notice inline <?php echo esc_attr( $notice_class ); ?> license-data">
			<p class="wpo-license-status-<?php echo esc_attr( $activation_status ); ?>">
				<?php if ( ! $this->license_is_active() ): ?>
					<p><label for="<?php echo esc_attr( $name ); ?>"><?php esc_attr_e( 'Enter your license key to receive plugin updates', 'wpo-updater' ) ?>:</label></p>
				<?php endif ?>
				<span class="state-indicator">
					<input type="<?php echo esc_attr( $license_field_type ); ?>" size="40" class="wpo-license-key" id="<?php echo esc_attr( $name ); ?>" name="<?php echo esc_attr( $name ); ?>" value="<?php echo esc_attr( $this->license_key ); ?>" />
					<span class="license-state"><?php echo ! empty( $license_state_message ) ? esc_html( $license_state_message ) : ''; ?></span>
				</span>
				<?php if ( 'valid' === $activation_status ): ?>
					<span class="button secondary deactivate" data-edd_action="deactivate_license"><?php esc_html_e( 'Deactivate', 'wpo-updater' ); ?></span>
				<?php else: ?>
					<span class="button secondary activate" data-edd_action="activate_license"><?php esc_html_e( 'Activate', 'wpo-updater' ); ?></span>
				<?php endif ?>
				<?php if ( 'expired' === $activation_status || 'expired' === $license_state_message ): ?>
					<a href="<?php echo esc_url( $this->get_renewal_url() ); ?>" target="_blank" rel="noopener noreferrer"><span class="button button-primary"><?php esc_html_e( 'Renew your license', 'wpo-updater' ); ?></span></a>
				<?php endif ?>
				<?php if ( ! empty( $status->action_message ) ): ?>
					<div class="activation-toggle-message">
						<?php echo wp_kses_post( $status->action_message ); ?>
					</div>
				<?php endif ?>
				<p class="license-info">
					<?php echo ! empty( $status->license_info ) ? wp_kses_post( $status->license_info ) : ''; ?>
				</p>
				<?php if ( ! in_array( $activation_status, array( 'valid', 'site_inactive' ), true ) ): ?>
					<p class="documentation">
						<span class="dashicons dashicons-editor-help"></span>
						<span>
							<?php
								printf(
									/* translators: 1: opening <a> tag, 2: closing </a> tag */
									esc_html__( 'More information about activating your license can be found in our documentation: %1$sActivating your license%2$s.', 'wpo-updater' ),
									'<a href="https://docs.wpovernight.com/general/how-to-get-the-latest-version/#receiving-automatic-plugin-updates" target="_blank" rel="noopener noreferrer">',
									'</a>'
								);
							?>
						</span>
					</p>
				<?php endif; ?>
			</p>
		</div>
		<?php
	}

	/**
	 * Check if license is active
	 * 
	 * @return bool
	 */
	public function license_is_active(): bool {
		return empty( $this->license_key ) ? false : true;
	}

	/**
	 * Load the EDD Updater class
	 * 
	 * @return void
	 */
	public function load_edd_updater(): void {
		// setup the updater
		if ( $this->license_is_active() && class_exists( '\\WPO\\EDD_SL_Plugin_Updater' ) ) {
			$this->edd_updater ??= new \WPO\EDD_SL_Plugin_Updater( $this->wpo_update_api, $this->plugin_file, array( 
					'version' 	=> $this->plugin_version,   // current version number
					'license' 	=> $this->license_key,      // license key (used get_option above to retrieve from DB)
					'item_name' => $this->plugin_item_name, // name of this plugin
					'author' 	=> $this->plugin_author     // author of this plugin
				)
			);
		}
	}

	/**
	 * Don't show update notifications on subsites when plugin is network activated
	 * 
	 * @return void
	 */	
	public function disable_subsite_update_notifications(): void {
		if (
			is_multisite()                                                  &&
			! is_network_admin()                                            &&
			is_plugin_active_for_network( $this->plugin_basename )          &&
			! empty( $this->edd_updater )                                   &&
			method_exists( $this->edd_updater, 'show_update_notification' )
		) {
			remove_action( 'after_plugin_row', array( $this->edd_updater, 'show_update_notification' ) );
		}
	}

	/**
	 * Handle AJAX license actions
	 * 
	 * @return void
	 */
	public function ajax_remote_license_actions(): void {
		check_ajax_referer( 'wpo_update_helper', 'security' );

		if ( empty( $_POST['remote_edd_action'] ) ) {
			return;
		}

		if ( 'show_license' === $_POST['remote_edd_action'] ) {
			$args = array(
				'edd_action'  => 'check_license',
				'license_key' => trim( $this->license_key ),
			);
		} else {
			$args = array(
				'edd_action'  => $_POST['remote_edd_action'],
				'license_key' => trim( $_POST['license_key'] ),
			);
		}

		ob_start();
		$this->plugin_license_field_content( $args );
		$html = ob_get_clean();

		wp_send_json( array( 'html' => $html ) );
	}

	/**
	 * Perform remote license actions (Activate/deactivate)
	 * 
	 * @param array $args
	 * @return object
	 */
	public function remote_license_actions( array $args ) {
		if ( empty( $args['edd_action'] ) ) {
			return;
		}

		// retrieve the license from the database
		$this->save_license_key( $args['license_key'] );

		if ( empty( $args['license_key'] ) ) {
			$response = array(
				'action_message'        => __( 'Please enter a license key.', 'wpo-updater' ),
				'license_info'          => '',
				'license_state'         => 'incomplete',
				'license_state_message' => __( 'invalid', 'wpo-updater' ),
			);

			return (object) $response;
		}
		
		// When activating a license, first verify if there’s a potential mismatch
		if ( 'activate_license' === $args['edd_action'] ) {
			$precheck = $this->precheck_license_mismatch( $args['license_key'] );
			if ( $precheck ) {
				return $precheck; // Early return: error or mismatch
			}
		}

		// data to send in our API request
		$api_params = array(
			'edd_action' => $args['edd_action'],
			'license' 	 => $args['license_key'],
			'item_name'  => urlencode( $this->plugin_item_name ), // the name of our product in EDD
			'url'        => $this->get_home_url(), // License server has a fallback to the User-Agent, but this could have been filtered so we can't rely on it
		);

		$response = $this->edd_api_action( $api_params );

		if ( isset( $response->wp_error ) ) {
			return $response;
		}

		if ( isset( $response->expires ) ) {
			if ( 'lifetime' === $response->expires ) {
				$exp_date = __( 'Forever', 'wpo-updater' );
			} else {
				$exp_date = date_i18n( get_option( 'date_format' ), strtotime( $response->expires ) );
			}
		}

		switch ( $response->license ) {
			case 'valid':
				if ( 'activate_license' === $args['edd_action'] ) {
					$response->action_message    = __( 'Activated your license for this site.', 'wpo-updater' );
				}
				$response->license_state         = 'valid';
				$response->license_state_message = __( 'valid', 'wpo-updater' );
				$response->license_info          = sprintf(
					/* translators: 1: license expiration date, 2: number of active sites, 3: site limit */
					__( 'This license is valid until: %1$s (Active sites: %2$s / %3$s)', 'wpo-updater' ),
					$exp_date,
					$response->site_count,
					$response->license_limit
				);
				break;
			case 'deactivated':
				// get activation count & limit
				$api_params = array(
					'edd_action'=> 'check_license',
					'license' 	=> $args['license_key'],
					'item_name' => urlencode( $this->plugin_item_name ), // the name of our product in EDD
				);
				$check_response = $this->edd_api_action( $api_params );
				
				if ( isset( $check_response->wp_error ) ) {
					return $check_response;
				}
		
				$exp_date      = date_i18n( get_option( 'date_format' ), strtotime( $check_response->expires ) );
				$license_limit = $check_response->license_limit;
				$site_count    = $check_response->site_count;

				$response->action_message        = __( 'Deactivated your license for this site.', 'wpo-updater' );
				$response->license_state         = 'invalid';
				$response->license_state_message = __( 'deactivated', 'wpo-updater' );
				$response->license_info          = sprintf(
					/* translators: 1: license expiration date, 2: number of active sites, 3: site limit */
					__( 'This license is valid until: %1$s (Active sites: %2$s / %3$s)', 'wpo-updater' ),
					$exp_date,
					$site_count,
					$license_limit
				);
				break;
			case 'expired':
				$response->license_state         = 'invalid';
				$response->license_state_message = __( 'expired', 'wpo-updater' );
				$response->license_info          = __( 'This license was valid until: ' . $exp_date, 'wpo-updater' );
				break;
			case 'inactive':
			case 'site_inactive':
				$response->license_state         = 'valid';
				$response->license_state_message = __( 'valid', 'wpo-updater' );
				$response->license_info = sprintf(
					/* translators: 1: license expiration date, 2: number of active sites, 3: site limit */
					__( 'This license is valid until: %1$s (Active sites: %2$s / %3$s)', 'wpo-updater' ),
					$exp_date,
					$response->site_count,
					$response->license_limit
				);
				break;
			case 'failed':
				$response->action_message        = __( 'Deactivated your license for this site.', 'wpo-updater' );
				$response->license_state         = 'invalid';
				$response->license_state_message = __( 'invalid', 'wpo-updater' );
				$response->license_info          = '';
				break;
			case 'invalid':
				$error = ! empty( $response->error ) ? $response->error : '';
				if ( 'missing' === $error ) {
					$response->action_message        = __( 'Your license key is incorrect.', 'wpo-updater' );
					$response->license_state         = 'incomplete';
					$response->license_state_message = __( 'invalid', 'wpo-updater' );
					$response->license_info          = __( 'Please enter the correct license key.', 'wpo-updater' );
				} elseif ( 'expired' === $error ) {
					$response->action_message        = __( 'Your license key is expired.', 'wpo-updater' );
					$response->license_state         = 'incomplete';
					$response->license_state_message = __( 'expired', 'wpo-updater' );
					$response->license_info          = sprintf(
						/* translators: expiration date */
						__( 'This license was valid until: %s', 'wpo-updater' ),
						$exp_date
					);
				} elseif ( 'no_activations_left' === $error ) {
					// get license ID for upgrade link
					$api_params = array(
						'edd_action'=> 'check_license',
						'license' 	=> $args['license_key'],
						'item_name' => urlencode( $this->plugin_item_name ), // the name of our product in EDD
					);
					$check_response = $this->edd_api_action( $api_params );
					
					if ( isset( $check_response->wp_error ) ) {
						return $check_response;
					}
				
					$response->license_id = isset( $check_response->license_id ) ? $check_response->license_id : 0;
					$upgrade_url          = $this->get_upgrade_url_for_exhausted_license( $response );
					
					if ( $upgrade_url ) {
						$response->action_message = sprintf(
							/* translators: 1: strong tag, 2: opening <a> tag, 3: closing </a> tag */
							__( '%1$s &mdash; You can %2$supgrade your license%3$s to increase your site limit.', 'wpo-updater' ),
							'<strong>' . __( 'No Activations Left', 'wpo-updater' ) . '</strong>',
							'<a href="' . esc_url( $upgrade_url ) . '" target="_blank" rel="noopener noreferrer">',
							'</a>'
						);
					} else {
						// Fallback when there are no higher tiers.
						$response->action_message = sprintf(
							/* translators: 1: strong tag, 2: opening <a> tag, 3: closing </a> tag */
							__( '%1$s &mdash; Please deactivate an existing site or contact our %2$ssupport team%3$s for assistance.', 'wpo-updater' ),
							'<strong>' . __( 'No Activations Left', 'wpo-updater' ) . '</strong>',
							'<a href="https://wpovernight.com/contact/" target="_blank" rel="noopener noreferrer">',
							'</a>'
						);
					}
					
					$response->license_state         = 'incomplete';
					$response->license_state_message = __( 'invalid', 'wpo-updater' );
					$response->license_info          = '';
				} else {
					$response->action_message        = sprintf(
						/* translators: 1: opening <a> tag, 2: closing </a> tag */
						__( 'An unknown error occurred while validating your license. Please double-check your license key or contact our %1$ssupport team%2$s if the problem persists.', 'wpo-updater' ),
						'<a href="https://wpovernight.com/contact/" target="_blank" rel="noopener noreferrer">',
						'</a>'
					);
					$response->license_info          = esc_html( $error );
					$response->license_state         = 'incomplete';
					$response->license_state_message = __( 'invalid', 'wpo-updater' );
				}
				break;
			default:
				break;
		}

		return $response;
	}

	/**
	 * Perform the API request to the WPO license server
	 * 
	 * @param array $api_params
	 * @return object
	 */
	public function edd_api_action( array $api_params ) {
		// Call the WPO API.
		$response = wp_remote_get( esc_url_raw( add_query_arg( $api_params, $this->wpo_update_api ) ), array( 'timeout' => 15, 'sslverify' => true ) );

		// make sure the response came back okay
		if ( is_wp_error( $response ) ) {
			$error_response             = new stdClass();
			$error_response->wp_error   = $response->get_error_message();
			$error_response->error_code = $response->get_error_code();
			return $error_response;
		}
		
		if ( wp_remote_retrieve_response_code( $response ) !== 200 ) {
			$error_response = new stdClass();
			$error_body     = wp_remote_retrieve_body( $response );
			
			if ( false !== strpos( $error_body, 'Wordfence' ) ) {
				$error_response->wp_error = sprintf(
					/* translators: support email */
					__( 'Your site host is blocked by the Wordfence network, please contact %s with this error message.', 'wpo-updater' ),
					'support@wpovernight.com'
				);
			} else {
				$error_response->wp_error = esc_html( sanitize_text_field( strip_tags( $error_body ) ) );
			}

			$error_response->error_code = wp_remote_retrieve_response_code( $response );
			return $error_response;
		}
		
		// decode the license data
		$response = json_decode( wp_remote_retrieve_body( $response ) );

		return $response;
	}

	/**
	 * Handle 401 Unauthorized responses from the license server
	 * 
	 * @param mixed $response
	 * @param array $args
	 * @param string $url
	 * @return mixed
	 */
	public function unauthorized_response( $response, array $args, string $url ) {
		if ( ! is_wp_error( $response ) && is_array( $response ) && isset( $response['response'] ) && is_array( $response['response'] ) ) {
			if ( isset( $response['response']['code'] ) && 401 === absint( $response['response']['code'] ) ) {
				// we have a 401 response, check if it's ours
				$license_server = 'https://wpovernight.com';
				if ( false !== strpos( $url, $license_server ) && false !== strpos( $url, 'package_download' ) ) {
					// this is our request

					// extract values from token
					$url_parts = parse_url( $url );
					$paths     = array_values( explode( '/', $url_parts['path'] ) );
					$token     = end( $paths );
					$values    = explode( ':', base64_decode( $token ) );
					
					if ( count( $values ) !== 6 ) {
						$response['response']['message'] = __( 'Invalid token supplied', 'wpo-updater' );
						return $response;
					}
					
					$expires       = $values[0];
					$license_key   = $values[1];
					$download_id   = (int) $values[2];
					$url           = str_replace( '@', ':', $values[4] );
					$download_beta = (bool) $values[5];

					// Check_license response with the above vars
					// data to send in our API request
					$api_params = array(
						'edd_action' => 'check_license',
						'url'        => $url,
						'license'    => $license_key,
						'item_id'    => $download_id,
					);
					
					$check_response = $this->edd_api_action( $api_params );

					if ( $check_response ) {
						if ( isset( $check_response->license ) ) {
							switch( $check_response->license ) {
								case 'expired':
									$message = __( 'Your license has expired, please renew it to install this update.', 'wpo-updater' );
									break;
								case 'inactive':
								case 'site_inactive':
									$message = sprintf(
										/* translators: site URL */
										__( 'Your license has not been activated for this site (%s), please activate it first.', 'wpo-updater' ),
										str_replace( array( 'http://','https://' ), '', $url )
									);
									break;
								case 'disabled':
									$message = __( 'Your license has been disabled.', 'wpo-updater' );
									break;
								case 'valid':
									$message = '';
									break;
								default:
									$message = __( 'Your license could not be validated.', 'wpo-updater' );
									break;
							}
							
						} elseif ( isset( $check_response->wp_error ) ) {
							$message = sprintf(
								/* translators: 1: error message, 2: error code, 3: host IP */
								__( 'API Error: %1$s (%2$s), IP: %3$s', 'wpo-updater' ),
								$check_response->wp_error,
								$check_response->error_code,
								$this->get_host_ip()
							);
						}
					} else {
						$message = __( 'License key expired or not activated for URL', 'wpo-updater' );
					}

					$response['response']['message'] = esc_html( $message );
				}
			}
		}
		
		return $response;
	}

	/**
	 * Get the host IP address
	 * 
	 * @return string
	 */
	public function get_host_ip(): string {
		$response = wp_remote_get( 'https://icanhazip.com', array( 'timeout' => 15, 'sslverify' => true ) );
		
		if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
			$ip = __( 'unknown', 'wpo-updater' );
		} else {
			$ip = wp_remote_retrieve_body( $response );
			// get v4 if we only have v6
			if ( false !== strpos( $ip, ':' ) ) {
				$response = wp_remote_get( 'https://ipv4.icanhazip.com', array( 'timeout' => 15, 'sslverify' => true ) );
				
				if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
					$ipv4 = wp_remote_retrieve_body( $response );
					$ip   = "{$ip} / {$ipv4}";
				}
			}
		}
		
		return $ip;
	}

	/**
	 * Sets the URL parameter sent in the API request that gets the current version information,
	 * so that it matches what we pass during activation
	 *
	 * @since 2.1.2
	 * @param array  $api_params  The array of data sent in the request.
	 * @param array  $api_data    The array of data set up in the class constructor.
	 * @param string $plugin_file The full path and filename of the file.
	 * 
	 * @return array $api_params
	 */
	public function set_url_for_version_request( array $api_params, array $api_data, string $plugin_file ): array {
		if ( $plugin_file === $this->plugin_file ) {
			$api_params['url'] = $this->get_home_url();
		}
		return $api_params;
	}

	/**
	 * Gets the "Site Address (URL)", using the default language in case WPML is installed
	 *
	 * @since 2.1.2
	 * @return string
	 */
	public function get_home_url(): string {
		global $sitepress;
		if ( is_callable( array( $sitepress, 'language_url' ) ) && is_callable( array( $sitepress, 'get_default_language' ) ) ) {
			// this is the same function that is used when using the wpml_home_url filter,
			// except that filter always passes $sitepress->get_current_language()
			$home_url = $sitepress->language_url( $sitepress->get_default_language() );
		} else {
			$home_url = home_url();
		}
		return $home_url;
	}

	/**
	 * Get the license key
	 *
	 * @since 2.1.3
	 * @return string
	 */
	public function get_license_key(): string {
		if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
			include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
		}
		
		if ( is_multisite() && is_plugin_active_for_network( $this->plugin_basename ) ) {
			$wpo_license_keys = get_blog_option( get_main_site_id(), $this->license_keys_option_name, array() );
		} elseif ( is_multisite() && ! is_plugin_active_for_network( $this->plugin_basename ) ) {
			$wpo_license_keys = get_blog_option( get_current_blog_id(), $this->license_keys_option_name, array() );
		} else {
			$wpo_license_keys = get_option( $this->license_keys_option_name, array() );
		}

		return isset( $wpo_license_keys[ $this->plugin_license_slug ] )
			? $wpo_license_keys[ $this->plugin_license_slug ]
			: '';
	}

	/**
	 * Save the license key
	 *
	 * @since 2.1.3
	 * @param string $license_key The plugin license key.
	 * @return void
	 */
	public function save_license_key( string $license_key ): void {
		if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
			include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
		}

		if ( is_multisite() && is_plugin_active_for_network( $this->plugin_basename ) ) {
			$wpo_license_keys                               = get_blog_option( get_main_site_id(), $this->license_keys_option_name, array() );
			$wpo_license_keys[ $this->plugin_license_slug ] = $license_key;
			update_blog_option( get_main_site_id(), $this->license_keys_option_name, $wpo_license_keys );
		} elseif ( is_multisite() && ! is_plugin_active_for_network( $this->plugin_basename ) ) {
			$wpo_license_keys                               = get_blog_option( get_current_blog_id(), $this->license_keys_option_name, array() );
			$wpo_license_keys[ $this->plugin_license_slug ] = $license_key;
			update_blog_option( get_current_blog_id(), $this->license_keys_option_name, $wpo_license_keys );
		} else {
			$wpo_license_keys                               = get_option( $this->license_keys_option_name, array() );
			$wpo_license_keys[ $this->plugin_license_slug ] = $license_key;
			update_option( $this->license_keys_option_name, $wpo_license_keys );
		}
	}

	/**
	 * Exclude the plugin from the default update checks
	 *
	 * @param array $args The arguments for the request.
	 * @param string|null $url The URL of the request.
	 *
	 * @return array $args
	 * @since 2.1.5
	 */
	public function exclude_plugin_from_wp_api( array $args, ?string $url ): array {
		if ( empty( $url ) ) {
			return $args;
		}

		// Is this an api.wordpress.org update check request?
		$parsed_url = wp_parse_url( $url );
		if ( ! isset( $parsed_url['host'] ) || ( 'api.wordpress.org' !== strtolower( $parsed_url['host'] ) ) ) {
			return $args;
		}
	
		$type_pluralised = 'plugins'; // We are dealing with plugins only
	
		// Check if the body contains the plugin list.
		if ( empty( $args['body'][ $type_pluralised ] ) ) {
			return $args;
		}
	
		$reporting_items = json_decode( $args['body'][ $type_pluralised ], true );
		if ( null === $reporting_items ) {
			return $args;
		}
	
		// Get the update list key (the plugin's basename).
		$update_list_key = $this->plugin_basename;
	
		// Remove the plugin from the reported list of plugins.
		if ( isset( $reporting_items[ $type_pluralised ][ $update_list_key ] ) ) {
			unset( $reporting_items[ $type_pluralised ][ $update_list_key ] );
		}
	
		// Also remove from the 'active' plugins if necessary.
		if ( ! empty( $reporting_items['active'] ) && is_array( $reporting_items['active'] ) ) {
			foreach ( $reporting_items['active'] as $index => $plugin_path ) {
				if ( $plugin_path === $update_list_key ) {
					unset( $reporting_items['active'][ $index ] );
				}
			}
			
			$reporting_items['active'] = array_values( $reporting_items['active'] ); // Re-index the array.
		}
	
		$args['body'][ $type_pluralised ] = wp_json_encode( $reporting_items );
	
		return $args;
	}
	
	/**
	 * Override plugins_api() for our commercial plugins to update the returned
	 * "Tested up to" value and ensure it reflects the latest compatible
	 * WordPress version.
	 *
	 * @param false|object|array $result
	 * @param string             $action
	 * @param object             $args
	 * @return false|object|array|\WP_Error
	 */
	public function override_plugins_api_info( $result, string $action, object $args ) {
		if (
			! is_admin()                       ||
			'plugin_information' !== $action   ||
			empty( $args->slug )               ||
			$this->plugin_slug !== $args->slug
		) {
			return $result;
		}
		
		if ( false === $result || is_wp_error( $result ) ) {
			return $result;
		}

		$tested = $this->get_latest_wp_stable_version_remote();

		if ( is_object( $result ) ) {
			$result->tested = $tested;
		} elseif ( is_array( $result ) ) {
			$result['tested'] = $tested;
		}

		return $result;
	}
	
	/**
	 * Retrieves the latest stable WordPress version from the WordPress.org API,
	 * cached with a site transient to avoid repeated remote requests.
	 *
	 * @return string Latest stable WP version or current site version on failure.
	 */
	private function get_latest_wp_stable_version_remote(): string {
		$cache_key = 'wpo_updater_latest_wp_stable';

		$cached = get_site_transient( $cache_key );
		if ( is_string( $cached ) && '' !== $cached ) {
			return $cached;
		}

		$response = wp_remote_get( 'https://api.wordpress.org/core/stable-check/1.0/', array( 'timeout' => 8 ) );
		if ( is_wp_error( $response ) ) {
			$fallback = get_bloginfo( 'version' );
			set_site_transient( $cache_key, $fallback, HOUR_IN_SECONDS );
			return $fallback;
		}

		$body = wp_remote_retrieve_body( $response );
		$data = json_decode( $body, true );

		if ( ! is_array( $data ) ) {
			$fallback = get_bloginfo( 'version' );
			set_site_transient( $cache_key, $fallback, HOUR_IN_SECONDS );
			return $fallback;
		}

		$latest = null;
		foreach ( $data as $version => $status ) {
			if ( 'latest' === $status ) {
				$latest = $version;
				break;
			}
		}

		if ( ! $latest ) {
			$latest = get_bloginfo( 'version' );
			set_site_transient( $cache_key, $latest, HOUR_IN_SECONDS );
			return $latest;
		}

		$ttl = (int) apply_filters( 'wpo_updater_latest_wp_stable_ttl', 12 * HOUR_IN_SECONDS );
		set_site_transient( $cache_key, $latest, max( HOUR_IN_SECONDS, $ttl ) );

		return $latest;
	}
	
	/**
	 * Pre-checks for license/product mismatch before activating a license.
	 *
	 * @param string $license_key
	 * @return object|null
	 */
	private function precheck_license_mismatch( string $license_key ) {
		$pre_check_args = array(
			'edd_action' => 'check_license',
			'license'    => trim( $license_key ),
			'item_name'  => urlencode( $this->plugin_item_name ), // Product name in EDD
			'url'        => $this->get_home_url(),                // Fallback if UA is filtered
		);

		$response = $this->edd_api_action( $pre_check_args );

		// Transport / API error
		if ( isset( $response->wp_error ) ) {
			return $response;
		}

		// Item name mismatch (likely wrong product or bundle vs. single mismatch)
		if ( isset( $response->license ) && 'item_name_mismatch' === $response->license ) {

			if ( false !== strpos( $this->plugin_item_name, 'PDF Invoices & Packing Slips for WooCommerce' ) ) {
				// Most likely trying to use a bundle license on a bundled plugin
				$response->action_message = sprintf(
					/* translators: 1. bold "Bundle", 2. plugin name */
					__( 'It looks like you are trying to activate the %1$s license or a license from another extension included in it, instead of the individual license for %2$s.', 'wpo-updater' ),
					'<strong>' . __( 'Bundle', 'wpo-updater' ) . '</strong>',
					'<strong>' . esc_html( $this->plugin_item_name ) . '</strong>'
				);
			} else {
				// Wrong product license
				$response->action_message = sprintf(
					/* translators: %s: plugin name */
					__( 'The license key you entered does not match %s. Please double-check that you are using the correct license key for this plugin.', 'wpo-updater' ),
					'<strong>' . $this->plugin_item_name . '</strong>'
				);
			}

			$response->license_state         = 'invalid';
			$response->license_state_message = __( 'invalid', 'wpo-updater' );
			$response->license_info          = '';

			return $response;
		}

		// No error/mismatch detected
		return null;
	}
	
	/**
	 * Handle API response errors from EDD requests.
	 *
	 * @param object $response
	 * @return object
	 */
	private function handle_api_response_error( $response ) {
		if ( empty( $response->wp_error ) ) {
			return $response;
		}

		$message = sprintf(
			/* translators: 1: error message, 2: error code, 3: host IP */
			__( 'API Error: %1$s (%2$s), IP: %3$s', 'wpo-updater' ),
			$response->wp_error,
			isset( $response->error_code ) ? $response->error_code : __( 'unknown', 'wpo-updater' ),
			$this->get_host_ip()
		);

		$response->action_message        = $message;
		$response->license_state         = 'incomplete';
		$response->license_state_message = __( 'unknown', 'wpo-updater' );
		$response->license_info          = '';

		return $response;
	}
	
	/**
	 * Get upgrade URL when no activations are left.
	 *
	 * @param object $status
	 * @return string
	 */
	private function get_upgrade_url_for_exhausted_license( $status ): string {
		// Must be exhausted to show upgrades.
		$current_limit = (int) ( $status->license_limit ?? 0 );
		$site_count    = (int) ( $status->site_count ?? 0 );

		if ( $site_count < $current_limit || $current_limit <= 0 ) {
			return '';
		}

		// Look up next upgrade tier for this extension.
		if ( empty( self::WPO_UPDATER_PLUGINS_WITH_UPDATER[ $this->plugin_basename ]['upgrades'] ) ) {
			return '';
		}

		$upgrades = self::WPO_UPDATER_PLUGINS_WITH_UPDATER[ $this->plugin_basename ]['upgrades'];
		$tiers    = array_map( 'intval', array_keys( $upgrades ) );
		sort( $tiers, SORT_NUMERIC );

		$next_limit  = null;
		$upgrade_id  = null;

		foreach ( $tiers as $tier ) {
			if ( $tier > $current_limit ) {
				$next_limit = $tier;
				$upgrade_id = (int) $upgrades[ $tier ];
				break;
			}
		}

		if ( null === $upgrade_id ) {
			return '';
		}

		// Build UTM tags.
		$utm  = 'utm_source=wpo-updater&utm_medium=wp-admin&utm_campaign=license-upgrade';
		$utm .= '&utm_term=' . rawurlencode( $this->plugin_slug );
		$utm .= '&utm_content=' . rawurlencode( "upgrade-from-{$current_limit}-to-{$next_limit}" );

		$license_id = (int) ( $status->license_id ?? 0 );
		
		if ( 0 === $license_id ) {
			return '';
		}

		return sprintf(
			'https://wpovernight.com/checkout/?edd_action=sl_license_upgrade&license_id=%1$d&upgrade_id=%2$d&%3$s',
			$license_id,
			$upgrade_id,
			$utm
		);
	}
	
	/**
	 * Get renewal URL for this license.
	 *
	 * @return string
	 */
	private function get_renewal_url(): string {
		$license_key = trim( (string) $this->license_key );
		$default_url = 'https://wpovernight.com/my-account/license-keys/';

		// Bail if we do not have a license key.
		if ( '' === $license_key ) {
			return $default_url;
		}

		// Bail if we do not have a download ID configured for this plugin.
		if ( empty( self::WPO_UPDATER_PLUGINS_WITH_UPDATER[ $this->plugin_basename ]['download'] ) ) {
			return $default_url;
		}

		$download_id = (int) self::WPO_UPDATER_PLUGINS_WITH_UPDATER[ $this->plugin_basename ]['download'];

		if ( $download_id <= 0 ) {
			return $default_url;
		}

		// Build UTM tags.
		$utm  = 'utm_source=wpo-updater&utm_medium=wp-admin&utm_campaign=license-renewal';
		$utm .= '&utm_term=' . rawurlencode( $this->plugin_slug );

		return sprintf(
			'https://wpovernight.com/checkout/?edd_license_key=%1$s&download_id=%2$d&%3$s',
			rawurlencode( $license_key ),
			$download_id,
			$utm
		);
	}
	
}
