summaryrefslogtreecommitdiffstats
path: root/wp-admin/js/updates.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 07:56:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 07:56:49 +0000
commita415c29efee45520ae252d2aa28f1083a521cd7b (patch)
treef4ade4b6668ecc0765de7e1424f7c1427ad433ff /wp-admin/js/updates.js
parentInitial commit. (diff)
downloadwordpress-a415c29efee45520ae252d2aa28f1083a521cd7b.tar.xz
wordpress-a415c29efee45520ae252d2aa28f1083a521cd7b.zip
Adding upstream version 6.4.3+dfsg1.upstream/6.4.3+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--wp-admin/js/updates.js3020
1 files changed, 3020 insertions, 0 deletions
diff --git a/wp-admin/js/updates.js b/wp-admin/js/updates.js
new file mode 100644
index 0000000..a994fda
--- /dev/null
+++ b/wp-admin/js/updates.js
@@ -0,0 +1,3020 @@
+/**
+ * Functions for ajaxified updates, deletions and installs inside the WordPress admin.
+ *
+ * @version 4.2.0
+ * @output wp-admin/js/updates.js
+ */
+
+/* global pagenow, _wpThemeSettings */
+
+/**
+ * @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 ),
+ __ = wp.i18n.__,
+ _x = wp.i18n._x,
+ _n = wp.i18n._n,
+ _nx = wp.i18n._nx,
+ sprintf = wp.i18n.sprintf;
+
+ wp = wp || {};
+
+ /**
+ * The WP Updates object.
+ *
+ * @since 4.2.0
+ *
+ * @namespace wp.updates
+ */
+ wp.updates = {};
+
+ /**
+ * Removed in 5.5.0, needed for back-compatibility.
+ *
+ * @since 4.2.0
+ * @deprecated 5.5.0
+ *
+ * @type {object}
+ */
+ wp.updates.l10n = {
+ searchResults: '',
+ searchResultsLabel: '',
+ noPlugins: '',
+ noItemsSelected: '',
+ updating: '',
+ pluginUpdated: '',
+ themeUpdated: '',
+ update: '',
+ updateNow: '',
+ pluginUpdateNowLabel: '',
+ updateFailedShort: '',
+ updateFailed: '',
+ pluginUpdatingLabel: '',
+ pluginUpdatedLabel: '',
+ pluginUpdateFailedLabel: '',
+ updatingMsg: '',
+ updatedMsg: '',
+ updateCancel: '',
+ beforeunload: '',
+ installNow: '',
+ pluginInstallNowLabel: '',
+ installing: '',
+ pluginInstalled: '',
+ themeInstalled: '',
+ installFailedShort: '',
+ installFailed: '',
+ pluginInstallingLabel: '',
+ themeInstallingLabel: '',
+ pluginInstalledLabel: '',
+ themeInstalledLabel: '',
+ pluginInstallFailedLabel: '',
+ themeInstallFailedLabel: '',
+ installingMsg: '',
+ installedMsg: '',
+ importerInstalledMsg: '',
+ aysDelete: '',
+ aysDeleteUninstall: '',
+ aysBulkDelete: '',
+ aysBulkDeleteThemes: '',
+ deleting: '',
+ deleteFailed: '',
+ pluginDeleted: '',
+ themeDeleted: '',
+ livePreview: '',
+ activatePlugin: '',
+ activateTheme: '',
+ activatePluginLabel: '',
+ activateThemeLabel: '',
+ activateImporter: '',
+ activateImporterLabel: '',
+ unknownError: '',
+ connectionError: '',
+ nonceError: '',
+ pluginsFound: '',
+ noPluginsFound: '',
+ autoUpdatesEnable: '',
+ autoUpdatesEnabling: '',
+ autoUpdatesEnabled: '',
+ autoUpdatesDisable: '',
+ autoUpdatesDisabling: '',
+ autoUpdatesDisabled: '',
+ autoUpdatesError: ''
+ };
+
+ wp.updates.l10n = window.wp.deprecateL10nObject( 'wp.updates.l10n', wp.updates.l10n, '5.5.0' );
+
+ /**
+ * User nonce for ajax calls.
+ *
+ * @since 4.2.0
+ *
+ * @type {string}
+ */
+ wp.updates.ajaxNonce = settings.ajax_nonce;
+
+ /**
+ * Current search term.
+ *
+ * @since 4.6.0
+ *
+ * @type {string}
+ */
+ wp.updates.searchTerm = '';
+
+ /**
+ * Whether filesystem credentials need to be requested from the user.
+ *
+ * @since 4.2.0
+ *
+ * @type {bool}
+ */
+ wp.updates.shouldRequestFilesystemCredentials = false;
+
+ /**
+ * Filesystem credentials to be packaged along with the request.
+ *
+ * @since 4.2.0
+ * @since 4.6.0 Added `available` property to indicate whether credentials have been provided.
+ *
+ * @type {Object}
+ * @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'.
+ * Default empty string.
+ * @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.
+ * Default 'false'.
+ */
+ wp.updates.filesystemCredentials = {
+ ftp: {
+ host: '',
+ username: '',
+ password: '',
+ connectionType: ''
+ },
+ ssh: {
+ publicKey: '',
+ privateKey: ''
+ },
+ fsNonce: '',
+ available: false
+ };
+
+ /**
+ * Whether we're waiting for an Ajax request to complete.
+ *
+ * @since 4.2.0
+ * @since 4.6.0 More accurately named `ajaxLocked`.
+ *
+ * @type {bool}
+ */
+ wp.updates.ajaxLocked = false;
+
+ /**
+ * Admin notice template.
+ *
+ * @since 4.6.0
+ *
+ * @type {function}
+ */
+ wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
+
+ /**
+ * Update queue.
+ *
+ * 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.2.0
+ * @since 4.6.0 More accurately named `queue`.
+ *
+ * @type {Array.object}
+ */
+ wp.updates.queue = [];
+
+ /**
+ * Holds a jQuery reference to return focus to when exiting the request credentials modal.
+ *
+ * @since 4.2.0
+ *
+ * @type {jQuery}
+ */
+ wp.updates.$elToReturnFocusToFromCredentialsModal = undefined;
+
+ /**
+ * Adds or updates an admin notice.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} data
+ * @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;
+
+ delete data.selector;
+ $adminNotice = wp.updates.adminNotice( data );
+
+ // Check if this admin notice already exists.
+ if ( ! $notice.length ) {
+ $notice = $( '#' + data.id );
+ }
+
+ if ( $notice.length ) {
+ $notice.replaceWith( $adminNotice );
+ } else if ( $headerEnd.length ) {
+ $headerEnd.after( $adminNotice );
+ } else {
+ if ( 'customize' === pagenow ) {
+ $( '.customize-themes-notifications' ).append( $adminNotice );
+ } else {
+ $( '.wrap' ).find( '> h1' ).after( $adminNotice );
+ }
+ }
+
+ $document.trigger( 'wp-updates-notice-added' );
+ };
+
+ /**
+ * Handles Ajax requests to WordPress.
+ *
+ * @since 4.6.0
+ *
+ * @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 ) {
+ var options = {};
+
+ if ( wp.updates.ajaxLocked ) {
+ wp.updates.queue.push( {
+ action: action,
+ data: data
+ } );
+
+ // Return a Deferred object so callbacks can always be registered.
+ return $.Deferred();
+ }
+
+ wp.updates.ajaxLocked = true;
+
+ if ( data.success ) {
+ options.success = data.success;
+ delete data.success;
+ }
+
+ if ( data.error ) {
+ options.error = data.error;
+ delete data.error;
+ }
+
+ options.data = _.extend( data, {
+ action: action,
+ _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.
+ *
+ * @since 4.6.0
+ *
+ * @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.
+ *
+ * @since 4.7.0
+ */
+ 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' ),
+ itemCount;
+
+ $adminBarUpdates.find( '.ab-label' ).text( settings.totals.counts.total );
+ $adminBarUpdates.find( '.updates-available-text' ).text(
+ sprintf(
+ /* translators: %s: Total number of updates available. */
+ _n( '%s update available', '%s updates available', settings.totals.counts.total ),
+ 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 );
+ } else {
+ $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 );
+ } else {
+ $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 );
+ } else {
+ $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;
+ }
+
+ if ( itemCount > 0 ) {
+ $( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' );
+ } else {
+ $( '.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.
+ *
+ * @since 3.9.0
+ *
+ * @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.2.0
+ * @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' );
+
+ args = _.extend( {
+ success: wp.updates.updatePluginSuccess,
+ error: wp.updates.updatePluginError
+ }, args );
+
+ 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' );
+ message = sprintf(
+ /* 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' );
+ message = sprintf(
+ /* translators: %s: Plugin name and version. */
+ _x( 'Updating %s...', 'plugin' ),
+ $message.data( 'name' )
+ );
+
+ // 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() );
+ }
+
+ $message
+ .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.2.0
+ * @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.
+ * @param {string} response.oldVersion Old version of the plugin.
+ * @param {string} response.newVersion New version of the plugin.
+ */
+ wp.updates.updatePluginSuccess = function( response ) {
+ var $pluginRow, $updateMessage, newText,
+ $adminBarUpdates = $( '#wp-admin-bar-updates' );
+
+ if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
+ $pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' )
+ .removeClass( 'update is-enqueued' )
+ .addClass( 'updated' );
+ $updateMessage = $pluginRow.find( '.update-message' )
+ .removeClass( 'updating-message notice-warning' )
+ .addClass( 'updated-message notice-success' ).find( 'p' );
+
+ // Update the version number in the row.
+ newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
+ $pluginRow.find( '.plugin-version-author-uri' ).html( newText );
+
+ // 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' )
+ .removeClass( 'updating-message' )
+ .addClass( 'button-disabled updated-message' );
+ }
+
+ $adminBarUpdates.removeClass( 'spin' );
+
+ $updateMessage
+ .attr(
+ 'aria-label',
+ sprintf(
+ /* translators: %s: Plugin name and version. */
+ _x( '%s updated!', 'plugin' ),
+ response.pluginName
+ )
+ )
+ .text( _x( 'Updated!', 'plugin' ) );
+
+ wp.a11y.speak( __( 'Update completed successfully.' ) );
+
+ wp.updates.decrementCount( 'plugin' );
+
+ $document.trigger( 'wp-plugin-update-success', response );
+ };
+
+ /**
+ * Updates the UI appropriately after a failed plugin update.
+ *
+ * @since 4.2.0
+ * @since 4.6.0 More accurately named `updatePluginError`.
+ *
+ * @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 Optional. Name of the plugin to be updated.
+ * @param {string} response.errorCode Error code for the error that occurred.
+ * @param {string} response.errorMessage The error that occurred.
+ */
+ wp.updates.updatePluginError = function( response ) {
+ var $pluginRow, $card, $message, errorMessage,
+ $adminBarUpdates = $( '#wp-admin-bar-updates' );
+
+ if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
+ return;
+ }
+
+ if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) {
+ return;
+ }
+
+ errorMessage = sprintf(
+ /* translators: %s: Error string for a failed update. */
+ __( 'Update failed: %s' ),
+ response.errorMessage
+ );
+
+ 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 {
+ $message = $( 'tr[data-slug="' + response.slug + '"]' ).find( '.update-message' );
+ }
+ $message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage );
+
+ if ( response.pluginName ) {
+ $message.find( 'p' )
+ .attr(
+ 'aria-label',
+ sprintf(
+ /* translators: %s: Plugin name and version. */
+ _x( '%s update failed.', 'plugin' ),
+ response.pluginName
+ )
+ );
+ } else {
+ $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' )
+ .append( wp.updates.adminNotice( {
+ className: 'update-message notice-error notice-alt is-dismissible',
+ message: errorMessage
+ } ) );
+
+ $card.find( '.update-now' )
+ .text( __( 'Update failed.' ) )
+ .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
+ )
+ );
+ } else {
+ $card.find( '.update-now' ).removeAttr( 'aria-label' );
+ }
+
+ $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
+
+ // Use same delay as the total duration of the notice fadeTo + slideUp animation.
+ setTimeout( function() {
+ $card
+ .removeClass( 'plugin-card-update-failed' )
+ .find( '.column-name a' ).trigger( 'focus' );
+
+ $card.find( '.update-now' )
+ .attr( 'aria-label', false )
+ .text( __( 'Update Now' ) );
+ }, 200 );
+ } );
+ }
+
+ $adminBarUpdates.removeClass( 'spin' );
+
+ wp.a11y.speak( errorMessage, 'assertive' );
+
+ $document.trigger( 'wp-plugin-update-error', response );
+ };
+
+ /**
+ * Sends an Ajax request to the server to install a plugin.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} args Arguments.
+ * @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository.
+ * @param {installPluginSuccess=} args.success Optional. Success callback. Default: wp.updates.installPluginSuccess
+ * @param {installPluginError=} args.error Optional. Error callback. Default: wp.updates.installPluginError
+ * @return {$.promise} A jQuery promise that represents the request,
+ * decorated with an abort() method.
+ */
+ wp.updates.installPlugin = function( args ) {
+ var $card = $( '.plugin-card-' + args.slug ),
+ $message = $card.find( '.install-now' );
+
+ args = _.extend( {
+ success: wp.updates.installPluginSuccess,
+ error: wp.updates.installPluginError
+ }, args );
+
+ if ( 'import' === pagenow ) {
+ $message = $( '[data-slug="' + args.slug + '"]' );
+ }
+
+ if ( $message.html() !== __( 'Installing...' ) ) {
+ $message.data( 'originaltext', $message.html() );
+ }
+
+ $message
+ .addClass( 'updating-message' )
+ .attr(
+ 'aria-label',
+ sprintf(
+ /* translators: %s: Plugin name and version. */
+ _x( 'Installing %s...', 'plugin' ),
+ $message.data( 'name' )
+ )
+ )
+ .text( __( 'Installing...' ) );
+
+ wp.a11y.speak( __( 'Installing... please wait.' ) );
+
+ // Remove previous error messages, if any.
+ $card.removeClass( 'plugin-card-install-failed' ).find( '.notice.notice-error' ).remove();
+
+ $document.trigger( 'wp-plugin-installing', args );
+
+ return wp.updates.ajax( 'install-plugin', args );
+ };
+
+ /**
+ * Updates the UI appropriately after a successful plugin install.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.slug Slug of the installed plugin.
+ * @param {string} response.pluginName Name of the installed plugin.
+ * @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' );
+
+ $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' ) );
+
+ wp.a11y.speak( __( 'Installation completed successfully.' ) );
+
+ $document.trigger( 'wp-plugin-install-success', response );
+
+ 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' ) );
+ }
+ }, 1000 );
+ }
+ };
+
+ /**
+ * Updates the UI appropriately after a failed plugin install.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.slug Slug of the plugin to be installed.
+ * @param {string=} response.pluginName Optional. Name of the plugin to be installed.
+ * @param {string} response.errorCode Error code for the error that occurred.
+ * @param {string} response.errorMessage The error that occurred.
+ */
+ wp.updates.installPluginError = function( response ) {
+ var $card = $( '.plugin-card-' + response.slug ),
+ $button = $card.find( '.install-now' ),
+ errorMessage;
+
+ if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
+ return;
+ }
+
+ if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
+ return;
+ }
+
+ errorMessage = sprintf(
+ /* translators: %s: Error string for a failed installation. */
+ __( 'Installation failed: %s' ),
+ response.errorMessage
+ );
+
+ $card
+ .addClass( 'plugin-card-update-failed' )
+ .append( '<div class="notice notice-error notice-alt is-dismissible"><p>' + errorMessage + '</p></div>' );
+
+ $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
+
+ // Use same delay as the total duration of the notice fadeTo + slideUp animation.
+ setTimeout( function() {
+ $card
+ .removeClass( 'plugin-card-update-failed' )
+ .find( '.column-name a' ).trigger( 'focus' );
+ }, 200 );
+ } );
+
+ $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.' ) );
+
+ wp.a11y.speak( errorMessage, 'assertive' );
+
+ $document.trigger( 'wp-plugin-install-error', response );
+ };
+
+ /**
+ * Updates the UI appropriately after a successful importer install.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.slug Slug of the installed plugin.
+ * @param {string} response.pluginName Name of the installed plugin.
+ * @param {string} response.activateUrl URL to activate the just installed plugin.
+ */
+ wp.updates.installImporterSuccess = function( response ) {
+ wp.updates.addAdminNotice( {
+ id: 'install-success',
+ className: 'notice-success is-dismissible',
+ message: sprintf(
+ /* translators: %s: Activation URL. */
+ __( 'Importer installed successfully. <a href="%s">Run importer</a>' ),
+ response.activateUrl + '&from=import'
+ )
+ } );
+
+ $( '[data-slug="' + response.slug + '"]' )
+ .removeClass( 'install-now updating-message' )
+ .addClass( 'activate-now' )
+ .attr({
+ 'href': response.activateUrl + '&from=import',
+ 'aria-label':sprintf(
+ /* translators: %s: Importer name. */
+ __( 'Run %s' ),
+ response.pluginName
+ )
+ })
+ .text( __( 'Run Importer' ) );
+
+ wp.a11y.speak( __( 'Installation completed successfully.' ) );
+
+ $document.trigger( 'wp-importer-install-success', response );
+ };
+
+ /**
+ * Updates the UI appropriately after a failed importer install.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.slug Slug of the plugin to be installed.
+ * @param {string=} response.pluginName Optional. Name of the plugin to be installed.
+ * @param {string} response.errorCode Error code for the error that occurred.
+ * @param {string} response.errorMessage The error that occurred.
+ */
+ wp.updates.installImporterError = function( response ) {
+ var errorMessage = sprintf(
+ /* translators: %s: Error string for a failed installation. */
+ __( 'Installation failed: %s' ),
+ response.errorMessage
+ ),
+ $installLink = $( '[data-slug="' + response.slug + '"]' ),
+ pluginName = $installLink.data( 'name' );
+
+ if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
+ return;
+ }
+
+ if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
+ return;
+ }
+
+ wp.updates.addAdminNotice( {
+ id: response.errorCode,
+ className: 'notice-error is-dismissible',
+ message: errorMessage
+ } );
+
+ $installLink
+ .removeClass( 'updating-message' )
+ .attr(
+ 'aria-label',
+ sprintf(
+ /* translators: %s: Plugin name. */
+ _x( 'Install %s now', 'plugin' ),
+ pluginName
+ )
+ )
+ .text( __( 'Install Now' ) );
+
+ wp.a11y.speak( errorMessage, 'assertive' );
+
+ $document.trigger( 'wp-importer-install-error', response );
+ };
+
+ /**
+ * Sends an Ajax request to the server to delete a plugin.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} args Arguments.
+ * @param {string} args.plugin Basename of the plugin to be deleted.
+ * @param {string} args.slug Slug of the plugin to be deleted.
+ * @param {deletePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.deletePluginSuccess
+ * @param {deletePluginError=} args.error Optional. Error callback. Default: wp.updates.deletePluginError
+ * @return {$.promise} A jQuery promise that represents the request,
+ * decorated with an abort() method.
+ */
+ wp.updates.deletePlugin = function( args ) {
+ var $link = $( '[data-plugin="' + args.plugin + '"]' ).find( '.row-actions a.delete' );
+
+ args = _.extend( {
+ success: wp.updates.deletePluginSuccess,
+ error: wp.updates.deletePluginError
+ }, args );
+
+ if ( $link.html() !== __( 'Deleting...' ) ) {
+ $link
+ .data( 'originaltext', $link.html() )
+ .text( __( 'Deleting...' ) );
+ }
+
+ wp.a11y.speak( __( 'Deleting...' ) );
+
+ $document.trigger( 'wp-plugin-deleting', args );
+
+ return wp.updates.ajax( 'delete-plugin', args );
+ };
+
+ /**
+ * Updates the UI appropriately after a successful plugin deletion.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.slug Slug of the plugin that was deleted.
+ * @param {string} response.plugin Base name of the plugin that was deleted.
+ * @param {string} response.pluginName Name of the plugin that was deleted.
+ */
+ wp.updates.deletePluginSuccess = function( response ) {
+
+ // Removes the plugin and updates rows.
+ $( '[data-plugin="' + response.plugin + '"]' ).css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
+ var $form = $( '#bulk-action-form' ),
+ $views = $( '.subsubsub' ),
+ $pluginRow = $( this ),
+ $currentView = $views.find( '[aria-current="page"]' ),
+ $itemsCount = $( '.displaying-num' ),
+ columnCount = $form.find( 'thead th:not(.hidden), thead td' ).length,
+ pluginDeletedRow = wp.template( 'item-deleted-row' ),
+ /**
+ * Plugins Base names of plugins in their different states.
+ *
+ * @type {Object}
+ */
+ plugins = settings.plugins,
+ remainingCount;
+
+ // Add a success message after deleting a plugin.
+ if ( ! $pluginRow.hasClass( 'plugin-update-tr' ) ) {
+ $pluginRow.after(
+ pluginDeletedRow( {
+ slug: response.slug,
+ plugin: response.plugin,
+ colspan: columnCount,
+ name: response.pluginName
+ } )
+ );
+ }
+
+ $pluginRow.remove();
+
+ // Remove plugin from update count.
+ if ( -1 !== _.indexOf( plugins.upgrade, response.plugin ) ) {
+ plugins.upgrade = _.without( plugins.upgrade, response.plugin );
+ wp.updates.decrementCount( 'plugin' );
+ }
+
+ // Remove from views.
+ if ( -1 !== _.indexOf( plugins.inactive, response.plugin ) ) {
+ plugins.inactive = _.without( plugins.inactive, response.plugin );
+ if ( plugins.inactive.length ) {
+ $views.find( '.inactive .count' ).text( '(' + plugins.inactive.length + ')' );
+ } else {
+ $views.find( '.inactive' ).remove();
+ }
+ }
+
+ if ( -1 !== _.indexOf( plugins.active, response.plugin ) ) {
+ plugins.active = _.without( plugins.active, response.plugin );
+ if ( plugins.active.length ) {
+ $views.find( '.active .count' ).text( '(' + plugins.active.length + ')' );
+ } else {
+ $views.find( '.active' ).remove();
+ }
+ }
+
+ if ( -1 !== _.indexOf( plugins.recently_activated, response.plugin ) ) {
+ plugins.recently_activated = _.without( plugins.recently_activated, response.plugin );
+ if ( plugins.recently_activated.length ) {
+ $views.find( '.recently_activated .count' ).text( '(' + plugins.recently_activated.length + ')' );
+ } else {
+ $views.find( '.recently_activated' ).remove();
+ }
+ }
+
+ if ( -1 !== _.indexOf( plugins['auto-update-enabled'], response.plugin ) ) {
+ plugins['auto-update-enabled'] = _.without( plugins['auto-update-enabled'], response.plugin );
+ if ( plugins['auto-update-enabled'].length ) {
+ $views.find( '.auto-update-enabled .count' ).text( '(' + plugins['auto-update-enabled'].length + ')' );
+ } else {
+ $views.find( '.auto-update-enabled' ).remove();
+ }
+ }
+
+ if ( -1 !== _.indexOf( plugins['auto-update-disabled'], response.plugin ) ) {
+ plugins['auto-update-disabled'] = _.without( plugins['auto-update-disabled'], response.plugin );
+ if ( plugins['auto-update-disabled'].length ) {
+ $views.find( '.auto-update-disabled .count' ).text( '(' + plugins['auto-update-disabled'].length + ')' );
+ } else {
+ $views.find( '.auto-update-disabled' ).remove();
+ }
+ }
+
+ plugins.all = _.without( plugins.all, response.plugin );
+
+ if ( plugins.all.length ) {
+ $views.find( '.all .count' ).text( '(' + plugins.all.length + ')' );
+ } else {
+ $form.find( '.tablenav' ).css( { visibility: 'hidden' } );
+ $views.find( '.all' ).remove();
+
+ if ( ! $form.find( 'tr.no-items' ).length ) {
+ $form.find( '#the-list' ).append( '<tr class="no-items"><td class="colspanchange" colspan="' + columnCount + '">' + __( 'No plugins are currently available.' ) + '</td></tr>' );
+ }
+ }
+
+ if ( $itemsCount.length && $currentView.length ) {
+ remainingCount = plugins[ $currentView.parent( 'li' ).attr('class') ].length;
+ $itemsCount.text(
+ sprintf(
+ /* translators: %s: The remaining number of plugins. */
+ _nx( '%s item', '%s items', 'plugin/plugins', remainingCount ),
+ remainingCount
+ )
+ );
+ }
+ } );
+
+ wp.a11y.speak( _x( 'Deleted!', 'plugin' ) );
+
+ $document.trigger( 'wp-plugin-delete-success', response );
+ };
+
+ /**
+ * Updates the UI appropriately after a failed plugin deletion.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.slug Slug of the plugin to be deleted.
+ * @param {string} response.plugin Base name of the plugin to be deleted
+ * @param {string=} response.pluginName Optional. Name of the plugin to be deleted.
+ * @param {string} response.errorCode Error code for the error that occurred.
+ * @param {string} response.errorMessage The error that occurred.
+ */
+ wp.updates.deletePluginError = function( response ) {
+ var $plugin, $pluginUpdateRow,
+ pluginUpdateRow = wp.template( 'item-update-row' ),
+ noticeContent = wp.updates.adminNotice( {
+ className: 'update-message notice-error notice-alt',
+ message: response.errorMessage
+ } );
+
+ if ( response.plugin ) {
+ $plugin = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' );
+ $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' );
+ } else {
+ $plugin = $( 'tr.inactive[data-slug="' + response.slug + '"]' );
+ $pluginUpdateRow = $plugin.siblings( '[data-slug="' + response.slug + '"]' );
+ }
+
+ if ( ! wp.updates.isValidResponse( response, 'delete' ) ) {
+ return;
+ }
+
+ if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) {
+ return;
+ }
+
+ // Add a plugin update row if it doesn't exist yet.
+ if ( ! $pluginUpdateRow.length ) {
+ $plugin.addClass( 'update' ).after(
+ pluginUpdateRow( {
+ slug: response.slug,
+ plugin: response.plugin || response.slug,
+ colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
+ content: noticeContent
+ } )
+ );
+ } else {
+
+ // Remove previous error messages, if any.
+ $pluginUpdateRow.find( '.notice-error' ).remove();
+
+ $pluginUpdateRow.find( '.plugin-update' ).append( noticeContent );
+ }
+
+ $document.trigger( 'wp-plugin-delete-error', response );
+ };
+
+ /**
+ * Sends an Ajax request to the server to update a theme.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} args Arguments.
+ * @param {string} args.slug Theme stylesheet.
+ * @param {updateThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.updateThemeSuccess
+ * @param {updateThemeError=} args.error Optional. Error callback. Default: wp.updates.updateThemeError
+ * @return {$.promise} A jQuery promise that represents the request,
+ * decorated with an abort() method.
+ */
+ wp.updates.updateTheme = function( args ) {
+ var $notice;
+
+ args = _.extend( {
+ success: wp.updates.updateThemeSuccess,
+ error: wp.updates.updateThemeError
+ }, args );
+
+ if ( 'themes-network' === pagenow ) {
+ $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
+
+ } else if ( 'customize' === pagenow ) {
+
+ // Update the theme details UI.
+ $notice = $( '[data-slug="' + args.slug + '"].notice' ).removeClass( 'notice-large' );
+
+ $notice.find( 'h3' ).remove();
+
+ // Add the top-level UI, and update both.
+ $notice = $notice.add( $( '#customize-control-installed_theme_' + args.slug ).find( '.update-message' ) );
+ $notice = $notice.addClass( 'updating-message' ).find( 'p' );
+
+ } else {
+ $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' );
+
+ $notice.find( 'h3' ).remove();
+
+ $notice = $notice.add( $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ) );
+ $notice = $notice.addClass( 'updating-message' ).find( 'p' );
+ }
+
+ if ( $notice.html() !== __( 'Updating...' ) ) {
+ $notice.data( 'originaltext', $notice.html() );
+ }
+
+ wp.a11y.speak( __( 'Updating... please wait.' ) );
+ $notice.text( __( 'Updating...' ) );
+
+ $document.trigger( 'wp-theme-updating', args );
+
+ return wp.updates.ajax( 'update-theme', args );
+ };
+
+ /**
+ * Updates the UI appropriately after a successful theme update.
+ *
+ * @since 4.6.0
+ * @since 5.5.0 Auto-update "time to next update" text cleared.
+ *
+ * @param {Object} response
+ * @param {string} response.slug Slug of the theme to be updated.
+ * @param {Object} response.theme Updated theme.
+ * @param {string} response.oldVersion Old version of the theme.
+ * @param {string} response.newVersion New version of the theme.
+ */
+ wp.updates.updateThemeSuccess = function( response ) {
+ var isModalOpen = $( 'body.modal-open' ).length,
+ $theme = $( '[data-slug="' + response.slug + '"]' ),
+ updatedMessage = {
+ className: 'updated-message notice-success notice-alt',
+ message: _x( 'Updated!', 'theme' )
+ },
+ $notice, newText;
+
+ if ( 'customize' === pagenow ) {
+ $theme = $( '.updating-message' ).siblings( '.theme-name' );
+
+ if ( $theme.length ) {
+
+ // Update the version number in the row.
+ newText = $theme.html().replace( response.oldVersion, response.newVersion );
+ $theme.html( newText );
+ }
+
+ $notice = $( '.theme-info .notice' ).add( wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' ).find( '.update-message' ) );
+ } else if ( 'themes-network' === pagenow ) {
+ $notice = $theme.find( '.update-message' );
+
+ // Update the version number in the row.
+ newText = $theme.find( '.theme-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
+ $theme.find( '.theme-version-author-uri' ).html( newText );
+
+ // Clear the "time to next auto-update" text.
+ $theme.find( '.auto-update-time' ).empty();
+ } else {
+ $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) );
+
+ // Focus on Customize button after updating.
+ if ( isModalOpen ) {
+ $( '.load-customize:visible' ).trigger( 'focus' );
+ $( '.theme-info .theme-autoupdate' ).find( '.auto-update-time' ).empty();
+ } else {
+ $theme.find( '.load-customize' ).trigger( 'focus' );
+ }
+ }
+
+ wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) );
+ wp.a11y.speak( __( 'Update completed successfully.' ) );
+
+ wp.updates.decrementCount( 'theme' );
+
+ $document.trigger( 'wp-theme-update-success', response );
+
+ // Show updated message after modal re-rendered.
+ if ( isModalOpen && 'customize' !== pagenow ) {
+ $( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) );
+ }
+ };
+
+ /**
+ * Updates the UI appropriately after a failed theme update.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.slug Slug of the theme to be updated.
+ * @param {string} response.errorCode Error code for the error that occurred.
+ * @param {string} response.errorMessage The error that occurred.
+ */
+ wp.updates.updateThemeError = function( response ) {
+ var $theme = $( '[data-slug="' + response.slug + '"]' ),
+ errorMessage = sprintf(
+ /* translators: %s: Error string for a failed update. */
+ __( 'Update failed: %s' ),
+ response.errorMessage
+ ),
+ $notice;
+
+ if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
+ return;
+ }
+
+ if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) {
+ return;
+ }
+
+ if ( 'customize' === pagenow ) {
+ $theme = wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' );
+ }
+
+ if ( 'themes-network' === pagenow ) {
+ $notice = $theme.find( '.update-message ' );
+ } else {
+ $notice = $( '.theme-info .notice' ).add( $theme.find( '.notice' ) );
+
+ $( 'body.modal-open' ).length ? $( '.load-customize:visible' ).trigger( 'focus' ) : $theme.find( '.load-customize' ).trigger( 'focus');
+ }
+
+ wp.updates.addAdminNotice( {
+ selector: $notice,
+ className: 'update-message notice-error notice-alt is-dismissible',
+ message: errorMessage
+ } );
+
+ wp.a11y.speak( errorMessage );
+
+ $document.trigger( 'wp-theme-update-error', response );
+ };
+
+ /**
+ * Sends an Ajax request to the server to install a theme.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} args
+ * @param {string} args.slug Theme stylesheet.
+ * @param {installThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.installThemeSuccess
+ * @param {installThemeError=} args.error Optional. Error callback. Default: wp.updates.installThemeError
+ * @return {$.promise} A jQuery promise that represents the request,
+ * decorated with an abort() method.
+ */
+ wp.updates.installTheme = function( args ) {
+ var $message = $( '.theme-install[data-slug="' + args.slug + '"]' );
+
+ args = _.extend( {
+ success: wp.updates.installThemeSuccess,
+ error: wp.updates.installThemeError
+ }, args );
+
+ $message.addClass( 'updating-message' );
+ $message.parents( '.theme' ).addClass( 'focus' );
+ if ( $message.html() !== __( 'Installing...' ) ) {
+ $message.data( 'originaltext', $message.html() );
+ }
+
+ $message
+ .attr(
+ 'aria-label',
+ sprintf(
+ /* translators: %s: Theme name and version. */
+ _x( 'Installing %s...', 'theme' ),
+ $message.data( 'name' )
+ )
+ )
+ .text( __( 'Installing...' ) );
+
+ wp.a11y.speak( __( 'Installing... please wait.' ) );
+
+ // Remove previous error messages, if any.
+ $( '.install-theme-info, [data-slug="' + args.slug + '"]' ).removeClass( 'theme-install-failed' ).find( '.notice.notice-error' ).remove();
+
+ $document.trigger( 'wp-theme-installing', args );
+
+ return wp.updates.ajax( 'install-theme', args );
+ };
+
+ /**
+ * Updates the UI appropriately after a successful theme install.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.slug Slug of the theme to be installed.
+ * @param {string} response.customizeUrl URL to the Customizer for the just installed theme.
+ * @param {string} response.activateUrl URL to activate the just installed theme.
+ */
+ wp.updates.installThemeSuccess = function( response ) {
+ var $card = $( '.wp-full-overlay-header, [data-slug=' + response.slug + ']' ),
+ $message;
+
+ $document.trigger( 'wp-theme-install-success', response );
+
+ $message = $card.find( '.button-primary' )
+ .removeClass( 'updating-message' )
+ .addClass( 'updated-message disabled' )
+ .attr(
+ 'aria-label',
+ sprintf(
+ /* translators: %s: Theme name and version. */
+ _x( '%s installed!', 'theme' ),
+ response.themeName
+ )
+ )
+ .text( _x( 'Installed!', 'theme' ) );
+
+ wp.a11y.speak( __( 'Installation completed successfully.' ) );
+
+ setTimeout( function() {
+
+ if ( response.activateUrl ) {
+
+ // Transform the 'Install' button into an 'Activate' button.
+ $message
+ .attr( 'href', response.activateUrl )
+ .removeClass( 'theme-install updated-message disabled' )
+ .addClass( 'activate' );
+
+ if ( 'themes-network' === pagenow ) {
+ $message
+ .attr(
+ 'aria-label',
+ sprintf(
+ /* translators: %s: Theme name. */
+ _x( 'Network Activate %s', 'theme' ),
+ response.themeName
+ )
+ )
+ .text( __( 'Network Enable' ) );
+ } else {
+ $message
+ .attr(
+ 'aria-label',
+ sprintf(
+ /* translators: %s: Theme name. */
+ _x( 'Activate %s', 'theme' ),
+ response.themeName
+ )
+ )
+ .text( __( 'Activate' ) );
+ }
+ }
+
+ if ( response.customizeUrl ) {
+
+ // Transform the 'Preview' button into a 'Live Preview' button.
+ $message.siblings( '.preview' ).replaceWith( function () {
+ return $( '<a>' )
+ .attr( 'href', response.customizeUrl )
+ .addClass( 'button load-customize' )
+ .text( __( 'Live Preview' ) );
+ } );
+ }
+ }, 1000 );
+ };
+
+ /**
+ * Updates the UI appropriately after a failed theme install.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.slug Slug of the theme to be installed.
+ * @param {string} response.errorCode Error code for the error that occurred.
+ * @param {string} response.errorMessage The error that occurred.
+ */
+ wp.updates.installThemeError = function( response ) {
+ var $card, $button,
+ errorMessage = sprintf(
+ /* translators: %s: Error string for a failed installation. */
+ __( 'Installation failed: %s' ),
+ response.errorMessage
+ ),
+ $message = wp.updates.adminNotice( {
+ className: 'update-message notice-error notice-alt',
+ message: errorMessage
+ } );
+
+ if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
+ return;
+ }
+
+ if ( wp.updates.maybeHandleCredentialError( response, 'install-theme' ) ) {
+ return;
+ }
+
+ if ( 'customize' === pagenow ) {
+ if ( $document.find( 'body' ).hasClass( 'modal-open' ) ) {
+ $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
+ $card = $( '.theme-overlay .theme-info' ).prepend( $message );
+ } else {
+ $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
+ $card = $button.closest( '.theme' ).addClass( 'theme-install-failed' ).append( $message );
+ }
+ wp.customize.notifications.remove( 'theme_installing' );
+ } else {
+ if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) {
+ $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
+ $card = $( '.install-theme-info' ).prepend( $message );
+ } else {
+ $card = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message );
+ $button = $card.find( '.theme-install' );
+ }
+ }
+
+ $button
+ .removeClass( 'updating-message' )
+ .attr(
+ 'aria-label',
+ sprintf(
+ /* translators: %s: Theme name and version. */
+ _x( '%s installation failed', 'theme' ),
+ $button.data( 'name' )
+ )
+ )
+ .text( __( 'Installation failed.' ) );
+
+ wp.a11y.speak( errorMessage, 'assertive' );
+
+ $document.trigger( 'wp-theme-install-error', response );
+ };
+
+ /**
+ * Sends an Ajax request to the server to delete a theme.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} args
+ * @param {string} args.slug Theme stylesheet.
+ * @param {deleteThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.deleteThemeSuccess
+ * @param {deleteThemeError=} args.error Optional. Error callback. Default: wp.updates.deleteThemeError
+ * @return {$.promise} A jQuery promise that represents the request,
+ * decorated with an abort() method.
+ */
+ wp.updates.deleteTheme = function( args ) {
+ var $button;
+
+ if ( 'themes' === pagenow ) {
+ $button = $( '.theme-actions .delete-theme' );
+ } else if ( 'themes-network' === pagenow ) {
+ $button = $( '[data-slug="' + args.slug + '"]' ).find( '.row-actions a.delete' );
+ }
+
+ args = _.extend( {
+ success: wp.updates.deleteThemeSuccess,
+ error: wp.updates.deleteThemeError
+ }, args );
+
+ if ( $button && $button.html() !== __( 'Deleting...' ) ) {
+ $button
+ .data( 'originaltext', $button.html() )
+ .text( __( 'Deleting...' ) );
+ }
+
+ wp.a11y.speak( __( 'Deleting...' ) );
+
+ // Remove previous error messages, if any.
+ $( '.theme-info .update-message' ).remove();
+
+ $document.trigger( 'wp-theme-deleting', args );
+
+ return wp.updates.ajax( 'delete-theme', args );
+ };
+
+ /**
+ * Updates the UI appropriately after a successful theme deletion.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.slug Slug of the theme that was deleted.
+ */
+ wp.updates.deleteThemeSuccess = function( response ) {
+ var $themeRows = $( '[data-slug="' + response.slug + '"]' );
+
+ if ( 'themes-network' === pagenow ) {
+
+ // Removes the theme and updates rows.
+ $themeRows.css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
+ var $views = $( '.subsubsub' ),
+ $themeRow = $( this ),
+ themes = settings.themes,
+ deletedRow = wp.template( 'item-deleted-row' );
+
+ if ( ! $themeRow.hasClass( 'plugin-update-tr' ) ) {
+ $themeRow.after(
+ deletedRow( {
+ slug: response.slug,
+ colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
+ name: $themeRow.find( '.theme-title strong' ).text()
+ } )
+ );
+ }
+
+ $themeRow.remove();
+
+ // Remove theme from update count.
+ if ( -1 !== _.indexOf( themes.upgrade, response.slug ) ) {
+ themes.upgrade = _.without( themes.upgrade, response.slug );
+ wp.updates.decrementCount( 'theme' );
+ }
+
+ // Remove from views.
+ if ( -1 !== _.indexOf( themes.disabled, response.slug ) ) {
+ themes.disabled = _.without( themes.disabled, response.slug );
+ if ( themes.disabled.length ) {
+ $views.find( '.disabled .count' ).text( '(' + themes.disabled.length + ')' );
+ } else {
+ $views.find( '.disabled' ).remove();
+ }
+ }
+
+ if ( -1 !== _.indexOf( themes['auto-update-enabled'], response.slug ) ) {
+ themes['auto-update-enabled'] = _.without( themes['auto-update-enabled'], response.slug );
+ if ( themes['auto-update-enabled'].length ) {
+ $views.find( '.auto-update-enabled .count' ).text( '(' + themes['auto-update-enabled'].length + ')' );
+ } else {
+ $views.find( '.auto-update-enabled' ).remove();
+ }
+ }
+
+ if ( -1 !== _.indexOf( themes['auto-update-disabled'], response.slug ) ) {
+ themes['auto-update-disabled'] = _.without( themes['auto-update-disabled'], response.slug );
+ if ( themes['auto-update-disabled'].length ) {
+ $views.find( '.auto-update-disabled .count' ).text( '(' + themes['auto-update-disabled'].length + ')' );
+ } else {
+ $views.find( '.auto-update-disabled' ).remove();
+ }
+ }
+
+ themes.all = _.without( themes.all, response.slug );
+
+ // There is always at least one theme available.
+ $views.find( '.all .count' ).text( '(' + themes.all.length + ')' );
+ } );
+ }
+
+ // 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 );
+ };
+
+ /**
+ * Updates the UI appropriately after a failed theme deletion.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.slug Slug of the theme to be deleted.
+ * @param {string} response.errorCode Error code for the error that occurred.
+ * @param {string} response.errorMessage The error that occurred.
+ */
+ wp.updates.deleteThemeError = function( response ) {
+ var $themeRow = $( 'tr.inactive[data-slug="' + response.slug + '"]' ),
+ $button = $( '.theme-actions .delete-theme' ),
+ updateRow = wp.template( 'item-update-row' ),
+ $updateRow = $themeRow.siblings( '#' + response.slug + '-update' ),
+ errorMessage = sprintf(
+ /* translators: %s: Error string for a failed deletion. */
+ __( 'Deletion failed: %s' ),
+ response.errorMessage
+ ),
+ $message = wp.updates.adminNotice( {
+ className: 'update-message notice-error notice-alt',
+ message: errorMessage
+ } );
+
+ if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) {
+ return;
+ }
+
+ if ( 'themes-network' === pagenow ) {
+ if ( ! $updateRow.length ) {
+ $themeRow.addClass( 'update' ).after(
+ updateRow( {
+ slug: response.slug,
+ colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
+ content: $message
+ } )
+ );
+ } else {
+ // Remove previous error messages, if any.
+ $updateRow.find( '.notice-error' ).remove();
+ $updateRow.find( '.plugin-update' ).append( $message );
+ }
+ } else {
+ $( '.theme-info .theme-description' ).before( $message );
+ }
+
+ $button.html( $button.data( 'originaltext' ) );
+
+ wp.a11y.speak( errorMessage, 'assertive' );
+
+ $document.trigger( 'wp-theme-delete-error', response );
+ };
+
+ /**
+ * Adds the appropriate callback based on the type of action and the current page.
+ *
+ * @since 4.6.0
+ * @private
+ *
+ * @param {Object} data Ajax payload.
+ * @param {string} action The type of request to perform.
+ * @return {Object} The Ajax payload with the appropriate callbacks.
+ */
+ wp.updates._addCallbacks = function( data, action ) {
+ if ( 'import' === pagenow && 'install-plugin' === action ) {
+ data.success = wp.updates.installImporterSuccess;
+ data.error = wp.updates.installImporterError;
+ }
+
+ return data;
+ };
+
+ /**
+ * Pulls available jobs from the queue and runs them.
+ *
+ * @since 4.2.0
+ * @since 4.6.0 Can handle multiple job types.
+ */
+ wp.updates.queueChecker = function() {
+ var job;
+
+ if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) {
+ return;
+ }
+
+ job = wp.updates.queue.shift();
+
+ // Handle a queue job.
+ switch ( job.action ) {
+ case 'install-plugin':
+ wp.updates.installPlugin( job.data );
+ break;
+
+ case 'update-plugin':
+ wp.updates.updatePlugin( job.data );
+ break;
+
+ case 'delete-plugin':
+ wp.updates.deletePlugin( job.data );
+ break;
+
+ case 'install-theme':
+ wp.updates.installTheme( job.data );
+ break;
+
+ case 'update-theme':
+ wp.updates.updateTheme( job.data );
+ break;
+
+ case 'delete-theme':
+ wp.updates.deleteTheme( job.data );
+ break;
+
+ default:
+ break;
+ }
+ };
+
+ /**
+ * Requests the users filesystem credentials if they aren't already known.
+ *
+ * @since 4.2.0
+ *
+ * @param {Event=} event Optional. Event interface.
+ */
+ wp.updates.requestFilesystemCredentials = function( event ) {
+ if ( false === wp.updates.filesystemCredentials.available ) {
+ /*
+ * After exiting the credentials request modal,
+ * return the focus to the element triggering the request.
+ */
+ if ( event && ! wp.updates.$elToReturnFocusToFromCredentialsModal ) {
+ wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target );
+ }
+
+ wp.updates.ajaxLocked = true;
+ wp.updates.requestForCredentialsModalOpen();
+ }
+ };
+
+ /**
+ * Requests the users filesystem credentials if needed and there is no lock.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event=} event Optional. Event interface.
+ */
+ wp.updates.maybeRequestFilesystemCredentials = function( event ) {
+ if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
+ wp.updates.requestFilesystemCredentials( event );
+ }
+ };
+
+ /**
+ * Keydown handler for the request for credentials modal.
+ *
+ * Closes the modal when the escape key is pressed and
+ * constrains keyboard navigation to inside the modal.
+ *
+ * @since 4.2.0
+ *
+ * @param {Event} event Event interface.
+ */
+ wp.updates.keydown = function( event ) {
+ if ( 27 === event.keyCode ) {
+ wp.updates.requestForCredentialsModalCancel();
+ } else if ( 9 === event.keyCode ) {
+
+ // #upgrade button must always be the last focus-able element in the dialog.
+ if ( 'upgrade' === event.target.id && ! event.shiftKey ) {
+ $( '#hostname' ).trigger( 'focus' );
+
+ event.preventDefault();
+ } else if ( 'hostname' === event.target.id && event.shiftKey ) {
+ $( '#upgrade' ).trigger( 'focus' );
+
+ event.preventDefault();
+ }
+ }
+ };
+
+ /**
+ * Opens the request for credentials modal.
+ *
+ * @since 4.2.0
+ */
+ wp.updates.requestForCredentialsModalOpen = function() {
+ var $modal = $( '#request-filesystem-credentials-dialog' );
+
+ $( 'body' ).addClass( 'modal-open' );
+ $modal.show();
+ $modal.find( 'input:enabled:first' ).trigger( 'focus' );
+ $modal.on( 'keydown', wp.updates.keydown );
+ };
+
+ /**
+ * Closes the request for credentials modal.
+ *
+ * @since 4.2.0
+ */
+ wp.updates.requestForCredentialsModalClose = function() {
+ $( '#request-filesystem-credentials-dialog' ).hide();
+ $( 'body' ).removeClass( 'modal-open' );
+
+ if ( wp.updates.$elToReturnFocusToFromCredentialsModal ) {
+ wp.updates.$elToReturnFocusToFromCredentialsModal.trigger( 'focus' );
+ }
+ };
+
+ /**
+ * Takes care of the steps that need to happen when the modal is canceled out.
+ *
+ * @since 4.2.0
+ * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions.
+ */
+ wp.updates.requestForCredentialsModalCancel = function() {
+
+ // Not ajaxLocked and no queue means we already have cleared things up.
+ if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) {
+ return;
+ }
+
+ _.each( wp.updates.queue, function( job ) {
+ $document.trigger( 'credential-modal-cancel', job );
+ } );
+
+ // Remove the lock, and clear the queue.
+ wp.updates.ajaxLocked = false;
+ wp.updates.queue = [];
+
+ wp.updates.requestForCredentialsModalClose();
+ };
+
+ /**
+ * Displays an error message in the request for credentials form.
+ *
+ * @since 4.2.0
+ *
+ * @param {string} message Error message.
+ */
+ wp.updates.showErrorInCredentialsForm = function( message ) {
+ var $filesystemForm = $( '#request-filesystem-credentials-form' );
+
+ // 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>' );
+ };
+
+ /**
+ * Handles credential errors and runs events that need to happen in that case.
+ *
+ * @since 4.2.0
+ *
+ * @param {Object} response Ajax response.
+ * @param {string} action The type of request to perform.
+ */
+ wp.updates.credentialError = function( response, action ) {
+
+ // Restore callbacks.
+ response = wp.updates._addCallbacks( response, action );
+
+ wp.updates.queue.unshift( {
+ action: action,
+
+ /*
+ * Not cool that we're depending on response for this data.
+ * This would feel more whole in a view all tied together.
+ */
+ data: response
+ } );
+
+ wp.updates.filesystemCredentials.available = false;
+ wp.updates.showErrorInCredentialsForm( response.errorMessage );
+ wp.updates.requestFilesystemCredentials();
+ };
+
+ /**
+ * Handles credentials errors if it could not connect to the filesystem.
+ *
+ * @since 4.6.0
+ *
+ * @param {Object} response Response from the server.
+ * @param {string} response.errorCode Error code for the error that occurred.
+ * @param {string} response.errorMessage The error that occurred.
+ * @param {string} action The type of request to perform.
+ * @return {boolean} Whether there is an error that needs to be handled or not.
+ */
+ wp.updates.maybeHandleCredentialError = function( response, action ) {
+ if ( wp.updates.shouldRequestFilesystemCredentials && response.errorCode && 'unable_to_connect_to_filesystem' === response.errorCode ) {
+ wp.updates.credentialError( response, action );
+ return true;
+ }
+
+ return false;
+ };
+
+ /**
+ * Validates an Ajax response to ensure it's a proper object.
+ *
+ * If the response deems to be invalid, an admin notice is being displayed.
+ *
+ * @param {(Object|string)} response Response from the server.
+ * @param {function=} response.always Optional. Callback for when the Deferred is resolved or rejected.
+ * @param {string=} response.statusText Optional. Status message corresponding to the status code.
+ * @param {string=} response.responseText Optional. Request response as text.
+ * @param {string} action Type of action the response is referring to. Can be 'delete',
+ * 'update' or 'install'.
+ */
+ wp.updates.isValidResponse = function( response, action ) {
+ var error = __( 'Something went wrong.' ),
+ errorMessage;
+
+ // Make sure the response is a valid data object and not a Promise object.
+ if ( _.isObject( response ) && ! _.isFunction( response.always ) ) {
+ return true;
+ }
+
+ if ( _.isString( response ) && '-1' === response ) {
+ error = __( 'An error has occurred. Please reload the page and try again.' );
+ } else if ( _.isString( response ) ) {
+ error = response;
+ } else if ( 'undefined' !== typeof response.readyState && 0 === response.readyState ) {
+ error = __( 'Connection lost or the server is busy. Please try again later.' );
+ } else if ( _.isString( response.responseText ) && '' !== response.responseText ) {
+ error = response.responseText;
+ } else if ( _.isString( response.statusText ) ) {
+ error = response.statusText;
+ }
+
+ switch ( action ) {
+ case 'update':
+ /* translators: %s: Error string for a failed update. */
+ errorMessage = __( 'Update failed: %s' );
+ break;
+
+ case 'install':
+ /* translators: %s: Error string for a failed installation. */
+ errorMessage = __( 'Installation failed: %s' );
+ break;
+
+ case 'delete':
+ /* translators: %s: Error string for a failed deletion. */
+ errorMessage = __( 'Deletion failed: %s' );
+ break;
+ }
+
+ // Messages are escaped, remove HTML tags to make them more readable.
+ error = error.replace( /<[\/a-z][^<>]*>/gi, '' );
+ errorMessage = errorMessage.replace( '%s', error );
+
+ // Add admin notice.
+ wp.updates.addAdminNotice( {
+ id: 'unknown_error',
+ className: 'notice-error is-dismissible',
+ message: _.escape( errorMessage )
+ } );
+
+ // Remove the lock, and clear the queue.
+ wp.updates.ajaxLocked = false;
+ wp.updates.queue = [];
+
+ // Change buttons of all running updates.
+ $( '.button.updating-message' )
+ .removeClass( 'updating-message' )
+ .removeAttr( 'aria-label' )
+ .prop( 'disabled', true )
+ .text( __( 'Update failed.' ) );
+
+ $( '.updating-message:not(.button):not(.thickbox)' )
+ .removeClass( 'updating-message notice-warning' )
+ .addClass( 'notice-error' )
+ .find( 'p' )
+ .removeAttr( 'aria-label' )
+ .text( errorMessage );
+
+ wp.a11y.speak( errorMessage, 'assertive' );
+
+ return false;
+ };
+
+ /**
+ * Potentially adds an AYS to a user attempting to leave the page.
+ *
+ * If an update is on-going and a user attempts to leave the page,
+ * opens an "Are you sure?" alert.
+ *
+ * @since 4.2.0
+ */
+ wp.updates.beforeunload = function() {
+ if ( wp.updates.ajaxLocked ) {
+ return __( 'Updates may not complete if you navigate away from this page.' );
+ }
+ };
+
+ $( function() {
+ var $pluginFilter = $( '#plugin-filter' ),
+ $bulkActionForm = $( '#bulk-action-form' ),
+ $filesystemForm = $( '#request-filesystem-credentials-form' ),
+ $filesystemModal = $( '#request-filesystem-credentials-dialog' ),
+ $pluginSearch = $( '.plugins-php .wp-filter-search' ),
+ $pluginInstallSearch = $( '.plugin-install-php .wp-filter-search' );
+
+ settings = _.extend( settings, window._wpUpdatesItemCounts || {} );
+
+ if ( settings.totals ) {
+ wp.updates.refreshCount();
+ }
+
+ /*
+ * Whether a user needs to submit filesystem credentials.
+ *
+ * This is based on whether the form was output on the page server-side.
+ *
+ * @see {wp_print_request_filesystem_credentials_modal() in PHP}
+ */
+ wp.updates.shouldRequestFilesystemCredentials = $filesystemModal.length > 0;
+
+ /**
+ * File system credentials form submit noop-er / handler.
+ *
+ * @since 4.2.0
+ */
+ $filesystemModal.on( 'submit', 'form', function( event ) {
+ event.preventDefault();
+
+ // Persist the credentials input by the user for the duration of the page load.
+ wp.updates.filesystemCredentials.ftp.hostname = $( '#hostname' ).val();
+ wp.updates.filesystemCredentials.ftp.username = $( '#username' ).val();
+ wp.updates.filesystemCredentials.ftp.password = $( '#password' ).val();
+ wp.updates.filesystemCredentials.ftp.connectionType = $( 'input[name="connection_type"]:checked' ).val();
+ wp.updates.filesystemCredentials.ssh.publicKey = $( '#public_key' ).val();
+ wp.updates.filesystemCredentials.ssh.privateKey = $( '#private_key' ).val();
+ wp.updates.filesystemCredentials.fsNonce = $( '#_fs_nonce' ).val();
+ wp.updates.filesystemCredentials.available = true;
+
+ // Unlock and invoke the queue.
+ wp.updates.ajaxLocked = false;
+ wp.updates.queueChecker();
+
+ wp.updates.requestForCredentialsModalClose();
+ } );
+
+ /**
+ * Closes the request credentials modal when clicking the 'Cancel' button or outside of the modal.
+ *
+ * @since 4.2.0
+ */
+ $filesystemModal.on( 'click', '[data-js-action="close"], .notification-dialog-background', wp.updates.requestForCredentialsModalCancel );
+
+ /**
+ * Hide SSH fields when not selected.
+ *
+ * @since 4.2.0
+ */
+ $filesystemForm.on( 'change', 'input[name="connection_type"]', function() {
+ $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) );
+ } ).trigger( 'change' );
+
+ /**
+ * Handles events after the credential modal was closed.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event} event Event interface.
+ * @param {string} job The install/update.delete request.
+ */
+ $document.on( 'credential-modal-cancel', function( event, job ) {
+ var $updatingMessage = $( '.updating-message' ),
+ $message, originalText;
+
+ if ( 'import' === pagenow ) {
+ $updatingMessage.removeClass( 'updating-message' );
+ } else if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
+ if ( 'update-plugin' === job.action ) {
+ $message = $( 'tr[data-plugin="' + job.data.plugin + '"]' ).find( '.update-message' );
+ } else if ( 'delete-plugin' === job.action ) {
+ $message = $( '[data-plugin="' + job.data.plugin + '"]' ).find( '.row-actions a.delete' );
+ }
+ } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) {
+ if ( 'update-theme' === job.action ) {
+ $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.update-message' );
+ } else if ( 'delete-theme' === job.action && 'themes-network' === pagenow ) {
+ $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.row-actions a.delete' );
+ } else if ( 'delete-theme' === job.action && 'themes' === pagenow ) {
+ $message = $( '.theme-actions .delete-theme' );
+ }
+ } else {
+ $message = $updatingMessage;
+ }
+
+ if ( $message && $message.hasClass( 'updating-message' ) ) {
+ originalText = $message.data( 'originaltext' );
+
+ if ( 'undefined' === typeof originalText ) {
+ originalText = $( '<p>' ).html( $message.find( 'p' ).data( 'originaltext' ) );
+ }
+
+ $message
+ .removeClass( 'updating-message' )
+ .html( originalText );
+
+ if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
+ if ( 'update-plugin' === job.action ) {
+ $message.attr(
+ 'aria-label',
+ sprintf(
+ /* translators: %s: Plugin name and version. */
+ _x( 'Update %s now', 'plugin' ),
+ $message.data( 'name' )
+ )
+ );
+ } else if ( 'install-plugin' === job.action ) {
+ $message.attr(
+ 'aria-label',
+ sprintf(
+ /* translators: %s: Plugin name. */
+ _x( 'Install %s now', 'plugin' ),
+ $message.data( 'name' )
+ )
+ );
+ }
+ }
+ }
+
+ wp.a11y.speak( __( 'Update canceled.' ) );
+ } );
+
+ /**
+ * Click handler for plugin updates in List Table view.
+ *
+ * @since 4.2.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) {
+ var $message = $( event.target ),
+ $pluginRow = $message.parents( 'tr' );
+
+ event.preventDefault();
+
+ if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
+ return;
+ }
+
+ wp.updates.maybeRequestFilesystemCredentials( event );
+
+ // Return the user to the input box of the plugin's table row after closing the modal.
+ wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' );
+ wp.updates.updatePlugin( {
+ plugin: $pluginRow.data( 'plugin' ),
+ slug: $pluginRow.data( 'slug' )
+ } );
+ } );
+
+ /**
+ * Click handler for plugin updates in plugin install view.
+ *
+ * @since 4.2.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $pluginFilter.on( 'click', '.update-now', function( event ) {
+ var $button = $( event.target );
+ event.preventDefault();
+
+ if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
+ return;
+ }
+
+ wp.updates.maybeRequestFilesystemCredentials( event );
+
+ wp.updates.updatePlugin( {
+ plugin: $button.data( 'plugin' ),
+ slug: $button.data( 'slug' )
+ } );
+ } );
+
+ /**
+ * Click handler for plugin installs in plugin install view.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $pluginFilter.on( 'click', '.install-now', function( event ) {
+ var $button = $( event.target );
+ event.preventDefault();
+
+ if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
+ return;
+ }
+
+ if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
+ wp.updates.requestFilesystemCredentials( event );
+
+ $document.on( 'credential-modal-cancel', function() {
+ var $message = $( '.install-now.updating-message' );
+
+ $message
+ .removeClass( 'updating-message' )
+ .text( __( 'Install Now' ) );
+
+ wp.a11y.speak( __( 'Update canceled.' ) );
+ } );
+ }
+
+ wp.updates.installPlugin( {
+ slug: $button.data( 'slug' )
+ } );
+ } );
+
+ /**
+ * Click handler for importer plugins installs in the Import screen.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $document.on( 'click', '.importer-item .install-now', function( event ) {
+ var $button = $( event.target ),
+ pluginName = $( this ).data( 'name' );
+
+ event.preventDefault();
+
+ if ( $button.hasClass( 'updating-message' ) ) {
+ return;
+ }
+
+ if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
+ wp.updates.requestFilesystemCredentials( event );
+
+ $document.on( 'credential-modal-cancel', function() {
+
+ $button
+ .removeClass( 'updating-message' )
+ .attr(
+ 'aria-label',
+ sprintf(
+ /* translators: %s: Plugin name. */
+ _x( 'Install %s now', 'plugin' ),
+ pluginName
+ )
+ )
+ .text( __( 'Install Now' ) );
+
+ wp.a11y.speak( __( 'Update canceled.' ) );
+ } );
+ }
+
+ wp.updates.installPlugin( {
+ slug: $button.data( 'slug' ),
+ pagenow: pagenow,
+ success: wp.updates.installImporterSuccess,
+ error: wp.updates.installImporterError
+ } );
+ } );
+
+ /**
+ * Click handler for plugin deletions.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) {
+ var $pluginRow = $( event.target ).parents( 'tr' ),
+ confirmMessage;
+
+ if ( $pluginRow.hasClass( 'is-uninstallable' ) ) {
+ confirmMessage = sprintf(
+ /* translators: %s: Plugin name. */
+ __( 'Are you sure you want to delete %s and its data?' ),
+ $pluginRow.find( '.plugin-title strong' ).text()
+ );
+ } else {
+ confirmMessage = sprintf(
+ /* translators: %s: Plugin name. */
+ __( 'Are you sure you want to delete %s?' ),
+ $pluginRow.find( '.plugin-title strong' ).text()
+ );
+ }
+
+ event.preventDefault();
+
+ if ( ! window.confirm( confirmMessage ) ) {
+ return;
+ }
+
+ wp.updates.maybeRequestFilesystemCredentials( event );
+
+ wp.updates.deletePlugin( {
+ plugin: $pluginRow.data( 'plugin' ),
+ slug: $pluginRow.data( 'slug' )
+ } );
+
+ } );
+
+ /**
+ * Click handler for theme updates.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) {
+ var $message = $( event.target ),
+ $themeRow = $message.parents( 'tr' );
+
+ event.preventDefault();
+
+ if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
+ return;
+ }
+
+ wp.updates.maybeRequestFilesystemCredentials( event );
+
+ // Return the user to the input box of the theme's table row after closing the modal.
+ wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' );
+ wp.updates.updateTheme( {
+ slug: $themeRow.data( 'slug' )
+ } );
+ } );
+
+ /**
+ * Click handler for theme deletions.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) {
+ var $themeRow = $( event.target ).parents( 'tr' ),
+ confirmMessage = sprintf(
+ /* translators: %s: Theme name. */
+ __( 'Are you sure you want to delete %s?' ),
+ $themeRow.find( '.theme-title strong' ).text()
+ );
+
+ event.preventDefault();
+
+ if ( ! window.confirm( confirmMessage ) ) {
+ return;
+ }
+
+ wp.updates.maybeRequestFilesystemCredentials( event );
+
+ wp.updates.deleteTheme( {
+ slug: $themeRow.data( 'slug' )
+ } );
+ } );
+
+ /**
+ * Bulk action handler for plugins and themes.
+ *
+ * Handles both deletions and updates.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $bulkActionForm.on( 'click', '[type="submit"]:not([name="clear-recent-list"])', function( event ) {
+ var bulkAction = $( event.target ).siblings( 'select' ).val(),
+ itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ),
+ success = 0,
+ error = 0,
+ errorMessages = [],
+ type, action;
+
+ // Determine which type of item we're dealing with.
+ switch ( pagenow ) {
+ case 'plugins':
+ case 'plugins-network':
+ type = 'plugin';
+ break;
+
+ case 'themes-network':
+ type = 'theme';
+ break;
+
+ default:
+ return;
+ }
+
+ // Bail if there were no items selected.
+ if ( ! itemsSelected.length ) {
+ event.preventDefault();
+ $( 'html, body' ).animate( { scrollTop: 0 } );
+
+ return wp.updates.addAdminNotice( {
+ id: 'no-items-selected',
+ className: 'notice-error is-dismissible',
+ message: __( 'Please select at least one item to perform this action on.' )
+ } );
+ }
+
+ // Determine the type of request we're dealing with.
+ switch ( bulkAction ) {
+ case 'update-selected':
+ action = bulkAction.replace( 'selected', type );
+ break;
+
+ case 'delete-selected':
+ var confirmMessage = 'plugin' === type ?
+ __( 'Are you sure you want to delete the selected plugins and their data?' ) :
+ __( 'Caution: These themes may be active on other sites in the network. Are you sure you want to proceed?' );
+
+ if ( ! window.confirm( confirmMessage ) ) {
+ event.preventDefault();
+ return;
+ }
+
+ action = bulkAction.replace( 'selected', type );
+ break;
+
+ default:
+ return;
+ }
+
+ wp.updates.maybeRequestFilesystemCredentials( event );
+
+ event.preventDefault();
+
+ // Un-check the bulk checkboxes.
+ $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false );
+
+ $document.trigger( 'wp-' + type + '-bulk-' + bulkAction, itemsSelected );
+
+ // Find all the checkboxes which have been checked.
+ itemsSelected.each( function( index, element ) {
+ var $checkbox = $( element ),
+ $itemRow = $checkbox.parents( 'tr' );
+
+ // Only add update-able items to the update queue.
+ if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) {
+
+ // Un-check the box.
+ $checkbox.prop( 'checked', false );
+ 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,
+ data: {
+ plugin: $itemRow.data( 'plugin' ),
+ slug: $itemRow.data( 'slug' )
+ }
+ } );
+ } );
+
+ // Display bulk notification for updates of any kind.
+ $document.on( 'wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error', function( event, response ) {
+ var $itemRow = $( '[data-slug="' + response.slug + '"]' ),
+ $bulkActionNotice, itemName;
+
+ if ( 'wp-' + response.update + '-update-success' === event.type ) {
+ success++;
+ } else {
+ itemName = response.pluginName ? response.pluginName : $itemRow.find( '.column-primary strong' ).text();
+
+ error++;
+ errorMessages.push( itemName + ': ' + response.errorMessage );
+ }
+
+ $itemRow.find( 'input[name="checked[]"]:checked' ).prop( 'checked', false );
+
+ wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' );
+
+ wp.updates.addAdminNotice( {
+ id: 'bulk-action-notice',
+ className: 'bulk-action-notice',
+ successes: success,
+ errors: error,
+ errorMessages: errorMessages,
+ type: response.update
+ } );
+
+ $bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() {
+ // $( this ) is the clicked button, no need to get it again.
+ $( this )
+ .toggleClass( 'bulk-action-errors-collapsed' )
+ .attr( 'aria-expanded', ! $( this ).hasClass( 'bulk-action-errors-collapsed' ) );
+ // Show the errors list.
+ $bulkActionNotice.find( '.bulk-action-errors' ).toggleClass( 'hidden' );
+ } );
+
+ if ( error > 0 && ! wp.updates.queue.length ) {
+ $( 'html, body' ).animate( { scrollTop: 0 } );
+ }
+ } );
+
+ // Reset admin notice template after #bulk-action-notice was added.
+ $document.on( 'wp-updates-notice-added', function() {
+ wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
+ } );
+
+ // Check the queue, now that the event handlers have been added.
+ wp.updates.queueChecker();
+ } );
+
+ if ( $pluginInstallSearch.length ) {
+ $pluginInstallSearch.attr( 'aria-describedby', 'live-search-desc' );
+ }
+
+ /**
+ * Handles changes to the plugin search box on the new-plugin page,
+ * searching the repository dynamically.
+ *
+ * @since 4.6.0
+ */
+ $pluginInstallSearch.on( 'keyup input', _.debounce( function( event, eventtype ) {
+ var $searchTab = $( '.plugin-install-search' ), data, searchLocation;
+
+ data = {
+ _ajax_nonce: wp.updates.ajaxNonce,
+ s: encodeURIComponent( event.target.value ),
+ tab: 'search',
+ type: $( '#typeselector' ).val(),
+ pagenow: pagenow
+ };
+ searchLocation = location.href.split( '?' )[ 0 ] + '?' + $.param( _.omit( data, [ '_ajax_nonce', 'pagenow' ] ) );
+
+ // Clear on escape.
+ if ( 'keyup' === event.type && 27 === event.which ) {
+ event.target.value = '';
+ }
+
+ if ( wp.updates.searchTerm === data.s && 'typechange' !== eventtype ) {
+ return;
+ } else {
+ $pluginFilter.empty();
+ wp.updates.searchTerm = data.s;
+ }
+
+ if ( window.history && window.history.replaceState ) {
+ window.history.replaceState( null, '', searchLocation );
+ }
+
+ if ( ! $searchTab.length ) {
+ $searchTab = $( '<li class="plugin-install-search" />' )
+ .append( $( '<a />', {
+ 'class': 'current',
+ 'href': searchLocation,
+ 'text': __( 'Search Results' )
+ } ) );
+
+ $( '.wp-filter .filter-links .current' )
+ .removeClass( 'current' )
+ .parents( '.filter-links' )
+ .prepend( $searchTab );
+
+ $pluginFilter.prev( 'p' ).remove();
+ $( '.plugins-popular-tags-wrapper' ).remove();
+ }
+
+ if ( 'undefined' !== typeof wp.updates.searchRequest ) {
+ wp.updates.searchRequest.abort();
+ }
+ $( 'body' ).addClass( 'loading-content' );
+
+ wp.updates.searchRequest = wp.ajax.post( 'search-install-plugins', data ).done( function( response ) {
+ $( 'body' ).removeClass( 'loading-content' );
+ $pluginFilter.append( response.items );
+ delete wp.updates.searchRequest;
+
+ if ( 0 === response.count ) {
+ wp.a11y.speak( __( 'You do not appear to have any plugins available at this time.' ) );
+ } else {
+ wp.a11y.speak(
+ sprintf(
+ /* translators: %s: Number of plugins. */
+ __( 'Number of plugins found: %d' ),
+ response.count
+ )
+ );
+ }
+ } );
+ }, 1000 ) );
+
+ if ( $pluginSearch.length ) {
+ $pluginSearch.attr( 'aria-describedby', 'live-search-desc' );
+ }
+
+ /**
+ * Handles changes to the plugin search box on the Installed Plugins screen,
+ * searching the plugin list dynamically.
+ *
+ * @since 4.6.0
+ */
+ $pluginSearch.on( 'keyup input', _.debounce( function( event ) {
+ var data = {
+ _ajax_nonce: wp.updates.ajaxNonce,
+ s: encodeURIComponent( event.target.value ),
+ pagenow: pagenow,
+ plugin_status: 'all'
+ },
+ queryArgs;
+
+ // Clear on escape.
+ if ( 'keyup' === event.type && 27 === event.which ) {
+ event.target.value = '';
+ }
+
+ if ( wp.updates.searchTerm === data.s ) {
+ return;
+ } else {
+ wp.updates.searchTerm = data.s;
+ }
+
+ queryArgs = _.object( _.compact( _.map( location.search.slice( 1 ).split( '&' ), function( item ) {
+ if ( item ) return item.split( '=' );
+ } ) ) );
+
+ data.plugin_status = queryArgs.plugin_status || 'all';
+
+ if ( window.history && window.history.replaceState ) {
+ window.history.replaceState( null, '', location.href.split( '?' )[ 0 ] + '?s=' + data.s + '&plugin_status=' + data.plugin_status );
+ }
+
+ if ( 'undefined' !== typeof wp.updates.searchRequest ) {
+ wp.updates.searchRequest.abort();
+ }
+
+ $bulkActionForm.empty();
+ $( 'body' ).addClass( 'loading-content' );
+ $( '.subsubsub .current' ).removeClass( 'current' );
+
+ wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) {
+
+ // Can we just ditch this whole subtitle business?
+ var $subTitle = $( '<span />' ).addClass( 'subtitle' ).html(
+ sprintf(
+ /* translators: %s: Search query. */
+ __( 'Search results for: %s' ),
+ '<strong>' + _.escape( decodeURIComponent( data.s ) ) + '</strong>'
+ ) ),
+ $oldSubTitle = $( '.wrap .subtitle' );
+
+ if ( ! data.s.length ) {
+ $oldSubTitle.remove();
+ $( '.subsubsub .' + data.plugin_status + ' a' ).addClass( 'current' );
+ } else if ( $oldSubTitle.length ) {
+ $oldSubTitle.replaceWith( $subTitle );
+ } else {
+ $( '.wp-header-end' ).before( $subTitle );
+ }
+
+ $( 'body' ).removeClass( 'loading-content' );
+ $bulkActionForm.append( response.items );
+ delete wp.updates.searchRequest;
+
+ if ( 0 === response.count ) {
+ wp.a11y.speak( __( 'No plugins found. Try a different search.' ) );
+ } else {
+ wp.a11y.speak(
+ sprintf(
+ /* translators: %s: Number of plugins. */
+ __( 'Number of plugins found: %d' ),
+ response.count
+ )
+ );
+ }
+ } );
+ }, 500 ) );
+
+ /**
+ * Trigger a search event when the search form gets submitted.
+ *
+ * @since 4.6.0
+ */
+ $document.on( 'submit', '.search-plugins', function( event ) {
+ event.preventDefault();
+
+ $( 'input.wp-filter-search' ).trigger( 'input' );
+ } );
+
+ /**
+ * Trigger a search event when the "Try Again" button is clicked.
+ *
+ * @since 4.9.0
+ */
+ $document.on( 'click', '.try-again', function( event ) {
+ event.preventDefault();
+ $pluginInstallSearch.trigger( 'input' );
+ } );
+
+ /**
+ * Trigger a search event when the search type gets changed.
+ *
+ * @since 4.6.0
+ */
+ $( '#typeselector' ).on( 'change', function() {
+ var $search = $( 'input[name="s"]' );
+
+ if ( $search.val().length ) {
+ $search.trigger( 'input', 'typechange' );
+ }
+ } );
+
+ /**
+ * Click handler for updating a plugin from the details modal on `plugin-install.php`.
+ *
+ * @since 4.2.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $( '#plugin_update_from_iframe' ).on( 'click', function( event ) {
+ var target = window.parent === window ? null : window.parent,
+ update;
+
+ $.support.postMessage = !! window.postMessage;
+
+ if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'update-core.php' ) ) {
+ return;
+ }
+
+ event.preventDefault();
+
+ update = {
+ action: 'update-plugin',
+ data: {
+ plugin: $( this ).data( 'plugin' ),
+ slug: $( this ).data( 'slug' )
+ }
+ };
+
+ 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.
+ *
+ * @since 4.2.0
+ * @since 4.6.0 Switched `update-plugin` action to use the queue.
+ *
+ * @param {Event} event Event interface.
+ */
+ $( window ).on( 'message', function( event ) {
+ var originalEvent = event.originalEvent,
+ expectedOrigin = document.location.protocol + '//' + document.location.host,
+ message;
+
+ if ( originalEvent.origin !== expectedOrigin ) {
+ return;
+ }
+
+ try {
+ message = JSON.parse( originalEvent.data );
+ } catch ( e ) {
+ return;
+ }
+
+ if ( ! message || 'undefined' === typeof message.action ) {
+ return;
+ }
+
+ switch ( message.action ) {
+
+ // Called from `wp-admin/includes/class-wp-upgrader-skins.php`.
+ case 'decrementUpdateCount':
+ /** @property {string} message.upgradeType */
+ wp.updates.decrementCount( message.upgradeType );
+ break;
+
+ 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 );
+ wp.updates.queueChecker();
+ break;
+ }
+ } );
+
+ /**
+ * Adds a callback to display a warning before leaving the page.
+ *
+ * @since 4.2.0
+ */
+ $( window ).on( 'beforeunload', wp.updates.beforeunload );
+
+ /**
+ * Prevents the page form scrolling when activating auto-updates with the Spacebar key.
+ *
+ * @since 5.5.0
+ */
+ $document.on( 'keydown', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) {
+ if ( 32 === event.which ) {
+ event.preventDefault();
+ }
+ } );
+
+ /**
+ * Click and keyup handler for enabling and disabling plugin and theme auto-updates.
+ *
+ * These controls can be either links or buttons. When JavaScript is enabled,
+ * we want them to behave like buttons. An ARIA role `button` is added via
+ * the JavaScript that targets elements with the CSS class `aria-button-if-js`.
+ *
+ * @since 5.5.0
+ */
+ $document.on( 'click keyup', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) {
+ var data, asset, type, $parent,
+ $toggler = $( this ),
+ action = $toggler.attr( 'data-wp-action' ),
+ $label = $toggler.find( '.label' );
+
+ if ( 'keyup' === event.type && 32 !== event.which ) {
+ return;
+ }
+
+ if ( 'themes' !== pagenow ) {
+ $parent = $toggler.closest( '.column-auto-updates' );
+ } else {
+ $parent = $toggler.closest( '.theme-autoupdate' );
+ }
+
+ event.preventDefault();
+
+ // Prevent multiple simultaneous requests.
+ if ( $toggler.attr( 'data-doing-ajax' ) === 'yes' ) {
+ return;
+ }
+
+ $toggler.attr( 'data-doing-ajax', 'yes' );
+
+ switch ( pagenow ) {
+ case 'plugins':
+ case 'plugins-network':
+ type = 'plugin';
+ asset = $toggler.closest( 'tr' ).attr( 'data-plugin' );
+ break;
+ case 'themes-network':
+ type = 'theme';
+ asset = $toggler.closest( 'tr' ).attr( 'data-slug' );
+ break;
+ case 'themes':
+ type = 'theme';
+ asset = $toggler.attr( 'data-slug' );
+ break;
+ }
+
+ // Clear any previous errors.
+ $parent.find( '.notice.notice-error' ).addClass( 'hidden' );
+
+ // Show loading status.
+ if ( 'enable' === action ) {
+ $label.text( __( 'Enabling...' ) );
+ } else {
+ $label.text( __( 'Disabling...' ) );
+ }
+
+ $toggler.find( '.dashicons-update' ).removeClass( 'hidden' );
+
+ data = {
+ action: 'toggle-auto-updates',
+ _ajax_nonce: settings.ajax_nonce,
+ state: action,
+ type: type,
+ asset: asset
+ };
+
+ $.post( window.ajaxurl, data )
+ .done( function( response ) {
+ var $enabled, $disabled, enabledNumber, disabledNumber, errorMessage,
+ href = $toggler.attr( 'href' );
+
+ if ( ! response.success ) {
+ // if WP returns 0 for response (which can happen in a few cases),
+ // output the general error message since we won't have response.data.error.
+ if ( response.data && response.data.error ) {
+ errorMessage = response.data.error;
+ } else {
+ errorMessage = __( 'The request could not be completed.' );
+ }
+
+ $parent.find( '.notice.notice-error' ).removeClass( 'hidden' ).find( 'p' ).text( errorMessage );
+ wp.a11y.speak( errorMessage, 'assertive' );
+ return;
+ }
+
+ // Update the counts in the enabled/disabled views if on a screen
+ // with a list table.
+ if ( 'themes' !== pagenow ) {
+ $enabled = $( '.auto-update-enabled span' );
+ $disabled = $( '.auto-update-disabled span' );
+ enabledNumber = parseInt( $enabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0;
+ disabledNumber = parseInt( $disabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0;
+
+ switch ( action ) {
+ case 'enable':
+ ++enabledNumber;
+ --disabledNumber;
+ break;
+ case 'disable':
+ --enabledNumber;
+ ++disabledNumber;
+ break;
+ }
+
+ enabledNumber = Math.max( 0, enabledNumber );
+ disabledNumber = Math.max( 0, disabledNumber );
+
+ $enabled.text( '(' + enabledNumber + ')' );
+ $disabled.text( '(' + disabledNumber + ')' );
+ }
+
+ if ( 'enable' === action ) {
+ // The toggler control can be either a link or a button.
+ if ( $toggler[ 0 ].hasAttribute( 'href' ) ) {
+ href = href.replace( 'action=enable-auto-update', 'action=disable-auto-update' );
+ $toggler.attr( 'href', href );
+ }
+ $toggler.attr( 'data-wp-action', 'disable' );
+
+ $label.text( __( 'Disable auto-updates' ) );
+ $parent.find( '.auto-update-time' ).removeClass( 'hidden' );
+ wp.a11y.speak( __( 'Auto-updates enabled' ) );
+ } else {
+ // The toggler control can be either a link or a button.
+ if ( $toggler[ 0 ].hasAttribute( 'href' ) ) {
+ href = href.replace( 'action=disable-auto-update', 'action=enable-auto-update' );
+ $toggler.attr( 'href', href );
+ }
+ $toggler.attr( 'data-wp-action', 'enable' );
+
+ $label.text( __( 'Enable auto-updates' ) );
+ $parent.find( '.auto-update-time' ).addClass( 'hidden' );
+ wp.a11y.speak( __( 'Auto-updates disabled' ) );
+ }
+
+ $document.trigger( 'wp-auto-update-setting-changed', { state: action, type: type, asset: asset } );
+ } )
+ .fail( function() {
+ $parent.find( '.notice.notice-error' )
+ .removeClass( 'hidden' )
+ .find( 'p' )
+ .text( __( 'The request could not be completed.' ) );
+
+ wp.a11y.speak( __( 'The request could not be completed.' ), 'assertive' );
+ } )
+ .always( function() {
+ $toggler.removeAttr( 'data-doing-ajax' ).find( '.dashicons-update' ).addClass( 'hidden' );
+ } );
+ }
+ );
+ } );
+})( jQuery, window.wp, window._wpUpdatesSettings );