Make WordPress Core

Changeset 57658

Timestamp:
02/20/2024 07:25:38 AM (5 months ago)
Author:
costdev
Message:

Plugin Dependencies: Remove auto-deactivation and bootstrapping logic.

Automatic deactivation of dependents with unmet dependencies requires a write operation to the database. This was performed during Core's bootstrap, which risked the database and cache becoming out-of-sync on sites with heavy traffic.

No longer loading plugins that have unmet requirements has not had a final approach decided core-wide, and is still in discussion in #60491 to be handled in a future release.

The plugin_data option, used to persistently store plugin data for detecting unmet dependencies during Core's bootstrap, is no longer needed.

Follow-up to [57545], [57592], [57606], [57617].

Props dd32, azaozz, swissspidy, desrosj, afragen, pbiron, zunaid321, costdev.
Fixes #60457. See #60491, #60510, #60518.

Location:
trunk
Files:
11 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/class-plugin-upgrader.php

    r57545 r57658  
    155155        // Force refresh of plugin update information.
    156156        wp_clean_plugins_cache( $parsed_args['clear_update_cache'] );
    157 
    158         $all_plugin_data = get_option( 'plugin_data', array() );
    159         $plugin_file     = $this->new_plugin_data['file'];
    160         unset( $this->new_plugin_data['file'] );
    161         $all_plugin_data[ $plugin_file ] = $this->new_plugin_data;
    162         update_option( 'plugin_data', $all_plugin_data );
    163157
    164158        if ( $parsed_args['overwrite_package'] ) {
     
    489483                $info = get_plugin_data( $file, false, false );
    490484                if ( ! empty( $info['Name'] ) ) {
    491                     $basename = basename( $file );
    492                     $dirname  = basename( dirname( $file ) );
    493 
    494                     if ( '.' === $dirname ) {
    495                         $plugin_file = $basename;
    496                     } else {
    497                         $plugin_file = "$dirname/$basename";
    498                     }
    499                     $this->new_plugin_data         = ( $info );
    500                     $this->new_plugin_data['file'] = $plugin_file;
     485                    $this->new_plugin_data = $info;
    501486                    break;
    502487                }
  • trunk/src/wp-admin/includes/plugin-install.php

    r57545 r57658  
    928928    // Determine the status of plugin dependencies.
    929929    $installed_plugins                   = get_plugins();
    930     $active_plugins                      = get_option( 'active_plugins' );
     930    $active_plugins                      = get_option( 'active_plugins' );
    931931    $plugin_dependencies_count           = count( $requires_plugins );
    932932    $installed_plugin_dependencies_count = 0;
  • trunk/src/wp-admin/includes/plugin.php

    r57644 r57658  
    334334    }
    335335
    336     $new_plugin_data = array();
    337336    foreach ( $plugin_files as $plugin_file ) {
    338337        if ( ! is_readable( "$plugin_root/$plugin_file" ) ) {
     
    347346        }
    348347
    349         $new_plugin_file = str_replace(
    350             trailingslashit( WP_PLUGIN_DIR ),
    351             '',
    352             "$plugin_root/$plugin_file"
    353         );
    354 
    355         $new_plugin_data[ $new_plugin_file ]           = $plugin_data;
    356348        $wp_plugins[ plugin_basename( $plugin_file ) ] = $plugin_data;
    357349    }
     
    361353    $cache_plugins[ $plugin_folder ] = $wp_plugins;
    362354    wp_cache_set( 'plugins', $cache_plugins, 'plugins' );
    363 
    364     if ( ! wp_installing() ) {
    365         update_option( 'plugin_data', $new_plugin_data );
    366     }
    367355
    368356    return $wp_plugins;
     
    976964
    977965    $plugin_translations = wp_get_installed_translations( 'plugins' );
    978     $all_plugin_data     = get_option( 'plugin_data', array() );
    979966
    980967    $errors = array();
     
    10211008            continue;
    10221009        }
    1023         unset( $all_plugin_data[ $plugin_file ] );
    10241010
    10251011        $plugin_slug = dirname( $plugin_file );
     
    10701056        return new WP_Error( 'could_not_remove_plugin', sprintf( $message, implode( ', ', $errors ) ) );
    10711057    }
    1072     update_option( 'plugin_data', $all_plugin_data );
    10731058
    10741059    return true;
     
    12151200    }
    12161201
     1202
     1203
    12171204    if ( WP_Plugin_Dependencies::has_unmet_dependencies( $plugin ) ) {
    12181205        $dependencies       = WP_Plugin_Dependencies::get_dependencies( $plugin );
  • trunk/src/wp-admin/plugin-install.php

    r57545 r57658  
    136136require_once ABSPATH . 'wp-admin/admin-header.php';
    137137
     138
    138139WP_Plugin_Dependencies::display_admin_notice_for_unmet_dependencies();
    139 WP_Plugin_Dependencies::display_admin_notice_for_deactivated_dependents();
    140140WP_Plugin_Dependencies::display_admin_notice_for_circular_dependencies();
    141141?>
  • trunk/src/wp-admin/plugins.php

    r57586 r57658  
    4040
    4141wp_enqueue_script( 'updates' );
     42
     43
    4244
    4345if ( $action ) {
     
    742744
    743745<?php WP_Plugin_Dependencies::display_admin_notice_for_unmet_dependencies(); ?>
    744 <?php WP_Plugin_Dependencies::display_admin_notice_for_deactivated_dependents(); ?>
    745746<?php WP_Plugin_Dependencies::display_admin_notice_for_circular_dependencies(); ?>
    746747<div class="wrap">
  • trunk/src/wp-includes/class-wp-plugin-dependencies.php

    r57617 r57658  
    106106
    107107    /**
    108      * Initializes by fetching plugin header and plugin API data,
    109      * and deactivating dependents with unmet dependencies.
     108     * Whether Plugin Dependencies have been initialized.
     109     *
     110     * @since 6.5.0
     111     *
     112     * @var bool
     113     */
     114    protected static $initialized = false;
     115
     116    /**
     117     * Initializes by fetching plugin header and plugin API data.
    110118     *
    111119     * @since 6.5.0
    112120     */
    113121    public static function initialize() {
    114         self::read_dependencies_from_plugin_headers();
    115         self::get_dependency_api_data();
    116         self::deactivate_dependents_with_unmet_dependencies();
     122        if ( false === self::$initialized ) {
     123            self::read_dependencies_from_plugin_headers();
     124            self::get_dependency_api_data();
     125            self::$initialized = true;
     126        }
    117127    }
    118128
     
    126136     */
    127137    public static function has_dependents( $plugin_file ) {
    128         return in_array( self::convert_to_slug( $plugin_file ), self::$dependency_slugs, true );
     138        return in_array( self::convert_to_slug( $plugin_file ), self::$dependency_slugs, true );
    129139    }
    130140
     
    173183        $dependents = array();
    174184
    175         foreach ( self::$dependencies as $dependent => $dependencies ) {
     185        foreach ( self::$dependencies as $dependent => $dependencies ) {
    176186            if ( in_array( $slug, $dependencies, true ) ) {
    177187                $dependents[] = $dependent;
     
    370380
    371381    /**
    372      * Displays an admin notice if dependencies have been deactivated.
    373      *
    374      * @since 6.5.0
    375      */
    376     public static function display_admin_notice_for_deactivated_dependents() {
    377         /*
    378          * Plugin deactivated if dependencies not met.
    379          * Transient on a 10 second timeout.
    380          */
    381         $deactivate_requires = get_site_transient( 'wp_plugin_dependencies_deactivated_plugins' );
    382         if ( ! empty( $deactivate_requires ) ) {
    383             $deactivated_plugins = '';
    384             foreach ( $deactivate_requires as $deactivated ) {
    385                 $deactivated_plugins .= '<li>' . esc_html( self::$plugins[ $deactivated ]['Name'] ) . '</li>';
    386             }
    387             wp_admin_notice(
    388                 sprintf(
    389                     /* translators: 1: plugin names */
    390                     __( 'The following plugin(s) have been deactivated due to uninstalled or inactive dependencies: %s' ),
    391                     "<ul>$deactivated_plugins</ul>"
    392                 ),
    393                 array(
    394                     'type'        => 'error',
    395                     'dismissible' => true,
    396                 )
    397             );
    398         }
    399     }
    400 
    401     /**
    402382     * Displays an admin notice if circular dependencies are installed.
    403383     *
     
    544524        }
    545525
    546         $all_plugin_data = get_option( 'plugin_data', array() );
    547 
    548         if ( empty( $all_plugin_data ) ) {
    549             require_once ABSPATH . '/wp-admin/includes/plugin.php';
    550             $all_plugin_data = get_plugins();
    551         }
    552 
    553         self::$plugins = $all_plugin_data;
     526        require_once ABSPATH . '/wp-admin/includes/plugin.php';
     527        self::$plugins = get_plugins();
    554528
    555529        return self::$plugins;
     
    622596
    623597    /**
    624      * Gets plugin filepaths for active plugins that depend on the dependency.
    625      *
    626      * Recurses for each dependent that is also a dependency.
    627      *
    628      * @param string $plugin_file The dependency's filepath, relative to the plugin directory.
    629      * @return string[] An array of active dependent plugin filepaths, relative to the plugin directory.
    630      */
    631     protected static function get_active_dependents_in_dependency_tree( $plugin_file ) {
    632         $all_dependents = array();
    633         $dependents     = self::get_dependents( self::convert_to_slug( $plugin_file ) );
    634 
    635         if ( empty( $dependents ) ) {
    636             return $all_dependents;
    637         }
    638 
    639         require_once ABSPATH . '/wp-admin/includes/plugin.php';
    640         foreach ( $dependents as $dependent ) {
    641             if ( is_plugin_active( $dependent ) ) {
    642                 $all_dependents[] = $dependent;
    643                 $all_dependents   = array_merge(
    644                     $all_dependents,
    645                     self::get_active_dependents_in_dependency_tree( $dependent )
    646                 );
    647             }
    648         }
    649 
    650         return $all_dependents;
    651     }
    652 
    653     /**
    654      * Deactivates dependent plugins with unmet dependencies.
    655      *
    656      * @since 6.5.0
    657      */
    658     protected static function deactivate_dependents_with_unmet_dependencies() {
    659         $dependents_to_deactivate = array();
    660         $circular_dependencies    = array_reduce(
    661             self::get_circular_dependencies(),
    662             function ( $all_circular, $circular_pair ) {
    663                 return array_merge( $all_circular, $circular_pair );
    664             },
    665             array()
    666         );
    667 
    668         require_once ABSPATH . '/wp-admin/includes/plugin.php';
    669         foreach ( self::$dependencies as $dependent => $dependencies ) {
    670             // Skip dependents that are no longer installed or aren't active.
    671             if ( ! array_key_exists( $dependent, self::$plugins ) || is_plugin_inactive( $dependent ) ) {
    672                 continue;
    673             }
    674 
    675             // Skip plugins within a circular dependency tree or plugins that have no unmet dependencies.
    676             if ( in_array( $dependent, $circular_dependencies, true ) || ! self::has_unmet_dependencies( $dependent ) ) {
    677                 continue;
    678             }
    679 
    680             $dependents_to_deactivate[] = $dependent;
    681 
    682             // Also add any plugins that rely on any of this plugin's dependents.
    683             $dependents_to_deactivate = array_merge(
    684                 $dependents_to_deactivate,
    685                 self::get_active_dependents_in_dependency_tree( $dependent )
    686             );
    687         }
    688 
    689         // Bail early if there are no dependents to deactivate.
    690         if ( empty( $dependents_to_deactivate ) ) {
    691             return;
    692         }
    693 
    694         $dependents_to_deactivate = array_unique( $dependents_to_deactivate );
    695 
    696         deactivate_plugins( $dependents_to_deactivate );
    697         set_site_transient( 'wp_plugin_dependencies_deactivated_plugins', $dependents_to_deactivate, 10 );
    698     }
    699 
    700     /**
    701598     * Gets the filepath of installed dependencies.
    702599     * If a dependency is not installed, the filepath defaults to false.
     
    709606        if ( is_array( self::$dependency_filepaths ) ) {
    710607            return self::$dependency_filepaths;
     608
     609
     610
     611
    711612        }
    712613
     
    847748        }
    848749
     750
     751
     752
     753
    849754        self::$circular_dependencies_slugs = array();
    850755
  • trunk/src/wp-settings.php

    r57628 r57658  
    389389require ABSPATH . WPINC . '/interactivity-api/class-wp-interactivity-api-directives-processor.php';
    390390require ABSPATH . WPINC . '/interactivity-api/interactivity-api.php';
     391
    391392
    392393wp_script_modules()->add_hooks();
     
    419420
    420421$GLOBALS['wp_plugin_paths'] = array();
    421 
    422 // Load and initialize WP_Plugin_Dependencies.
    423 require_once ABSPATH . WPINC . '/class-wp-plugin-dependencies.php';
    424 if ( ! defined( 'WP_RUN_CORE_TESTS' ) ) {
    425     WP_Plugin_Dependencies::initialize();
    426 }
    427422
    428423// Load must-use plugins.
     
    500495
    501496// Load active plugins.
    502 $all_plugin_data    = get_option( 'plugin_data', array() );
    503 $failed_plugins     = array();
    504 $plugins_dir_strlen = strlen( trailingslashit( WP_PLUGIN_DIR ) );
    505497foreach ( wp_get_active_and_valid_plugins() as $plugin ) {
    506     $plugin_file = substr( $plugin, $plugins_dir_strlen );
    507 
    508     /*
    509      * Skip any plugins that have not been added to the 'plugin_data' option yet.
    510      *
    511      * Some plugin files may be added locally and activated, but will not yet be
    512      * added to the 'plugin_data' option. This causes the 'active_plugins' option
    513      * and the 'plugin_data' option to be temporarily out of sync until the next
    514      * call to `get_plugins()`.
    515      */
    516     if ( isset( $all_plugin_data[ $plugin_file ] ) ) {
    517         $plugin_headers = $all_plugin_data[ $plugin_file ];
    518         $errors         = array();
    519         $requirements   = array(
    520             'requires'     => ! empty( $plugin_headers['RequiresWP'] ) ? $plugin_headers['RequiresWP'] : '',
    521             'requires_php' => ! empty( $plugin_headers['RequiresPHP'] ) ? $plugin_headers['RequiresPHP'] : '',
    522         );
    523         $compatible_wp  = is_wp_version_compatible( $requirements['requires'] );
    524         $compatible_php = is_php_version_compatible( $requirements['requires_php'] );
    525 
    526         $php_update_message = '</p><p>' . sprintf(
    527             /* translators: %s: URL to Update PHP page. */
    528             __( '<a href="%s">Learn more about updating PHP</a>.' ),
    529             esc_url( wp_get_update_php_url() )
    530         );
    531 
    532         $annotation = wp_get_update_php_annotation();
    533 
    534         if ( $annotation ) {
    535             $php_update_message .= '</p><p><em>' . $annotation . '</em>';
    536         }
    537 
    538         if ( ! $compatible_wp && ! $compatible_php ) {
    539             $errors[] = sprintf(
    540                 /* translators: 1: Current WordPress version, 2: Current PHP version, 3: Plugin name, 4: Required WordPress version, 5: Required PHP version. */
    541                 _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' ),
    542                 get_bloginfo( 'version' ),
    543                 PHP_VERSION,
    544                 $plugin_headers['Name'],
    545                 $requirements['requires'],
    546                 $requirements['requires_php']
    547             ) . $php_update_message;
    548         } elseif ( ! $compatible_php ) {
    549             $errors[] = sprintf(
    550                 /* translators: 1: Current PHP version, 2: Plugin name, 3: Required PHP version. */
    551                 _x( '<strong>Error:</strong> Current PHP version (%1$s) does not meet minimum requirements for %2$s. The plugin requires PHP %3$s.', 'plugin' ),
    552                 PHP_VERSION,
    553                 $plugin_headers['Name'],
    554                 $requirements['requires_php']
    555             ) . $php_update_message;
    556         } elseif ( ! $compatible_wp ) {
    557             $errors[] = sprintf(
    558                 /* translators: 1: Current WordPress version, 2: Plugin name, 3: Required WordPress version. */
    559                 _x( '<strong>Error:</strong> Current WordPress version (%1$s) does not meet minimum requirements for %2$s. The plugin requires WordPress %3$s.', 'plugin' ),
    560                 get_bloginfo( 'version' ),
    561                 $plugin_headers['Name'],
    562                 $requirements['requires']
    563             );
    564         }
    565 
    566         if ( ! empty( $errors ) ) {
    567             $failed_plugins[ $plugin_file ] = '';
    568             foreach ( $errors as $error ) {
    569                 $failed_plugins[ $plugin_file ] .= wp_get_admin_notice(
    570                     $error,
    571                     array(
    572                         'type'        => 'error',
    573                         'dismissible' => true,
    574                     )
    575                 );
    576             }
    577             continue;
    578         }
    579     }
    580 
    581498    wp_register_plugin_realpath( $plugin );
    582499
     
    596513unset( $plugin, $_wp_plugin_file );
    597514
    598 if ( ! empty( $failed_plugins ) ) {
    599     add_action(
    600         'admin_notices',
    601         function () use ( $failed_plugins ) {
    602             global $pagenow;
    603 
    604             if ( 'index.php' === $pagenow || 'plugins.php' === $pagenow ) {
    605                 echo implode( '', $failed_plugins );
    606             }
    607         }
    608     );
    609 }
    610 unset( $failed_plugins );
    611 
    612515// Load pluggable functions.
    613516require ABSPATH . WPINC . '/pluggable.php';
  • trunk/tests/phpunit/tests/admin/plugin-dependencies/base.php

    r57545 r57658  
    3333        'circular_dependencies_pairs' => null,
    3434        'circular_dependencies_slugs' => null,
     35
    3536    );
    3637
     
    6364     * Resets all static properties to a default value after each test.
    6465     */
    65     public function set_up() {
    66         parent::set_up();
    67 
     66    public function tear_down() {
    6867        foreach ( self::$static_properties as $name => $default_value ) {
    6968            $this->set_property_value( $name, $default_value );
    7069        }
     70
     71
    7172    }
    7273
  • trunk/tests/phpunit/tests/admin/plugin-dependencies/getDependencyFilepath.php

    r57545 r57658  
    1717 */
    1818class Tests_Admin_WPPluginDependencies_GetDependencyFilepath extends WP_PluginDependencies_UnitTestCase {
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
    1943
    2044    /**
  • trunk/tests/phpunit/tests/admin/plugin-dependencies/hasCircularDependency.php

    r57545 r57658  
    1717 */
    1818class Tests_Admin_WPPluginDependencies_HasCircularDependency extends WP_PluginDependencies_UnitTestCase {
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
    1957
    2058    /**
  • trunk/tests/phpunit/tests/admin/plugin-dependencies/initialize.php

    r57545 r57658  
    1515 */
    1616class Tests_Admin_WPPluginDependencies_Initialize extends WP_PluginDependencies_UnitTestCase {
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
    1779
    1880    /**
     
    239301        $this->assertSame( $expected_slugs, $this->get_property_value( 'dependent_slugs' ) );
    240302    }
    241 
    242     /**
    243      * Tests that dependents with unmet dependencies are deactivated.
    244      *
    245      * @ticket 22316
    246      *
    247      * @covers WP_Plugin_Dependencies::deactivate_dependents_with_unmet_dependencies
    248      * @covers WP_Plugin_Dependencies::has_unmet_dependencies
    249      * @covers WP_Plugin_Dependencies::get_active_dependents_in_dependency_tree
    250      *
    251      * @dataProvider data_should_only_deactivate_dependents_with_unmet_dependencies
    252      *
    253      * @param array $active_plugins An array of active plugin paths.
    254      * @param array $plugins        An array of installed plugins.
    255      * @param array $expected       The expected value of 'active_plugins' after initialization.
    256      */
    257     public function test_should_deactivate_dependents_with_uninstalled_dependencies( $active_plugins, $plugins, $expected ) {
    258         update_option( 'active_plugins', $active_plugins );
    259 
    260         $this->set_property_value( 'plugins', $plugins );
    261         self::$instance::initialize();
    262 
    263         $this->assertSame( $expected, array_values( get_option( 'active_plugins', array() ) ) );
    264     }
    265 
    266     /**
    267      * Data provider.
    268      *
    269      * @return array[]
    270      */
    271     public function data_should_only_deactivate_dependents_with_unmet_dependencies() {
    272         return array(
    273             'a dependent with an uninstalled dependency' => array(
    274                 'active_plugins' => array( 'dependent/dependent.php' ),
    275                 'plugins'        => array(
    276                     'dependent/dependent.php' => array( 'RequiresPlugins' => 'dependency' ),
    277                 ),
    278                 'expected'       => array(),
    279             ),
    280             'a dependent with an inactive dependency'    => array(
    281                 'active_plugins' => array( 'dependent/dependent.php' ),
    282                 'plugins'        => array(
    283                     'dependent/dependent.php'   => array( 'RequiresPlugins' => 'dependency' ),
    284                     'dependency/dependency.php' => array( 'RequiresPlugins' => '' ),
    285                 ),
    286                 'expected'       => array(),
    287             ),
    288             'a dependent with two dependencies, one uninstalled, one inactive' => array(
    289                 'active_plugins' => array( 'dependent/dependent.php' ),
    290                 'plugins'        => array(
    291                     'dependent/dependent.php'     => array( 'RequiresPlugins' => 'dependency, dependency2' ),
    292                     'dependency2/dependency2.php' => array( 'RequiresPlugins' => '' ),
    293                 ),
    294                 'expected'       => array(),
    295             ),
    296             'a dependent with a dependency that is installed and active' => array(
    297                 'active_plugins' => array( 'dependent/dependent.php', 'dependency/dependency.php' ),
    298                 'plugins'        => array(
    299                     'dependent/dependent.php'   => array( 'RequiresPlugins' => 'dependency' ),
    300                     'dependency/dependency.php' => array( 'RequiresPlugins' => '' ),
    301                 ),
    302                 'expected'       => array( 'dependent/dependent.php', 'dependency/dependency.php' ),
    303             ),
    304             'one dependent with two dependencies that are installed and active' => array(
    305                 'active_plugins' => array(
    306                     'dependent/dependent.php',
    307                     'dependency/dependency.php',
    308                     'dependency2/dependency2.php',
    309                 ),
    310                 'plugins'        => array(
    311                     'dependent/dependent.php'     => array( 'RequiresPlugins' => 'dependency, dependency2' ),
    312                     'dependency/dependency.php'   => array( 'RequiresPlugins' => '' ),
    313                     'dependency2/dependency2.php' => array( 'RequiresPlugins' => '' ),
    314                 ),
    315                 'expected'       => array(
    316                     'dependent/dependent.php',
    317                     'dependency/dependency.php',
    318                     'dependency2/dependency2.php',
    319                 ),
    320             ),
    321             'two dependents, one with an uninstalled dependency, and one with an active dependency' => array(
    322                 'active_plugins' => array(
    323                     'dependent/dependent.php',
    324                     'dependent2/dependent2.php',
    325                     'dependency2/dependency2.php',
    326                 ),
    327                 'plugins'        => array(
    328                     'dependent/dependent.php'     => array( 'RequiresPlugins' => 'dependency' ),
    329                     'dependent2/dependent2.php'   => array( 'RequiresPlugins' => 'dependency2' ),
    330                     'dependency2/dependency2.php' => array( 'RequiresPlugins' => '' ),
    331                 ),
    332                 'expected'       => array( 'dependent2/dependent2.php', 'dependency2/dependency2.php' ),
    333             ),
    334         );
    335     }
    336303}
Note: See TracChangeset for help on using the changeset viewer.