do_action( 'deleted_plugin', $plugin_file, $deleted );
$errors[] = $plugin_file;
$plugin_slug = dirname( $plugin_file );
if ( 'hello.php' === $plugin_file ) {
$plugin_slug = 'hello-dolly';
// Remove language files, silently.
if ( '.' !== $plugin_slug && ! empty( $plugin_translations[ $plugin_slug ] ) ) {
$translations = $plugin_translations[ $plugin_slug ];
foreach ( $translations as $translation => $data ) {
$wp_filesystem->delete( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '.po' );
$wp_filesystem->delete( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '.mo' );
$wp_filesystem->delete( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '.l10n.php' );
$json_translation_files = glob( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '-*.json' );
if ( $json_translation_files ) {
array_map( array( $wp_filesystem, 'delete' ), $json_translation_files );
// Remove deleted plugins from the plugin updates list.
$current = get_site_transient( 'update_plugins' );
// Don't remove the plugins that weren't deleted.
$deleted = array_diff( $plugins, $errors );
foreach ( $deleted as $plugin_file ) {
unset( $current->response[ $plugin_file ] );
set_site_transient( 'update_plugins', $current );
if ( ! empty( $errors ) ) {
if ( 1 === count( $errors ) ) {
/* translators: %s: Plugin filename. */
$message = __( 'Could not fully remove the plugin %s.' );
/* translators: %s: Comma-separated list of plugin filenames. */
$message = __( 'Could not fully remove the plugins %s.' );
return new WP_Error( 'could_not_remove_plugin', sprintf( $message, implode( ', ', $errors ) ) );
* Validates active plugins.
* Validate all active plugins, deactivates invalid and
* returns an array of deactivated ones.
* @return WP_Error[] Array of plugin errors keyed by plugin file name.
function validate_active_plugins() {
$plugins = get_option( 'active_plugins', array() );
// Validate vartype: array.
if ( ! is_array( $plugins ) ) {
update_option( 'active_plugins', array() );
if ( is_multisite() && current_user_can( 'manage_network_plugins' ) ) {
$network_plugins = (array) get_site_option( 'active_sitewide_plugins', array() );
$plugins = array_merge( $plugins, array_keys( $network_plugins ) );
if ( empty( $plugins ) ) {
// Invalid plugins get deactivated.
foreach ( $plugins as $plugin ) {
$result = validate_plugin( $plugin );
if ( is_wp_error( $result ) ) {
$invalid[ $plugin ] = $result;
deactivate_plugins( $plugin, true );
* Validates the plugin path.
* Checks that the main plugin file exists and is a valid plugin. See validate_file().
* @param string $plugin Path to the plugin file relative to the plugins directory.
* @return int|WP_Error 0 on success, WP_Error on failure.
function validate_plugin( $plugin ) {
if ( validate_file( $plugin ) ) {
return new WP_Error( 'plugin_invalid', __( 'Invalid plugin path.' ) );
if ( ! file_exists( WP_PLUGIN_DIR . '/' . $plugin ) ) {
return new WP_Error( 'plugin_not_found', __( 'Plugin file does not exist.' ) );
$installed_plugins = get_plugins();
if ( ! isset( $installed_plugins[ $plugin ] ) ) {
return new WP_Error( 'no_plugin_header', __( 'The plugin does not have a valid header.' ) );
* Validates the plugin requirements for WordPress version and PHP version.
* Uses the information from `Requires at least`, `Requires PHP` and `Requires Plugins` headers
* defined in the plugin's main PHP file.
* @since 5.3.0 Added support for reading the headers from the plugin's
* main PHP file, with `readme.txt` as a fallback.
* @since 5.8.0 Removed support for using `readme.txt` as a fallback.
* @since 6.5.0 Added support for the 'Requires Plugins' header.
* @param string $plugin Path to the plugin file relative to the plugins directory.
* @return true|WP_Error True if requirements are met, WP_Error on failure.
function validate_plugin_requirements( $plugin ) {
$plugin_headers = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
'requires' => ! empty( $plugin_headers['RequiresWP'] ) ? $plugin_headers['RequiresWP'] : '',
'requires_php' => ! empty( $plugin_headers['RequiresPHP'] ) ? $plugin_headers['RequiresPHP'] : '',
'requires_plugins' => ! empty( $plugin_headers['RequiresPlugins'] ) ? $plugin_headers['RequiresPlugins'] : '',
$compatible_wp = is_wp_version_compatible( $requirements['requires'] );
$compatible_php = is_php_version_compatible( $requirements['requires_php'] );
$php_update_message = '</p><p>' . sprintf(
/* translators: %s: URL to Update PHP page. */
__( '<a href="%s">Learn more about updating PHP</a>.' ),
esc_url( wp_get_update_php_url() )
$annotation = wp_get_update_php_annotation();
$php_update_message .= '</p><p><em>' . $annotation . '</em>';
if ( ! $compatible_wp && ! $compatible_php ) {
'plugin_wp_php_incompatible',
/* translators: 1: Current WordPress version, 2: Current PHP version, 3: Plugin name, 4: Required WordPress version, 5: Required PHP version. */
_x( '<strong>Error:</strong> Current versions of WordPress (%1$s) and PHP (%2$s) do not meet minimum requirements for %3$s. The plugin requires WordPress %4$s and PHP %5$s.', 'plugin' ),
get_bloginfo( 'version' ),
$requirements['requires'],
$requirements['requires_php']
) . $php_update_message . '</p>'
} elseif ( ! $compatible_php ) {
'plugin_php_incompatible',
/* translators: 1: Current PHP version, 2: Plugin name, 3: Required PHP version. */
_x( '<strong>Error:</strong> Current PHP version (%1$s) does not meet minimum requirements for %2$s. The plugin requires PHP %3$s.', 'plugin' ),
$requirements['requires_php']
) . $php_update_message . '</p>'
} elseif ( ! $compatible_wp ) {
'plugin_wp_incompatible',
/* translators: 1: Current WordPress version, 2: Plugin name, 3: Required WordPress version. */
_x( '<strong>Error:</strong> Current WordPress version (%1$s) does not meet minimum requirements for %2$s. The plugin requires WordPress %3$s.', 'plugin' ),
get_bloginfo( 'version' ),
$requirements['requires']
WP_Plugin_Dependencies::initialize();
if ( WP_Plugin_Dependencies::has_unmet_dependencies( $plugin ) ) {
$dependency_names = WP_Plugin_Dependencies::get_dependency_names( $plugin );
$unmet_dependencies = array();
$unmet_dependency_names = array();
foreach ( $dependency_names as $dependency => $dependency_name ) {
$dependency_file = WP_Plugin_Dependencies::get_dependency_filepath( $dependency );
if ( false === $dependency_file ) {
$unmet_dependencies['not_installed'][ $dependency ] = $dependency_name;
$unmet_dependency_names[] = $dependency_name;
} elseif ( is_plugin_inactive( $dependency_file ) ) {
$unmet_dependencies['inactive'][ $dependency ] = $dependency_name;
$unmet_dependency_names[] = $dependency_name;
$error_message = sprintf(
/* translators: 1: Plugin name, 2: Number of plugins, 3: A comma-separated list of plugin names. */
'<strong>Error:</strong> %1$s requires %2$d plugin to be installed and activated: %3$s.',
'<strong>Error:</strong> %1$s requires %2$d plugins to be installed and activated: %3$s.',
count( $unmet_dependency_names )
count( $unmet_dependency_names ),
implode( wp_get_list_item_separator(), $unmet_dependency_names )
if ( current_user_can( 'manage_network_plugins' ) ) {
$error_message .= ' ' . sprintf(
/* translators: %s: Link to the plugins page. */
__( '<a href="%s">Manage plugins</a>.' ),
esc_url( network_admin_url( 'plugins.php' ) )
$error_message .= ' ' . __( 'Please contact your network administrator.' );
$error_message .= ' ' . sprintf(
/* translators: %s: Link to the plugins page. */
__( '<a href="%s">Manage plugins</a>.' ),
esc_url( admin_url( 'plugins.php' ) )
'plugin_missing_dependencies',
"<p>{$error_message}</p>",
* Determines whether the plugin can be uninstalled.
* @param string $plugin Path to the plugin file relative to the plugins directory.
* @return bool Whether plugin can be uninstalled.
function is_uninstallable_plugin( $plugin ) {
$file = plugin_basename( $plugin );
$uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
if ( isset( $uninstallable_plugins[ $file ] ) || file_exists( WP_PLUGIN_DIR . '/' . dirname( $file ) . '/uninstall.php' ) ) {
* Uninstalls a single plugin.
* Calls the uninstall hook, if it is available.
* @param string $plugin Path to the plugin file relative to the plugins directory.
* @return true|void True if a plugin's uninstall.php file has been found and included.
function uninstall_plugin( $plugin ) {
$file = plugin_basename( $plugin );
$uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
* Fires in uninstall_plugin() immediately before the plugin is uninstalled.
* @param string $plugin Path to the plugin file relative to the plugins directory.
* @param array $uninstallable_plugins Uninstallable plugins.
do_action( 'pre_uninstall_plugin', $plugin, $uninstallable_plugins );
if ( file_exists( WP_PLUGIN_DIR . '/' . dirname( $file ) . '/uninstall.php' ) ) {
if ( isset( $uninstallable_plugins[ $file ] ) ) {
unset( $uninstallable_plugins[ $file ] );
update_option( 'uninstall_plugins', $uninstallable_plugins );
unset( $uninstallable_plugins );
define( 'WP_UNINSTALL_PLUGIN', $file );
wp_register_plugin_realpath( WP_PLUGIN_DIR . '/' . $file );
include_once WP_PLUGIN_DIR . '/' . dirname( $file ) . '/uninstall.php';
if ( isset( $uninstallable_plugins[ $file ] ) ) {
$callable = $uninstallable_plugins[ $file ];
unset( $uninstallable_plugins[ $file ] );
update_option( 'uninstall_plugins', $uninstallable_plugins );
unset( $uninstallable_plugins );
wp_register_plugin_realpath( WP_PLUGIN_DIR . '/' . $file );
include_once WP_PLUGIN_DIR . '/' . $file;
add_action( "uninstall_{$file}", $callable );
* Fires in uninstall_plugin() once the plugin has been uninstalled.
* The action concatenates the 'uninstall_' prefix with the basename of the
* plugin passed to uninstall_plugin() to create a dynamically-named action.
do_action( "uninstall_{$file}" );
* Adds a top-level menu page.
* This function takes a capability which will be used to determine whether
* or not a page is included in the menu.
* The function which is hooked in to handle the output of the page must check
* that the user has the required capability as well.
* @global array $admin_page_hooks
* @global array $_registered_pages
* @global array $_parent_pages
* @param string $page_title The text to be displayed in the title tags of the page when the menu is selected.
* @param string $menu_title The text to be used for the menu.
* @param string $capability The capability required for this menu to be displayed to the user.
* @param string $menu_slug The slug name to refer to this menu by. Should be unique for this menu page and only
* include lowercase alphanumeric, dashes, and underscores characters to be compatible
* @param callable $callback Optional. The function to be called to output the content for this page.
* @param string $icon_url Optional. The URL to the icon to be used for this menu.
* * Pass a base64-encoded SVG using a data URI, which will be colored to match
* the color scheme. This should begin with 'data:image/svg+xml;base64,'.
* * Pass the name of a Dashicons helper class to use a font icon,
* e.g. 'dashicons-chart-pie'.
* * Pass 'none' to leave div.wp-menu-image empty so an icon can be added via CSS.
* @param int|float $position Optional. The position in the menu order this item should appear.
* @return string The resulting page's hook_suffix.
function add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $icon_url = '', $position = null ) {
global $menu, $admin_page_hooks, $_registered_pages, $_parent_pages;
$menu_slug = plugin_basename( $menu_slug );
$admin_page_hooks[ $menu_slug ] = sanitize_title( $menu_title );
$hookname = get_plugin_page_hookname( $menu_slug, '' );
if ( ! empty( $callback ) && ! empty( $hookname ) && current_user_can( $capability ) ) {
add_action( $hookname, $callback );
if ( empty( $icon_url ) ) {
$icon_url = 'dashicons-admin-generic';
$icon_class = 'menu-icon-generic ';
$icon_url = set_url_scheme( $icon_url );
$new_menu = array( $menu_title, $capability, $menu_slug, $page_title, 'menu-top ' . $icon_class . $hookname, $hookname, $icon_url );
if ( null !== $position && ! is_numeric( $position ) ) {
/* translators: %s: add_menu_page() */
__( 'The seventh parameter passed to %s should be numeric representing menu position.' ),
'<code>add_menu_page()</code>'
if ( null === $position || ! is_numeric( $position ) ) {
} elseif ( isset( $menu[ (string) $position ] ) ) {
$collision_avoider = base_convert( substr( md5( $menu_slug . $menu_title ), -4 ), 16, 10 ) * 0.00001;
$position = (string) ( $position + $collision_avoider );
$menu[ $position ] = $new_menu;
* Cast menu position to a string.
* This allows for floats to be passed as the position. PHP will normally cast a float to an
* integer value, this ensures the float retains its mantissa (positive fractional part).
* A string containing an integer value, eg "10", is treated as a numeric index.
$position = (string) $position;
$menu[ $position ] = $new_menu;
$_registered_pages[ $hookname ] = true;
// No parent as top level.
$_parent_pages[ $menu_slug ] = false;
* This function takes a capability which will be used to determine whether
* or not a page is included in the menu.
* The function which is hooked in to handle the output of the page must check
* that the user has the required capability as well.
* @since 5.3.0 Added the `$position` parameter.
* @global array $_wp_real_parent_file
* @global bool $_wp_submenu_nopriv
* @global array $_registered_pages
* @global array $_parent_pages
* @param string $parent_slug The slug name for the parent menu (or the file name of a standard
* @param string $page_title The text to be displayed in the title tags of the page when the menu
* @param string $menu_title The text to be used for the menu.
* @param string $capability The capability required for this menu to be displayed to the user.
* @param string $menu_slug The slug name to refer to this menu by. Should be unique for this menu
* and only include lowercase alphanumeric, dashes, and underscores characters
* to be compatible with sanitize_key().
* @param callable $callback Optional. The function to be called to output the content for this page.
* @param int|float $position Optional. The position in the menu order this item should appear.
* @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required.
function add_submenu_page( $parent_slug, $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) {
global $submenu, $menu, $_wp_real_parent_file, $_wp_submenu_nopriv,
$_registered_pages, $_parent_pages;
$menu_slug = plugin_basename( $menu_slug );
$parent_slug = plugin_basename( $parent_slug );
if ( isset( $_wp_real_parent_file[ $parent_slug ] ) ) {
$parent_slug = $_wp_real_parent_file[ $parent_slug ];
if ( ! current_user_can( $capability ) ) {
$_wp_submenu_nopriv[ $parent_slug ][ $menu_slug ] = true;
* If the parent doesn't already have a submenu, add a link to the parent
* as the first item in the submenu. If the submenu file is the same as the
* parent file someone is trying to link back to the parent manually. In
* this case, don't automatically add a link back to avoid duplication.
if ( ! isset( $submenu[ $parent_slug ] ) && $menu_slug !== $parent_slug ) {
foreach ( (array) $menu as $parent_menu ) {
if ( $parent_menu[2] === $parent_slug && current_user_can( $parent_menu[1] ) ) {
$submenu[ $parent_slug ][] = array_slice( $parent_menu, 0, 4 );