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', 'wp-fix-plugin-does-not-exist-notices' ),
'pluginMissing' => esc_html__( 'File Missing', 'wp-fix-plugin-does-not-exist-notices' ),
'removeNotice' => esc_html__( 'Remove Notice', 'wp-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 );
$plugin_slug = dirname( $plugin_path );
if ( '.' === $plugin_slug ) {
$plugin_slug = basename( $plugin_path, '.php' );
}
$plugins[ $plugin_path ] = array(
'Name' => $plugin_name . ' (File Missing)',
/* 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.', 'wp-fix-plugin-does-not-exist-notices' ),
'/wp-content/plugins/
'
),
'Version' => FPDEN_VERSION, // Use our plugin version instead of 'N/A'
'Author' => 'Marcus Quinn & WP ALLSTARS',
'PluginURI' => 'https://www.wpallstars.com',
'AuthorURI' => 'https://www.wpallstars.com',
'Title' => $plugin_name . ' (' . __( 'Missing', 'wp-fix-plugin-does-not-exist-notices' ) . ')',
'AuthorName' => 'Marcus Quinn & WP ALLSTARS',
// Add fields needed for the "View details" link
'slug' => $plugin_slug,
'plugin' => $plugin_path,
);
}
}
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', 'wp-fix-plugin-does-not-exist-notices' ), esc_attr( $plugin_file ) );
$actions['remove_reference'] = '' . esc_html__( 'Remove Notice', 'wp-fix-plugin-does-not-exist-notices' ) . '';
}
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.', 'wp-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.', 'wp-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'] ) {
?>
/wp-content/plugins/
'
),
'changelog' => 'Yes, this plugin only removes entries from the WordPress active_plugins option, which is safe to modify when a plugin no longer exists.
' ); // Add contributors information $new_result->contributors = array( 'marcusquinn' => array( 'profile' => 'https://profiles.wordpress.org/marcusquinn/', 'avatar' => 'https://secure.gravatar.com/avatar/', 'display_name' => 'Marcus Quinn' ), 'wpallstars' => array( 'profile' => 'https://profiles.wordpress.org/wpallstars/', 'avatar' => 'https://secure.gravatar.com/avatar/', 'display_name' => 'WP ALLSTARS' ) ); // Add a random number to force cache refresh $new_result->download_link = 'https://www.wpallstars.com/plugins/wp-fix-plugin-does-not-exist-notices.zip?v=' . FPDEN_VERSION . '&cb=' . mt_rand(1000000, 9999999); // Add active installations count $new_result->active_installs = 1000; // Add rating information $new_result->rating = 100; $new_result->num_ratings = 5; $new_result->ratings = array( 5 => 5, 4 => 0, 3 => 0, 2 => 0, 1 => 0 ); // Add homepage and download link $new_result->homepage = 'https://www.wpallstars.com'; // Return our completely new result object return $new_result; } } return $result; } /** * Prevent WordPress from caching our plugin API responses. * * @param object|WP_Error $result The result object or WP_Error. * @param string $action The type of information being requested. * @param object $args Plugin API arguments. * @return object|WP_Error The result object or WP_Error. */ public function prevent_plugins_api_caching( $result, $action, $args ) { // Only modify plugin_information requests if ( 'plugin_information' !== $action ) { return $result; } // Check if we have a slug to work with if ( empty( $args->slug ) ) { return $result; } // Get our list of invalid plugins $invalid_plugins = $this->get_invalid_plugins(); // Check if the requested plugin is one of our missing plugins foreach ( $invalid_plugins as $plugin_file ) { // Extract the plugin slug from the plugin file path $plugin_slug = dirname( $plugin_file ); if ( '.' === $plugin_slug ) { $plugin_slug = basename( $plugin_file, '.php' ); } // If this is one of our missing plugins, prevent caching if ( $args->slug === $plugin_slug ) { // Add a filter to prevent caching of this response add_filter( 'plugins_api_result_' . $args->slug, '__return_false' ); // Add a timestamp to force cache busting if ( is_object( $result ) ) { $result->last_updated = current_time( 'mysql' ); $result->cache_time = 0; } } } return $result; } /** * Clear plugin API cache when viewing the plugins page. * * @return void */ public function maybe_clear_plugin_api_cache() { // Only run on the plugins page if ( ! $this->is_plugins_page() ) { return; } // Get our list of invalid plugins $invalid_plugins = $this->get_invalid_plugins(); // Clear transients for each invalid plugin foreach ( $invalid_plugins as $plugin_file ) { // Extract the plugin slug from the plugin file path $plugin_slug = dirname( $plugin_file ); if ( '.' === $plugin_slug ) { $plugin_slug = basename( $plugin_file, '.php' ); } // Delete the transient for this plugin delete_transient( 'plugins_api_' . $plugin_slug ); delete_site_transient( 'plugins_api_' . $plugin_slug ); // Also delete the update transient which might cache plugin info delete_site_transient( 'update_plugins' ); } } } // 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__); } }