* Functions for ajaxified updates, deletions and installs inside the WordPress admin.
* @output wp-admin/js/updates.js
* @param {jQuery} $ jQuery object.
* @param {object} wp WP object.
* @param {object} settings WP Updates settings.
* @param {string} settings.ajax_nonce Ajax nonce.
* @param {object=} settings.plugins Base names of plugins in their different states.
* @param {Array} settings.plugins.all Base names of all plugins.
* @param {Array} settings.plugins.active Base names of active plugins.
* @param {Array} settings.plugins.inactive Base names of inactive plugins.
* @param {Array} settings.plugins.upgrade Base names of plugins with updates available.
* @param {Array} settings.plugins.recently_activated Base names of recently activated plugins.
* @param {Array} settings.plugins['auto-update-enabled'] Base names of plugins set to auto-update.
* @param {Array} settings.plugins['auto-update-disabled'] Base names of plugins set to not auto-update.
* @param {object=} settings.themes Slugs of themes in their different states.
* @param {Array} settings.themes.all Slugs of all themes.
* @param {Array} settings.themes.upgrade Slugs of themes with updates available.
* @param {Arrat} settings.themes.disabled Slugs of disabled themes.
* @param {Array} settings.themes['auto-update-enabled'] Slugs of themes set to auto-update.
* @param {Array} settings.themes['auto-update-disabled'] Slugs of themes set to not auto-update.
* @param {object=} settings.totals Combined information for available update counts.
* @param {number} settings.totals.count Holds the amount of available updates.
(function( $, wp, settings ) {
var $document = $( document ),
sprintf = wp.i18n.sprintf;
* Removed in 5.5.0, needed for back-compatibility.
pluginUpdateNowLabel: '',
pluginUpdateFailedLabel: '',
pluginInstallNowLabel: '',
pluginInstallingLabel: '',
themeInstallingLabel: '',
pluginInstalledLabel: '',
pluginInstallFailedLabel: '',
themeInstallFailedLabel: '',
importerInstalledMsg: '',
activateImporterLabel: '',
autoUpdatesDisabling: '',
wp.updates.l10n = window.wp.deprecateL10nObject( 'wp.updates.l10n', wp.updates.l10n, '5.5.0' );
* User nonce for ajax calls.
wp.updates.ajaxNonce = settings.ajax_nonce;
wp.updates.searchTerm = '';
* Whether filesystem credentials need to be requested from the user.
wp.updates.shouldRequestFilesystemCredentials = false;
* Filesystem credentials to be packaged along with the request.
* @since 4.6.0 Added `available` property to indicate whether credentials have been provided.
* @property {Object} filesystemCredentials.ftp Holds FTP credentials.
* @property {string} filesystemCredentials.ftp.host FTP host. Default empty string.
* @property {string} filesystemCredentials.ftp.username FTP user name. Default empty string.
* @property {string} filesystemCredentials.ftp.password FTP password. Default empty string.
* @property {string} filesystemCredentials.ftp.connectionType Type of FTP connection. 'ssh', 'ftp', or 'ftps'.
* @property {Object} filesystemCredentials.ssh Holds SSH credentials.
* @property {string} filesystemCredentials.ssh.publicKey The public key. Default empty string.
* @property {string} filesystemCredentials.ssh.privateKey The private key. Default empty string.
* @property {string} filesystemCredentials.fsNonce Filesystem credentials form nonce.
* @property {bool} filesystemCredentials.available Whether filesystem credentials have been provided.
wp.updates.filesystemCredentials = {
* Whether we're waiting for an Ajax request to complete.
* @since 4.6.0 More accurately named `ajaxLocked`.
wp.updates.ajaxLocked = false;
wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
* If the user tries to update a plugin while an update is
* already happening, it can be placed in this queue to perform later.
* @since 4.6.0 More accurately named `queue`.
* Holds a jQuery reference to return focus to when exiting the request credentials modal.
wp.updates.$elToReturnFocusToFromCredentialsModal = undefined;
* Adds or updates an admin notice.
* @param {*=} data.selector Optional. Selector of an element to be replaced with the admin notice.
* @param {string=} data.id Optional. Unique id that will be used as the notice's id attribute.
* @param {string=} data.className Optional. Class names that will be used in the admin notice.
* @param {string=} data.message Optional. The message displayed in the notice.
* @param {number=} data.successes Optional. The amount of successful operations.
* @param {number=} data.errors Optional. The amount of failed operations.
* @param {Array=} data.errorMessages Optional. Error messages of failed operations.
wp.updates.addAdminNotice = function( data ) {
var $notice = $( data.selector ),
$headerEnd = $( '.wp-header-end' ),
$adminNotice = wp.updates.adminNotice( data );
// Check if this admin notice already exists.
if ( ! $notice.length ) {
$notice = $( '#' + data.id );
$notice.replaceWith( $adminNotice );
} else if ( $headerEnd.length ) {
$headerEnd.after( $adminNotice );
if ( 'customize' === pagenow ) {
$( '.customize-themes-notifications' ).append( $adminNotice );
$( '.wrap' ).find( '> h1' ).after( $adminNotice );
$document.trigger( 'wp-updates-notice-added' );
* Handles Ajax requests to WordPress.
* @param {string} action The type of Ajax request ('update-plugin', 'install-theme', etc).
* @param {Object} data Data that needs to be passed to the ajax callback.
* @return {$.promise} A jQuery promise that represents the request,
* decorated with an abort() method.
wp.updates.ajax = function( action, data ) {
if ( wp.updates.ajaxLocked ) {
// Return a Deferred object so callbacks can always be registered.
wp.updates.ajaxLocked = true;
options.success = data.success;
options.error = data.error;
options.data = _.extend( data, {
_ajax_nonce: wp.updates.ajaxNonce,
_fs_nonce: wp.updates.filesystemCredentials.fsNonce,
username: wp.updates.filesystemCredentials.ftp.username,
password: wp.updates.filesystemCredentials.ftp.password,
hostname: wp.updates.filesystemCredentials.ftp.hostname,
connection_type: wp.updates.filesystemCredentials.ftp.connectionType,
public_key: wp.updates.filesystemCredentials.ssh.publicKey,
private_key: wp.updates.filesystemCredentials.ssh.privateKey
return wp.ajax.send( options ).always( wp.updates.ajaxAlways );
* Actions performed after every Ajax request.
* @param {Object} response
* @param {Array=} response.debug Optional. Debug information.
* @param {string=} response.errorCode Optional. Error code for an error that occurred.
wp.updates.ajaxAlways = function( response ) {
if ( ! response.errorCode || 'unable_to_connect_to_filesystem' !== response.errorCode ) {
wp.updates.ajaxLocked = false;
wp.updates.queueChecker();
if ( 'undefined' !== typeof response.debug && window.console && window.console.log ) {
_.map( response.debug, function( message ) {
// Remove all HTML tags and write a message to the console.
window.console.log( wp.sanitize.stripTagsAndEncodeText( message ) );
* Refreshes update counts everywhere on the screen.
wp.updates.refreshCount = function() {
var $adminBarUpdates = $( '#wp-admin-bar-updates' ),
$dashboardNavMenuUpdateCount = $( 'a[href="update-core.php"] .update-plugins' ),
$pluginsNavMenuUpdateCount = $( 'a[href="plugins.php"] .update-plugins' ),
$appearanceNavMenuUpdateCount = $( 'a[href="themes.php"] .update-plugins' ),
$adminBarUpdates.find( '.ab-item' ).removeAttr( 'title' );
$adminBarUpdates.find( '.ab-label' ).text( settings.totals.counts.total );
// Remove the update count from the toolbar if it's zero.
if ( 0 === settings.totals.counts.total ) {
$adminBarUpdates.find( '.ab-label' ).parents( 'li' ).remove();
// Update the "Updates" menu item.
$dashboardNavMenuUpdateCount.each( function( index, element ) {
element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.total );
if ( settings.totals.counts.total > 0 ) {
$dashboardNavMenuUpdateCount.find( '.update-count' ).text( settings.totals.counts.total );
$dashboardNavMenuUpdateCount.remove();
// Update the "Plugins" menu item.
$pluginsNavMenuUpdateCount.each( function( index, element ) {
element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.plugins );
if ( settings.totals.counts.total > 0 ) {
$pluginsNavMenuUpdateCount.find( '.plugin-count' ).text( settings.totals.counts.plugins );
$pluginsNavMenuUpdateCount.remove();
// Update the "Appearance" menu item.
$appearanceNavMenuUpdateCount.each( function( index, element ) {
element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.themes );
if ( settings.totals.counts.total > 0 ) {
$appearanceNavMenuUpdateCount.find( '.theme-count' ).text( settings.totals.counts.themes );
$appearanceNavMenuUpdateCount.remove();
// Update list table filter navigation.
if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
itemCount = settings.totals.counts.plugins;
} else if ( 'themes' === pagenow || 'themes-network' === pagenow ) {
itemCount = settings.totals.counts.themes;
$( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' );
$( '.subsubsub .upgrade' ).remove();
$( '.subsubsub li:last' ).html( function() { return $( this ).children(); } );
* Decrements the update counts throughout the various menus.
* This includes the toolbar, the "Updates" menu item and the menu items
* for plugins and themes.
* @param {string} type The type of item that was updated or deleted.
* Can be 'plugin', 'theme'.
wp.updates.decrementCount = function( type ) {
settings.totals.counts.total = Math.max( --settings.totals.counts.total, 0 );
if ( 'plugin' === type ) {
settings.totals.counts.plugins = Math.max( --settings.totals.counts.plugins, 0 );
} else if ( 'theme' === type ) {
settings.totals.counts.themes = Math.max( --settings.totals.counts.themes, 0 );
wp.updates.refreshCount( type );
* Sends an Ajax request to the server to update a plugin.
* @since 4.6.0 More accurately named `updatePlugin`.
* @param {Object} args Arguments.
* @param {string} args.plugin Plugin basename.
* @param {string} args.slug Plugin slug.
* @param {updatePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.updatePluginSuccess
* @param {updatePluginError=} args.error Optional. Error callback. Default: wp.updates.updatePluginError
* @return {$.promise} A jQuery promise that represents the request,
* decorated with an abort() method.
wp.updates.updatePlugin = function( args ) {
var $updateRow, $card, $message, message,
$adminBarUpdates = $( '#wp-admin-bar-updates' );
success: wp.updates.updatePluginSuccess,
error: wp.updates.updatePluginError
if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
$updateRow = $( 'tr[data-plugin="' + args.plugin + '"]' );
$message = $updateRow.find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
/* translators: %s: Plugin name and version. */
_x( 'Updating %s...', 'plugin' ),
$updateRow.find( '.plugin-title strong' ).text()
} else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
$card = $( '.plugin-card-' + args.slug );
$message = $card.find( '.update-now' ).addClass( 'updating-message' );
/* translators: %s: Plugin name and version. */
_x( 'Updating %s...', 'plugin' ),
// Remove previous error messages, if any.
$card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove();
$adminBarUpdates.addClass( 'spin' );
if ( $message.html() !== __( 'Updating...' ) ) {
$message.data( 'originaltext', $message.html() );
.attr( 'aria-label', message )
.text( __( 'Updating...' ) );
$document.trigger( 'wp-plugin-updating', args );
return wp.updates.ajax( 'update-plugin', args );
* Updates the UI appropriately after a successful plugin update.
* @since 4.6.0 More accurately named `updatePluginSuccess`.
* @since 5.5.0 Auto-update "time to next update" text cleared.
* @param {Object} response Response from the server.
* @param {string} response.slug Slug of the plugin to be updated.
* @param {string} response.plugin Basename of the plugin to be updated.
* @param {string} response.pluginName Name of the plugin to be updated.