<?php
/**
 * Fix 'Plugin file does not exist.' Notices
 *
 * @package           FixPluginDoesNotExistNotices
 * @author            Marcus Quinn
 * @copyright         2023 WP ALLSTARS
 * @license           GPL-2.0+
 * @noinspection      PhpUndefinedFunctionInspection
 * @noinspection      PhpUndefinedConstantInspection
 *
 * @wordpress-plugin
 * Plugin Name: Fix 'Plugin file does not exist.' Notices
 * Plugin URI: https://wordpress.org/plugins/fix-plugin-does-not-exist-notices/
 * Description: Adds missing plugins to the plugins list with a "Remove Reference" link so you can permanently clean up invalid plugin entries and remove error notices.
 * Version: 1.6.28
 * Author: Marcus Quinn & WP ALLSTARS
 * Author URI: https://www.wpallstars.com
 * License: GPL-2.0+
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: fix-plugin-does-not-exist-notices
 * Domain Path: /languages
 * Requires at least: 5.0
 * Requires PHP: 7.0
 * Update URI: https://git-updater.wpallstars.com
 * GitHub Plugin URI: wpallstars/fix-plugin-does-not-exist-notices
 * GitHub Branch: main
 * Gitea Plugin URI: wpallstars/fix-plugin-does-not-exist-notices
 * Gitea Branch: main
 *
 * This plugin is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * any later version.
 *
 * This plugin is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this plugin. If not, see https://www.gnu.org/licenses/gpl-2.0.html.
 */

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

// Define plugin constants
define( 'FPDEN_VERSION', '1.6.28' );
define( 'FPDEN_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'FPDEN_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'FPDEN_PLUGIN_FILE', __FILE__ );
define( 'FPDEN_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );

/**
 * Load plugin text domain.
 *
 * @return void
 */
function fpden_load_textdomain() {
	load_plugin_textdomain(
		'fix-plugin-does-not-exist-notices',
		false,
		dirname( plugin_basename( __FILE__ ) ) . '/languages/'
	);
}
add_action( 'plugins_loaded', 'fpden_load_textdomain' );

/**
 * Main class for the plugin.
 */
class Fix_Plugin_Does_Not_Exist_Notices {

	/**
	 * Cached list of invalid plugins.
	 *
	 * @var array
	 */
	private $invalid_plugins = null;

	/**
	 * Constructor. Hooks into WordPress actions and filters.
	 */
	public function __construct() {
		// Add our plugin to the plugins list.
		add_filter( 'all_plugins', array( $this, 'add_missing_plugins_references' ) );

		// Add our action link to the plugins list.
		add_filter( 'plugin_action_links', array( $this, 'add_remove_reference_action' ), 20, 4 );

		// Handle the remove reference action.
		add_action( 'admin_init', array( $this, 'handle_remove_reference' ) );

		// Add admin notices for operation feedback.
		add_action( 'admin_notices', array( $this, 'admin_notices' ) );

		// Enqueue admin scripts and styles.
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );

		// We're no longer trying to prevent WordPress from auto-deactivating plugins
		// as it was causing critical errors in some environments
	}

	/**
	 * Enqueue scripts and styles needed for the admin area.
	 *
	 * @param string $hook_suffix The current admin page hook.
	 * @return void
	 */
	public function enqueue_admin_assets( $hook_suffix ) {
		// Only load on the plugins page.
		if ( 'plugins.php' !== $hook_suffix ) {
			return;
		}

		// Get invalid plugins to decide if assets are needed.
		$invalid_plugins = $this->get_invalid_plugins();
		if ( empty( $invalid_plugins ) ) {
			return; // No missing plugins, no need for the special notice JS/CSS.
		}

		wp_enqueue_style(
			'fpden-admin-styles',
			FPDEN_PLUGIN_URL . 'assets/css/admin-styles.css',
			array(),
			FPDEN_VERSION
		);

		wp_enqueue_script(
			'fpden-admin-scripts',
			FPDEN_PLUGIN_URL . 'assets/js/admin-scripts.js',
			array( 'jquery' ), // Add dependencies if needed, e.g., jQuery.
			FPDEN_VERSION,
			true // Load in footer.
		);

		// Add translation strings for JavaScript
		wp_localize_script(
			'fpden-admin-scripts',
			'fpdenData',
			array(
				'i18n' => array(
					'clickToScroll' => esc_html__( 'Click here to scroll to missing plugins', 'fix-plugin-does-not-exist-notices' ),
					'pluginMissing' => esc_html__( 'File Missing', 'fix-plugin-does-not-exist-notices' ),
					'removeNotice' => esc_html__( 'Remove Notice', 'fix-plugin-does-not-exist-notices' ),
				),
			)
		);
	}

	/**
	 * Find and add invalid plugin references to the plugins list.
	 *
	 * Filters the list of plugins displayed on the plugins page to include
	 * entries for active plugins whose files are missing.
	 *
	 * @param array $plugins An array of plugin data.
	 * @return array The potentially modified array of plugin data.
	 */
	public function add_missing_plugins_references( $plugins ) {
		// Only run on the plugins page.
		if ( ! $this->is_plugins_page() ) {
			return $plugins;
		}

		// Get active plugins that don't exist.
		$invalid_plugins = $this->get_invalid_plugins();

		// Add each invalid plugin to the plugin list.
		foreach ( $invalid_plugins as $plugin_path ) {
			if ( ! isset( $plugins[ $plugin_path ] ) ) {
				$plugin_name          = basename( $plugin_path );
				$plugins[ $plugin_path ] = array(
					'Name'        => $plugin_name . ' <span class="error">(File Missing)</span>',
					/* translators: %s: Path to wp-content/plugins */
					'Description' => sprintf(
						__( 'This plugin is still marked as "Active" in your database — but its folder and files can\'t be found in %s. Click "Remove Notice" to permanently remove it from your active plugins list and eliminate the error notice.', 'fix-plugin-does-not-exist-notices' ),
						'<code>/wp-content/plugins/</code>'
					),
					'Version'     => __( 'N/A', 'fix-plugin-does-not-exist-notices' ),
					'Author'      => '',
					'PluginURI'   => '',
					'AuthorURI'   => '',
					'Title'       => $plugin_name . ' (' . __( 'Missing', 'fix-plugin-does-not-exist-notices' ) . ')',
					'AuthorName'  => '',
				);
			}
		}

		return $plugins;
	}

	/**
	 * Add the Remove Notice action link to invalid plugins.
	 *
	 * Filters the action links displayed for each plugin on the plugins page.
	 * Adds a "Remove Notice" link for plugins identified as missing.
	 *
	 * @param array  $actions     An array of plugin action links.
	 * @param string $plugin_file Path to the plugin file relative to the plugins directory.
	 * @param array  $plugin_data An array of plugin data.
	 * @param string $context     The plugin context (e.g., 'all', 'active', 'inactive').
	 * @return array The potentially modified array of plugin action links.
	 * @noinspection PhpUnusedParameterInspection
	 */
	public function add_remove_reference_action( $actions, $plugin_file, $plugin_data, $context ) {
		// Only run on the plugins page.
		if ( ! $this->is_plugins_page() ) {
			return $actions;
		}

		// Get our list of invalid plugins
		$invalid_plugins = $this->get_invalid_plugins();

		// Check if this plugin file is in our list of invalid plugins
		if ( in_array( $plugin_file, $invalid_plugins, true ) ) {
			// Clear existing actions like "Activate", "Deactivate", "Edit".
			$actions = array();

			// Add our custom action.
			$nonce      = wp_create_nonce( 'remove_plugin_reference_' . $plugin_file );
			$remove_url = admin_url( 'plugins.php?action=remove_reference&plugin=' . urlencode( $plugin_file ) . '&_wpnonce=' . $nonce );
			/* translators: %s: Plugin file path */
			$aria_label                 = sprintf( __( 'Remove reference to missing plugin %s', 'fix-plugin-does-not-exist-notices' ), esc_attr( $plugin_file ) );
			$actions['remove_reference'] = '<a href="' . esc_url( $remove_url ) . '" class="delete" aria-label="' . $aria_label . '">' . esc_html__( 'Remove Notice', 'fix-plugin-does-not-exist-notices' ) . '</a>';
		}

		return $actions;
	}

	/**
	 * Handle the remove reference action triggered by the link.
	 *
	 * Checks for the correct action, verifies nonce and permissions,
	 * calls the removal function, and redirects back to the plugins page.
	 *
	 * @return void
	 */
	public function handle_remove_reference() {
		// Check if our specific action is being performed.
		if ( ! isset( $_GET['action'] ) || 'remove_reference' !== $_GET['action'] || ! isset( $_GET['plugin'] ) ) {
			return;
		}

		// Verify user permissions.
		if ( ! current_user_can( 'activate_plugins' ) ) {
			wp_die( esc_html__( 'You do not have sufficient permissions to perform this action.', 'fix-plugin-does-not-exist-notices' ) );
		}

		// Sanitize and get the plugin file path.
		$plugin_file = isset( $_GET['plugin'] ) ? sanitize_text_field( wp_unslash( $_GET['plugin'] ) ) : '';
		if ( empty( $plugin_file ) ) {
			wp_die( esc_html__( 'Invalid plugin specified.', 'fix-plugin-does-not-exist-notices' ) );
		}

		// Verify nonce for security.
		check_admin_referer( 'remove_plugin_reference_' . $plugin_file );

		// Attempt to remove the plugin reference.
		$success = $this->remove_plugin_reference( $plugin_file );

		// Prepare redirect URL with feedback query args.
		$redirect_url = admin_url( 'plugins.php' );
		$redirect_url = add_query_arg( $success ? 'reference_removed' : 'reference_removal_failed', '1', $redirect_url );

		// Redirect and exit.
		wp_safe_redirect( $redirect_url );
		exit;
	}

	/**
	 * Remove a plugin reference from the active plugins list in the database.
	 *
	 * Handles both single site and multisite network activated plugins.
	 *
	 * @param string $plugin_file The plugin file path to remove.
	 * @return bool True on success, false on failure or if the plugin wasn't found.
	 */
	public function remove_plugin_reference( $plugin_file ) {
		$success = false;

		// Ensure plugin file path is provided.
		if ( empty( $plugin_file ) ) {
			return false;
		}

		// Handle multisite network admin context.
		if ( is_multisite() && is_network_admin() ) {
			$active_plugins = get_site_option( 'active_sitewide_plugins', array() );
			// Network active plugins are stored as key => timestamp.
			if ( isset( $active_plugins[ $plugin_file ] ) ) {
				unset( $active_plugins[ $plugin_file ] );
				$success = update_site_option( 'active_sitewide_plugins', $active_plugins );
			}
		} else { // Handle single site or non-network admin context.
			$active_plugins = get_option( 'active_plugins', array() );
			// Single site active plugins are stored as an indexed array.
			$key = array_search( $plugin_file, $active_plugins, true ); // Use strict comparison.
			if ( false !== $key ) {
				unset( $active_plugins[ $key ] );
				// Re-index the array numerically.
				$active_plugins = array_values( $active_plugins );
				$success        = update_option( 'active_plugins', $active_plugins );
			}
		}

		return $success;
	}

	/**
	 * Display admin notices on the plugins page.
	 *
	 * Shows feedback messages after attempting to remove a reference.
	 * The main informational notice is handled by JavaScript to position it
	 * directly below the WordPress error message.
	 *
	 * @return void
	 */
	public function admin_notices() {
		// Only run on the plugins page.
		if ( ! $this->is_plugins_page() ) {
			return;
		}

		// Check for feedback messages from the remove action.
		if ( isset( $_GET['reference_removed'] ) && '1' === $_GET['reference_removed'] ) {
			?>
			<div class="notice notice-success is-dismissible">
				<p><?php esc_html_e( 'Plugin reference removed successfully.', 'fix-plugin-does-not-exist-notices' ); ?></p>
			</div>
			<?php
		}

		if ( isset( $_GET['reference_removal_failed'] ) && '1' === $_GET['reference_removal_failed'] ) {
			?>
			<div class="notice notice-error is-dismissible">
				<p><?php esc_html_e( 'Failed to remove plugin reference. The plugin may already have been removed, or there was a database issue.', 'fix-plugin-does-not-exist-notices' ); ?></p>
			</div>
			<?php
		}
		// The main informational notice is now handled entirely by JavaScript
		// to position it directly below the WordPress error message.
	}

	/**
	 * Check if the current admin page is the plugins page.
	 *
	 * @global string $pagenow WordPress global variable for the current admin page filename.
	 * @return bool True if the current page is plugins.php, false otherwise.
	 */
	private function is_plugins_page() {
		global $pagenow;
		// Check if it's an admin page and the filename is plugins.php.
		return is_admin() && isset( $pagenow ) && 'plugins.php' === $pagenow;
	}

	/**
	 * Get a list of active plugin file paths that do not exist on the filesystem.
	 *
	 * Checks both single site and network active plugins based on the context.
	 * Uses caching to avoid repeated filesystem checks.
	 *
	 * @return array An array of plugin file paths (relative to WP_PLUGIN_DIR) that are missing.
	 */
	private function get_invalid_plugins() {
		// Return cached result if available
		if ( null !== $this->invalid_plugins ) {
			return $this->invalid_plugins;
		}

		$this->invalid_plugins = array();
		$active_plugins  = array();

		// Determine which option to check based on context (Network Admin or single site).
		if ( is_multisite() && is_network_admin() ) {
			// Network active plugins are stored as keys in an associative array.
			$active_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
		} else {
			// Single site active plugins are stored in a numerically indexed array.
			$active_plugins = get_option( 'active_plugins', array() );
		}

		// Check if the file exists for each active plugin.
		foreach ( $active_plugins as $plugin_file ) {
			// Construct the full path to the main plugin file.
			$plugin_path = WP_PLUGIN_DIR . '/' . $plugin_file;
			// Use validate_file to prevent directory traversal issues, although less likely here.
			if ( validate_file( $plugin_file ) === 0 && ! file_exists( $plugin_path ) ) {
				$this->invalid_plugins[] = $plugin_file;
			}
		}

		return $this->invalid_plugins;
	}

// We've removed the prevent_auto_deactivation method as it was causing critical errors
} // End class Fix_Plugin_Does_Not_Exist_Notices

// Initialize the plugin class.
new Fix_Plugin_Does_Not_Exist_Notices();

// Initialize the updater if composer autoload exists
$autoloader = __DIR__ . '/vendor/autoload.php';
if (file_exists($autoloader)) {
    require_once $autoloader;

    // Initialize the updater if the class exists
    if (class_exists('\WPALLSTARS\FixPluginDoesNotExistNotices\Updater')) {
        new \WPALLSTARS\FixPluginDoesNotExistNotices\Updater(__FILE__);
    }
}