invalid_plugins)) {
return $this->invalid_plugins;
}
// Initialize empty array
$invalid_plugins = array();
// 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
$active_plugins = array_keys($active_plugins);
} else {
// Single site or non-network admin context
$active_plugins = get_option('active_plugins', array());
}
// Check each active plugin
foreach ($active_plugins as $plugin_file) {
$plugin_path = WP_PLUGIN_DIR . '/' . $plugin_file;
if (!file_exists($plugin_path)) {
$invalid_plugins[] = $plugin_file;
}
}
// Cache the result
$this->invalid_plugins = $invalid_plugins;
return $invalid_plugins;
}
/**
* Check if the current page is the plugins page.
*
* @return bool True if on the plugins page, false otherwise.
*/
public function is_plugins_page() {
global $pagenow;
return is_admin() && 'plugins.php' === $pagenow;
}
/**
* 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');
}
// Create a basic plugin data array
$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 & WPALLSTARS',
'PluginURI' => 'https://www.wpallstars.com',
'AuthorURI' => 'https://www.wpallstars.com',
'Title' => $plugin_name . ' (' . __('Missing', 'wp-fix-plugin-does-not-exist-notices') . ')',
'AuthorName' => 'Marcus Quinn & WPALLSTARS',
);
// Add the data needed for the "View details" link
$plugins[$plugin_path]['slug'] = $plugin_slug;
$plugins[$plugin_path]['plugin'] = $plugin_path;
$plugins[$plugin_path]['type'] = 'plugin';
// Add Git Updater fields
$plugins[$plugin_path]['GitHub Plugin URI'] = 'wpallstars/wp-fix-plugin-does-not-exist-notices';
$plugins[$plugin_path]['GitHub Branch'] = 'main';
$plugins[$plugin_path]['TextDomain'] = 'wp-fix-plugin-does-not-exist-notices';
}
}
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.
*/
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/
'
);
}
// Prepare sections
$new_result->sections = array(
'description' => $description,
'changelog' => !empty($changelog) ? wpautop($changelog) : $changelog,
'faq' => !empty($faq) ? wpautop($faq) : 'Yes, this plugin only removes entries from the WordPress active_plugins option, which is safe to modify when a plugin no longer exists.
', ); // Add installation section if available if (!empty($installation)) { $new_result->sections['installation'] = wpautop($installation); } // Add screenshots section if available if (!empty($screenshots)) { $new_result->sections['screenshots'] = wpautop($screenshots); } // 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' => 'WPALLSTARS' ) ); // Add a random number and timestamp 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) . '&t=' . time(); // 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'; // Set no caching $new_result->cache_time = 0; // Return our completely new result object return $new_result; } return $result; } /** * Check if a slug matches one of our missing plugins. * * @param string $slug The plugin slug to check. * @param array $invalid_plugins List of invalid plugin paths. * @return bool True if the slug matches a missing plugin. */ private function is_missing_plugin($slug, $invalid_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 ($slug === $plugin_slug) { return true; } } return false; } /** * 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 all possible transients for this plugin delete_transient('plugins_api_' . $plugin_slug); delete_site_transient('plugins_api_' . $plugin_slug); delete_transient('plugin_information_' . $plugin_slug); delete_site_transient('plugin_information_' . $plugin_slug); // Clear any other transients that might be caching plugin info $this->clear_all_plugin_transients(); } // Also clear our own plugin's cache $this->clear_own_plugin_cache(); } /** * Clear all plugin-related transients that might be caching information. * * @return void */ private function clear_all_plugin_transients() { // Clear update cache delete_site_transient('update_plugins'); delete_site_transient('update_themes'); delete_site_transient('update_core'); // Clear plugins API cache delete_site_transient('plugin_information'); // Clear plugin update counts delete_transient('plugin_updates_count'); delete_site_transient('plugin_updates_count'); // Clear plugin slugs cache delete_transient('plugin_slugs'); delete_site_transient('plugin_slugs'); } /** * Clear cache specifically for our own plugin. * * @return void */ private function clear_own_plugin_cache() { // Clear our own plugin's cache (both old and new slugs) $our_slugs = array('wp-fix-plugin-does-not-exist-notices', 'fix-plugin-does-not-exist-notices'); foreach ($our_slugs as $slug) { delete_transient('plugins_api_' . $slug); delete_site_transient('plugins_api_' . $slug); delete_transient('plugin_information_' . $slug); delete_site_transient('plugin_information_' . $slug); } // Clear plugin update transients delete_site_transient('update_plugins'); delete_site_transient('plugin_information'); // Force refresh of plugin update information if function exists if (function_exists('wp_clean_plugins_cache')) { wp_clean_plugins_cache(true); } // Clear object cache if function exists if (function_exists('wp_cache_flush')) { wp_cache_flush(); } } }