Upgrade/Install: Introduce Plugin Dependencies.

Introduces a new "Requires Plugins" plugin header so that plugin developers can list the slugs of the plugins theirs depends on.

This will inform users of the requirements, and provide links to the WordPress.org Plugins Repository that they can click to install and activate the dependencies first.

Plugins whose requirements are not met cannot be installed or activated, and they will be deactivated automatically if their requirements become unmet.
Plugins that others rely on cannot be deactivated or deleted until their dependent plugins are deactivated or deleted.

In memory of Alex Mills and Alex King.
WordPress Remembers.

Props ahoereth, afragen, alanfuller, alexkingorg, amykamala, anonymized_10690803, apeatling, ashfame, atimmer, audrasjb, aristath, azaozz, batmoo, beaulebens, blobaugh, bobbingwide, boonebgorges, brianhenryie, chanthaboune, chrisdavidmiles, coolmann, costdev, courane01, danielbachhuber, davidperez, dd32, Denis-de-Bernardy, dingo_d, DJPaul, dougal, DrewAPicture, ethitter, filosofo, georgestephanis, giuseppemazzapica-1, goldenapples, griffinjt, hellofromTonya, husobj, ideag, jarednova, jbobich, jbrinley, jltallon, joedolson, johnciacia, johnjamesjacoby, joppuyo, jsmoriss, karmatosed, kebbet, knutsp, kraftbj, kraftner, kurtpayne, lkraav, logikal16, luisherranz, man4toman, markjaquith, matt, mbijon, megphillips91, mikeschinkel, mordauk, morehawes, mrwweb, mte90, mukesh27, mzaweb, nacin, norcross, nvwd, nwjames, obliviousharmony, ocean90, oglekler, paaljoachim, pauldewouters, pbaylies, pbiron, peterwilsoncc, Philipp15b, poena, pogidude, retlehs, rmccue, ryan, sabreuse, sc0ttkclark, scribu, sereedmedia, SergeyBiryukov, ShaneF, shidouhikari, soean, spacedmonkey, stephenh1988, swissspidy, taylorde, tazotodua, threadi, TimothyBlynJacobs, TJNowell, tollmanz, toscho, tropicalista, Viper007Bond, westi, whiteshadow, williamsba1, wpsmith, ZaneMatthew.
Fixes #22316.
Built from https://develop.svn.wordpress.org/trunk@57545


git-svn-id: http://core.svn.wordpress.org/trunk@57046 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
costdev 2024-02-06 23:46:14 +00:00
parent 15007f8b47
commit c60fc98b33
24 changed files with 2607 additions and 370 deletions

View File

@ -117,6 +117,7 @@ $core_actions_post = array(
'parse-media-shortcode',
'destroy-sessions',
'install-plugin',
'activate-plugin',
'update-plugin',
'crop-image',
'generate-password',
@ -169,6 +170,9 @@ add_action( 'wp_ajax_nopriv_generate-password', 'wp_ajax_nopriv_generate_passwor
add_action( 'wp_ajax_nopriv_heartbeat', 'wp_ajax_nopriv_heartbeat', 1 );
// Register Plugin Dependencies Ajax calls.
add_action( 'wp_ajax_check_plugin_dependencies', array( 'WP_Plugin_Dependencies', 'check_plugin_dependencies_during_ajax' ) );
$action = $_REQUEST['action'];
if ( is_user_logged_in() ) {

View File

@ -1501,6 +1501,22 @@ div.error {
background-color: #f0f6fc;
}
#plugin-information-footer .update-now:not(.button-disabled):before {
color: #d63638;
content: "\f463";
display: inline-block;
font: normal 20px/1 dashicons;
margin: -3px -2px 0 5px;
speak: never;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
vertical-align: middle;
}
#plugin-information-footer .notice {
margin-top: -5px;
}
.update-message p:before,
.updating-message p:before,
.updated-message p:before,
@ -1508,7 +1524,9 @@ div.error {
.button.updating-message:before,
.button.updated-message:before,
.button.installed:before,
.button.installing:before {
.button.installing:before,
.button.activating-message:before,
.button.activated-message:before {
display: inline-block;
font: normal 20px/1 'dashicons';
-webkit-font-smoothing: antialiased;
@ -1545,7 +1563,8 @@ div.error {
.updating-message p:before,
.import-php .updating-message:before,
.button.updating-message:before,
.button.installing:before {
.button.installing:before,
.button.activating-message:before {
color: #d63638;
content: "\f463";
}
@ -1555,6 +1574,7 @@ div.error {
.import-php .updating-message:before,
.button.updating-message:before,
.button.installing:before,
.button.activating-message:before,
.plugins .column-auto-updates .dashicons-update.spin,
.theme-overlay .theme-autoupdate .dashicons-update.spin {
animation: rotation 2s infinite linear;
@ -1565,6 +1585,7 @@ div.error {
.import-php .updating-message:before,
.button.updating-message:before,
.button.installing:before,
.button.activating-message:before,
.plugins .column-auto-updates .dashicons-update.spin,
.theme-overlay .theme-autoupdate .dashicons-update.spin {
animation: none;
@ -1578,7 +1599,8 @@ div.error {
/* Updated icon (check mark). */
.updated-message p:before,
.installed p:before,
.button.updated-message:before {
.button.updated-message:before,
.button.activated-message:before {
color: #68de7c;
content: "\f147";
}
@ -1663,19 +1685,37 @@ p.auto-update-status {
.button.updating-message:before,
.button.updated-message:before,
.button.installed:before,
.button.installing:before {
.button.installing:before,
.button.activated-message:before,
.button.activating-message:before {
margin: 3px -2px 0 5px;
}
.button-primary.updating-message:before {
#plugin-information-footer .button.installed:before,
#plugin-information-footer .button.installing:before,
#plugin-information-footer .button.updating-message:before,
#plugin-information-footer .button.updated-message:before,
#plugin-information-footer .button.activated-message:before,
#plugin-information-footer .button.activating-message:before {
margin: 9px -2px 0 5px;
}
#plugin-information-footer .button.update-now.updating-message:before {
margin: -3px -2px 0 5px;
}
.button-primary.updating-message:before,
.button-primary.activating-message:before {
color: #fff;
}
.button-primary.updated-message:before {
.button-primary.updated-message:before,
.button-primary.activated-message:before {
color: #9ec2e6;
}
.button.updated-message {
.button.updated-message,
.button.activated-message {
transition-property: border, background, color;
transition-duration: .05s;
transition-timing-function: ease-in-out;

File diff suppressed because one or more lines are too long

View File

@ -1500,6 +1500,22 @@ div.error {
background-color: #f0f6fc;
}
#plugin-information-footer .update-now:not(.button-disabled):before {
color: #d63638;
content: "\f463";
display: inline-block;
font: normal 20px/1 dashicons;
margin: -3px 5px 0 -2px;
speak: never;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
vertical-align: middle;
}
#plugin-information-footer .notice {
margin-top: -5px;
}
.update-message p:before,
.updating-message p:before,
.updated-message p:before,
@ -1507,7 +1523,9 @@ div.error {
.button.updating-message:before,
.button.updated-message:before,
.button.installed:before,
.button.installing:before {
.button.installing:before,
.button.activating-message:before,
.button.activated-message:before {
display: inline-block;
font: normal 20px/1 'dashicons';
-webkit-font-smoothing: antialiased;
@ -1544,7 +1562,8 @@ div.error {
.updating-message p:before,
.import-php .updating-message:before,
.button.updating-message:before,
.button.installing:before {
.button.installing:before,
.button.activating-message:before {
color: #d63638;
content: "\f463";
}
@ -1554,6 +1573,7 @@ div.error {
.import-php .updating-message:before,
.button.updating-message:before,
.button.installing:before,
.button.activating-message:before,
.plugins .column-auto-updates .dashicons-update.spin,
.theme-overlay .theme-autoupdate .dashicons-update.spin {
animation: rotation 2s infinite linear;
@ -1564,6 +1584,7 @@ div.error {
.import-php .updating-message:before,
.button.updating-message:before,
.button.installing:before,
.button.activating-message:before,
.plugins .column-auto-updates .dashicons-update.spin,
.theme-overlay .theme-autoupdate .dashicons-update.spin {
animation: none;
@ -1577,7 +1598,8 @@ div.error {
/* Updated icon (check mark). */
.updated-message p:before,
.installed p:before,
.button.updated-message:before {
.button.updated-message:before,
.button.activated-message:before {
color: #68de7c;
content: "\f147";
}
@ -1662,19 +1684,37 @@ p.auto-update-status {
.button.updating-message:before,
.button.updated-message:before,
.button.installed:before,
.button.installing:before {
.button.installing:before,
.button.activated-message:before,
.button.activating-message:before {
margin: 3px 5px 0 -2px;
}
.button-primary.updating-message:before {
#plugin-information-footer .button.installed:before,
#plugin-information-footer .button.installing:before,
#plugin-information-footer .button.updating-message:before,
#plugin-information-footer .button.updated-message:before,
#plugin-information-footer .button.activated-message:before,
#plugin-information-footer .button.activating-message:before {
margin: 9px 5px 0 -2px;
}
#plugin-information-footer .button.update-now.updating-message:before {
margin: -3px 5px 0 -2px;
}
.button-primary.updating-message:before,
.button-primary.activating-message:before {
color: #fff;
}
.button-primary.updated-message:before {
.button-primary.updated-message:before,
.button-primary.activated-message:before {
color: #9ec2e6;
}
.button.updated-message {
.button.updated-message,
.button.activated-message {
transition-property: border, background, color;
transition-duration: .05s;
transition-timing-function: ease-in-out;

File diff suppressed because one or more lines are too long

View File

@ -586,8 +586,7 @@ th.sorted.desc:hover .sorting-indicator.asc:before {
z-index: 1;
}
.check-column input:where(:not(:disabled)):hover,
.check-column:hover input:where(:not(:disabled)) {
.check-column .label-covers-full-cell:hover + input:not(:disabled) {
box-shadow: 0 0 0 1px #2271b1;
}
@ -1549,10 +1548,96 @@ div.action-links,
line-height: 1.3;
}
.plugin-card .name,
.plugin-card .desc {
margin-right: 148px; /* icon + margin */
margin-left: 128px; /* action links + margin */
margin-inline: 0;
}
.plugin-card .name, .plugin-card .desc > p {
margin-right: 148px;
}
@media (min-width: 1101px) {
.plugin-card .name, .plugin-card .desc > p {
margin-left: 128px;
}
}
@media (min-width: 481px) and (max-width: 781px) {
.plugin-card .name, .plugin-card .desc > p {
margin-left: 128px;
}
}
.plugin-card .column-description {
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.plugin-card .column-description > p {
margin-top: 0;
}
.plugin-card .column-description .authors {
order: 1;
}
.plugin-card .column-description .plugin-dependencies {
order: 2;
}
.plugin-card .column-description p:empty {
display: none;
}
.plugin-card .plugin-dependencies {
background-color: #e5f5fa;
border-right: 3px solid #72aee6;
margin-bottom: .5em;
padding: 15px;
}
.plugin-card .plugin-dependencies-explainer-text {
margin-block: 0;
}
.plugin-card .plugin-dependency {
align-items: center;
display: flex;
flex-wrap: wrap;
margin-top: .5em;
column-gap: 1%;
row-gap: .5em;
}
.plugin-card .plugin-dependency:nth-child(2),
.plugin-card .plugin-dependency:last-child {
margin-top: 1em;
}
.plugin-card .plugin-dependency-name {
flex-basis: 74%;
}
.plugin-card .plugin-dependency .more-details-link {
margin-right: auto;
}
.rtl .plugin-card .plugin-dependency .more-details-link {
margin-left: auto;
}
@media (max-width: 939px) {
.plugin-card .plugin-dependency-name {
flex-basis: 69%;
}
.plugin-card .plugin-dependency .more-details-link {
}
}
.plugins #the-list .required-by,
.plugins #the-list .requires {
margin-top: 1em;
}
.plugin-card .action-links {

File diff suppressed because one or more lines are too long

View File

@ -585,8 +585,7 @@ th.sorted.desc:hover .sorting-indicator.asc:before {
z-index: 1;
}
.check-column input:where(:not(:disabled)):hover,
.check-column:hover input:where(:not(:disabled)) {
.check-column .label-covers-full-cell:hover + input:not(:disabled) {
box-shadow: 0 0 0 1px #2271b1;
}
@ -1548,10 +1547,96 @@ div.action-links,
line-height: 1.3;
}
.plugin-card .name,
.plugin-card .desc {
margin-left: 148px; /* icon + margin */
margin-right: 128px; /* action links + margin */
margin-inline: 0;
}
.plugin-card .name, .plugin-card .desc > p {
margin-left: 148px;
}
@media (min-width: 1101px) {
.plugin-card .name, .plugin-card .desc > p {
margin-right: 128px;
}
}
@media (min-width: 481px) and (max-width: 781px) {
.plugin-card .name, .plugin-card .desc > p {
margin-right: 128px;
}
}
.plugin-card .column-description {
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.plugin-card .column-description > p {
margin-top: 0;
}
.plugin-card .column-description .authors {
order: 1;
}
.plugin-card .column-description .plugin-dependencies {
order: 2;
}
.plugin-card .column-description p:empty {
display: none;
}
.plugin-card .plugin-dependencies {
background-color: #e5f5fa;
border-left: 3px solid #72aee6;
margin-bottom: .5em;
padding: 15px;
}
.plugin-card .plugin-dependencies-explainer-text {
margin-block: 0;
}
.plugin-card .plugin-dependency {
align-items: center;
display: flex;
flex-wrap: wrap;
margin-top: .5em;
column-gap: 1%;
row-gap: .5em;
}
.plugin-card .plugin-dependency:nth-child(2),
.plugin-card .plugin-dependency:last-child {
margin-top: 1em;
}
.plugin-card .plugin-dependency-name {
flex-basis: 74%;
}
.plugin-card .plugin-dependency .more-details-link {
margin-left: auto;
}
.rtl .plugin-card .plugin-dependency .more-details-link {
margin-right: auto;
}
@media (max-width: 939px) {
.plugin-card .plugin-dependency-name {
flex-basis: 69%;
}
.plugin-card .plugin-dependency .more-details-link {
}
}
.plugins #the-list .required-by,
.plugins #the-list .requires {
margin-top: 1em;
}
.plugin-card .action-links {

File diff suppressed because one or more lines are too long

View File

@ -4578,6 +4578,56 @@ function wp_ajax_install_plugin() {
wp_send_json_success( $status );
}
/**
* Handles activating a plugin via AJAX.
*
* @since 6.5.0
*/
function wp_ajax_activate_plugin() {
check_ajax_referer( 'updates' );
if ( empty( $_POST['name'] ) || empty( $_POST['slug'] ) || empty( $_POST['plugin'] ) ) {
wp_send_json_error(
array(
'slug' => '',
'pluginName' => '',
'plugin' => '',
'errorCode' => 'no_plugin_specified',
'errorMessage' => __( 'No plugin specified.' ),
)
);
}
$status = array(
'activate' => 'plugin',
'slug' => wp_unslash( $_POST['slug'] ),
'pluginName' => wp_unslash( $_POST['name'] ),
'plugin' => wp_unslash( $_POST['plugin'] ),
);
if ( ! current_user_can( 'activate_plugin', $status['plugin'] ) ) {
$status['errorMessage'] = __( 'Sorry, you are not allowed to activate plugins on this site.' );
wp_send_json_error( $status );
}
if ( is_plugin_active( $status['plugin'] ) ) {
$status['errorMessage'] = sprintf(
/* translators: %s: Plugin name. */
__( '%s is already active.' ),
$status['pluginName']
);
}
$activated = activate_plugin( $status['plugin'] );
if ( is_wp_error( $activated ) ) {
$status['errorMessage'] = $activated->get_error_message();
wp_send_json_error( $status );
}
wp_send_json_success( $status );
}
/**
* Handles updating a plugin via AJAX.
*

View File

@ -155,6 +155,12 @@ class Plugin_Upgrader extends WP_Upgrader {
// Force refresh of plugin update information.
wp_clean_plugins_cache( $parsed_args['clear_update_cache'] );
$all_plugin_data = get_option( 'plugin_data', array() );
$plugin_file = $this->new_plugin_data['file'];
unset( $this->new_plugin_data['file'] );
$all_plugin_data[ $plugin_file ] = $this->new_plugin_data;
update_option( 'plugin_data', $all_plugin_data );
if ( $parsed_args['overwrite_package'] ) {
/**
* Fires when the upgrader has successfully overwritten a currently installed
@ -482,7 +488,16 @@ class Plugin_Upgrader extends WP_Upgrader {
foreach ( $files as $file ) {
$info = get_plugin_data( $file, false, false );
if ( ! empty( $info['Name'] ) ) {
$this->new_plugin_data = $info;
$basename = basename( $file );
$dirname = basename( dirname( $file ) );
if ( '.' === $dirname ) {
$plugin_file = $basename;
} else {
$plugin_file = "$dirname/$basename";
}
$this->new_plugin_data = ( $info );
$this->new_plugin_data['file'] = $plugin_file;
break;
}
}

View File

@ -525,6 +525,8 @@ class WP_Plugin_Install_List_Table extends WP_List_Table {
// Remove any HTML from the description.
$description = strip_tags( $plugin['short_description'] );
$description .= $this->get_dependencies_notice( $plugin );
/**
* Filters the plugin card description on the Add Plugins screen.
*
@ -555,102 +557,7 @@ class WP_Plugin_Install_List_Table extends WP_List_Table {
$action_links = array();
if ( current_user_can( 'install_plugins' ) || current_user_can( 'update_plugins' ) ) {
$status = install_plugin_install_status( $plugin );
switch ( $status['status'] ) {
case 'install':
if ( $status['url'] ) {
if ( $compatible_php && $compatible_wp ) {
$action_links[] = sprintf(
'<a class="install-now button" data-slug="%s" href="%s" aria-label="%s" data-name="%s">%s</a>',
esc_attr( $plugin['slug'] ),
esc_url( $status['url'] ),
/* translators: %s: Plugin name and version. */
esc_attr( sprintf( _x( 'Install %s now', 'plugin' ), $name ) ),
esc_attr( $name ),
__( 'Install Now' )
);
} else {
$action_links[] = sprintf(
'<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
_x( 'Cannot Install', 'plugin' )
);
}
}
break;
case 'update_available':
if ( $status['url'] ) {
if ( $compatible_php && $compatible_wp ) {
$action_links[] = sprintf(
'<a class="update-now button aria-button-if-js" data-plugin="%s" data-slug="%s" href="%s" aria-label="%s" data-name="%s">%s</a>',
esc_attr( $status['file'] ),
esc_attr( $plugin['slug'] ),
esc_url( $status['url'] ),
/* translators: %s: Plugin name and version. */
esc_attr( sprintf( _x( 'Update %s now', 'plugin' ), $name ) ),
esc_attr( $name ),
__( 'Update Now' )
);
} else {
$action_links[] = sprintf(
'<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
_x( 'Cannot Update', 'plugin' )
);
}
}
break;
case 'latest_installed':
case 'newer_installed':
if ( is_plugin_active( $status['file'] ) ) {
$action_links[] = sprintf(
'<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
_x( 'Active', 'plugin' )
);
} elseif ( current_user_can( 'activate_plugin', $status['file'] ) ) {
if ( $compatible_php && $compatible_wp ) {
$button_text = __( 'Activate' );
/* translators: %s: Plugin name. */
$button_label = _x( 'Activate %s', 'plugin' );
$activate_url = add_query_arg(
array(
'_wpnonce' => wp_create_nonce( 'activate-plugin_' . $status['file'] ),
'action' => 'activate',
'plugin' => $status['file'],
),
network_admin_url( 'plugins.php' )
);
if ( is_network_admin() ) {
$button_text = __( 'Network Activate' );
/* translators: %s: Plugin name. */
$button_label = _x( 'Network Activate %s', 'plugin' );
$activate_url = add_query_arg( array( 'networkwide' => 1 ), $activate_url );
}
$action_links[] = sprintf(
'<a href="%1$s" class="button activate-now" aria-label="%2$s">%3$s</a>',
esc_url( $activate_url ),
esc_attr( sprintf( $button_label, $plugin['name'] ) ),
$button_text
);
} else {
$action_links[] = sprintf(
'<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
_x( 'Cannot Activate', 'plugin' )
);
}
} else {
$action_links[] = sprintf(
'<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
_x( 'Installed', 'plugin' )
);
}
break;
}
}
$action_links[] = wp_get_plugin_action_button( $name, $plugin, $compatible_php, $compatible_wp );
$details_link = self_admin_url(
'plugin-install.php?tab=plugin-information&amp;plugin=' . $plugin['slug'] .
@ -828,4 +735,90 @@ class WP_Plugin_Install_List_Table extends WP_List_Table {
echo '</div></div>';
}
}
/**
* Returns a notice containing a list of dependencies required by the plugin.
*
* @since 6.5.0
*
* @param array $plugin_data An array of plugin data. See {@see plugins_api()}
* for the list of possible values.
* @return string A notice containing a list of dependencies required by the plugin,
* or an empty string if none is required.
*/
protected function get_dependencies_notice( $plugin_data ) {
if ( empty( $plugin_data['requires_plugins'] ) ) {
return '';
}
$no_name_markup = '<div class="plugin-dependency"><span class="plugin-dependency-name">%s</span></div>';
$has_name_markup = '<div class="plugin-dependency"><span class="plugin-dependency-name">%s</span> %s</div>';
$dependencies_list = '';
foreach ( $plugin_data['requires_plugins'] as $dependency ) {
$dependency_data = WP_Plugin_Dependencies::get_dependency_data( $dependency );
if (
false !== $dependency_data &&
! empty( $dependency_data['name'] ) &&
! empty( $dependency_data['slug'] ) &&
! empty( $dependency_data['version'] )
) {
$more_details_link = $this->get_more_details_link( $dependency_data['name'], $dependency_data['slug'] );
$dependencies_list .= sprintf( $has_name_markup, esc_html( $dependency_data['name'] ), $more_details_link );
continue;
}
$result = plugins_api( 'plugin_information', array( 'slug' => $dependency ) );
if ( ! empty( $result->name ) ) {
$more_details_link = $this->get_more_details_link( $result->name, $result->slug );
$dependencies_list .= sprintf( $has_name_markup, esc_html( $result->name ), $more_details_link );
continue;
}
$dependencies_list .= sprintf( $no_name_markup, esc_html( $dependency ) );
}
$dependencies_notice = sprintf(
'<div class="plugin-dependencies"><p class="plugin-dependencies-explainer-text">%s</p> %s</div>',
'<strong>' . __( 'Additional plugins are required' ) . '</strong>',
$dependencies_list
);
return $dependencies_notice;
}
/**
* Creates a 'More details' link for the plugin.
*
* @since 6.5.0
*
* @param string $name The plugin's name.
* @param string $slug The plugin's slug.
* @return string The 'More details' link for the plugin.
*/
protected function get_more_details_link( $name, $slug ) {
$url = add_query_arg(
array(
'tab' => 'plugin-information',
'plugin' => $slug,
'TB_iframe' => 'true',
'width' => '600',
'height' => '550',
),
network_admin_url( 'plugin-install.php' )
);
$more_details_link = sprintf(
'<a href="%1$s" class="more-details-link thickbox open-plugin-details-modal" aria-label="%2$s" data-title="%3$s">%4$s</a>',
esc_url( $url ),
/* translators: %s: Plugin name. */
sprintf( __( 'More information about %s' ), esc_html( $name ) ),
esc_attr( $name ),
__( 'More Details' )
);
return $more_details_link;
}
}

View File

@ -754,6 +754,11 @@ class WP_Plugins_List_Table extends WP_List_Table {
$compatible_php = is_php_version_compatible( $requires_php );
$compatible_wp = is_wp_version_compatible( $requires_wp );
$has_dependents = WP_Plugin_Dependencies::has_dependents( $plugin_file );
$has_active_dependents = WP_Plugin_Dependencies::has_active_dependents( $plugin_file );
$has_unmet_dependencies = WP_Plugin_Dependencies::has_unmet_dependencies( $plugin_file );
$has_circular_dependency = WP_Plugin_Dependencies::has_circular_dependency( $plugin_file );
if ( 'mustuse' === $context ) {
$is_active = true;
} elseif ( 'dropins' === $context ) {
@ -796,26 +801,53 @@ class WP_Plugins_List_Table extends WP_List_Table {
if ( $screen->in_admin( 'network' ) ) {
if ( $is_active ) {
if ( current_user_can( 'manage_network_plugins' ) ) {
$actions['deactivate'] = sprintf(
'<a href="%s" id="deactivate-%s" aria-label="%s">%s</a>',
wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'deactivate-plugin_' . $plugin_file ),
esc_attr( $plugin_id_attr ),
/* translators: %s: Plugin name. */
esc_attr( sprintf( _x( 'Network Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ),
__( 'Network Deactivate' )
);
if ( $has_active_dependents ) {
$actions['deactivate'] = __( 'Deactivate' ) .
'<span class="screen-reader-text">' .
__( 'You cannot deactivate this plugin as other plugins require it.' ) .
'</span>';
} else {
$deactivate_url = 'plugins.php?action=deactivate' .
'&amp;plugin=' . urlencode( $plugin_file ) .
'&amp;plugin_status=' . $context .
'&amp;paged=' . $page .
'&amp;s=' . $s;
$actions['deactivate'] = sprintf(
'<a href="%s" id="deactivate-%s" aria-label="%s">%s</a>',
wp_nonce_url( $deactivate_url, 'deactivate-plugin_' . $plugin_file ),
esc_attr( $plugin_id_attr ),
/* translators: %s: Plugin name. */
esc_attr( sprintf( _x( 'Network Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ),
__( 'Network Deactivate' )
);
}
}
} else {
if ( current_user_can( 'manage_network_plugins' ) ) {
if ( $compatible_php && $compatible_wp ) {
$actions['activate'] = sprintf(
'<a href="%s" id="activate-%s" class="edit" aria-label="%s">%s</a>',
wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'activate-plugin_' . $plugin_file ),
esc_attr( $plugin_id_attr ),
/* translators: %s: Plugin name. */
esc_attr( sprintf( _x( 'Network Activate %s', 'plugin' ), $plugin_data['Name'] ) ),
__( 'Network Activate' )
);
if ( $has_unmet_dependencies ) {
$actions['activate'] = __( 'Network Activate' ) .
'<span class="screen-reader-text">' .
__( 'You cannot activate this plugin as it has unmet requirements.' ) .
'</span>';
} else {
$activate_url = 'plugins.php?action=activate' .
'&amp;plugin=' . urlencode( $plugin_file ) .
'&amp;plugin_status=' . $context .
'&amp;paged=' . $page .
'&amp;s=' . $s;
$actions['activate'] = sprintf(
'<a href="%s" id="activate-%s" class="edit" aria-label="%s">%s</a>',
wp_nonce_url( $activate_url, 'activate-plugin_' . $plugin_file ),
esc_attr( $plugin_id_attr ),
/* translators: %s: Plugin name. */
esc_attr( sprintf( _x( 'Network Activate %s', 'plugin' ), $plugin_data['Name'] ) ),
__( 'Network Activate' )
);
}
} else {
$actions['activate'] = sprintf(
'<span>%s</span>',
@ -825,14 +857,27 @@ class WP_Plugins_List_Table extends WP_List_Table {
}
if ( current_user_can( 'delete_plugins' ) && ! is_plugin_active( $plugin_file ) ) {
$actions['delete'] = sprintf(
'<a href="%s" id="delete-%s" class="delete" aria-label="%s">%s</a>',
wp_nonce_url( 'plugins.php?action=delete-selected&amp;checked[]=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'bulk-plugins' ),
esc_attr( $plugin_id_attr ),
/* translators: %s: Plugin name. */
esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ),
__( 'Delete' )
);
if ( $has_dependents && ! $has_circular_dependency ) {
$actions['delete'] = __( 'Delete' ) .
'<span class="screen-reader-text">' .
__( 'You cannot delete this plugin as other plugins require it.' ) .
'</span>';
} else {
$delete_url = 'plugins.php?action=delete-selected' .
'&amp;checked[]=' . urlencode( $plugin_file ) .
'&amp;plugin_status=' . $context .
'&amp;paged=' . $page .
'&amp;s=' . $s;
$actions['delete'] = sprintf(
'<a href="%s" id="delete-%s" class="delete" aria-label="%s">%s</a>',
wp_nonce_url( $delete_url, 'bulk-plugins' ),
esc_attr( $plugin_id_attr ),
/* translators: %s: Plugin name. */
esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ),
__( 'Delete' )
);
}
}
}
} else {
@ -846,20 +891,39 @@ class WP_Plugins_List_Table extends WP_List_Table {
);
} elseif ( $is_active ) {
if ( current_user_can( 'deactivate_plugin', $plugin_file ) ) {
$actions['deactivate'] = sprintf(
'<a href="%s" id="deactivate-%s" aria-label="%s">%s</a>',
wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'deactivate-plugin_' . $plugin_file ),
esc_attr( $plugin_id_attr ),
/* translators: %s: Plugin name. */
esc_attr( sprintf( _x( 'Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ),
__( 'Deactivate' )
);
if ( $has_active_dependents ) {
$actions['deactivate'] = __( 'Deactivate' ) .
'<span class="screen-reader-text">' .
__( 'You cannot deactivate this plugin as other plugins depend on it.' ) .
'</span>';
} else {
$deactivate_url = 'plugins.php?action=deactivate' .
'&amp;plugin=' . urlencode( $plugin_file ) .
'&amp;plugin_status=' . $context .
'&amp;paged=' . $page .
'&amp;s=' . $s;
$actions['deactivate'] = sprintf(
'<a href="%s" id="deactivate-%s" aria-label="%s">%s</a>',
wp_nonce_url( $deactivate_url, 'deactivate-plugin_' . $plugin_file ),
esc_attr( $plugin_id_attr ),
/* translators: %s: Plugin name. */
esc_attr( sprintf( _x( 'Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ),
__( 'Deactivate' )
);
}
}
if ( current_user_can( 'resume_plugin', $plugin_file ) && is_plugin_paused( $plugin_file ) ) {
$resume_url = 'plugins.php?action=resume' .
'&amp;plugin=' . urlencode( $plugin_file ) .
'&amp;plugin_status=' . $context .
'&amp;paged=' . $page .
'&amp;s=' . $s;
$actions['resume'] = sprintf(
'<a href="%s" id="resume-%s" class="resume-link" aria-label="%s">%s</a>',
wp_nonce_url( 'plugins.php?action=resume&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'resume-plugin_' . $plugin_file ),
wp_nonce_url( $resume_url, 'resume-plugin_' . $plugin_file ),
esc_attr( $plugin_id_attr ),
/* translators: %s: Plugin name. */
esc_attr( sprintf( _x( 'Resume %s', 'plugin' ), $plugin_data['Name'] ) ),
@ -869,14 +933,27 @@ class WP_Plugins_List_Table extends WP_List_Table {
} else {
if ( current_user_can( 'activate_plugin', $plugin_file ) ) {
if ( $compatible_php && $compatible_wp ) {
$actions['activate'] = sprintf(
'<a href="%s" id="activate-%s" class="edit" aria-label="%s">%s</a>',
wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'activate-plugin_' . $plugin_file ),
esc_attr( $plugin_id_attr ),
/* translators: %s: Plugin name. */
esc_attr( sprintf( _x( 'Activate %s', 'plugin' ), $plugin_data['Name'] ) ),
__( 'Activate' )
);
if ( $has_unmet_dependencies ) {
$actions['activate'] = __( 'Activate' ) .
'<span class="screen-reader-text">' .
__( 'You cannot activate this plugin as it has unmet requirements.' ) .
'</span>';
} else {
$activate_url = 'plugins.php?action=activate' .
'&amp;plugin=' . urlencode( $plugin_file ) .
'&amp;plugin_status=' . $context .
'&amp;paged=' . $page .
'&amp;s=' . $s;
$actions['activate'] = sprintf(
'<a href="%s" id="activate-%s" class="edit" aria-label="%s">%s</a>',
wp_nonce_url( $activate_url, 'activate-plugin_' . $plugin_file ),
esc_attr( $plugin_id_attr ),
/* translators: %s: Plugin name. */
esc_attr( sprintf( _x( 'Activate %s', 'plugin' ), $plugin_data['Name'] ) ),
__( 'Activate' )
);
}
} else {
$actions['activate'] = sprintf(
'<span>%s</span>',
@ -886,14 +963,27 @@ class WP_Plugins_List_Table extends WP_List_Table {
}
if ( ! is_multisite() && current_user_can( 'delete_plugins' ) ) {
$actions['delete'] = sprintf(
'<a href="%s" id="delete-%s" class="delete" aria-label="%s">%s</a>',
wp_nonce_url( 'plugins.php?action=delete-selected&amp;checked[]=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'bulk-plugins' ),
esc_attr( $plugin_id_attr ),
/* translators: %s: Plugin name. */
esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ),
__( 'Delete' )
);
if ( $has_dependents && ! $has_circular_dependency ) {
$actions['delete'] = __( 'Delete' ) .
'<span class="screen-reader-text">' .
__( 'You cannot delete this plugin as other plugins require it.' ) .
'</span>';
} else {
$delete_url = 'plugins.php?action=delete-selected' .
'&amp;checked[]=' . urlencode( $plugin_file ) .
'&amp;plugin_status=' . $context .
'&amp;paged=' . $page .
'&amp;s=' . $s;
$actions['delete'] = sprintf(
'<a href="%s" id="delete-%s" class="delete" aria-label="%s">%s</a>',
wp_nonce_url( $delete_url, 'bulk-plugins' ),
esc_attr( $plugin_id_attr ),
/* translators: %s: Plugin name. */
esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ),
__( 'Delete' )
);
}
}
} // End if $is_active.
} // End if $screen->in_admin( 'network' ).
@ -988,17 +1078,28 @@ class WP_Plugins_List_Table extends WP_List_Table {
$class = $is_active ? 'active' : 'inactive';
$checkbox_id = 'checkbox_' . md5( $plugin_file );
$disabled = '';
if ( $restrict_network_active || $restrict_network_only || in_array( $status, array( 'mustuse', 'dropins' ), true ) || ! $compatible_php ) {
if ( $has_active_dependents || $has_unmet_dependencies ) {
$disabled = 'disabled';
}
if (
$restrict_network_active ||
$restrict_network_only ||
in_array( $status, array( 'mustuse', 'dropins' ), true ) ||
! $compatible_php
) {
$checkbox = '';
} else {
$checkbox = sprintf(
'<input type="checkbox" name="checked[]" value="%1$s" id="%2$s" />' .
'<label for="%2$s"><span class="screen-reader-text">%3$s</span></label>',
esc_attr( $plugin_file ),
'<label class="label-covers-full-cell" for="%1$s">' .
'<span class="screen-reader-text">%2$s</span></label>' .
'<input type="checkbox" name="checked[]" value="%3$s" id="%1$s" ' . $disabled . '/>',
$checkbox_id,
/* translators: Hidden accessibility text. %s: Plugin name. */
sprintf( __( 'Select %s' ), $plugin_data['Name'] )
sprintf( __( 'Select %s' ), $plugin_data['Name'] ),
esc_attr( $plugin_file )
);
}
@ -1007,8 +1108,11 @@ class WP_Plugins_List_Table extends WP_List_Table {
$plugin_name = $plugin_data['Name'];
}
if ( ! empty( $totals['upgrade'] ) && ! empty( $plugin_data['update'] )
|| ! $compatible_php || ! $compatible_wp
if (
! empty( $totals['upgrade'] ) &&
! empty( $plugin_data['update'] ) ||
! $compatible_php ||
! $compatible_wp
) {
$class .= ' update';
}
@ -1057,15 +1161,19 @@ class WP_Plugins_List_Table extends WP_List_Table {
<div class='$class second plugin-version-author-uri'>";
$plugin_meta = array();
if ( ! empty( $plugin_data['Version'] ) ) {
/* translators: %s: Plugin version number. */
$plugin_meta[] = sprintf( __( 'Version %s' ), $plugin_data['Version'] );
}
if ( ! empty( $plugin_data['Author'] ) ) {
$author = $plugin_data['Author'];
if ( ! empty( $plugin_data['AuthorURI'] ) ) {
$author = '<a href="' . $plugin_data['AuthorURI'] . '">' . $plugin_data['Author'] . '</a>';
}
/* translators: %s: Plugin author name. */
$plugin_meta[] = sprintf( __( 'By %s' ), $author );
}
@ -1149,6 +1257,24 @@ class WP_Plugins_List_Table extends WP_List_Table {
echo '</div>';
if ( $has_dependents ) {
$this->add_dependents_to_dependency_plugin_row( $plugin_file );
}
if ( WP_Plugin_Dependencies::has_dependencies( $plugin_file ) ) {
$this->add_dependencies_to_dependent_plugin_row( $plugin_file );
}
/**
* Fires after plugin row meta.
*
* @since 6.5.0
*
* @param string $plugin_file Refer to {@see 'plugin_row_meta'} filter.
* @param array $plugin_data Refer to {@see 'plugin_row_meta'} filter.
*/
do_action( 'after_plugin_row_meta', $plugin_file, $plugin_data );
if ( $paused ) {
$notice_text = __( 'This plugin failed to load properly and is paused during recovery mode.' );
@ -1391,4 +1517,111 @@ class WP_Plugins_List_Table extends WP_List_Table {
protected function get_primary_column_name() {
return 'name';
}
/**
* Prints a list of other plugins that depend on the plugin.
*
* @since 6.5.0
*
* @param string $dependency The dependency's filepath, relative to the plugins directory.
*/
protected function add_dependents_to_dependency_plugin_row( $dependency ) {
$dependent_names = WP_Plugin_Dependencies::get_dependent_names( $dependency );
if ( empty( $dependent_names ) ) {
return;
}
$dependency_note = __( 'Note: this plugin cannot be deactivated or deleted until the plugins that require it are deactivated or deleted.' );
printf(
'<div class="required-by"><p><strong>%1$s</strong> %2$s</p><p>%3$s</p></div>',
__( 'Required by:' ),
esc_html( implode( ' | ', $dependent_names ) ),
$dependency_note
);
}
/**
* Prints a list of other plugins that the plugin depends on.
*
* @since 6.5.0
*
* @param string $dependent The dependent plugin's filepath, relative to the plugins directory.
*/
protected function add_dependencies_to_dependent_plugin_row( $dependent ) {
$dependency_names = WP_Plugin_Dependencies::get_dependency_names( $dependent );
if ( array() === $dependency_names ) {
return;
}
$links = array();
foreach ( $dependency_names as $slug => $name ) {
$links[] = $this->get_dependency_view_details_link( $name, $slug );
}
$dependency_note = __( 'Note: this plugin cannot be activated until the plugins that are required by it are activated.' );
printf(
'<div class="requires"><p><strong>%1$s</strong> %2$s</p><p>%3$s</p></div>',
__( 'Requires:' ),
implode( ' | ', $links ),
$dependency_note
);
}
/**
* Returns a 'View details' like link for a dependency.
*
* @since 6.5.0
*
* @param string $name The dependency's name.
* @param string $slug The dependency's slug.
* @return string A 'View details' link for the dependency.
*/
protected function get_dependency_view_details_link( $name, $slug ) {
$dependency_data = WP_Plugin_Dependencies::get_dependency_data( $slug );
if ( false === $dependency_data
|| $name === $slug
|| $name !== $dependency_data['name']
|| empty( $dependency_data['version'] )
) {
return $name;
}
return $this->get_view_details_link( $name, $slug );
}
/**
* Returns a 'View details' link for the plugin.
*
* @since 6.5.0
*
* @param string $name The plugin's name.
* @param string $slug The plugin's slug.
* @return string A 'View details' link for the plugin.
*/
protected function get_view_details_link( $name, $slug ) {
$url = add_query_arg(
array(
'tab' => 'plugin-information',
'plugin' => $slug,
'TB_iframe' => 'true',
'width' => '600',
'height' => '550',
),
network_admin_url( 'plugin-install.php' )
);
$name_attr = esc_attr( $name );
return sprintf(
"<a href='%s' class='thickbox open-plugin-details-modal' aria-label='%s' data-title='%s'>%s</a>",
esc_url( $url ),
/* translators: %s: Plugin name. */
sprintf( __( 'More information about %s' ), $name_attr ),
$name_attr,
esc_html( $name )
);
}
}

View File

@ -884,43 +884,176 @@ function install_plugin_information() {
echo "</div>\n"; // #plugin-information-scrollable
echo "<div id='$tab-footer'>\n";
if ( ! empty( $api->download_link ) && ( current_user_can( 'install_plugins' ) || current_user_can( 'update_plugins' ) ) ) {
$status = install_plugin_install_status( $api );
switch ( $status['status'] ) {
case 'install':
if ( $status['url'] ) {
if ( $compatible_php && $compatible_wp ) {
echo '<a data-slug="' . esc_attr( $api->slug ) . '" id="plugin_install_from_iframe" class="button button-primary right" href="' . $status['url'] . '" target="_parent">' . __( 'Install Now' ) . '</a>';
} else {
printf(
'<button type="button" class="button button-primary button-disabled right" disabled="disabled">%s</button>',
_x( 'Cannot Install', 'plugin' )
);
}
}
break;
case 'update_available':
if ( $status['url'] ) {
if ( $compatible_php ) {
echo '<a data-slug="' . esc_attr( $api->slug ) . '" data-plugin="' . esc_attr( $status['file'] ) . '" id="plugin_update_from_iframe" class="button button-primary right" href="' . $status['url'] . '" target="_parent">' . __( 'Install Update Now' ) . '</a>';
} else {
printf(
'<button type="button" class="button button-primary button-disabled right" disabled="disabled">%s</button>',
_x( 'Cannot Update', 'plugin' )
);
}
}
break;
case 'newer_installed':
/* translators: %s: Plugin version. */
echo '<a class="button button-primary right disabled">' . sprintf( __( 'Newer Version (%s) Installed' ), esc_html( $status['version'] ) ) . '</a>';
break;
case 'latest_installed':
echo '<a class="button button-primary right disabled">' . __( 'Latest Version Installed' ) . '</a>';
break;
$button = wp_get_plugin_action_button( $api->name, $api, $compatible_php, $compatible_wp );
$button = str_replace( 'class="', 'class="right ', $button );
if ( ! str_contains( $button, __( 'Activate' ) ) ) {
$button = str_replace( 'class="', 'id="plugin_install_from_iframe" class="', $button );
}
echo wp_kses_post( $button );
}
echo "</div>\n";
wp_print_request_filesystem_credentials_modal();
wp_print_admin_notice_templates();
iframe_footer();
exit;
}
/**
* Gets the markup for the plugin install action button.
*
* @since 6.5.0
*
* @param string $name Plugin name.
* @param array|object $data {
* An array or object of plugin data. Can be retrieved from the API.
*
* @type string $slug The plugin slug.
* @type string[] $requires_plugins An array of plugin dependency slugs.
* @type string $version The plugin's version string. Used when getting the install status.
* }
* @param bool $compatible_php The result of a PHP compatibility check.
* @param bool $compatible_wp The result of a WP compatibility check.
* @return string $button The markup for the dependency row button.
*/
function wp_get_plugin_action_button( $name, $data, $compatible_php, $compatible_wp ) {
$button = '';
$data = (object) $data;
$status = install_plugin_install_status( $data );
$requires_plugins = $data->requires_plugins ?? array();
// Determine the status of plugin dependencies.
$installed_plugins = get_plugins();
$active_plugins = get_option( 'active_plugins' );
$plugin_dependencies_count = count( $requires_plugins );
$installed_plugin_dependencies_count = 0;
$active_plugin_dependencies_count = 0;
foreach ( $requires_plugins as $dependency ) {
foreach ( array_keys( $installed_plugins ) as $installed_plugin_file ) {
if ( str_contains( $installed_plugin_file, '/' ) && explode( '/', $installed_plugin_file )[0] === $dependency ) {
++$installed_plugin_dependencies_count;
}
}
foreach ( $active_plugins as $active_plugin_file ) {
if ( str_contains( $active_plugin_file, '/' ) && explode( '/', $active_plugin_file )[0] === $dependency ) {
++$active_plugin_dependencies_count;
}
}
}
$all_plugin_dependencies_installed = $installed_plugin_dependencies_count === $plugin_dependencies_count;
$all_plugin_dependencies_active = $active_plugin_dependencies_count === $plugin_dependencies_count;
sprintf(
'<a class="install-now button" data-slug="%s" href="%s" aria-label="%s" data-name="%s">%s</a>',
esc_attr( $data->slug ),
esc_url( $status['url'] ),
/* translators: %s: Plugin name and version. */
esc_attr( sprintf( _x( 'Install %s now', 'plugin' ), $name ) ),
esc_attr( $name ),
__( 'Install Now' )
);
if ( current_user_can( 'install_plugins' ) || current_user_can( 'update_plugins' ) ) {
switch ( $status['status'] ) {
case 'install':
if ( $status['url'] ) {
if ( $compatible_php && $compatible_wp && $all_plugin_dependencies_installed && ! empty( $data->download_link ) ) {
$button = sprintf(
'<a class="install-now button" data-slug="%s" href="%s" aria-label="%s" data-name="%s">%s</a>',
esc_attr( $data->slug ),
esc_url( $status['url'] ),
/* translators: %s: Plugin name and version. */
esc_attr( sprintf( _x( 'Install %s now', 'plugin' ), $name ) ),
esc_attr( $name ),
__( 'Install Now' )
);
} else {
$button = sprintf(
'<button type="button" class="install-now button button-disabled" disabled="disabled">%s</button>',
_x( 'Install Now', 'plugin' )
);
}
}
break;
case 'update_available':
if ( $status['url'] ) {
if ( $compatible_php && $compatible_wp ) {
$button = sprintf(
'<a class="update-now button aria-button-if-js" data-plugin="%s" data-slug="%s" href="%s" aria-label="%s" data-name="%s">%s</a>',
esc_attr( $status['file'] ),
esc_attr( $data->slug ),
esc_url( $status['url'] ),
/* translators: %s: Plugin name and version. */
esc_attr( sprintf( _x( 'Update %s now', 'plugin' ), $name ) ),
esc_attr( $name ),
__( 'Update Now' )
);
} else {
$button = sprintf(
'<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
_x( 'Update Now', 'plugin' )
);
}
}
break;
case 'latest_installed':
case 'newer_installed':
if ( is_plugin_active( $status['file'] ) ) {
$button = sprintf(
'<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
_x( 'Active', 'plugin' )
);
} elseif ( current_user_can( 'activate_plugin', $status['file'] ) ) {
if ( $compatible_php && $compatible_wp && $all_plugin_dependencies_active ) {
$button_text = __( 'Activate' );
/* translators: %s: Plugin name. */
$button_label = _x( 'Activate %s', 'plugin' );
$activate_url = add_query_arg(
array(
'_wpnonce' => wp_create_nonce( 'activate-plugin_' . $status['file'] ),
'action' => 'activate',
'plugin' => $status['file'],
),
network_admin_url( 'plugins.php' )
);
if ( is_network_admin() ) {
$button_text = __( 'Network Activate' );
/* translators: %s: Plugin name. */
$button_label = _x( 'Network Activate %s', 'plugin' );
$activate_url = add_query_arg( array( 'networkwide' => 1 ), $activate_url );
}
$button = sprintf(
'<a href="%1$s" data-name="%2$s" data-slug="%3$s" data-plugin="%4$s" class="button button-primary activate-now" aria-label="%5$s">%6$s</a>',
esc_url( $activate_url ),
esc_attr( $name ),
esc_attr( $data->slug ),
esc_attr( $status['file'] ),
esc_attr( sprintf( $button_label, $name ) ),
$button_text
);
} else {
$button = sprintf(
'<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
is_network_admin() ? _x( 'Network Activate %s', 'plugin' ) : _x( 'Activate', 'plugin' )
);
}
} else {
$button = sprintf(
'<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
_x( 'Installed', 'plugin' )
);
}
break;
}
return $button;
}
}

View File

@ -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' );
@ -330,6 +333,7 @@ function get_plugins( $plugin_folder = '' ) {
return $wp_plugins;
}
$new_plugin_data = array();
foreach ( $plugin_files as $plugin_file ) {
if ( ! is_readable( "$plugin_root/$plugin_file" ) ) {
continue;
@ -342,6 +346,13 @@ function get_plugins( $plugin_folder = '' ) {
continue;
}
$new_plugin_file = str_replace(
trailingslashit( WP_PLUGIN_DIR ),
'',
"$plugin_root/$plugin_file"
);
$new_plugin_data[ $new_plugin_file ] = $plugin_data;
$wp_plugins[ plugin_basename( $plugin_file ) ] = $plugin_data;
}
@ -349,6 +360,7 @@ function get_plugins( $plugin_folder = '' ) {
$cache_plugins[ $plugin_folder ] = $wp_plugins;
wp_cache_set( 'plugins', $cache_plugins, 'plugins' );
update_option( 'plugin_data', $new_plugin_data );
return $wp_plugins;
}
@ -951,6 +963,7 @@ function delete_plugins( $plugins, $deprecated = '' ) {
$plugins_dir = trailingslashit( $plugins_dir );
$plugin_translations = wp_get_installed_translations( 'plugins' );
$all_plugin_data = get_option( 'plugin_data', array() );
$errors = array();
@ -995,6 +1008,7 @@ function delete_plugins( $plugins, $deprecated = '' ) {
$errors[] = $plugin_file;
continue;
}
unset( $all_plugin_data[ $plugin_file ] );
$plugin_slug = dirname( $plugin_file );
@ -1043,6 +1057,7 @@ function delete_plugins( $plugins, $deprecated = '' ) {
return new WP_Error( 'could_not_remove_plugin', sprintf( $message, implode( ', ', $errors ) ) );
}
update_option( 'plugin_data', $all_plugin_data );
return true;
}
@ -1114,13 +1129,14 @@ function validate_plugin( $plugin ) {
/**
* 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.
@ -1129,8 +1145,9 @@ function validate_plugin_requirements( $plugin ) {
$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'] );
@ -1185,6 +1202,31 @@ function validate_plugin_requirements( $plugin ) {
);
}
if ( WP_Plugin_Dependencies::has_unmet_dependencies( $plugin ) ) {
$dependencies = WP_Plugin_Dependencies::get_dependencies( $plugin );
$unmet_dependencies = array();
foreach ( $dependencies as $dependency ) {
$dependency_file = WP_Plugin_Dependencies::get_dependency_filepath( $dependency );
if ( false === $dependency_file ) {
$unmet_dependencies['not_installed'][] = $dependency;
} elseif ( is_plugin_inactive( $dependency_file ) ) {
$unmet_dependencies['inactive'][] = $dependency;
}
}
return new WP_Error(
'plugin_missing_dependencies',
'<p>' . sprintf(
/* translators: %s: Plugin name. */
_x( '<strong>Error:</strong> %s requires plugins that are not installed or activated.', 'plugin' ),
$plugin_headers['Name']
) . '</p>',
$unmet_dependencies
);
}
return true;
}

View File

@ -413,6 +413,31 @@
}
};
/**
* Sends a message from a modal to the main screen to update buttons in plugin cards.
*
* @since 6.5.0
*
* @param {Object} data An object of data to use for the button.
* @param {string} data.slug The plugin's slug.
* @param {string} data.text The text to use for the button.
* @param {string} data.ariaLabel The value for the button's aria-label attribute. An empty string removes the attribute.
* @param {string=} data.status Optional. An identifier for the status.
* @param {string=} data.removeClasses Optional. A space-separated list of classes to remove from the button.
* @param {string=} data.addClasses Optional. A space-separated list of classes to add to the button.
* @param {string=} data.href Optional. The button's URL.
* @param {string=} data.pluginName Optional. The plugin's name.
* @param {string=} data.plugin Optional. The plugin file, relative to the plugins directory.
*/
wp.updates.setCardButtonStatus = function( data ) {
var target = window.parent === window ? null : window.parent;
$.support.postMessage = !! window.postMessage;
if ( false !== $.support.postMessage && null !== target && -1 === window.parent.location.pathname.indexOf( 'index.php' ) ) {
target.postMessage( JSON.stringify( data ), window.location.origin );
}
};
/**
* Decrements the update counts throughout the various menus.
*
@ -452,7 +477,8 @@
*/
wp.updates.updatePlugin = function( args ) {
var $updateRow, $card, $message, message,
$adminBarUpdates = $( '#wp-admin-bar-updates' );
$adminBarUpdates = $( '#wp-admin-bar-updates' ),
buttonText = __( 'Updating...' );
args = _.extend( {
success: wp.updates.updatePluginSuccess,
@ -468,7 +494,7 @@
$updateRow.find( '.plugin-title strong' ).text()
);
} else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
$card = $( '.plugin-card-' + args.slug );
$card = $( '.plugin-card-' + args.slug + ', #plugin-information-footer' );
$message = $card.find( '.update-now' ).addClass( 'updating-message' );
message = sprintf(
/* translators: %s: Plugin name and version. */
@ -488,10 +514,22 @@
$message
.attr( 'aria-label', message )
.text( __( 'Updating...' ) );
.text( buttonText );
$document.trigger( 'wp-plugin-updating', args );
if ( 'plugin-information-footer' === $card.attr('id' ) ) {
wp.updates.setCardButtonStatus(
{
status: 'updating-plugin',
slug: args.slug,
addClasses: 'updating-message',
text: buttonText,
ariaLabel: message
}
);
}
return wp.updates.ajax( 'update-plugin', args );
};
@ -511,7 +549,13 @@
*/
wp.updates.updatePluginSuccess = function( response ) {
var $pluginRow, $updateMessage, newText,
$adminBarUpdates = $( '#wp-admin-bar-updates' );
$adminBarUpdates = $( '#wp-admin-bar-updates' ),
buttonText = _x( 'Updated!', 'plugin' ),
ariaLabel = sprintf(
/* translators: %s: Plugin name and version. */
_x( '%s updated!', 'plugin' ),
response.pluginName
);
if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
$pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' )
@ -528,7 +572,7 @@
// Clear the "time to next auto-update" text.
$pluginRow.find( '.auto-update-time' ).empty();
} else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
$updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' )
$updateMessage = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.update-now' )
.removeClass( 'updating-message' )
.addClass( 'button-disabled updated-message' );
}
@ -536,19 +580,25 @@
$adminBarUpdates.removeClass( 'spin' );
$updateMessage
.attr(
'aria-label',
sprintf(
/* translators: %s: Plugin name and version. */
_x( '%s updated!', 'plugin' ),
response.pluginName
)
)
.text( _x( 'Updated!', 'plugin' ) );
.attr( 'aria-label', ariaLabel )
.text( buttonText );
wp.a11y.speak( __( 'Update completed successfully.' ) );
wp.updates.decrementCount( 'plugin' );
if ( 'plugin_install_from_iframe' !== $updateMessage.attr( 'id' ) ) {
wp.updates.decrementCount( 'plugin' );
} else {
wp.updates.setCardButtonStatus(
{
status: 'updated-plugin',
slug: response.slug,
removeClasses: 'updating-message',
addClasses: 'button-disabled updated-message',
text: buttonText,
ariaLabel: ariaLabel
}
);
}
$document.trigger( 'wp-plugin-update-success', response );
};
@ -567,7 +617,7 @@
* @param {string} response.errorMessage The error that occurred.
*/
wp.updates.updatePluginError = function( response ) {
var $pluginRow, $card, $message, errorMessage,
var $pluginRow, $card, $message, errorMessage, buttonText, ariaLabel,
$adminBarUpdates = $( '#wp-admin-bar-updates' );
if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
@ -608,28 +658,32 @@
$message.find( 'p' ).removeAttr( 'aria-label' );
}
} else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
$card = $( '.plugin-card-' + response.slug )
.addClass( 'plugin-card-update-failed' )
buttonText = __( 'Update failed.' );
$card = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' )
.append( wp.updates.adminNotice( {
className: 'update-message notice-error notice-alt is-dismissible',
message: errorMessage
} ) );
if ( $card.hasClass( 'plugin-card-' + response.slug ) ) {
$card.addClass( 'plugin-card-update-failed' );
}
$card.find( '.update-now' )
.text( __( 'Update failed.' ) )
.text( buttonText )
.removeClass( 'updating-message' );
if ( response.pluginName ) {
$card.find( '.update-now' )
.attr(
'aria-label',
sprintf(
/* translators: %s: Plugin name and version. */
_x( '%s update failed.', 'plugin' ),
response.pluginName
)
);
ariaLabel = sprintf(
/* translators: %s: Plugin name and version. */
_x( '%s update failed.', 'plugin' ),
response.pluginName
);
$card.find( '.update-now' ).attr( 'aria-label', ariaLabel );
} else {
ariaLabel = '';
$card.find( '.update-now' ).removeAttr( 'aria-label' );
}
@ -652,6 +706,18 @@
wp.a11y.speak( errorMessage, 'assertive' );
if ( 'plugin-information-footer' === $card.attr('id' ) ) {
wp.updates.setCardButtonStatus(
{
status: 'plugin-update-failed',
slug: response.slug,
removeClasses: 'updating-message',
text: buttonText,
ariaLabel: ariaLabel
}
);
}
$document.trigger( 'wp-plugin-update-error', response );
};
@ -668,8 +734,10 @@
* decorated with an abort() method.
*/
wp.updates.installPlugin = function( args ) {
var $card = $( '.plugin-card-' + args.slug ),
$message = $card.find( '.install-now' );
var $card = $( '.plugin-card-' + args.slug + ', #plugin-information-footer' ),
$message = $card.find( '.install-now' ),
buttonText = __( 'Installing...' ),
ariaLabel;
args = _.extend( {
success: wp.updates.installPluginSuccess,
@ -684,17 +752,16 @@
$message.data( 'originaltext', $message.html() );
}
ariaLabel = sprintf(
/* translators: %s: Plugin name and version. */
_x( 'Installing %s...', 'plugin' ),
$message.data( 'name' )
);
$message
.addClass( 'updating-message' )
.attr(
'aria-label',
sprintf(
/* translators: %s: Plugin name and version. */
_x( 'Installing %s...', 'plugin' ),
$message.data( 'name' )
)
)
.text( __( 'Installing...' ) );
.attr( 'aria-label', ariaLabel )
.text( buttonText );
wp.a11y.speak( __( 'Installing... please wait.' ) );
@ -703,6 +770,18 @@
$document.trigger( 'wp-plugin-installing', args );
if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
wp.updates.setCardButtonStatus(
{
status: 'installing-plugin',
slug: args.slug,
addClasses: 'updating-message',
text: buttonText,
ariaLabel: ariaLabel
}
);
}
return wp.updates.ajax( 'install-plugin', args );
};
@ -717,20 +796,19 @@
* @param {string} response.activateUrl URL to activate the just installed plugin.
*/
wp.updates.installPluginSuccess = function( response ) {
var $message = $( '.plugin-card-' + response.slug ).find( '.install-now' );
var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.install-now' ),
buttonText = _x( 'Installed!', 'plugin' ),
ariaLabel = sprintf(
/* translators: %s: Plugin name and version. */
_x( '%s installed!', 'plugin' ),
response.pluginName
);
$message
.removeClass( 'updating-message' )
.addClass( 'updated-message installed button-disabled' )
.attr(
'aria-label',
sprintf(
/* translators: %s: Plugin name and version. */
_x( '%s installed!', 'plugin' ),
response.pluginName
)
)
.text( _x( 'Installed!', 'plugin' ) );
.attr( 'aria-label', ariaLabel )
.text( buttonText );
wp.a11y.speak( __( 'Installation completed successfully.' ) );
@ -738,37 +816,24 @@
if ( response.activateUrl ) {
setTimeout( function() {
// Transform the 'Install' button into an 'Activate' button.
$message.removeClass( 'install-now installed button-disabled updated-message' )
.addClass( 'activate-now button-primary' )
.attr( 'href', response.activateUrl );
if ( 'plugins-network' === pagenow ) {
$message
.attr(
'aria-label',
sprintf(
/* translators: %s: Plugin name. */
_x( 'Network Activate %s', 'plugin' ),
response.pluginName
)
)
.text( __( 'Network Activate' ) );
} else {
$message
.attr(
'aria-label',
sprintf(
/* translators: %s: Plugin name. */
_x( 'Activate %s', 'plugin' ),
response.pluginName
)
)
.text( __( 'Activate' ) );
}
wp.updates.checkPluginDependencies( {
slug: response.slug
} );
}, 1000 );
}
if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
wp.updates.setCardButtonStatus(
{
status: 'installed-plugin',
slug: response.slug,
removeClasses: 'updating-message',
addClasses: 'updated-message installed button-disabled',
text: buttonText,
ariaLabel: ariaLabel
}
);
}
};
/**
@ -783,8 +848,14 @@
* @param {string} response.errorMessage The error that occurred.
*/
wp.updates.installPluginError = function( response ) {
var $card = $( '.plugin-card-' + response.slug ),
var $card = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ),
$button = $card.find( '.install-now' ),
buttonText = __( 'Installation failed.' ),
ariaLabel = sprintf(
/* translators: %s: Plugin name and version. */
_x( '%s installation failed', 'plugin' ),
$button.data( 'name' )
),
errorMessage;
if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
@ -817,21 +888,334 @@
$button
.removeClass( 'updating-message' ).addClass( 'button-disabled' )
.attr(
'aria-label',
sprintf(
/* translators: %s: Plugin name and version. */
_x( '%s installation failed', 'plugin' ),
$button.data( 'name' )
)
)
.text( __( 'Installation failed.' ) );
.attr( 'aria-label', ariaLabel )
.text( buttonText );
wp.a11y.speak( errorMessage, 'assertive' );
wp.updates.setCardButtonStatus(
{
status: 'plugin-install-failed',
slug: response.slug,
removeClasses: 'updating-message',
addClasses: 'button-disabled',
text: buttonText,
ariaLabel: ariaLabel
}
);
$document.trigger( 'wp-plugin-install-error', response );
};
/**
* Sends an Ajax request to the server to check a plugin's dependencies.
*
* @since 6.5.0
*
* @param {Object} args Arguments.
* @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository.
* @param {checkPluginDependenciesSuccess=} args.success Optional. Success callback. Default: wp.updates.checkPluginDependenciesSuccess
* @param {checkPluginDependenciesError=} args.error Optional. Error callback. Default: wp.updates.checkPluginDependenciesError
* @return {$.promise} A jQuery promise that represents the request,
* decorated with an abort() method.
*/
wp.updates.checkPluginDependencies = function( args ) {
args = _.extend( {
success: wp.updates.checkPluginDependenciesSuccess,
error: wp.updates.checkPluginDependenciesError
}, args );
wp.a11y.speak( __( 'Checking plugin dependencies... please wait.' ) );
$document.trigger( 'wp-checking-plugin-dependencies', args );
return wp.updates.ajax( 'check_plugin_dependencies', args );
};
/**
* Updates the UI appropriately after a successful plugin dependencies check.
*
* @since 6.5.0
*
* @param {Object} response Response from the server.
* @param {string} response.slug Slug of the checked plugin.
* @param {string} response.pluginName Name of the checked plugin.
* @param {string} response.plugin The plugin file, relative to the plugins directory.
* @param {string} response.activateUrl URL to activate the just checked plugin.
*/
wp.updates.checkPluginDependenciesSuccess = function( response ) {
var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.install-now' ),
buttonText, ariaLabel;
// Transform the 'Install' button into an 'Activate' button.
$message
.removeClass( 'install-now installed button-disabled updated-message' )
.addClass( 'activate-now button-primary' )
.attr( 'href', response.activateUrl );
wp.a11y.speak( __( 'Plugin dependencies check completed successfully.' ) );
$document.trigger( 'wp-check-plugin-dependencies-success', response );
if ( 'plugins-network' === pagenow ) {
buttonText = _x( 'Network Activate' );
ariaLabel = sprintf(
/* translators: %s: Plugin name. */
_x( 'Network Activate %s', 'plugin' ),
response.pluginName
);
$message
.attr( 'aria-label', ariaLabel )
.text( buttonText );
} else {
buttonText = _x( 'Activate', 'plugin' );
ariaLabel = sprintf(
/* translators: %s: Plugin name. */
_x( 'Activate %s', 'plugin' ),
response.pluginName
);
$message
.attr( 'aria-label', ariaLabel )
.attr( 'data-name', response.pluginName )
.attr( 'data-slug', response.slug )
.attr( 'data-plugin', response.plugin )
.text( buttonText );
}
if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
wp.updates.setCardButtonStatus(
{
status: 'dependencies-check-success',
slug: response.slug,
removeClasses: 'install-now installed button-disabled updated-message',
addClasses: 'activate-now button-primary',
text: buttonText,
ariaLabel: ariaLabel,
pluginName: response.pluginName,
plugin: response.plugin,
href: response.activateUrl
}
);
}
};
/**
* Updates the UI appropriately after a failed plugin dependencies check.
*
* @since 6.5.0
*
* @param {Object} response Response from the server.
* @param {string} response.slug Slug of the plugin to be checked.
* @param {string=} response.pluginName Optional. Name of the plugin to be checked.
* @param {string} response.errorCode Error code for the error that occurred.
* @param {string} response.errorMessage The error that occurred.
*/
wp.updates.checkPluginDependenciesError = function( response ) {
var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.install-now' ),
buttonText = __( 'Activate' ),
ariaLabel = sprintf(
/* translators: 1: Plugin name, 2. The reason the plugin cannot be activated. */
_x( 'Cannot activate %1$s. %2$s', 'plugin' ),
response.pluginName,
response.errorMessage
),
errorMessage;
if ( ! wp.updates.isValidResponse( response, 'check-dependencies' ) ) {
return;
}
errorMessage = sprintf(
/* translators: %s: Error string for a failed activation. */
__( 'Activation failed: %s' ),
response.errorMessage
);
wp.a11y.speak( errorMessage, 'assertive' );
$document.trigger( 'wp-check-plugin-dependencies-error', response );
$message
.removeClass( 'install-now installed updated-message' )
.addClass( 'activate-now button-primary' )
.attr( 'aria-label', ariaLabel )
.text( buttonText );
if ( 'plugin-information-footer' === $message.parent().attr('id' ) ) {
wp.updates.setCardButtonStatus(
{
status: 'dependencies-check-failed',
slug: response.slug,
removeClasses: 'install-now installed updated-message',
addClasses: 'activate-now button-primary',
text: buttonText,
ariaLabel: ariaLabel
}
);
}
};
/**
* Sends an Ajax request to the server to activate a plugin.
*
* @since 6.5.0
*
* @param {Object} args Arguments.
* @param {string} args.name The name of the plugin.
* @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository.
* @param {string} args.plugin The plugin file, relative to the plugins directory.
* @param {activatePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.activatePluginSuccess
* @param {activatePluginError=} args.error Optional. Error callback. Default: wp.updates.activatePluginError
* @return {$.promise} A jQuery promise that represents the request,
* decorated with an abort() method.
*/
wp.updates.activatePlugin = function( args ) {
var $message = $( '.plugin-card-' + args.slug + ', #plugin-information-footer' ).find( '.activate-now, .activating-message' );
args = _.extend( {
success: wp.updates.activatePluginSuccess,
error: wp.updates.activatePluginError
}, args );
wp.a11y.speak( __( 'Activating... please wait.' ) );
$document.trigger( 'wp-activating-plugin', args );
if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
wp.updates.setCardButtonStatus(
{
status: 'activating-plugin',
slug: args.slug,
removeClasses: 'installed updated-message button-primary',
addClasses: 'activating-message',
text: _x( 'Activating...', 'plugin' ),
ariaLabel: sprintf(
/* translators: %s: Plugin name. */
_x( 'Activating %s', 'plugin' ),
args.name
)
}
);
}
return wp.updates.ajax( 'activate-plugin', args );
};
/**
* Updates the UI appropriately after a successful plugin activation.
*
* @since 6.5.0
*
* @param {Object} response Response from the server.
* @param {string} response.slug Slug of the activated plugin.
* @param {string} response.pluginName Name of the activated plugin.
* @param {string} response.plugin The plugin file, relative to the plugins directory.
*/
wp.updates.activatePluginSuccess = function( response ) {
var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.activating-message' ),
buttonText = _x( 'Activated!', 'plugin' ),
ariaLabel = sprintf(
/* translators: %s: The plugin name. */
'%s activated successfully.',
response.pluginName
);
wp.a11y.speak( __( 'Activation completed successfully.' ) );
$document.trigger( 'wp-plugin-activate-success', response );
$message
.removeClass( 'activating-message' )
.addClass( 'activated-message button-disabled' )
.attr( 'aria-label', ariaLabel )
.text( buttonText );
if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
wp.updates.setCardButtonStatus(
{
status: 'activated-plugin',
slug: response.slug,
removeClasses: 'activating-message',
addClasses: 'activated-message button-disabled',
text: buttonText,
ariaLabel: ariaLabel
}
);
}
setTimeout( function() {
$message.removeClass( 'activated-message' )
.text( _x( 'Active', 'plugin' ) );
if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
wp.updates.setCardButtonStatus(
{
status: 'plugin-active',
slug: response.slug,
removeClasses: 'activated-message',
text: _x( 'Active', 'plugin' ),
ariaLabel: sprintf(
/* translators: %s: The plugin name. */
'%s is active.',
response.pluginName
)
}
);
}
}, 1000 );
};
/**
* Updates the UI appropriately after a failed plugin activation.
*
* @since 6.5.0
*
* @param {Object} response Response from the server.
* @param {string} response.slug Slug of the plugin to be activated.
* @param {string=} response.pluginName Optional. Name of the plugin to be activated.
* @param {string} response.errorCode Error code for the error that occurred.
* @param {string} response.errorMessage The error that occurred.
*/
wp.updates.activatePluginError = function( response ) {
var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.activating-message' ),
buttonText = __( 'Activation failed.' ),
ariaLabel = sprintf(
/* translators: %s: Plugin name. */
_x( '%s activation failed', 'plugin' ),
response.pluginName
),
errorMessage;
if ( ! wp.updates.isValidResponse( response, 'activate' ) ) {
return;
}
errorMessage = sprintf(
/* translators: %s: Error string for a failed activation. */
__( 'Activation failed: %s' ),
response.errorMessage
);
wp.a11y.speak( errorMessage, 'assertive' );
$document.trigger( 'wp-plugin-activate-error', response );
$message
.removeClass( 'install-now installed activating-message' )
.addClass( 'button-disabled' )
.attr( 'aria-label', ariaLabel )
.text( buttonText );
if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
wp.updates.setCardButtonStatus(
{
status: 'plugin-activation-failed',
slug: response.slug,
removeClasses: 'install-now installed activating-message',
addClasses: 'button-disabled',
text: buttonText,
ariaLabel: ariaLabel
}
);
}
};
/**
* Updates the UI appropriately after a successful importer install.
*
@ -1970,6 +2354,16 @@
errorMessage = __( 'Installation failed: %s' );
break;
case 'check-dependencies':
/* translators: %s: Error string for a failed dependencies check. */
errorMessage = __( 'Dependencies check failed: %s' );
break;
case 'activate':
/* translators: %s: Error string for a failed activation. */
errorMessage = __( 'Activation failed: %s' );
break;
case 'delete':
/* translators: %s: Error string for a failed deletion. */
errorMessage = __( 'Deletion failed: %s' );
@ -2025,7 +2419,7 @@
};
$( function() {
var $pluginFilter = $( '#plugin-filter' ),
var $pluginFilter = $( '#plugin-filter, #plugin-information-footer' ),
$bulkActionForm = $( '#bulk-action-form' ),
$filesystemForm = $( '#request-filesystem-credentials-form' ),
$filesystemModal = $( '#request-filesystem-credentials-dialog' ),
@ -2241,6 +2635,44 @@
} );
} );
/**
* Click handler for plugin activations in plugin activation view.
*
* @since 6.5.0
*
* @param {Event} event Event interface.
*/
$pluginFilter.on( 'click', '.activate-now', function( event ) {
var $activateButton = $( event.target );
event.preventDefault();
if ( $activateButton.hasClass( 'activating-message' ) || $activateButton.hasClass( 'button-disabled' ) ) {
return;
}
$activateButton
.removeClass( 'activate-now button-primary' )
.addClass( 'activating-message' )
.attr(
'aria-label',
sprintf(
/* translators: %s: Plugin name. */
_x( 'Activating %s', 'plugin' ),
$activateButton.data( 'name' )
)
)
.text( _x( 'Activating...', 'plugin' ) );
wp.updates.activatePlugin(
{
name: $activateButton.data( 'name' ),
slug: $activateButton.data( 'slug' ),
plugin: $activateButton.data( 'plugin' )
}
);
});
/**
* Click handler for importer plugins installs in the Import screen.
*
@ -2766,35 +3198,6 @@
target.postMessage( JSON.stringify( update ), window.location.origin );
} );
/**
* Click handler for installing a plugin from the details modal on `plugin-install.php`.
*
* @since 4.6.0
*
* @param {Event} event Event interface.
*/
$( '#plugin_install_from_iframe' ).on( 'click', function( event ) {
var target = window.parent === window ? null : window.parent,
install;
$.support.postMessage = !! window.postMessage;
if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'index.php' ) ) {
return;
}
event.preventDefault();
install = {
action: 'install-plugin',
data: {
slug: $( this ).data( 'slug' )
}
};
target.postMessage( JSON.stringify( install ), window.location.origin );
} );
/**
* Handles postMessage events.
*
@ -2818,7 +3221,45 @@
return;
}
if ( ! message || 'undefined' === typeof message.action ) {
if ( ! message ) {
return;
}
if (
'undefined' !== typeof message.status &&
'undefined' !== typeof message.slug &&
'undefined' !== typeof message.text &&
'undefined' !== typeof message.ariaLabel
) {
var $card = $( '.plugin-card-' + message.slug ),
$message = $card.find( '[data-slug="' + message.slug + '"]' );
if ( 'undefined' !== typeof message.removeClasses ) {
$message.removeClass( message.removeClasses );
}
if ( 'undefined' !== typeof message.addClasses ) {
$message.addClass( message.addClasses );
}
if ( '' === message.ariaLabel ) {
$message.removeAttr( 'aria-label' );
} else {
$message.attr( 'aria-label', message.ariaLabel );
}
if ( 'dependencies-check-success' === message.status ) {
$message
.attr( 'data-name', message.pluginName )
.attr( 'data-slug', message.slug )
.attr( 'data-plugin', message.plugin )
.attr( 'href', message.href );
}
$message.text( message.text );
}
if ( 'undefined' === typeof message.action || 'undefined' === typeof message.data.slug ) {
return;
}
@ -2832,10 +3273,6 @@
case 'install-plugin':
case 'update-plugin':
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
window.tb_remove();
/* jscs:enable */
message.data = wp.updates._addCallbacks( message.data, message.action );
wp.updates.queue.push( message );

File diff suppressed because one or more lines are too long

View File

@ -134,6 +134,10 @@ get_current_screen()->set_screen_reader_content(
* WordPress Administration Template Header.
*/
require_once ABSPATH . 'wp-admin/admin-header.php';
WP_Plugin_Dependencies::display_admin_notice_for_unmet_dependencies();
WP_Plugin_Dependencies::display_admin_notice_for_deactivated_dependents();
WP_Plugin_Dependencies::display_admin_notice_for_circular_dependencies();
?>
<div class="wrap <?php echo esc_attr( "plugin-install-tab-$tab" ); ?>">
<h1 class="wp-heading-inline">

View File

@ -739,6 +739,9 @@ if ( isset( $_GET['error'] ) ) {
}
?>
<?php WP_Plugin_Dependencies::display_admin_notice_for_unmet_dependencies(); ?>
<?php WP_Plugin_Dependencies::display_admin_notice_for_deactivated_dependents(); ?>
<?php WP_Plugin_Dependencies::display_admin_notice_for_circular_dependencies(); ?>
<div class="wrap">
<h1 class="wp-heading-inline">
<?php

View File

@ -0,0 +1,949 @@
<?php
/**
* WordPress Plugin Administration API: WP_Plugin_Dependencies class
*
* @package WordPress
* @subpackage Administration
* @since 6.5.0
*/
/**
* Core class for installing plugin dependencies.
*
* It is designed to add plugin dependencies as designated in the
* `Requires Plugins` header to a new view in the plugins install page.
*/
class WP_Plugin_Dependencies {
/**
* Holds 'get_plugins()'.
*
* @since 6.5.0
*
* @var array
*/
protected static $plugins;
/**
* Holds plugin directory names to compare with cache.
*
* @since 6.5.0
*
* @var array
*/
protected static $plugin_dirnames;
/**
* Holds sanitized plugin dependency slugs.
*
* Keyed on the dependent plugin's filepath,
* relative to the plugins directory.
*
* @since 6.5.0
*
* @var array
*/
protected static $dependencies;
/**
* Holds an array of sanitized plugin dependency slugs.
*
* @since 6.5.0
*
* @var array
*/
protected static $dependency_slugs;
/**
* Holds an array of dependent plugin slugs.
*
* Keyed on the dependent plugin's filepath,
* relative to the plugins directory.
*
* @since 6.5.0
*
* @var array
*/
protected static $dependent_slugs;
/**
* Holds 'plugins_api()' data for plugin dependencies.
*
* @since 6.5.0
*
* @var array
*/
protected static $dependency_api_data;
/**
* Holds plugin dependency filepaths, relative to the plugins directory.
*
* Keyed on the dependency's slug.
*
* @since 6.5.0
*
* @var string[]
*/
protected static $dependency_filepaths;
/**
* An array of circular dependency pairings.
*
* @since 6.5.0
*
* @var array[]
*/
protected static $circular_dependencies_pairs;
/**
* An array of circular dependency slugs.
*
* @since 6.5.0
*
* @var string[]
*/
protected static $circular_dependencies_slugs;
/**
* Initializes by fetching plugin header and plugin API data,
* and deactivating dependents with unmet dependencies.
*
* @since 6.5.0
*/
public static function initialize() {
self::read_dependencies_from_plugin_headers();
self::get_dependency_api_data();
self::deactivate_dependents_with_unmet_dependencies();
}
/**
* Determines whether the plugin has plugins that depend on it.
*
* @since 6.5.0
*
* @param string $plugin_file The plugin's filepath, relative to the plugins directory.
* @return bool Whether the plugin has plugins that depend on it.
*/
public static function has_dependents( $plugin_file ) {
return in_array( self::convert_to_slug( $plugin_file ), self::$dependency_slugs, true );
}
/**
* Determines whether the plugin has plugin dependencies.
*
* @since 6.5.0
*
* @param string $plugin_file The plugin's filepath, relative to the plugins directory.
* @return bool Whether a plugin has plugin dependencies.
*/
public static function has_dependencies( $plugin_file ) {
return isset( self::$dependencies[ $plugin_file ] );
}
/**
* Determines whether the plugin has active dependents.
*
* @since 6.5.0
*
* @param string $plugin_file The plugin's filepath, relative to the plugins directory.
* @return bool Whether the plugin has active dependents.
*/
public static function has_active_dependents( $plugin_file ) {
require_once ABSPATH . '/wp-admin/includes/plugin.php';
$dependents = self::get_dependents( self::convert_to_slug( $plugin_file ) );
foreach ( $dependents as $dependent ) {
if ( is_plugin_active( $dependent ) ) {
return true;
}
}
return false;
}
/**
* Gets filepaths of plugins that require the dependency.
*
* @since 6.5.0
*
* @param string $slug The dependency's slug.
* @return array An array of dependent plugin filepaths, relative to the plugins directory.
*/
public static function get_dependents( $slug ) {
$dependents = array();
foreach ( self::$dependencies as $dependent => $dependencies ) {
if ( in_array( $slug, $dependencies, true ) ) {
$dependents[] = $dependent;
}
}
return $dependents;
}
/**
* Gets the slugs of plugins that the dependent requires.
*
* @since 6.5.0
*
* @param string $plugin_file The dependent plugin's filepath, relative to the plugins directory.
* @return array An array of dependency plugin slugs.
*/
public static function get_dependencies( $plugin_file ) {
if ( isset( self::$dependencies[ $plugin_file ] ) ) {
return self::$dependencies[ $plugin_file ];
}
return array();
}
/**
* Gets a dependent plugin's filepath.
*
* @since 6.5.0
*
* @param string $slug The dependent plugin's slug.
* @return string|false The dependent plugin's filepath, relative to the plugins directory,
* or false if the plugin has no dependencies.
*/
public static function get_dependent_filepath( $slug ) {
$filepath = array_search( $slug, self::$dependent_slugs, true );
return $filepath ? $filepath : false;
}
/**
* Determines whether the plugin has unmet dependencies.
*
* @since 6.5.0
*
* @param string $plugin_file The plugin's filepath, relative to the plugins directory.
* @return bool Whether the plugin has unmet dependencies.
*/
public static function has_unmet_dependencies( $plugin_file ) {
if ( ! isset( self::$dependencies[ $plugin_file ] ) ) {
return false;
}
require_once ABSPATH . '/wp-admin/includes/plugin.php';
foreach ( self::$dependencies[ $plugin_file ] as $dependency ) {
$dependency_filepath = self::get_dependency_filepath( $dependency );
if ( false === $dependency_filepath || is_plugin_inactive( $dependency_filepath ) ) {
return true;
}
}
return false;
}
/**
* Determines whether the plugin has a circular dependency.
*
* @since 6.5.0
*
* @param string $plugin_file The plugin's filepath, relative to the plugins directory.
* @return bool Whether the plugin has a circular dependency.
*/
public static function has_circular_dependency( $plugin_file ) {
if ( ! is_array( self::$circular_dependencies_slugs ) ) {
self::get_circular_dependencies();
}
if ( ! empty( self::$circular_dependencies_slugs ) ) {
$slug = self::convert_to_slug( $plugin_file );
if ( in_array( $slug, self::$circular_dependencies_slugs, true ) ) {
return true;
}
}
return false;
}
/**
* Gets the names of plugins that require the plugin.
*
* @since 6.5.0
*
* @param string $plugin_file The plugin's filepath, relative to the plugins directory.
* @return array An array of dependent names.
*/
public static function get_dependent_names( $plugin_file ) {
$dependent_names = array();
$plugins = self::get_plugins();
$slug = self::convert_to_slug( $plugin_file );
foreach ( self::get_dependents( $slug ) as $dependent ) {
$dependent_names[ $dependent ] = $plugins[ $dependent ]['Name'];
}
sort( $dependent_names );
return $dependent_names;
}
/**
* Gets the names of plugins required by the plugin.
*
* @since 6.5.0
*
* @param string $plugin_file The dependent plugin's filepath, relative to the plugins directory.
* @return array An array of dependency names.
*/
public static function get_dependency_names( $plugin_file ) {
$dependency_api_data = self::get_dependency_api_data();
$dependencies = self::get_dependencies( $plugin_file );
$plugins = self::get_plugins();
$dependency_names = array();
foreach ( $dependencies as $dependency ) {
// Use the name if it's available, otherwise fall back to the slug.
if ( isset( $dependency_api_data[ $dependency ]['name'] ) ) {
$name = $dependency_api_data[ $dependency ]['name'];
} else {
$dependency_filepath = self::get_dependency_filepath( $dependency );
if ( false !== $dependency_filepath ) {
$name = $plugins[ $dependency_filepath ]['Name'];
} else {
$name = $dependency;
}
}
$dependency_names[ $dependency ] = $name;
}
return $dependency_names;
}
/**
* Gets the filepath for a dependency, relative to the plugin's directory.
*
* @since 6.5.0
*
* @param string $slug The dependency's slug.
* @return string|false If installed, the dependency's filepath relative to the plugins directory, otherwise false.
*/
public static function get_dependency_filepath( $slug ) {
$dependency_filepaths = self::get_dependency_filepaths();
if ( ! isset( $dependency_filepaths[ $slug ] ) ) {
return false;
}
return $dependency_filepaths[ $slug ];
}
/**
* Returns API data for the dependency.
*
* @since 6.5.0
*
* @param string $slug The dependency's slug.
* @return array|false The dependency's API data on success, otherwise false.
*/
public static function get_dependency_data( $slug ) {
$dependency_api_data = self::get_dependency_api_data();
if ( isset( $dependency_api_data[ $slug ] ) ) {
return $dependency_api_data[ $slug ];
}
return false;
}
/**
* Displays an admin notice if dependencies are not installed.
*
* @since 6.5.0
*/
public static function display_admin_notice_for_unmet_dependencies() {
if ( in_array( false, self::get_dependency_filepaths(), true ) ) {
wp_admin_notice(
__( 'There are additional plugin dependencies that must be installed.' ),
array(
'type' => 'info',
)
);
}
}
/**
* Displays an admin notice if dependencies have been deactivated.
*
* @since 6.5.0
*/
public static function display_admin_notice_for_deactivated_dependents() {
/*
* Plugin deactivated if dependencies not met.
* Transient on a 10 second timeout.
*/
$deactivate_requires = get_site_transient( 'wp_plugin_dependencies_deactivated_plugins' );
if ( ! empty( $deactivate_requires ) ) {
$deactivated_plugins = '';
foreach ( $deactivate_requires as $deactivated ) {
$deactivated_plugins .= '<li>' . esc_html( self::$plugins[ $deactivated ]['Name'] ) . '</li>';
}
wp_admin_notice(
sprintf(
/* translators: 1: plugin names */
__( 'The following plugin(s) have been deactivated due to uninstalled or inactive dependencies: %s' ),
"<ul>$deactivated_plugins</ul>"
),
array(
'type' => 'error',
'dismissible' => true,
)
);
}
}
/**
* Displays an admin notice if circular dependencies are installed.
*
* @since 6.5.0
*/
public static function display_admin_notice_for_circular_dependencies() {
$circular_dependencies = self::get_circular_dependencies();
if ( ! empty( $circular_dependencies ) && count( $circular_dependencies ) > 1 ) {
$circular_dependencies = array_unique( $circular_dependencies, SORT_REGULAR );
$plugins = self::get_plugins();
$plugin_dirnames = self::get_plugin_dirnames();
// Build output lines.
$circular_dependency_lines = '';
foreach ( $circular_dependencies as $circular_dependency ) {
$first_filepath = $plugin_dirnames[ $circular_dependency[0] ];
$second_filepath = $plugin_dirnames[ $circular_dependency[1] ];
$circular_dependency_lines .= sprintf(
/* translators: 1: First plugin name, 2: Second plugin name. */
'<li>' . _x( '%1$s requires %2$s', 'The first plugin requires the second plugin.' ) . '</li>',
'<strong>' . esc_html( $plugins[ $first_filepath ]['Name'] ) . '</strong>',
'<strong>' . esc_html( $plugins[ $second_filepath ]['Name'] ) . '</strong>'
);
}
wp_admin_notice(
sprintf(
'<p>%1$s</p><ul>%2$s</ul><p>%3$s</p>',
__( 'These plugins cannot be activated because their requirements are invalid. ' ),
$circular_dependency_lines,
__( 'Please contact the plugin authors for more information.' )
),
array(
'type' => 'warning',
'paragraph_wrap' => false,
)
);
}
}
/**
* Checks plugin dependencies after a plugin is installed via AJAX.
*
* @since 6.5.0
*/
public static function check_plugin_dependencies_during_ajax() {
check_ajax_referer( 'updates' );
if ( empty( $_POST['slug'] ) ) {
wp_send_json_error(
array(
'slug' => '',
'pluginName' => '',
'errorCode' => 'no_plugin_specified',
'errorMessage' => __( 'No plugin specified.' ),
)
);
}
$slug = sanitize_key( wp_unslash( $_POST['slug'] ) );
$status = array( 'slug' => $slug );
self::get_plugins();
self::get_plugin_dirnames();
if ( ! isset( self::$plugin_dirnames[ $slug ] ) ) {
$status['errorCode'] = 'plugin_not_installed';
$status['errorMessage'] = __( 'The plugin is not installed.' );
wp_send_json_error( $status );
}
$plugin_file = self::$plugin_dirnames[ $slug ];
$status['pluginName'] = self::$plugins[ $plugin_file ]['Name'];
$status['plugin'] = $plugin_file;
if ( current_user_can( 'activate_plugin', $plugin_file ) && is_plugin_inactive( $plugin_file ) ) {
$status['activateUrl'] = add_query_arg(
array(
'_wpnonce' => wp_create_nonce( 'activate-plugin_' . $plugin_file ),
'action' => 'activate',
'plugin' => $plugin_file,
),
is_multisite() ? network_admin_url( 'plugins.php' ) : admin_url( 'plugins.php' )
);
}
if ( is_multisite() && current_user_can( 'manage_network_plugins' ) ) {
$status['activateUrl'] = add_query_arg( array( 'networkwide' => 1 ), $status['activateUrl'] );
}
$dependencies = self::get_dependencies( $plugin_file );
if ( empty( $dependencies ) ) {
$status['message'] = __( 'The plugin has no required plugins.' );
wp_send_json_success( $status );
}
require_once ABSPATH . '/wp-admin/includes/plugin.php';
$inactive_dependencies = array();
foreach ( $dependencies as $dependency ) {
if ( false === self::$plugin_dirnames[ $dependency ] || is_plugin_inactive( self::$plugin_dirnames[ $dependency ] ) ) {
$inactive_dependencies[] = $dependency;
}
}
if ( ! empty( $inactive_dependencies ) ) {
$inactive_dependency_names = array_map(
function ( $dependency ) {
if ( isset( self::$dependency_api_data[ $dependency ]['Name'] ) ) {
$inactive_dependency_name = self::$dependency_api_data[ $dependency ]['Name'];
} else {
$inactive_dependency_name = $dependency;
}
return $inactive_dependency_name;
},
$inactive_dependencies
);
$status['errorCode'] = 'inactive_dependencies';
$status['errorMessage'] = sprintf(
/* translators: %s: A list of inactive dependency plugin names. */
__( 'The following plugins must be activated first: %s.' ),
implode( ', ', $inactive_dependency_names )
);
$status['errorData'] = array_combine( $inactive_dependencies, $inactive_dependency_names );
wp_send_json_error( $status );
}
$status['message'] = __( 'All required plugins are installed and activated.' );
wp_send_json_success( $status );
}
/**
* Gets data for installed plugins.
*
* @since 6.5.0
*
* @return array An array of plugin data.
*/
protected static function get_plugins() {
if ( is_array( self::$plugins ) ) {
return self::$plugins;
}
$all_plugin_data = get_option( 'plugin_data', array() );
if ( empty( $all_plugin_data ) ) {
require_once ABSPATH . '/wp-admin/includes/plugin.php';
$all_plugin_data = get_plugins();
}
self::$plugins = $all_plugin_data;
return self::$plugins;
}
/**
* Reads and stores dependency slugs from a plugin's 'Requires Plugins' header.
*
* @since 6.5.0
*
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
*/
protected static function read_dependencies_from_plugin_headers() {
self::$dependencies = array();
self::$dependency_slugs = array();
self::$dependent_slugs = array();
$plugins = self::get_plugins();
foreach ( $plugins as $plugin => $header ) {
if ( '' === $header['RequiresPlugins'] ) {
continue;
}
$dependency_slugs = self::sanitize_dependency_slugs( $header['RequiresPlugins'] );
self::$dependencies[ $plugin ] = $dependency_slugs;
self::$dependency_slugs = array_merge( self::$dependency_slugs, $dependency_slugs );
$dependent_slug = self::convert_to_slug( $plugin );
self::$dependent_slugs[ $plugin ] = $dependent_slug;
}
self::$dependency_slugs = array_unique( self::$dependency_slugs );
}
/**
* Sanitizes slugs.
*
* @since 6.5.0
*
* @param string $slugs A comma-separated string of plugin dependency slugs.
* @return array An array of sanitized plugin dependency slugs.
*/
protected static function sanitize_dependency_slugs( $slugs ) {
$sanitized_slugs = array();
$slugs = explode( ',', $slugs );
foreach ( $slugs as $slug ) {
$slug = trim( $slug );
/**
* Filters a plugin dependency's slug before matching to
* the WordPress.org slug format.
*
* Can be used to switch between free and premium plugin slugs, for example.
*
* @since 6.5.0
*
* @param string $slug The slug.
*/
$slug = apply_filters( 'wp_plugin_dependencies_slug', $slug );
// Match to WordPress.org slug format.
if ( preg_match( '/^[a-z0-9]+(-[a-z0-9]+)*$/mu', $slug ) ) {
$sanitized_slugs[] = $slug;
}
}
$sanitized_slugs = array_unique( $sanitized_slugs );
sort( $sanitized_slugs );
return $sanitized_slugs;
}
/**
* Gets plugin filepaths for active plugins that depend on the dependency.
*
* Recurses for each dependent that is also a dependency.
*
* @param string $plugin_file The dependency's filepath, relative to the plugin directory.
* @return string[] An array of active dependent plugin filepaths, relative to the plugin directory.
*/
protected static function get_active_dependents_in_dependency_tree( $plugin_file ) {
$all_dependents = array();
$dependents = self::get_dependents( self::convert_to_slug( $plugin_file ) );
if ( empty( $dependents ) ) {
return $all_dependents;
}
require_once ABSPATH . '/wp-admin/includes/plugin.php';
foreach ( $dependents as $dependent ) {
if ( is_plugin_active( $dependent ) ) {
$all_dependents[] = $dependent;
$all_dependents = array_merge(
$all_dependents,
self::get_active_dependents_in_dependency_tree( $dependent )
);
}
}
return $all_dependents;
}
/**
* Deactivates dependent plugins with unmet dependencies.
*
* @since 6.5.0
*/
protected static function deactivate_dependents_with_unmet_dependencies() {
$dependents_to_deactivate = array();
$circular_dependencies = array_reduce(
self::get_circular_dependencies(),
function ( $all_circular, $circular_pair ) {
return array_merge( $all_circular, $circular_pair );
},
array()
);
require_once ABSPATH . '/wp-admin/includes/plugin.php';
foreach ( self::$dependencies as $dependent => $dependencies ) {
// Skip dependents that are no longer installed or aren't active.
if ( ! array_key_exists( $dependent, self::$plugins ) || is_plugin_inactive( $dependent ) ) {
continue;
}
// Skip plugins within a circular dependency tree or plugins that have no unmet dependencies.
if ( in_array( $dependent, $circular_dependencies, true ) || ! self::has_unmet_dependencies( $dependent ) ) {
continue;
}
$dependents_to_deactivate[] = $dependent;
// Also add any plugins that rely on any of this plugin's dependents.
$dependents_to_deactivate = array_merge(
$dependents_to_deactivate,
self::get_active_dependents_in_dependency_tree( $dependent )
);
}
$dependents_to_deactivate = array_unique( $dependents_to_deactivate );
deactivate_plugins( $dependents_to_deactivate );
set_site_transient( 'wp_plugin_dependencies_deactivated_plugins', $dependents_to_deactivate, 10 );
}
/**
* Gets the filepath of installed dependencies.
* If a dependency is not installed, the filepath defaults to false.
*
* @since 6.5.0
*
* @return array An array of install dependencies filepaths, relative to the plugins directory.
*/
protected static function get_dependency_filepaths() {
if ( is_array( self::$dependency_filepaths ) ) {
return self::$dependency_filepaths;
}
self::$dependency_filepaths = array();
$plugin_dirnames = self::get_plugin_dirnames();
foreach ( self::$dependency_slugs as $slug ) {
if ( isset( $plugin_dirnames[ $slug ] ) ) {
self::$dependency_filepaths[ $slug ] = $plugin_dirnames[ $slug ];
continue;
}
self::$dependency_filepaths[ $slug ] = false;
}
return self::$dependency_filepaths;
}
/**
* Retrieves and stores dependency plugin data from the WordPress.org Plugin API.
*
* @since 6.5.0
*
* @global string $pagenow The filename of the current screen.
*
* @return array|void An array of dependency API data, or void on early exit.
*/
protected static function get_dependency_api_data() {
global $pagenow;
if ( ! is_admin() || ( 'plugins.php' !== $pagenow && 'plugin-install.php' !== $pagenow ) ) {
return;
}
if ( is_array( self::$dependency_api_data ) ) {
return self::$dependency_api_data;
}
$plugins = self::get_plugins();
self::$dependency_api_data = (array) get_site_transient( 'wp_plugin_dependencies_plugin_data' );
foreach ( self::$dependency_slugs as $slug ) {
// Set transient for individual data, remove from self::$dependency_api_data if transient expired.
if ( ! get_site_transient( "wp_plugin_dependencies_plugin_timeout_{$slug}" ) ) {
unset( self::$dependency_api_data[ $slug ] );
set_site_transient( "wp_plugin_dependencies_plugin_timeout_{$slug}", true, 12 * HOUR_IN_SECONDS );
}
if ( isset( self::$dependency_api_data[ $slug ] ) ) {
if ( false === self::$dependency_api_data[ $slug ] ) {
$dependency_file = self::get_dependency_filepath( $slug );
if ( false === $dependency_file ) {
self::$dependency_api_data[ $slug ] = array( 'Name' => $slug );
} else {
self::$dependency_api_data[ $slug ] = array( 'Name' => $plugins[ $dependency_file ]['Name'] );
}
continue;
}
// Don't hit the Plugin API if data exists.
if ( ! empty( self::$dependency_api_data[ $slug ]['last_updated'] ) ) {
continue;
}
}
if ( ! function_exists( 'plugins_api' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
}
$information = plugins_api(
'plugin_information',
array(
'slug' => $slug,
'fields' => array(
'short_description' => true,
'icons' => true,
),
)
);
if ( is_wp_error( $information ) ) {
continue;
}
self::$dependency_api_data[ $slug ] = (array) $information;
// plugins_api() returns 'name' not 'Name'.
self::$dependency_api_data[ $information->slug ]['Name'] = self::$dependency_api_data[ $information->slug ]['name'];
set_site_transient( 'wp_plugin_dependencies_plugin_data', self::$dependency_api_data, 0 );
}
// Remove from self::$dependency_api_data if slug no longer a dependency.
$differences = array_diff( array_keys( self::$dependency_api_data ), self::$dependency_slugs );
foreach ( $differences as $difference ) {
unset( self::$dependency_api_data[ $difference ] );
}
ksort( self::$dependency_api_data );
// Remove empty elements.
self::$dependency_api_data = array_filter( self::$dependency_api_data );
set_site_transient( 'wp_plugin_dependencies_plugin_data', self::$dependency_api_data, 0 );
return self::$dependency_api_data;
}
/**
* Gets plugin directory names.
*
* @since 6.5.0
*
* @return array An array of plugin directory names.
*/
protected static function get_plugin_dirnames() {
if ( is_array( self::$plugin_dirnames ) ) {
return self::$plugin_dirnames;
}
self::$plugin_dirnames = array();
$plugin_files = array_keys( self::get_plugins() );
foreach ( $plugin_files as $plugin_file ) {
$slug = self::convert_to_slug( $plugin_file );
self::$plugin_dirnames[ $slug ] = $plugin_file;
}
return self::$plugin_dirnames;
}
/**
* Gets circular dependency data.
*
* @since 6.5.0
*
* @return array[] An array of circular dependency pairings.
*/
protected static function get_circular_dependencies() {
if ( is_array( self::$circular_dependencies_pairs ) ) {
return self::$circular_dependencies_pairs;
}
self::$circular_dependencies_slugs = array();
self::$circular_dependencies_pairs = array();
foreach ( self::$dependencies as $dependent => $dependencies ) {
/*
* $dependent is in 'a/a.php' format. Dependencies are stored as slugs, i.e. 'a'.
*
* Convert $dependent to slug format for checking.
*/
$dependent_slug = self::convert_to_slug( $dependent );
self::$circular_dependencies_pairs = array_merge(
self::$circular_dependencies_pairs,
self::check_for_circular_dependencies( array( $dependent_slug ), $dependencies )
);
}
return self::$circular_dependencies_pairs;
}
/**
* Checks for circular dependencies.
*
* @since 6.5.0
*
* @param array $dependents Array of dependent plugins.
* @param array $dependencies Array of plugins dependencies.
* @return array A circular dependency pairing, or an empty array if none exists.
*/
protected static function check_for_circular_dependencies( $dependents, $dependencies ) {
$circular_dependencies_pairs = array();
// Check for a self-dependency.
$dependents_location_in_its_own_dependencies = array_intersect( $dependents, $dependencies );
if ( ! empty( $dependents_location_in_its_own_dependencies ) ) {
foreach ( $dependents_location_in_its_own_dependencies as $self_dependency ) {
self::$circular_dependencies_slugs[] = $self_dependency;
$circular_dependencies_pairs[] = array( $self_dependency, $self_dependency );
// No need to check for itself again.
unset( $dependencies[ array_search( $self_dependency, $dependencies, true ) ] );
}
}
/*
* Check each dependency to see:
* 1. If it has dependencies.
* 2. If its list of dependencies includes one of its own dependents.
*/
foreach ( $dependencies as $dependency ) {
// Check if the dependency is also a dependent.
$dependency_location_in_dependents = array_search( $dependency, self::$dependent_slugs, true );
if ( false !== $dependency_location_in_dependents ) {
$dependencies_of_the_dependency = self::$dependencies[ $dependency_location_in_dependents ];
foreach ( $dependents as $dependent ) {
// Check if its dependencies includes one of its own dependents.
$dependent_location_in_dependency_dependencies = array_search(
$dependent,
$dependencies_of_the_dependency,
true
);
if ( false !== $dependent_location_in_dependency_dependencies ) {
self::$circular_dependencies_slugs[] = $dependent;
self::$circular_dependencies_slugs[] = $dependency;
$circular_dependencies_pairs[] = array( $dependent, $dependency );
// Remove the dependent from its dependency's dependencies.
unset( $dependencies_of_the_dependency[ $dependent_location_in_dependency_dependencies ] );
}
}
$dependents[] = $dependency;
/*
* Now check the dependencies of the dependency's dependencies for the dependent.
*
* Yes, that does make sense.
*/
$circular_dependencies_pairs = array_merge(
$circular_dependencies_pairs,
self::check_for_circular_dependencies( $dependents, array_unique( $dependencies_of_the_dependency ) )
);
}
}
return $circular_dependencies_pairs;
}
/**
* Converts a plugin filepath to a slug.
*
* @since 6.5.0
*
* @param string $plugin_file The plugin's filepath, relative to the plugins directory.
* @return string The plugin's slug.
*/
protected static function convert_to_slug( $plugin_file ) {
if ( 'hello.php' === $plugin_file ) {
return 'hello-dolly';
}
return str_contains( $plugin_file, '/' ) ? dirname( $plugin_file ) : str_replace( '.php', '', $plugin_file );
}
}

View File

@ -3359,7 +3359,7 @@ function wp_get_image_mime( $file ) {
* specification and the AV1-AVIF spec, see https://aomediacodec.github.io/av1-avif/v1.1.0.html#brands.
*/
// Divide the header string into 4 byte groups.
// Divide the header string into 4 byte groups.
$magic = str_split( $magic, 8 );
if (

View File

@ -986,6 +986,7 @@ function wp_get_active_and_valid_plugins() {
$network_plugins = is_multisite() ? wp_get_active_network_plugins() : false;
$invalid_plugins = array();
foreach ( $active_plugins as $plugin ) {
if ( ! validate_file( $plugin ) // $plugin must validate as file.
&& str_ends_with( $plugin, '.php' ) // $plugin must end with '.php'.
@ -994,6 +995,20 @@ function wp_get_active_and_valid_plugins() {
&& ( ! $network_plugins || ! in_array( WP_PLUGIN_DIR . '/' . $plugin, $network_plugins, true ) )
) {
$plugins[] = WP_PLUGIN_DIR . '/' . $plugin;
} else {
$invalid_plugins[] = $plugin;
}
}
if ( ! empty( $invalid_plugins ) ) {
$all_plugin_data = get_option( 'plugin_data', array() );
if ( ! empty( $all_plugin_data ) ) {
foreach ( $invalid_plugins as $invalid_plugin ) {
unset( $all_plugin_data[ $invalid_plugin ] );
}
update_option( 'plugin_data', $all_plugin_data );
}
}
@ -1190,6 +1205,7 @@ function is_protected_ajax_action() {
'search-install-plugins', // Searching for a plugin in the plugin install screen.
'update-plugin', // Update an existing plugin.
'update-theme', // Update an existing theme.
'activate-plugin', // Activating an existing plugin.
);
/**

View File

@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
$wp_version = '6.5-alpha-57544';
$wp_version = '6.5-alpha-57545';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.

View File

@ -412,6 +412,12 @@ wp_plugin_directory_constants();
$GLOBALS['wp_plugin_paths'] = array();
// Load and initialize WP_Plugin_Dependencies.
require_once ABSPATH . WPINC . '/class-wp-plugin-dependencies.php';
if ( ! defined( 'WP_RUN_CORE_TESTS' ) ) {
WP_Plugin_Dependencies::initialize();
}
// Load must-use plugins.
foreach ( wp_get_mu_plugins() as $mu_plugin ) {
$_wp_plugin_file = $mu_plugin;
@ -486,7 +492,91 @@ if ( ! is_multisite() && wp_is_fatal_error_handler_enabled() ) {
}
// Load active plugins.
$all_plugin_data = get_option( 'plugin_data', array() );
$failed_plugins = array();
$update_plugin_data = false;
foreach ( wp_get_active_and_valid_plugins() as $plugin ) {
$plugin_file = str_replace( trailingslashit( WP_PLUGIN_DIR ), '', $plugin );
if ( ! isset( $all_plugin_data[ $plugin_file ] ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$all_plugin_data[ $plugin_file ] = get_plugin_data( WP_PLUGIN_DIR . "/$plugin_file" );
$update_plugin_data = true;
}
$plugin_headers = $all_plugin_data[ $plugin_file ];
$errors = array();
$requirements = array(
'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'] );
$dependencies_met = ! WP_Plugin_Dependencies::has_unmet_dependencies( $plugin_file );
$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();
if ( $annotation ) {
$php_update_message .= '</p><p><em>' . $annotation . '</em>';
}
if ( ! $dependencies_met ) {
$errors[] = sprintf(
/* translators: %s: The plugin's name. */
_x( '%s has unmet dependencies.', 'plugin' ),
$plugin_headers['Name']
);
}
if ( ! $compatible_wp && ! $compatible_php ) {
$errors[] = sprintf(
/* 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' ),
PHP_VERSION,
$plugin_headers['Name'],
$requirements['requires'],
$requirements['requires_php']
) . $php_update_message;
} elseif ( ! $compatible_php ) {
$errors[] = sprintf(
/* 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' ),
PHP_VERSION,
$plugin_headers['Name'],
$requirements['requires_php']
) . $php_update_message;
} elseif ( ! $compatible_wp ) {
$errors[] = sprintf(
/* 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' ),
$plugin_headers['Name'],
$requirements['requires']
);
}
if ( ! empty( $errors ) ) {
$failed_plugins[ $plugin_file ] = '';
foreach ( $errors as $error ) {
$failed_plugins[ $plugin_file ] .= wp_get_admin_notice(
$error,
array(
'type' => 'error',
'dismissible' => true,
)
);
}
continue;
}
wp_register_plugin_realpath( $plugin );
$_wp_plugin_file = $plugin;
@ -504,6 +594,24 @@ foreach ( wp_get_active_and_valid_plugins() as $plugin ) {
}
unset( $plugin, $_wp_plugin_file );
if ( $update_plugin_data ) {
update_option( 'plugin_data', $all_plugin_data );
}
if ( ! empty( $failed_plugins ) ) {
add_action(
'admin_notices',
function () use ( $failed_plugins ) {
global $pagenow;
if ( 'index.php' === $pagenow || 'plugins.php' === $pagenow ) {
echo implode( '', $failed_plugins );
}
}
);
}
unset( $failed_plugins );
// Load pluggable functions.
require ABSPATH . WPINC . '/pluggable.php';
require ABSPATH . WPINC . '/pluggable-deprecated.php';