HEX
Server: Apache
System: Linux 4801f1b1.ptr.provps.com 6.17.8-1.el9.elrepo.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Nov 13 18:02:25 EST 2025 x86_64
User: nassaugo (1004)
PHP: 8.1.34
Disabled: exec,passthru,shell_exec,system
Upload Files
File: //home/nassaugo/www/wp-content/plugins/wpai-acf-add-on/src/AddonInstaller.php
<?php

namespace PMXI\AddonInstaller;

use Exception;
use Plugin_Installer_Skin;
use Plugin_Upgrader;
use WP_Error;

/**
 * PMXI Addon Installer SDK
 * 
 * A reusable SDK for installing and managing free plugin dependencies for pro plugins.
 */
class AddonInstaller {

	/**
	 * Configuration for the addon being managed.
	 *
	 * @var array
	 */
	protected $config;

	/**
	 * The detected addon version.
	 *
	 * @var string
	 */
	protected $addon_version = '0';

	/**
	 * The detected addon plugin file.
	 *
	 * @var string
	 */
	protected $addon_file;

	/**
	 * The detected addon directory.
	 *
	 * @var string
	 */
	protected $addon_dir;

	/**
	 * Constructor.
	 *
	 * @param array $config Configuration array with the following keys:
	 *   - 'addon_name' (string): Display name of the free addon (e.g., 'ACF Add-On Free')
	 *   - 'addon_slug' (string): WordPress.org plugin slug (e.g., 'csv-xml-import-for-acf')
	 *   - 'addon_author' (string): Plugin author name (e.g., 'Soflyy')
	 *   - 'minimum_version' (string): Minimum required version (e.g., '1.0.4')
	 *   - 'pro_plugin_name' (string): Name of the pro plugin (e.g., 'WP All Import - ACF Add-On Pro')
	 *   - 'pro_plugin_file' (string): Pro plugin main file path
	 *   - 'textdomain' (string): Text domain for translations
	 *   - 'version_constant' (string, optional): Constant name that holds the addon version
	 *   - 'edition_constant' (string, optional): Constant name that holds the edition type
	 *   - 'expected_edition' (string, optional): Expected edition value (default: 'free')
	 *   - 'free_plugin_file' (string, optional): Free plugin filename if different from plugin.php
	 *   - 'disable_deactivation' (bool, optional): Whether to prevent deactivation of the free plugin
	 *   - 'send_email_alert' (bool, optional): Whether to send email alerts for failures (default: true)
	 */
	public function __construct( array $config ) {
		$this->config = $this->validate_config( $config );
		
		// Use custom filename or default to plugin.php
		$plugin_filename = $this->config['free_plugin_file'] ?? 'plugin.php';
		$this->addon_file = $this->config['addon_slug'] . '/' . $plugin_filename;
		$this->addon_dir = WP_PLUGIN_DIR . '/' . $this->config['addon_slug'];
		
		// Initialize hooks for import blocking
		$this->init_import_blocking_hooks();
		
		// Initialize CLI hooks for preventing deactivation
		$this->init_cli_hooks();
	}

	/**
	 * Validates and sets defaults for the configuration.
	 *
	 * @param array $config Configuration array.
	 * @return array Validated configuration.
	 * @throws Exception If required configuration is missing.
	 */
	protected function validate_config( array $config ) {
		$required = [
			'addon_name',
			'addon_slug', 
			'addon_author',
			'minimum_version',
			'pro_plugin_name',
			'pro_plugin_file',
			'textdomain'
		];

		foreach ( $required as $key ) {
			if ( empty( $config[ $key ] ) ) {
				throw new Exception( "Required configuration key '{$key}' is missing." );
			}
		}

		// Set defaults for optional parameters
		$defaults = [
			'expected_edition' => 'free',
			'disable_deactivation' => false,
			'send_email_alert' => true
		];
		
		foreach ( $defaults as $key => $default_value ) {
			if ( ! isset( $config[ $key ] ) ) {
				$config[ $key ] = $default_value;
			}
		}
		
		// Ensure boolean values are properly cast
		$config['disable_deactivation'] = (bool) $config['disable_deactivation'];
		$config['send_email_alert'] = (bool) $config['send_email_alert'];

		return $config;
	}

	/**
	 * Initialize CLI-specific hooks.
	 *
	 * @return void
	 */
	protected function init_cli_hooks() {
		if ( $this->is_cli_context() ) {
			// Hook into CLI plugin deactivation to prevent or immediately reactivate
			add_action( 'deactivated_plugin', array( $this, 'handle_cli_deactivation' ), 10, 2 );
			
			// Also check on every CLI operation that might run imports
			add_action( 'wp_loaded', array( $this, 'ensure_cli_addon_activation' ), 5 );
		}
	}

	/**
	 * Handles plugin deactivation in CLI context.
	 *
	 * @param string $plugin Plugin file that was deactivated.
	 * @param bool   $network_deactivating Whether the plugin is being network deactivated.
	 * @return void
	 */
	public function handle_cli_deactivation( $plugin, $network_deactivating ) {
		if ( $plugin === $this->addon_file && $this->config['disable_deactivation'] ) {
			// Immediately reactivate the plugin
			$this->reactivate_addon_silently();
		}
	}

	/**
	 * Ensures addon is activated in CLI context before imports.
	 *
	 * @return void
	 */
	public function ensure_cli_addon_activation() {
		if ( ! $this->is_cli_context() ) {
			return;
		}
		
		// If we're about to run an import operation, ensure addon is active
		if ( $this->is_import_operation() && ! is_plugin_active( $this->addon_file ) ) {
			$this->reactivate_addon_silently();
		}
	}

	/**
	 * Reactivates the addon silently without output.
	 *
	 * @return void
	 */
	protected function reactivate_addon_silently() {
		if ( ! function_exists( 'activate_plugin' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		
		// Simple activation - WordPress handles translation timing internally
		$result = activate_plugin( $this->addon_file, '', false, true );
		
		// If activation failed, try network activation
		if ( is_wp_error( $result ) ) {
			activate_plugin( $this->addon_file, '', true, true );
		}
	}

	/**
	 * Checks if this is an import operation.
	 *
	 * @return bool
	 */
	protected function is_import_operation() {
		global $argv;
		
		if ( ! is_array( $argv ) ) {
			return false;
		}
		
		$command_line = implode( ' ', $argv );
		
		// Check for common import-related operations
		$import_indicators = [
			'wp-all-import',
			'pmxi',
			'import',
			'cron',
			'scheduled'
		];
		
		foreach ( $import_indicators as $indicator ) {
			if ( strpos( $command_line, $indicator ) !== false ) {
				return true;
			}
		}
		
		return false;
	}

	/**
	 * Initialize hooks to block imports when dependencies are not met.
	 *
	 * @return void
	 */
	protected function init_import_blocking_hooks() {
		// Hook to block manual imports
		add_action( 'pmxi_before_xml_import', array( $this, 'block_manual_import_if_dependency_missing' ), 1, 1 );
		
		// Hook to block scheduled imports (early in the process)
		add_action( 'init', array( $this, 'block_scheduled_import_if_dependency_missing' ), 1 );
		
		// Additional safety hook for any PMXI operations
		add_action( 'pmxi_before_post_import', array( $this, 'block_import_operation_if_dependency_missing' ), 1, 2 );
	}

	/**
	 * Blocks manual imports if dependency is missing.
	 *
	 * @param int $import_id Import ID.
	 * @return void
	 */
	public function block_manual_import_if_dependency_missing( $import_id ) {
		$this->block_if_dependency_missing( 'manual_import' );
	}

	/**
	 * Blocks scheduled imports if dependency is missing.
	 *
	 * @return void
	 */
	public function block_scheduled_import_if_dependency_missing() {
		// Check if this is a scheduling request
		if ( ! $this->is_scheduling_request() ) {
			return;
		}
		
		$this->block_if_dependency_missing( 'scheduled_import' );
	}

	/**
	 * Additional safety block for any import operations.
	 *
	 * @param int   $import_id Import ID.
	 * @param array $import Import data.
	 * @return void
	 */
	public function block_import_operation_if_dependency_missing( $import_id, $import = null ) {
		$this->block_if_dependency_missing( 'import_operation' );
	}

	/**
	 * Unified method to block operations if dependency is missing.
	 *
	 * @param string $operation_type Type of operation being blocked.
	 * @return void
	 */
	protected function block_if_dependency_missing( $operation_type ) {
		if ( ! $this->is_addon_up_to_date() ) {
			// Only attempt installation if user has proper permissions (or we're in CLI)
			if ( $this->can_install_automatically() ) {
				// Attempt to install dependency - if successful, allow import to continue
				if ( $this->attempt_dependency_installation() ) {
					// Dependencies are now satisfied, allow import to continue
					return;
				}
			}
			
			// Installation failed or not permitted, block the import
			$this->send_failure_notification_once();
			
			$this->handle_operation_blocking( $operation_type );
		}
	}

	/**
	 * Handles blocking of different operation types.
	 *
	 * @param string $operation_type Type of operation being blocked.
	 * @return void
	 */
	protected function handle_operation_blocking( $operation_type ) {
		switch ( $operation_type ) {
			case 'manual_import':
				$this->block_manual_import();
				break;
			case 'scheduled_import':
				$this->block_scheduled_import();
				break;
			case 'import_operation':
				throw new Exception( $this->get_dependency_error_message() );
				break;
		}
	}

	/**
	 * Blocks manual import operations.
	 *
	 * @return void
	 */
	protected function block_manual_import() {
		// For admin UI, show HTML error
		if ( is_admin() && ! wp_doing_ajax() ) {
			wp_die( 
				$this->get_dependency_error_html(),
				$this->safe_translate( 'Dependency Missing', 'Dependency Missing' ),
				array( 'response' => 400 )
			);
		}
		
		// For AJAX requests, send JSON response
		if ( wp_doing_ajax() ) {
			wp_send_json_error( array(
				'message' => $this->get_dependency_error_message()
			), 400 );
		}
		
		// Fallback - just die with message
		die( $this->get_dependency_error_message() );
	}

	/**
	 * Blocks scheduled import operations.
	 *
	 * @return void
	 */
	protected function block_scheduled_import() {
		// Send proper JSON response for scheduling
		if ( ! headers_sent() ) {
			header( 'Content-Type: application/json; charset=' . get_option( 'blog_charset' ) );
			header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );
			http_response_code( 424 ); // Failed Dependency
		}
		
		echo wp_json_encode( array(
			'status' => 424,
			'message' => $this->get_dependency_error_message(),
			'error' => 'dependency_missing'
		) );
		
		exit;
	}

	/**
	 * Safely translates a string or returns fallback if translations cause issues.
	 *
	 * @param string $text Text to translate.
	 * @param string $fallback Fallback text if translation isn't safe.
	 * @return string
	 */
	protected function safe_translate( $text, $fallback ) {
		// Only translate if we're past the early loading phase
		if ( did_action( 'init' ) && function_exists( '__' ) ) {
			return __( $text, $this->config['textdomain'] );
		}
		
		return $fallback;
	}

	/**
	 * Attempts to install missing dependency with retry logic for multiple dependencies.
	 *
	 * @return bool True if all dependencies are now satisfied, false otherwise.
	 */
	protected function attempt_dependency_installation() {
		$max_attempts = 3;
		$attempts = 0;
		
		while ( $attempts < $max_attempts && ! $this->is_addon_up_to_date() ) {
			try {
				// Clear any existing status to force fresh installation
				delete_option( $this->get_option_key() );
				
				// Attempt installation with immediate activation and loading
				$this->install_and_activate_immediately();
				
				// Give time for the installation to register
				if ( ! $this->is_cli_context() ) {
					sleep( 1 );
				}
				
				$attempts++;
				
				// Break early if we've successfully installed
				if ( $this->is_addon_up_to_date() ) {
					break;
				}
				
			} catch ( Exception $e ) {
				$attempts++;
				if ( $attempts >= $max_attempts ) {
					return false;
				}
			}
		}
		
		return $this->is_addon_up_to_date();
	}

	/**
	 * Installs and immediately activates the addon, then forces loading of constants.
	 *
	 * @return void
	 * @throws Exception If installation or activation fails.
	 */
	protected function install_and_activate_immediately() {
		$this->perform_installation_steps();
		
		// Mark installation as completed
		update_option( $this->get_option_key(), 'completed', true );
	}

	/**
	 * Performs the core installation steps.
	 *
	 * @return void
	 * @throws Exception If installation or activation fails.
	 */
	protected function perform_installation_steps() {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		$this->detect_addon();
		
		// Check if we need to install or update
		if ( version_compare( $this->addon_version, $this->config['minimum_version'], '<' ) ) {
			$this->install_or_upgrade_plugin();
			
			// Re-detect the addon after installation
			$this->detect_addon();
		}

		// Ensure addon is activated immediately
		$this->ensure_addon_is_activated();
		
		// Force load the addon plugin file to make constants available immediately
		$this->force_load_addon();
		
		// Transfer auto-update settings
		$this->transfer_auto_update_settings();
	}

	/**
	 * Installs or upgrades the plugin.
	 *
	 * @return void
	 * @throws Exception If installation fails.
	 */
	protected function install_or_upgrade_plugin() {
		// Ensure we have required WordPress functions available
		$this->include_required_files();

		// Silent installer skin
		$skin = $this->get_silent_installer_skin();

		// Get plugin download URL
		$url = $this->get_plugin_download_url();

		$upgrader = new Plugin_Upgrader( $skin );
		$installed = $upgrader->install( $url );
		
		if ( is_wp_error( $installed ) || ! $installed ) {
			throw new Exception( $this->get_dependency_error_message() );
		}

		// Force plugin cache refresh after installation
		$this->refresh_plugin_cache();
	}

	/**
	 * Includes required WordPress files for installation.
	 *
	 * @return void
	 */
	protected function include_required_files() {
		if ( ! function_exists( 'wp_get_current_user' ) ) {
			include_once ABSPATH . 'wp-includes/pluggable.php';
		}
		include_once ABSPATH . 'wp-admin/includes/file.php';
		include_once ABSPATH . 'wp-admin/includes/misc.php';
		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
	}

	/**
	 * Gets a silent installer skin.
	 *
	 * @return object Anonymous class extending Plugin_Installer_Skin.
	 */
	protected function get_silent_installer_skin() {
		return new class() extends Plugin_Installer_Skin {
			public function header() {}
			public function footer() {}
			public function error( $errors ) {}
			public function feedback( $feedback, ...$args ) {}
		};
	}

	/**
	 * Gets the plugin download URL.
	 *
	 * @return string Plugin download URL.
	 */
	protected function get_plugin_download_url() {
		// Try to get the specific version first, fallback to latest
		$url = sprintf(
			'https://downloads.wordpress.org/plugin/%s.%s.zip',
			$this->config['addon_slug'],
			$this->config['minimum_version']
		);
		
		$check_result = wp_remote_retrieve_response_code( wp_remote_head( $url ) );
		if ( $check_result !== 200 ) {
			$url = sprintf(
				'https://downloads.wordpress.org/plugin/%s.zip',
				$this->config['addon_slug']
			);
		}
		
		return $url;
	}

	/**
	 * Refreshes the plugin cache.
	 *
	 * @return void
	 */
	protected function refresh_plugin_cache() {
		if ( function_exists( 'wp_clean_plugins_cache' ) ) {
			wp_clean_plugins_cache();
		}
		
		// Also clear general cache in CLI context
		if ( $this->is_cli_context() ) {
			wp_cache_flush();
		}
	}

	/**
	 * Checks if the current request is a scheduling request.
	 *
	 * @return bool
	 */
	protected function is_scheduling_request() {
		//todo: validate import_key to prevent automatic dependency installation for invalid requests
		return (
			( isset( $_GET['action'] ) && $_GET['action'] == 'wpai_public_api' ) ||
			( isset( $_GET['import_key'] ) && isset( $_GET['action'] ) && 
			  in_array( $_GET['action'], array( 'processing', 'trigger', 'pipe', 'cancel', 'cleanup' ) ) )
		);
	}

	/**
	 * Gets the dependency error message.
	 *
	 * @param bool $check_admin Whether to include admin check message.
	 * @return string
	 */
	protected function get_dependency_error_message( $check_admin = true ) {
		$message = 'Dependency installation failed and import cannot continue.';
		if ( $check_admin ) {
			$message .= ' Please check the WordPress admin area for more details about missing dependencies.';
		}
		return $this->safe_translate( $message, $message );
	}

	/**
	 * Gets the dependency error as HTML for admin display.
	 *
	 * @return string
	 */
	protected function get_dependency_error_html() {
		$message = $this->get_dependency_error_message( false );
		
		$install_link = '';
		if ( current_user_can( 'install_plugins' ) ) {
			$install_link = sprintf(
				'<p><a href="%s" class="button button-primary">%s</a></p>',
				esc_url( self_admin_url( 'plugins.php' ) ),
				esc_html( $this->safe_translate( 'Check Installed Plugins', 'Check Installed Plugins' ) )
			);
		}
		
		return sprintf(
			'<div style="padding: 20px;"><h2>%s</h2><p>%s</p>%s</div>',
			esc_html( $this->safe_translate( 'Missing Dependency', 'Missing Dependency' ) ),
			esc_html( $message ),
			$install_link
		);
	}

	/**
	 * Sends failure notification email to admin (only once).
	 *
	 * @return void
	 */
	protected function send_failure_notification_once() {
		// Check if email alerts are disabled
		if ( ! $this->config['send_email_alert'] ) {
			return;
		}
		
		$notification_key = 'pmxi_addon_failure_notified_' . sanitize_key( $this->config['addon_slug'] );
		
		// Check if we've already sent notification for this addon
		if ( get_option( $notification_key ) ) {
			return;
		}
		
		// Mark as notified
		update_option( $notification_key, time(), false );
		
		// Ensure wp_mail is available
		if ( ! function_exists( 'wp_mail' ) ) {
			require_once ABSPATH . 'wp-includes/pluggable.php';
		}
		
		// Get admin email
		$admin_email = get_option( 'admin_email' );
		if ( ! $admin_email ) {
			return;
		}
		
		$subject = sprintf(
			'[%s] Dependency Installation Failed',
			get_bloginfo( 'name' )
		);
		
		$message = sprintf(
			"Hello,\n\nA dependency installation failed on your website and this might prevent imports from running properly.\n\nPlease login to the WordPress admin area for details about which dependencies need to be installed.\n\nSite: %s",
			home_url()
		);
		
		// Send email
		wp_mail( $admin_email, $subject, $message );
	}

	/**
	 * Gets the option key for tracking installer status.
	 *
	 * @return string
	 */
	protected function get_option_key() {
		return 'pmxi_addon_installer_' . sanitize_key( $this->config['addon_slug'] );
	}

	/**
	 * Performs the installer if it hasn't been done yet.
	 *
	 * @return void
	 */
	public function install_addon_from_repository() {
		// Defer activation if we're too early in the WordPress loading process
		if ( ! did_action( 'plugins_loaded' ) && ! $this->is_cli_context() ) {
			add_action( 'plugins_loaded', array( $this, 'install_addon_from_repository' ) );
			return;
		}
		
		// If we're in CLI context, handle dependencies immediately and synchronously
		if ( $this->is_cli_context() ) {
			$this->handle_cli_installation();
			return;
		}

		// Normal web interface installation
		$this->setup_web_interface_hooks();
		
		// Check current installation status
		$status = $this->get_status();
		
		if ( ! $status ) {
			// Send notification if installation will fail before attempting
			if ( ! $this->can_install_automatically() ) {
				$this->send_failure_notification_once();
			}
			
			try {
				$this->install();
			} catch ( Exception $e ) {
				// Auto installation failed, the notice will be displayed.
				return;
			}
		} elseif ( $status === 'started' ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
			$this->detect_addon();
			if ( is_plugin_active( $this->addon_file ) ) {
				// Addon is active so mark installation as successful.
				update_option( $this->get_option_key(), 'completed', true );
			}
		}
	}

	/**
	 * Sets up hooks for web interface.
	 *
	 * @return void
	 */
	protected function setup_web_interface_hooks() {
		// Only add notification hooks if translations are ready
		if ( did_action( 'init' ) ) {
			add_action( 'admin_notices', array( $this, 'show_install_addon_notification' ) );
			add_action( 'network_admin_notices', array( $this, 'show_install_addon_notification' ) );
		} else {
			// Defer notification hooks until translations are ready
			add_action( 'init', function() {
				add_action( 'admin_notices', array( $this, 'show_install_addon_notification' ) );
				add_action( 'network_admin_notices', array( $this, 'show_install_addon_notification' ) );
			} );
		}
		
		add_action( 'plugins_loaded', array( $this, 'validate_installation_status' ) );
		
		// Auto-install dependencies when WordPress is ready
		add_action( 'wp_loaded', array( $this, 'auto_install_dependencies' ) );
		
		if ( $this->config['disable_deactivation'] ) {
			add_filter( 'plugin_action_links', array( $this, 'disable_deactivation_link' ), 10, 2 );
			add_filter( 'network_admin_plugin_action_links', array( $this, 'disable_deactivation_link' ), 10, 2 );
			add_action( 'admin_init', array( $this, 'prevent_deactivation' ) );
		}
	}

	/**
	 * Auto-installs dependencies when WordPress is loaded.
	 *
	 * @return void
	 */
	public function auto_install_dependencies() {
		// Skip if we're in admin and doing AJAX (to avoid conflicts)
		if ( is_admin() && wp_doing_ajax() ) {
			return;
		}
		
		// Skip if already up to date
		if ( $this->is_addon_up_to_date() ) {
			return;
		}
		
		// Only auto-install if we have the necessary permissions (or we're in CLI)
		if ( ! $this->can_install_automatically() ) {
			return;
		}
		
		// Prevent multiple simultaneous installations
		$lock_key = 'pmxi_auto_install_lock_' . sanitize_key( $this->config['addon_slug'] );
		if ( get_transient( $lock_key ) ) {
			return;
		}
		
		// Set a lock for 5 minutes
		set_transient( $lock_key, true, 300 );
		
		try {
			$this->attempt_dependency_installation();
		} catch ( Exception $e ) {
			// Silent failure for auto-installation
		}
		
		// Release lock
		delete_transient( $lock_key );
	}

	/**
	 * Prevents deactivation of the addon plugin.
	 *
	 * @return void
	 */
	public function prevent_deactivation() {
		if ( ! isset( $_GET['action'] ) || $_GET['action'] !== 'deactivate' ) {
			return;
		}
		
		if ( ! isset( $_GET['plugin'] ) ) {
			return;
		}
		
		$plugin = sanitize_text_field( wp_unslash( $_GET['plugin'] ) );
		
		if ( $plugin === $this->addon_file ) {
			wp_die(
				sprintf(
					'%1$s cannot be deactivated because it is required by %2$s.',
					esc_html( $this->config['addon_name'] ),
					esc_html( $this->config['pro_plugin_name'] )
				),
				'Plugin Deactivation Prevented',
				array( 'back_link' => true )
			);
		}
	}

	/**
	 * Checks if automatic installation is possible.
	 *
	 * @return bool
	 */
	protected function can_install_automatically() {
		// In CLI and scheduling context, allow installation regardless of user permissions
		// since CLI/scheduling typically runs with elevated system permissions
		if ( $this->is_cli_context() || $this->is_scheduling_request() ) {
			return true;
		}
		
		// For web/manual context, strictly require both permissions
		$can_install = current_user_can( 'install_plugins' );
		$can_activate = current_user_can( 'activate_plugins' );
		
		return $can_install && $can_activate;
	}

	/**
	 * Checks if we're in CLI context.
	 *
	 * @return bool
	 */
	protected function is_cli_context() {
		return defined( 'WP_CLI' ) && WP_CLI;
	}

	/**
	 * Handles installation in CLI context with immediate dependency resolution.
	 *
	 * @return void
	 * @throws Exception If dependencies cannot be satisfied.
	 */
	protected function handle_cli_installation() {
		try {
			if ( ! $this->is_addon_up_to_date() ) {
				// Clear any existing status to force fresh installation
				delete_option( $this->get_option_key() );
				
				// For CLI, defer activation until after WordPress is fully loaded
				if ( did_action( 'init' ) ) {
					// We're past init, safe to install synchronously
					$this->install_synchronous();
				} else {
					// Defer until after init to avoid translation loading issues
					add_action( 'init', function() {
						$this->install_synchronous();
					}, 999 );
					return; // Exit early, installation will happen on init
				}
				
				// Force WordPress to refresh plugin cache
				$this->refresh_plugin_cache();
				
				// Wait a moment for filesystem operations to complete
				sleep( 1 );
				
				// Verify installation succeeded
				if ( ! $this->is_addon_up_to_date() ) {
					$error_message = $this->get_dependency_error_message();
					
					if ( class_exists( 'WP_CLI' ) ) {
						\WP_CLI::error( $error_message );
					}
					
					throw new Exception( $error_message );
				}
			}
		} catch ( Exception $e ) {
			$error_message = $e->getMessage();
			
			if ( class_exists( 'WP_CLI' ) ) {
				\WP_CLI::error( $error_message );
			}
			
			throw new Exception( $error_message );
		}
	}

	/**
	 * Performs synchronous installation for CLI context.
	 *
	 * @return void
	 * @throws Exception If installation fails.
	 */
	protected function install_synchronous() {
		$this->perform_installation_steps();
		
		// Mark installation as completed
		update_option( $this->get_option_key(), 'completed', true );
	}

	/**
	 * Disables the deactivation link for the free addon and shows a required message.
	 *
	 * @param array  $actions Plugin action links.
	 * @param string $plugin_file Plugin file path.
	 * @return array Modified action links.
	 */
	public function disable_deactivation_link( array $actions, $plugin_file ) {
		if ( $plugin_file === $this->addon_file ) {
			if ( isset( $actions['deactivate'] ) ) {
				$actions['deactivate'] = sprintf(
					'<span style="color: #666;">%s</span>',
					sprintf(
						'Required by %s',
						$this->config['pro_plugin_name']
					)
				);
			}
		}
		return $actions;
	}

	/**
	 * Displays a notification to install the addon.
	 *
	 * @return void
	 */
	public function show_install_addon_notification() {
		if ( ! $this->should_show_notification() ) {
			return;
		}

		require_once ABSPATH . 'wp-admin/includes/plugin.php';
		$this->detect_addon();

		$action = $this->get_notification_action();

		if ( ! $action ) {
			return;
		}

		printf(
			'<div class="error pmxi-addon-notice">
				<h4 class="pmxi-notice-header">%s</h4>
				<div class="notice-pmxi-content">
					<p>%s</p>
					<p>%s</p>
				</div>
			</div>',
			sprintf(
				$this->safe_translate( 'Install latest %s', 'Install latest %s' ),
				esc_html( $this->config['addon_name'] )
			),
			sprintf(
				$this->safe_translate( '%1$s %2$s must be installed and activated in order to use %3$s.', '%1$s %2$s must be installed and activated in order to use %3$s.' ),
				esc_html( $this->config['addon_name'] ),
				esc_html( $this->config['minimum_version'] ),
				esc_html( $this->config['pro_plugin_name'] )
			),
			$action // Already escaped in get_notification_action()
		);
	}

	/**
	 * Returns the notification action to display.
	 *
	 * @return string|false The notification action or false if no action should be taken.
	 */
	protected function get_notification_action() {
		$minimum_version_met = version_compare( $this->addon_version, $this->config['minimum_version'], '>=' );
		$network_active = is_plugin_active_for_network( plugin_basename( $this->config['pro_plugin_file'] ) );
		$addon_active = ( $network_active ) ? is_plugin_active_for_network( $this->addon_file ) : is_plugin_active( $this->addon_file );

		if ( $minimum_version_met && $addon_active ) {
			return false;
		}

		if ( $minimum_version_met ) {
			$permission = 'activate_plugins';
		} elseif ( $this->addon_version !== '0' ) {
			$permission = 'update_plugins';
		} else {
			$permission = 'install_plugins';
		}

		if ( current_user_can( $permission ) ) {
			switch ( $permission ) {
				case 'activate_plugins':
					if ( $network_active ) {
						$base_url = network_admin_url( 'plugins.php?action=activate&plugin=' . $this->addon_file );
						$button_content = $this->safe_translate( '%2$sNetwork Activate %1$s now%3$s', '%2$sNetwork Activate %1$s now%3$s' );
					} else {
						$base_url = self_admin_url( 'plugins.php?action=activate&plugin=' . $this->addon_file );
						$button_content = $this->safe_translate( '%2$sActivate %1$s now%3$s', '%2$sActivate %1$s now%3$s' );
					}
					$url = wp_nonce_url( $base_url, 'activate-plugin_' . $this->addon_file );
					break;
				case 'update_plugins':
					$url = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' . $this->addon_file ), 'upgrade-plugin_' . $this->addon_file );
					$button_content = $this->safe_translate( '%2$sUpgrade %1$s now%3$s', '%2$sUpgrade %1$s now%3$s' );
					break;
				case 'install_plugins':
					$url = wp_nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=' . $this->config['addon_slug'] ), 'install-plugin_' . $this->config['addon_slug'] );
					$button_content = $this->safe_translate( '%2$sInstall %1$s now%3$s', '%2$sInstall %1$s now%3$s' );
					break;
			}
			return sprintf(
				esc_html( $button_content ),
				esc_html( $this->config['addon_name'] ),
				'<a class="button" href="' . esc_url( $url ) . '">',
				'</a>'
			);
		}

		if ( is_multisite() ) {
			$message = $this->safe_translate( 'Please contact a network administrator to install %1$s %2$s.', 'Please contact a network administrator to install %1$s %2$s.' );
		} else {
			$message = $this->safe_translate( 'Please contact an administrator to install %1$s %2$s.', 'Please contact an administrator to install %1$s %2$s.' );
		}
		return sprintf(
			esc_html( $message ),
			esc_html( $this->config['addon_name'] ),
			esc_html( $this->config['minimum_version'] )
		);
	}

	/**
	 * Checks if the addon is at the minimum required version.
	 *
	 * @return bool True if addon is at the minimal required version
	 */
	public function is_addon_up_to_date() {
		// If version and edition constants are provided, check them
		if ( ! empty( $this->config['version_constant'] ) ) {
			if ( ! defined( $this->config['version_constant'] ) ) {
				// In CLI context, try to force load the addon to make constants available
				if ( $this->is_cli_context() ) {
					$this->detect_addon();
					$this->force_load_addon();
				}
				
				// Check again after force loading
				if ( ! defined( $this->config['version_constant'] ) ) {
					return false;
				}
			}

			$version_check = version_compare( 
				constant( $this->config['version_constant'] ), 
				$this->config['minimum_version'], 
				'>=' 
			);

			// Also check edition if specified
			if ( ! empty( $this->config['edition_constant'] ) ) {
				$edition_check = defined( $this->config['edition_constant'] ) && 
					constant( $this->config['edition_constant'] ) === $this->config['expected_edition'];
				return $version_check && $edition_check;
			}

			return $version_check;
		}

		// Fallback to plugin detection
		$this->detect_addon();
		
		// Check if plugin is active
		require_once ABSPATH . 'wp-admin/includes/plugin.php';
		if ( ! is_plugin_active( $this->addon_file ) ) {
			return false;
		}
		
		return version_compare( $this->addon_version, $this->config['minimum_version'], '>=' );
	}

	/**
	 * Resets the installation status if addon is not installed or outdated.
	 *
	 * @return void
	 */
	public function validate_installation_status() {
		if ( ! $this->is_addon_up_to_date() ) {
			delete_option( $this->get_option_key() );
		}
	}

	/**
	 * Returns the status of the installer.
	 *
	 * @return string|false false if the installer hasn't been started.
	 *                      "started" if it has but hasn't completed.
	 *                      "completed" if it has been completed.
	 */
	protected function get_status() {
		return get_option( $this->get_option_key() );
	}

	/**
	 * Installs the addon.
	 *
	 * @return void
	 * @throws Exception If the installer failed.
	 */
	protected function install() {
		if ( $this->get_status() ) {
			return;
		}

		// Mark the installer as having been started but not completed.
		update_option( $this->get_option_key(), 'started', true );

		$this->perform_installation_steps();
		
		// Mark the installer as having been completed.
		update_option( $this->get_option_key(), 'completed', true );
	}

	/**
	 * Force loads the addon plugin file to make constants available immediately.
	 *
	 * @return void
	 */
	protected function force_load_addon() {
		if ( ! empty( $this->addon_file ) ) {
			$addon_path = WP_PLUGIN_DIR . '/' . $this->addon_file;
			if ( file_exists( $addon_path ) && is_readable( $addon_path ) ) {
				// Include the plugin file to make constants available
				include_once $addon_path;
				
				// Also try to include the main plugin file if it has a different name
				$addon_dir = dirname( $addon_path );
				$main_files = array(
					$addon_dir . '/' . $this->config['addon_slug'] . '.php',
					$addon_dir . '/plugin.php',
					$addon_dir . '/index.php',
					$addon_dir . '/main.php'
				);
				
				foreach ( $main_files as $main_file ) {
					if ( file_exists( $main_file ) && is_readable( $main_file ) && $main_file !== $addon_path ) {
						include_once $main_file;
						break;
					}
				}
			}
		}
	}

	/**
	 * Detects the addon plugin file and version.
	 *
	 * @return void
	 */
	protected function detect_addon() {
		// Make sure addon isn't already installed in another directory.
		foreach ( get_plugins() as $file => $plugin ) {
			// Look for the exact plugin name and author to identify it correctly.
			if (
				isset( $plugin['Name'] ) && $plugin['Name'] === $this->config['addon_name']
				&& isset( $plugin['Author'] ) && $plugin['Author'] === $this->config['addon_author']
			) {
				$this->addon_file = $file;
				$this->addon_version = isset( $plugin['Version'] ) ? $plugin['Version'] : '0';
				$this->addon_dir = WP_PLUGIN_DIR . '/' . dirname( $file );
				break;
			}
		}
	}

	/**
	 * Activates the addon.
	 *
	 * @return void
	 * @throws Exception If addon could not be activated.
	 */
	protected function ensure_addon_is_activated() {
		if ( ! is_plugin_active( $this->addon_file ) ) {
			$network_active = is_plugin_active_for_network( plugin_basename( $this->config['pro_plugin_file'] ) );
			// If we're not active at all it means we're being activated.
			if ( ! $network_active && ! is_plugin_active( plugin_basename( $this->config['pro_plugin_file'] ) ) ) {
				// So set network active to whether or not we're in the network admin.
				$network_active = is_network_admin();
			}
			
			// Activate addon. If pro plugin is network active then make sure addon is as well.
			$activation = activate_plugin( $this->addon_file, '', $network_active, true );
			
			if ( is_wp_error( $activation ) ) {
				throw new Exception( sprintf(
					'Could not activate %s: %s',
					$this->config['addon_name'],
					$activation->get_error_message()
				) );
			}
		}
	}

	/**
	 * Transfers the auto update settings from pro plugin to addon.
	 *
	 * @return void
	 */
	protected function transfer_auto_update_settings() {
		$auto_updates = (array) get_site_option( 'auto_update_plugins', array() );

		if ( in_array( plugin_basename( $this->config['pro_plugin_file'] ), $auto_updates, true ) ) {
			$auto_updates[] = $this->addon_file;
			$auto_updates = array_unique( $auto_updates );
			update_site_option( 'auto_update_plugins', $auto_updates );
		}
	}

	/**
	 * Whether or not the notification to install addon should be shown.
	 *
	 * @return bool
	 */
	protected function should_show_notification() {
		global $pagenow;

		// Do not output on plugin / theme upgrade pages or when WordPress is upgrading.
		if ( ( defined( 'IFRAME_REQUEST' ) && IFRAME_REQUEST ) || wp_installing() ) {
			return false;
		}

		// IFRAME_REQUEST is not defined on these pages, though these action pages do show when upgrading themes or plugins.
		$actions = array( 'do-theme-upgrade', 'do-plugin-upgrade', 'do-core-upgrade', 'do-core-reinstall' );
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET['action'] ) && in_array( $_GET['action'], $actions, true ) ) {
			return false;
		}

		if ( (isset( $_GET['action'] ) && $_GET['action'] == 'install-plugin') && (isset( $_GET['plugin'] ) && $_GET['plugin'] == $this->config['addon_slug'])) {
			return false;
		}

		// Show on ALL admin pages when dependency is missing
		if ( is_admin() && ! $this->is_addon_up_to_date() ) {
			return true;
		}

		return false;
	}
}