diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php index b5adb946cf0d3..33b64a9e2def1 100644 --- a/src/wp-admin/includes/admin-filters.php +++ b/src/wp-admin/includes/admin-filters.php @@ -133,6 +133,8 @@ add_action( 'admin_notices', 'update_nag', 3 ); add_action( 'admin_notices', 'deactivated_plugins_notice', 5 ); +add_action( 'admin_init', '_deactivate_incompatible_for_core_plugins', 5 ); +add_action( 'admin_notices', '_render_admin_notice_for_incompatible_plugins', 5 ); add_action( 'admin_notices', 'paused_plugins_notice', 5 ); add_action( 'admin_notices', 'paused_themes_notice', 5 ); add_action( 'admin_notices', 'maintenance_nag', 10 ); diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 123c9d8f5ff44..05bedd40b086f 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2596,3 +2596,97 @@ function deactivated_plugins_notice() { update_site_option( 'wp_force_deactivated_plugins', array() ); } } + +/** + * Deactivate incompatible for-core plugins. + * + * @since 6.5.0 + */ +function _deactivate_incompatible_for_core_plugins() { + $blog_plugins = get_option( 'found_incompatible_for_core_plugins' ); + $site_plugins = array(); + + // Option not in database, add an empty array to avoid extra DB queries on subsequent loads. + if ( false === $blog_plugins ) { + update_option( 'found_incompatible_for_core_plugins', array() ); + } + + if ( is_multisite() ) { + $site_plugins = get_site_option( 'found_incompatible_for_core_plugins' ); + // Option not in database, add an empty array to avoid extra DB queries on subsequent loads. + if ( false === $site_plugins ) { + update_site_option( 'found_incompatible_for_core_plugins', array() ); + } + } + + // No incompatible plugins. Bail out. + if ( empty( $blog_plugins ) && empty( $site_plugins ) ) { + return; + } + + $incompatible_plugins = array_merge( $blog_plugins, $site_plugins ); + + // Deactivate. + $plugins_to_deactivate = array_keys( $incompatible_plugins ); + deactivate_plugins( $plugins_to_deactivate, true ); +} + +/** + * Renders an admin notice for each incompatible plugin. + * + * These plugins were not loaded during WordPress' early loading + * due to incompatibility with the current version of WordPress. + * + * @since 6.5.0 + * @access private + * + * @global string $wp_version The WordPress version string. + */ +function _render_admin_notice_for_incompatible_plugins() { + + if ( ! current_user_can( 'activate_plugins' ) ) { + return; + } + + $blog_plugins = get_option( 'found_incompatible_for_core_plugins' ); + $site_plugins = is_multisite() + ? get_site_option( 'found_incompatible_for_core_plugins' ) + : array(); + + // No incompatible plugins. Bail out. + if ( empty( $blog_plugins ) && empty( $site_plugins ) ) { + return; + } + + $incompatible_plugins = array_merge( $blog_plugins, $site_plugins ); + + foreach ( $incompatible_plugins as $plugin ) { + // Skip if information is missing. + if ( empty( $plugin['version_compatible'] ) || empty( $plugin['version_deactivated'] ) ) { + continue; + } + + $explanation = sprintf( + /* translators: 1: Name of deactivated plugin, 2: Plugin version deactivated, 3: Current WP version, 4: Compatible plugin version. */ + __( '%1$s %2$s was deactivated due to incompatibility with WordPress %3$s, please upgrade to %1$s %4$s or later.' ), + $plugin['plugin_name'], + $plugin['version_deactivated'], + $GLOBALS['wp_version'], + $plugin['version_compatible'] + ); + + $message = sprintf( + '%s

%s', + $explanation, + esc_url( admin_url( 'plugins.php?plugin_status=inactive' ) ), + __( 'Go to the Plugins screen' ) + ); + wp_admin_notice( $message, array( 'type' => 'warning' ) ); + } + + // Empty the options. + update_option( 'found_incompatible_for_core_plugins', array() ); + if ( is_multisite() ) { + update_site_option( 'found_incompatible_for_core_plugins', array() ); + } +} diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index 6c28453b216d3..5b73eb91072cd 100644 --- a/src/wp-includes/load.php +++ b/src/wp-includes/load.php @@ -919,6 +919,30 @@ function wp_not_installed() { die(); } +/** + * Gets the for-core plugin's minimum compatible version. + * + * @since 6.5.0 + * @access private + * + * @param string $plugin Plugin path relative to the plugins' directory. + * @return string Returns the minimum version on success, otherwise an empty string. + */ +function _get_plugin_wp_min_compatible_version( $plugin ) { + static $plugin_data = array( + 'gutenberg/gutenberg.php' => array( + 'name' => 'Gutenberg', + 'minimum_compatible_version' => '17.6', + ), + ); + + if ( ! isset( $plugin_data[ $plugin ] ) ) { + return ''; + } + + return $plugin_data[ $plugin ]['minimum_compatible_version']; +} + /** * Retrieves an array of must-use plugin files. * diff --git a/src/wp-settings.php b/src/wp-settings.php index 22683b37d1f5d..6ec40df7201fd 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -412,6 +412,117 @@ $GLOBALS['wp_plugin_paths'] = array(); +/* + * Compatibility handler for for-core plugins. + * + * "For-core" are approved plugins being developed specifically to be merged into + * Core, e.g. Gutenberg and feature plugins. + * + * As these "for-core" plugins are frequently released, the handler identifies + * each's minimum compatible version, detects the version of each that is activated, + * and deactivates and does not load each incompatible plugin. + * + * This handler is done at the activated plugin loading loops to ensure + * sites stay running (to avoid fatal errors) and experiences are expected (by + * not loading older versions of features). + * + * @since 6.5.0 + */ +global $_found_incompatible_for_core_plugins, $_is_plugin_compatible_with_wp, $_handle_incompatible_for_core_plugins; + +// Found incompatible for-core plugins. +$_found_incompatible_for_core_plugins = array( + 'sitewide_plugins' => array(), + 'single_plugins' => array(), +); + +/** + * Checks if the given plugin is compatible with this WordPress version. + * + * @since 6.5.0 + * + * @global $_found_incompatible_for_core_plugins + * + * @param string $plugin_absolute_path Absolute path to the plugin's file. + * @param bool $is_network Optional. Whether the plugin is activated sitewide. Default false. + */ +$_is_plugin_compatible_with_wp = static function ( $plugin_absolute_path, $is_network = false ) { + global $_found_incompatible_for_core_plugins; + + static $plugins_dir_strlen; + if ( empty( $plugins_dir_strlen ) ) { + $plugins_dir_strlen = strlen( WP_PLUGIN_DIR . '/' ); + } + + $plugin = substr( $plugin_absolute_path, $plugins_dir_strlen ); + + // Not a for-core plugin. + $min_compat_version = _get_plugin_wp_min_compatible_version( $plugin ); + if ( ! $min_compat_version ) { + return true; + } + + // The plugin is already marked as incompatible. + if ( isset( $_found_incompatible_for_core_plugins[ $plugin ] ) ) { + return false; + } + + // Get the Name and Version from the plugin's header. + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + $plugin_data = get_plugin_data( + $plugin_absolute_path, + array( + 'Name' => 'Plugin Name', + 'Version' => 'Version', + ), + 'plugin' + ); + + // Whoops, something went wrong. Bail out. + if ( ! ( isset( $plugin_data['Version'] ) && isset( $plugin_data['Name'] ) ) ) { + return true; + } + + // Plugin is compatible. + if ( version_compare( $plugin_data['Version'], $min_compat_version, '>=' ) ) { + return true; + } + + // Found an incompatible for-core plugin. Add it to the found global for later batch processing. + $key = $is_network ? 'sitewide_plugins' : 'single_plugins'; + $_found_incompatible_for_core_plugins[ $key ][ $plugin ] = array( + 'plugin_absolute_path' => $plugin_absolute_path, + 'plugin_name' => $plugin_data['Name'], + 'version_deactivated' => $plugin_data['Version'], + 'version_compatible' => $min_compat_version, + ); + + return false; +}; + +/** + * Handle incompatible for-core plugins. + * + * The handler does adds a 'found_incompatible_for_core_plugins' option for single site + * and multisites. The option is used in wp-admin to deactivate and render a notice. + * + * @since 6.5.0 + * + * @global $_found_incompatible_for_core_plugins + */ +$_handle_incompatible_for_core_plugins = static function () { + global $_found_incompatible_for_core_plugins; + + if ( ! empty( $_found_incompatible_for_core_plugins['sitewide_plugins'] ) ) { + update_site_option( 'found_incompatible_for_core_plugins', $_found_incompatible_for_core_plugins['sitewide_plugins'] ); + } + + if ( ! empty( $_found_incompatible_for_core_plugins['single_plugins'] ) ) { + update_option( 'found_incompatible_for_core_plugins', $_found_incompatible_for_core_plugins['single_plugins'] ); + } +}; +// End of the for-core plugins compatibility handler. + // Load must-use plugins. foreach ( wp_get_mu_plugins() as $mu_plugin ) { $_wp_plugin_file = $mu_plugin; @@ -432,6 +543,16 @@ // Load network activated plugins. if ( is_multisite() ) { foreach ( wp_get_active_network_plugins() as $network_plugin ) { + + /* + * If the plugin is incompatible with this WordPress version, skip loading it. + * + * @since 6.5.0 + */ + if ( ! $_is_plugin_compatible_with_wp( $network_plugin, true ) ) { + continue; + } + wp_register_plugin_realpath( $network_plugin ); $_wp_plugin_file = $network_plugin; @@ -487,6 +608,16 @@ // Load active plugins. foreach ( wp_get_active_and_valid_plugins() as $plugin ) { + + /* + * If the plugin is incompatible with this WordPress version, skip loading it. + * + * @since 6.5.0 + */ + if ( ! $_is_plugin_compatible_with_wp( $plugin ) ) { + continue; + } + wp_register_plugin_realpath( $plugin ); $_wp_plugin_file = $plugin; @@ -502,7 +633,15 @@ */ do_action( 'plugin_loaded', $plugin ); } -unset( $plugin, $_wp_plugin_file ); +$_handle_incompatible_for_core_plugins(); +unset( + $plugin, + $_wp_plugin_file, + // Remove the for-core compatibility handler. + $_wp_activated_plugins_to_compat_check, + $_is_plugin_compatible_with_wp, + $_handle_incompatible_for_core_plugins +); // Load pluggable functions. require ABSPATH . WPINC . '/pluggable.php';