diff -r 7b1b88e27a20 -r 48c4eec2b7e6 wp/wp-admin/includes/plugin.php --- a/wp/wp-admin/includes/plugin.php Thu Sep 29 08:06:27 2022 +0200 +++ b/wp/wp-admin/includes/plugin.php Fri Sep 05 18:40:08 2025 +0200 @@ -45,6 +45,7 @@ * @since 1.5.0 * @since 5.3.0 Added support for `Requires at least` and `Requires PHP` headers. * @since 5.8.0 Added support for `Update URI` header. + * @since 6.5.0 Added support for `Requires Plugins` header. * * @param string $plugin_file Absolute path to the main plugin file. * @param bool $markup Optional. If the returned data should have HTML markup applied. @@ -53,39 +54,41 @@ * @return array { * Plugin data. Values will be empty if not supplied by the plugin. * - * @type string $Name Name of the plugin. Should be unique. - * @type string $PluginURI Plugin URI. - * @type string $Version Plugin version. - * @type string $Description Plugin description. - * @type string $Author Plugin author's name. - * @type string $AuthorURI Plugin author's website address (if set). - * @type string $TextDomain Plugin textdomain. - * @type string $DomainPath Plugin's relative directory path to .mo files. - * @type bool $Network Whether the plugin can only be activated network-wide. - * @type string $RequiresWP Minimum required version of WordPress. - * @type string $RequiresPHP Minimum required version of PHP. - * @type string $UpdateURI ID of the plugin for update purposes, should be a URI. - * @type string $Title Title of the plugin and link to the plugin's site (if set). - * @type string $AuthorName Plugin author's name. + * @type string $Name Name of the plugin. Should be unique. + * @type string $PluginURI Plugin URI. + * @type string $Version Plugin version. + * @type string $Description Plugin description. + * @type string $Author Plugin author's name. + * @type string $AuthorURI Plugin author's website address (if set). + * @type string $TextDomain Plugin textdomain. + * @type string $DomainPath Plugin's relative directory path to .mo files. + * @type bool $Network Whether the plugin can only be activated network-wide. + * @type string $RequiresWP Minimum required version of WordPress. + * @type string $RequiresPHP Minimum required version of PHP. + * @type string $UpdateURI ID of the plugin for update purposes, should be a URI. + * @type string $RequiresPlugins Comma separated list of dot org plugin slugs. + * @type string $Title Title of the plugin and link to the plugin's site (if set). + * @type string $AuthorName Plugin author's name. * } */ function get_plugin_data( $plugin_file, $markup = true, $translate = true ) { $default_headers = array( - 'Name' => 'Plugin Name', - 'PluginURI' => 'Plugin URI', - 'Version' => 'Version', - 'Description' => 'Description', - 'Author' => 'Author', - 'AuthorURI' => 'Author URI', - 'TextDomain' => 'Text Domain', - 'DomainPath' => 'Domain Path', - 'Network' => 'Network', - 'RequiresWP' => 'Requires at least', - 'RequiresPHP' => 'Requires PHP', - 'UpdateURI' => 'Update URI', + 'Name' => 'Plugin Name', + 'PluginURI' => 'Plugin URI', + 'Version' => 'Version', + 'Description' => 'Description', + 'Author' => 'Author', + 'AuthorURI' => 'Author URI', + 'TextDomain' => 'Text Domain', + 'DomainPath' => 'Domain Path', + 'Network' => 'Network', + 'RequiresWP' => 'Requires at least', + 'RequiresPHP' => 'Requires PHP', + 'UpdateURI' => 'Update URI', + 'RequiresPlugins' => 'Requires Plugins', // Site Wide Only is deprecated in favor of Network. - '_sitewide' => 'Site Wide Only', + '_sitewide' => 'Site Wide Only', ); $plugin_data = get_file_data( $plugin_file, $default_headers, 'plugin' ); @@ -102,7 +105,7 @@ // If no text domain is defined fall back to the plugin slug. if ( ! $plugin_data['TextDomain'] ) { $plugin_slug = dirname( plugin_basename( $plugin_file ) ); - if ( '.' !== $plugin_slug && false === strpos( $plugin_slug, '/' ) ) { + if ( '.' !== $plugin_slug && ! str_contains( $plugin_slug, '/' ) ) { $plugin_data['TextDomain'] = $plugin_slug; } } @@ -127,7 +130,7 @@ * @access private * * @param string $plugin_file Path to the main plugin file. - * @param array $plugin_data An array of plugin data. See `get_plugin_data()`. + * @param array $plugin_data An array of plugin data. See get_plugin_data(). * @param bool $markup Optional. If the returned data should have HTML markup applied. * Default true. * @param bool $translate Optional. If the returned data should be translated. Default true. @@ -178,8 +181,10 @@ 'title' => true, ); - // Name is marked up inside tags. Don't allow these. - // Author is too, but some plugins have used here (omitting Author URI). + /* + * Name is marked up inside tags. Don't allow these. + * Author is too, but some plugins have used here (omitting Author URI). + */ $plugin_data['Name'] = wp_kses( $plugin_data['Name'], $allowed_tags_in_links ); $plugin_data['Author'] = wp_kses( $plugin_data['Author'], $allowed_tags ); @@ -269,7 +274,7 @@ * @since 1.5.0 * * @param string $plugin_folder Optional. Relative path to single plugin folder. - * @return array[] Array of arrays of plugin data, keyed by plugin file name. See `get_plugin_data()`. + * @return array[] Array of arrays of plugin data, keyed by plugin file name. See get_plugin_data(). */ function get_plugins( $plugin_folder = '' ) { @@ -294,7 +299,7 @@ if ( $plugins_dir ) { while ( ( $file = readdir( $plugins_dir ) ) !== false ) { - if ( '.' === substr( $file, 0, 1 ) ) { + if ( str_starts_with( $file, '.' ) ) { continue; } @@ -303,21 +308,19 @@ if ( $plugins_subdir ) { while ( ( $subfile = readdir( $plugins_subdir ) ) !== false ) { - if ( '.' === substr( $subfile, 0, 1 ) ) { + if ( str_starts_with( $subfile, '.' ) ) { continue; } - if ( '.php' === substr( $subfile, -4 ) ) { + if ( str_ends_with( $subfile, '.php' ) ) { $plugin_files[] = "$file/$subfile"; } } closedir( $plugins_subdir ); } - } else { - if ( '.php' === substr( $file, -4 ) ) { - $plugin_files[] = $file; - } + } elseif ( str_ends_with( $file, '.php' ) ) { + $plugin_files[] = $file; } } @@ -357,7 +360,7 @@ * WordPress only includes mu-plugin files in the base mu-plugins directory (wp-content/mu-plugins). * * @since 3.0.0 - * @return array[] Array of arrays of mu-plugin data, keyed by plugin file name. See `get_plugin_data()`. + * @return array[] Array of arrays of mu-plugin data, keyed by plugin file name. See get_plugin_data(). */ function get_mu_plugins() { $wp_plugins = array(); @@ -371,7 +374,7 @@ $plugins_dir = @opendir( WPMU_PLUGIN_DIR ); if ( $plugins_dir ) { while ( ( $file = readdir( $plugins_dir ) ) !== false ) { - if ( '.php' === substr( $file, -4 ) ) { + if ( str_ends_with( $file, '.php' ) ) { $plugin_files[] = $file; } } @@ -429,7 +432,7 @@ * Checks the wp-content directory and retrieve all drop-ins with any plugin data. * * @since 3.0.0 - * @return array[] Array of arrays of dropin plugin data, keyed by plugin file name. See `get_plugin_data()`. + * @return array[] Array of arrays of dropin plugin data, keyed by plugin file name. See get_plugin_data(). */ function get_dropins() { $dropins = array(); @@ -476,14 +479,23 @@ } /** - * Returns drop-ins that WordPress uses. + * Returns drop-in plugins that WordPress uses. * * Includes Multisite drop-ins only when is_multisite() * * @since 3.0.0 - * @return array[] Key is file name. The value is an array, with the first value the - * purpose of the drop-in and the second value the name of the constant that must be - * true for the drop-in to be used, or true if no constant is required. + * + * @return array[] { + * Key is file name. The value is an array of data about the drop-in. + * + * @type array ...$0 { + * Data about the drop-in. + * + * @type string $0 The purpose of the drop-in. + * @type string|true $1 Name of the constant that must be true for the drop-in + * to be used, or true if no constant is required. + * } + * } */ function _get_dropins() { $dropins = array( @@ -852,7 +864,7 @@ * @param bool $network_wide Whether to enable the plugin for all sites in the network. * Default false. * @param bool $silent Prevent calling activation hooks. Default false. - * @return bool|WP_Error True when finished or WP_Error if there were errors during a plugin activation. + * @return true|WP_Error True when finished or WP_Error if there were errors during a plugin activation. */ function activate_plugins( $plugins, $redirect = '', $network_wide = false, $silent = false ) { if ( ! is_array( $plugins ) ) { @@ -969,8 +981,10 @@ $this_plugin_dir = trailingslashit( dirname( $plugins_dir . $plugin_file ) ); - // If plugin is in its own directory, recursively delete the directory. - // Base check on if plugin includes directory separator AND that it's not the root plugin folder. + /* + * If plugin is in its own directory, recursively delete the directory. + * Base check on if plugin includes directory separator AND that it's not the root plugin folder. + */ if ( strpos( $plugin_file, '/' ) && $this_plugin_dir !== $plugins_dir ) { $deleted = $wp_filesystem->delete( $this_plugin_dir, true ); } else { @@ -1005,6 +1019,7 @@ 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 ) { @@ -1109,13 +1124,14 @@ /** * Validates the plugin requirements for WordPress version and PHP version. * - * Uses the information from `Requires at least` and `Requires PHP` headers + * Uses the information from `Requires at least`, `Requires PHP` and `Requires Plugins` headers * defined in the plugin's main PHP file. * * @since 5.2.0 * @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. @@ -1124,8 +1140,9 @@ $plugin_headers = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); $requirements = array( - 'requires' => ! empty( $plugin_headers['RequiresWP'] ) ? $plugin_headers['RequiresWP'] : '', - 'requires_php' => ! empty( $plugin_headers['RequiresPHP'] ) ? $plugin_headers['RequiresPHP'] : '', + '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'] ); @@ -1150,7 +1167,7 @@ /* translators: 1: Current WordPress version, 2: Current PHP version, 3: Plugin name, 4: Required WordPress version, 5: Required PHP version. */ _x( 'Error: 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' ), - phpversion(), + PHP_VERSION, $plugin_headers['Name'], $requirements['requires'], $requirements['requires_php'] @@ -1162,7 +1179,7 @@ '

' . sprintf( /* translators: 1: Current PHP version, 2: Plugin name, 3: Required PHP version. */ _x( 'Error: Current PHP version (%1$s) does not meet minimum requirements for %2$s. The plugin requires PHP %3$s.', 'plugin' ), - phpversion(), + PHP_VERSION, $plugin_headers['Name'], $requirements['requires_php'] ) . $php_update_message . '

' @@ -1180,6 +1197,62 @@ ); } + 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. */ + _n( + 'Error: %1$s requires %2$d plugin to be installed and activated: %3$s.', + 'Error: %1$s requires %2$d plugins to be installed and activated: %3$s.', + count( $unmet_dependency_names ) + ), + $plugin_headers['Name'], + count( $unmet_dependency_names ), + implode( wp_get_list_item_separator(), $unmet_dependency_names ) + ); + + if ( is_multisite() ) { + if ( current_user_can( 'manage_network_plugins' ) ) { + $error_message .= ' ' . sprintf( + /* translators: %s: Link to the plugins page. */ + __( '
Manage plugins.' ), + esc_url( network_admin_url( 'plugins.php' ) ) + ); + } else { + $error_message .= ' ' . __( 'Please contact your network administrator.' ); + } + } else { + $error_message .= ' ' . sprintf( + /* translators: %s: Link to the plugins page. */ + __( 'Manage plugins.' ), + esc_url( admin_url( 'plugins.php' ) ) + ); + } + + return new WP_Error( + 'plugin_missing_dependencies', + "

{$error_message}

", + $unmet_dependencies + ); + } + return true; } @@ -1454,6 +1527,7 @@ // For negative or `0` positions, prepend the submenu. array_unshift( $submenu[ $parent_slug ], $new_sub_menu ); } else { + $position = absint( $position ); // Grab all of the items before the insertion point. $before_items = array_slice( $submenu[ $parent_slug ], 0, $position, true ); // Grab all of the items after the insertion point. @@ -1934,7 +2008,7 @@ $parent_file = $parent_page; return $parent_page; } elseif ( empty( $typenow ) && $pagenow === $submenu_array[2] - && ( empty( $parent_file ) || false === strpos( $parent_file, '?' ) ) + && ( empty( $parent_file ) || ! str_contains( $parent_file, '?' ) ) ) { $parent_file = $parent_page; return $parent_page; @@ -1956,7 +2030,7 @@ * * @since 1.5.0 * - * @global string $title + * @global string $title The title of the current screen. * @global array $menu * @global array $submenu * @global string $pagenow The filename of the current screen. @@ -2381,6 +2455,8 @@ * * @since 5.2.0 * + * @global WP_Paused_Extensions_Storage $_paused_plugins + * * @param string $plugin Path to the plugin file relative to the plugins directory. * @return bool True, if in the list of paused plugins. False, if not in the list. */ @@ -2403,6 +2479,8 @@ * * @since 5.2.0 * + * @global WP_Paused_Extensions_Storage $_paused_plugins + * * @param string $plugin Path to the plugin file relative to the plugins directory. * @return array|false Array of error information as returned by `error_get_last()`, * or false if none was recorded. @@ -2435,7 +2513,7 @@ * * @param string $plugin Single plugin to resume. * @param string $redirect Optional. URL to redirect to. Default empty string. - * @return bool|WP_Error True on success, false if `$plugin` was not paused, + * @return true|WP_Error True on success, false if `$plugin` was not paused, * `WP_Error` on failure. */ function resume_plugin( $plugin, $redirect = '' ) { @@ -2477,7 +2555,8 @@ * * @since 5.2.0 * - * @global string $pagenow The filename of the current screen. + * @global string $pagenow The filename of the current screen. + * @global WP_Paused_Extensions_Storage $_paused_plugins */ function paused_plugins_notice() { if ( 'plugins.php' === $GLOBALS['pagenow'] ) { @@ -2492,13 +2571,17 @@ return; } - printf( - '

%s
%s

%s

', + $message = sprintf( + '%s
%s

%s', __( 'One or more plugins failed to load properly.' ), __( 'You can find more details and make changes on the Plugins screen.' ), esc_url( admin_url( 'plugins.php?plugin_status=paused' ) ), __( 'Go to the Plugins screen' ) ); + wp_admin_notice( + $message, + array( 'type' => 'error' ) + ); } /** @@ -2566,8 +2649,8 @@ ); } - printf( - '

%s
%s

%s

', + $message = sprintf( + '%s
%s

%s', sprintf( /* translators: %s: Name of deactivated plugin. */ __( '%s plugin deactivated during WordPress upgrade.' ), @@ -2577,6 +2660,7 @@ esc_url( admin_url( 'plugins.php?plugin_status=inactive' ) ), __( 'Go to the Plugins screen' ) ); + wp_admin_notice( $message, array( 'type' => 'warning' ) ); } // Empty the options.