wp/wp-admin/js/updates.js
changeset 21 48c4eec2b7e6
parent 19 3d72ae0968f4
child 22 8c2e4d02f4ef
--- a/wp/wp-admin/js/updates.js	Thu Sep 29 08:06:27 2022 +0200
+++ b/wp/wp-admin/js/updates.js	Fri Sep 05 18:40:08 2025 +0200
@@ -5,7 +5,7 @@
  * @output wp-admin/js/updates.js
  */
 
-/* global pagenow */
+/* global pagenow, _wpThemeSettings */
 
 /**
  * @param {jQuery}  $                                        jQuery object.
@@ -414,6 +414,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.
 	 *
 	 * This includes the toolbar, the "Updates" menu item and the menu items
@@ -452,7 +477,9 @@
 	 */
 	wp.updates.updatePlugin = function( args ) {
 		var $updateRow, $card, $message, message,
-			$adminBarUpdates = $( '#wp-admin-bar-updates' );
+			$adminBarUpdates = $( '#wp-admin-bar-updates' ),
+			buttonText = __( 'Updating...' ),
+			isPluginInstall = 'plugin-install' === pagenow || 'plugin-install-network' === pagenow;
 
 		args = _.extend( {
 			success: wp.updates.updatePluginSuccess,
@@ -467,8 +494,8 @@
  				_x( 'Updating %s...', 'plugin' ),
 				$updateRow.find( '.plugin-title strong' ).text()
 			);
-		} else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
-			$card    = $( '.plugin-card-' + args.slug );
+		} else if ( isPluginInstall ) {
+			$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 +515,22 @@
 
 		$message
 			.attr( 'aria-label', message )
-			.text( __( 'Updating...' ) );
+			.text( buttonText );
 
 		$document.trigger( 'wp-plugin-updating', args );
 
+		if ( isPluginInstall && '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,11 +550,17 @@
 	 */
 	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 + '"]' )
-				.removeClass( 'update' )
+				.removeClass( 'update is-enqueued' )
 				.addClass( 'updated' );
 			$updateMessage = $pluginRow.find( '.update-message' )
 				.removeClass( 'updating-message notice-warning' )
@@ -528,7 +573,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 +581,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 +618,7 @@
 	 * @param {string}  response.errorMessage The error that occurred.
 	 */
 	wp.updates.updatePluginError = function( response ) {
-		var $card, $message, errorMessage,
+		var $pluginRow, $card, $message, errorMessage, buttonText, ariaLabel,
 			$adminBarUpdates = $( '#wp-admin-bar-updates' );
 
 		if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
@@ -585,6 +636,8 @@
 		);
 
 		if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
+			$pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' ).removeClass( 'is-enqueued' );
+
 			if ( response.plugin ) {
 				$message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' );
 			} else {
@@ -606,28 +659,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' );
 			}
 
@@ -650,6 +707,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 );
 	};
 
@@ -666,8 +735,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,
@@ -682,17 +753,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.' ) );
 
@@ -701,6 +771,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 );
 	};
 
@@ -715,20 +797,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.' ) );
 
@@ -736,36 +817,23 @@
 
 		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
 				}
-			}, 1000 );
+			);
 		}
 	};
 
@@ -781,8 +849,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' ) ) {
@@ -801,7 +875,7 @@
 
 		$card
 			.addClass( 'plugin-card-update-failed' )
-			.append( '<div class="notice notice-error notice-alt is-dismissible"><p>' + errorMessage + '</p></div>' );
+			.append( '<div class="notice notice-error notice-alt is-dismissible" role="alert"><p>' + errorMessage + '</p></div>' );
 
 		$card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
 
@@ -815,19 +889,332 @@
 
 		$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', 'plugin' );
+			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 = _x( 'Activate', 'plugin' ),
+			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-plugin-install-error', response );
+		$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: __( 'Activating...' ),
+					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
+				}
+			);
+		}
 	};
 
 	/**
@@ -913,7 +1300,7 @@
 					pluginName
 				)
 			)
-			.text( __( 'Install Now' ) );
+			.text( _x( 'Install Now', 'plugin' ) );
 
 		wp.a11y.speak( errorMessage, 'assertive' );
 
@@ -1067,7 +1454,7 @@
 				$itemsCount.text(
 					sprintf(
 						/* translators: %s: The remaining number of plugins. */
-						_nx( '%s item', '%s items', 'plugin/plugins', remainingCount ),
+						_nx( '%s item', '%s items', remainingCount, 'plugin/plugins'  ),
 						remainingCount
 					)
 				);
@@ -1416,7 +1803,7 @@
 								response.themeName
 							)
 						)
-						.text( __( 'Activate' ) );
+						.text( _x( 'Activate', 'theme' ) );
 				}
 			}
 
@@ -1614,6 +2001,14 @@
 			} );
 		}
 
+		// DecrementCount from update count.
+		if ( 'themes' === pagenow ) {
+		    var theme = _.find( _wpThemeSettings.themes, { id: response.slug } );
+		    if ( theme.hasUpdate ) {
+		        wp.updates.decrementCount( 'theme' );
+		    }
+		}
+
 		wp.a11y.speak( _x( 'Deleted!', 'theme' ) );
 
 		$document.trigger( 'wp-theme-delete-success', response );
@@ -1865,7 +2260,7 @@
 
 		// Remove any existing error.
 		$filesystemForm.find( '.notice' ).remove();
-		$filesystemForm.find( '#request-filesystem-credentials-title' ).after( '<div class="notice notice-alt notice-error"><p>' + message + '</p></div>' );
+		$filesystemForm.find( '#request-filesystem-credentials-title' ).after( '<div class="notice notice-alt notice-error" role="alert"><p>' + message + '</p></div>' );
 	};
 
 	/**
@@ -1960,6 +2355,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' );
@@ -2015,7 +2420,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' ),
@@ -2220,7 +2625,7 @@
 
 					$message
 						.removeClass( 'updating-message' )
-						.text( __( 'Install Now' ) );
+						.text( _x( 'Install Now', 'plugin' ) );
 
 					wp.a11y.speak( __( 'Update canceled.' ) );
 				} );
@@ -2232,6 +2637,19 @@
 		} );
 
 		/**
+		 * Click handler for plugin activations in plugin activation modal view.
+		 *
+		 * @since 6.5.0
+		 * @since 6.5.4 Redirect the parent window to the activation URL.
+		 *
+		 * @param {Event} event Event interface.
+		 */
+		$document.on( 'click', '#plugin-information-footer .activate-now', function( event ) {
+			event.preventDefault();
+			window.parent.location.href = $( event.target ).attr( 'href' );
+		});
+
+		/**
 		 * Click handler for importer plugins installs in the Import screen.
 		 *
 		 * @since 4.6.0
@@ -2263,7 +2681,7 @@
 								pluginName
 							)
 						)
-						.text( __( 'Install Now' ) );
+						.text( _x( 'Install Now', 'plugin' ) );
 
 					wp.a11y.speak( __( 'Update canceled.' ) );
 				} );
@@ -2460,6 +2878,13 @@
 					return;
 				}
 
+				// Don't add items to the update queue again, even if the user clicks the update button several times.
+				if ( 'update-selected' === bulkAction && $itemRow.hasClass( 'is-enqueued' ) ) {
+					return;
+				}
+
+				$itemRow.addClass( 'is-enqueued' );
+
 				// Add it to the queue.
 				wp.updates.queue.push( {
 					action: action,
@@ -2488,13 +2913,41 @@
 
 				wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' );
 
+				var successMessage = null;
+
+				if ( success ) {
+					if ( 'plugin' === response.update ) {
+						successMessage = sprintf(
+							/* translators: %s: Number of plugins. */
+							_n( '%s plugin successfully updated.', '%s plugins successfully updated.', success ),
+							success
+						);
+					} else {
+						successMessage = sprintf(
+							/* translators: %s: Number of themes. */
+							_n( '%s theme successfully updated.', '%s themes successfully updated.', success ),
+							success
+						);
+					}
+				}
+
+				var errorMessage = null;
+
+				if ( error ) {
+					errorMessage = sprintf(
+						/* translators: %s: Number of failed updates. */
+						_n( '%s update failed.', '%s updates failed.', error ),
+						error
+					);
+				}
+
 				wp.updates.addAdminNotice( {
 					id:            'bulk-action-notice',
 					className:     'bulk-action-notice',
-					successes:     success,
-					errors:        error,
-					errorMessages: errorMessages,
-					type:          response.update
+					successMessage: successMessage,
+					errorMessage:   errorMessage,
+					errorMessages:  errorMessages,
+					type:           response.update
 				} );
 
 				$bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() {
@@ -2535,7 +2988,7 @@
 
 			data = {
 				_ajax_nonce: wp.updates.ajaxNonce,
-				s:           event.target.value,
+				s:           encodeURIComponent( event.target.value ),
 				tab:         'search',
 				type:        $( '#typeselector' ).val(),
 				pagenow:     pagenow
@@ -2612,7 +3065,7 @@
 		$pluginSearch.on( 'keyup input', _.debounce( function( event ) {
 			var data = {
 				_ajax_nonce:   wp.updates.ajaxNonce,
-				s:             event.target.value,
+				s:             encodeURIComponent( event.target.value ),
 				pagenow:       pagenow,
 				plugin_status: 'all'
 			},
@@ -2654,7 +3107,7 @@
 					sprintf(
 						/* translators: %s: Search query. */
 						__( 'Search results for: %s' ),
-						'<strong>' + _.escape( data.s ) + '</strong>'
+						'<strong>' + _.escape( decodeURIComponent( data.s ) ) + '</strong>'
 					) ),
 					$oldSubTitle = $( '.wrap .subtitle' );
 
@@ -2750,35 +3203,6 @@
 		} );
 
 		/**
-		 * 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.
 		 *
 		 * @since 4.2.0
@@ -2801,7 +3225,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 ) {
 				return;
 			}
 
@@ -2815,9 +3277,9 @@
 
 				case 'install-plugin':
 				case 'update-plugin':
-					/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
-					window.tb_remove();
-					/* jscs:enable */
+					if ( 'undefined' === typeof message.data || 'undefined' === typeof message.data.slug ) {
+						return;
+					}
 
 					message.data = wp.updates._addCallbacks( message.data, message.action );