Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC: Handler for preventing the load of incompatible "for Core" plugins #5855

Draft
wants to merge 7 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/wp-admin/includes/admin-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
94 changes: 94 additions & 0 deletions src/wp-admin/includes/plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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</p><p><a href="%s">%s</a>',
$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() );
}
}
24 changes: 24 additions & 0 deletions src/wp-includes/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
141 changes: 140 additions & 1 deletion src/wp-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Copy link
Contributor Author

@hellofromtonya hellofromtonya Feb 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why substr() rather than using str_replace()?

It's more performant. See https://3v4l.org/TbQ9U.


// 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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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';
Expand Down