ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

updates.js (94103B)


      1 /**
      2  * Functions for ajaxified updates, deletions and installs inside the WordPress admin.
      3  *
      4  * @version 4.2.0
      5  * @output wp-admin/js/updates.js
      6  */
      7 
      8 /* global pagenow */
      9 
     10 /**
     11  * @param {jQuery}  $                                        jQuery object.
     12  * @param {object}  wp                                       WP object.
     13  * @param {object}  settings                                 WP Updates settings.
     14  * @param {string}  settings.ajax_nonce                      Ajax nonce.
     15  * @param {object=} settings.plugins                         Base names of plugins in their different states.
     16  * @param {Array}   settings.plugins.all                     Base names of all plugins.
     17  * @param {Array}   settings.plugins.active                  Base names of active plugins.
     18  * @param {Array}   settings.plugins.inactive                Base names of inactive plugins.
     19  * @param {Array}   settings.plugins.upgrade                 Base names of plugins with updates available.
     20  * @param {Array}   settings.plugins.recently_activated      Base names of recently activated plugins.
     21  * @param {Array}   settings.plugins['auto-update-enabled']  Base names of plugins set to auto-update.
     22  * @param {Array}   settings.plugins['auto-update-disabled'] Base names of plugins set to not auto-update.
     23  * @param {object=} settings.themes                          Slugs of themes in their different states.
     24  * @param {Array}   settings.themes.all                      Slugs of all themes.
     25  * @param {Array}   settings.themes.upgrade                  Slugs of themes with updates available.
     26  * @param {Arrat}   settings.themes.disabled                 Slugs of disabled themes.
     27  * @param {Array}   settings.themes['auto-update-enabled']   Slugs of themes set to auto-update.
     28  * @param {Array}   settings.themes['auto-update-disabled']  Slugs of themes set to not auto-update.
     29  * @param {object=} settings.totals                          Combined information for available update counts.
     30  * @param {number}  settings.totals.count                    Holds the amount of available updates.
     31  */
     32 (function( $, wp, settings ) {
     33 	var $document = $( document ),
     34 		__ = wp.i18n.__,
     35 		_x = wp.i18n._x,
     36 		sprintf = wp.i18n.sprintf;
     37 
     38 	wp = wp || {};
     39 
     40 	/**
     41 	 * The WP Updates object.
     42 	 *
     43 	 * @since 4.2.0
     44 	 *
     45 	 * @namespace wp.updates
     46 	 */
     47 	wp.updates = {};
     48 
     49 	/**
     50 	 * Removed in 5.5.0, needed for back-compatibility.
     51 	 *
     52 	 * @since 4.2.0
     53 	 * @deprecated 5.5.0
     54 	 *
     55 	 * @type {object}
     56 	 */
     57 	wp.updates.l10n = {
     58 		searchResults: '',
     59 		searchResultsLabel: '',
     60 		noPlugins: '',
     61 		noItemsSelected: '',
     62 		updating: '',
     63 		pluginUpdated: '',
     64 		themeUpdated: '',
     65 		update: '',
     66 		updateNow: '',
     67 		pluginUpdateNowLabel: '',
     68 		updateFailedShort: '',
     69 		updateFailed: '',
     70 		pluginUpdatingLabel: '',
     71 		pluginUpdatedLabel: '',
     72 		pluginUpdateFailedLabel: '',
     73 		updatingMsg: '',
     74 		updatedMsg: '',
     75 		updateCancel: '',
     76 		beforeunload: '',
     77 		installNow: '',
     78 		pluginInstallNowLabel: '',
     79 		installing: '',
     80 		pluginInstalled: '',
     81 		themeInstalled: '',
     82 		installFailedShort: '',
     83 		installFailed: '',
     84 		pluginInstallingLabel: '',
     85 		themeInstallingLabel: '',
     86 		pluginInstalledLabel: '',
     87 		themeInstalledLabel: '',
     88 		pluginInstallFailedLabel: '',
     89 		themeInstallFailedLabel: '',
     90 		installingMsg: '',
     91 		installedMsg: '',
     92 		importerInstalledMsg: '',
     93 		aysDelete: '',
     94 		aysDeleteUninstall: '',
     95 		aysBulkDelete: '',
     96 		aysBulkDeleteThemes: '',
     97 		deleting: '',
     98 		deleteFailed: '',
     99 		pluginDeleted: '',
    100 		themeDeleted: '',
    101 		livePreview: '',
    102 		activatePlugin: '',
    103 		activateTheme: '',
    104 		activatePluginLabel: '',
    105 		activateThemeLabel: '',
    106 		activateImporter: '',
    107 		activateImporterLabel: '',
    108 		unknownError: '',
    109 		connectionError: '',
    110 		nonceError: '',
    111 		pluginsFound: '',
    112 		noPluginsFound: '',
    113 		autoUpdatesEnable: '',
    114 		autoUpdatesEnabling: '',
    115 		autoUpdatesEnabled: '',
    116 		autoUpdatesDisable: '',
    117 		autoUpdatesDisabling: '',
    118 		autoUpdatesDisabled: '',
    119 		autoUpdatesError: ''
    120 	};
    121 
    122 	wp.updates.l10n = window.wp.deprecateL10nObject( 'wp.updates.l10n', wp.updates.l10n, '5.5.0' );
    123 
    124 	/**
    125 	 * User nonce for ajax calls.
    126 	 *
    127 	 * @since 4.2.0
    128 	 *
    129 	 * @type {string}
    130 	 */
    131 	wp.updates.ajaxNonce = settings.ajax_nonce;
    132 
    133 	/**
    134 	 * Current search term.
    135 	 *
    136 	 * @since 4.6.0
    137 	 *
    138 	 * @type {string}
    139 	 */
    140 	wp.updates.searchTerm = '';
    141 
    142 	/**
    143 	 * Whether filesystem credentials need to be requested from the user.
    144 	 *
    145 	 * @since 4.2.0
    146 	 *
    147 	 * @type {bool}
    148 	 */
    149 	wp.updates.shouldRequestFilesystemCredentials = false;
    150 
    151 	/**
    152 	 * Filesystem credentials to be packaged along with the request.
    153 	 *
    154 	 * @since 4.2.0
    155 	 * @since 4.6.0 Added `available` property to indicate whether credentials have been provided.
    156 	 *
    157 	 * @type {Object}
    158 	 * @property {Object} filesystemCredentials.ftp                Holds FTP credentials.
    159 	 * @property {string} filesystemCredentials.ftp.host           FTP host. Default empty string.
    160 	 * @property {string} filesystemCredentials.ftp.username       FTP user name. Default empty string.
    161 	 * @property {string} filesystemCredentials.ftp.password       FTP password. Default empty string.
    162 	 * @property {string} filesystemCredentials.ftp.connectionType Type of FTP connection. 'ssh', 'ftp', or 'ftps'.
    163 	 *                                                             Default empty string.
    164 	 * @property {Object} filesystemCredentials.ssh                Holds SSH credentials.
    165 	 * @property {string} filesystemCredentials.ssh.publicKey      The public key. Default empty string.
    166 	 * @property {string} filesystemCredentials.ssh.privateKey     The private key. Default empty string.
    167 	 * @property {string} filesystemCredentials.fsNonce            Filesystem credentials form nonce.
    168 	 * @property {bool}   filesystemCredentials.available          Whether filesystem credentials have been provided.
    169 	 *                                                             Default 'false'.
    170 	 */
    171 	wp.updates.filesystemCredentials = {
    172 		ftp:       {
    173 			host:           '',
    174 			username:       '',
    175 			password:       '',
    176 			connectionType: ''
    177 		},
    178 		ssh:       {
    179 			publicKey:  '',
    180 			privateKey: ''
    181 		},
    182 		fsNonce: '',
    183 		available: false
    184 	};
    185 
    186 	/**
    187 	 * Whether we're waiting for an Ajax request to complete.
    188 	 *
    189 	 * @since 4.2.0
    190 	 * @since 4.6.0 More accurately named `ajaxLocked`.
    191 	 *
    192 	 * @type {bool}
    193 	 */
    194 	wp.updates.ajaxLocked = false;
    195 
    196 	/**
    197 	 * Admin notice template.
    198 	 *
    199 	 * @since 4.6.0
    200 	 *
    201 	 * @type {function}
    202 	 */
    203 	wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
    204 
    205 	/**
    206 	 * Update queue.
    207 	 *
    208 	 * If the user tries to update a plugin while an update is
    209 	 * already happening, it can be placed in this queue to perform later.
    210 	 *
    211 	 * @since 4.2.0
    212 	 * @since 4.6.0 More accurately named `queue`.
    213 	 *
    214 	 * @type {Array.object}
    215 	 */
    216 	wp.updates.queue = [];
    217 
    218 	/**
    219 	 * Holds a jQuery reference to return focus to when exiting the request credentials modal.
    220 	 *
    221 	 * @since 4.2.0
    222 	 *
    223 	 * @type {jQuery}
    224 	 */
    225 	wp.updates.$elToReturnFocusToFromCredentialsModal = undefined;
    226 
    227 	/**
    228 	 * Adds or updates an admin notice.
    229 	 *
    230 	 * @since 4.6.0
    231 	 *
    232 	 * @param {Object}  data
    233 	 * @param {*=}      data.selector      Optional. Selector of an element to be replaced with the admin notice.
    234 	 * @param {string=} data.id            Optional. Unique id that will be used as the notice's id attribute.
    235 	 * @param {string=} data.className     Optional. Class names that will be used in the admin notice.
    236 	 * @param {string=} data.message       Optional. The message displayed in the notice.
    237 	 * @param {number=} data.successes     Optional. The amount of successful operations.
    238 	 * @param {number=} data.errors        Optional. The amount of failed operations.
    239 	 * @param {Array=}  data.errorMessages Optional. Error messages of failed operations.
    240 	 *
    241 	 */
    242 	wp.updates.addAdminNotice = function( data ) {
    243 		var $notice = $( data.selector ),
    244 			$headerEnd = $( '.wp-header-end' ),
    245 			$adminNotice;
    246 
    247 		delete data.selector;
    248 		$adminNotice = wp.updates.adminNotice( data );
    249 
    250 		// Check if this admin notice already exists.
    251 		if ( ! $notice.length ) {
    252 			$notice = $( '#' + data.id );
    253 		}
    254 
    255 		if ( $notice.length ) {
    256 			$notice.replaceWith( $adminNotice );
    257 		} else if ( $headerEnd.length ) {
    258 			$headerEnd.after( $adminNotice );
    259 		} else {
    260 			if ( 'customize' === pagenow ) {
    261 				$( '.customize-themes-notifications' ).append( $adminNotice );
    262 			} else {
    263 				$( '.wrap' ).find( '> h1' ).after( $adminNotice );
    264 			}
    265 		}
    266 
    267 		$document.trigger( 'wp-updates-notice-added' );
    268 	};
    269 
    270 	/**
    271 	 * Handles Ajax requests to WordPress.
    272 	 *
    273 	 * @since 4.6.0
    274 	 *
    275 	 * @param {string} action The type of Ajax request ('update-plugin', 'install-theme', etc).
    276 	 * @param {Object} data   Data that needs to be passed to the ajax callback.
    277 	 * @return {$.promise}    A jQuery promise that represents the request,
    278 	 *                        decorated with an abort() method.
    279 	 */
    280 	wp.updates.ajax = function( action, data ) {
    281 		var options = {};
    282 
    283 		if ( wp.updates.ajaxLocked ) {
    284 			wp.updates.queue.push( {
    285 				action: action,
    286 				data:   data
    287 			} );
    288 
    289 			// Return a Deferred object so callbacks can always be registered.
    290 			return $.Deferred();
    291 		}
    292 
    293 		wp.updates.ajaxLocked = true;
    294 
    295 		if ( data.success ) {
    296 			options.success = data.success;
    297 			delete data.success;
    298 		}
    299 
    300 		if ( data.error ) {
    301 			options.error = data.error;
    302 			delete data.error;
    303 		}
    304 
    305 		options.data = _.extend( data, {
    306 			action:          action,
    307 			_ajax_nonce:     wp.updates.ajaxNonce,
    308 			_fs_nonce:       wp.updates.filesystemCredentials.fsNonce,
    309 			username:        wp.updates.filesystemCredentials.ftp.username,
    310 			password:        wp.updates.filesystemCredentials.ftp.password,
    311 			hostname:        wp.updates.filesystemCredentials.ftp.hostname,
    312 			connection_type: wp.updates.filesystemCredentials.ftp.connectionType,
    313 			public_key:      wp.updates.filesystemCredentials.ssh.publicKey,
    314 			private_key:     wp.updates.filesystemCredentials.ssh.privateKey
    315 		} );
    316 
    317 		return wp.ajax.send( options ).always( wp.updates.ajaxAlways );
    318 	};
    319 
    320 	/**
    321 	 * Actions performed after every Ajax request.
    322 	 *
    323 	 * @since 4.6.0
    324 	 *
    325 	 * @param {Object}  response
    326 	 * @param {Array=}  response.debug     Optional. Debug information.
    327 	 * @param {string=} response.errorCode Optional. Error code for an error that occurred.
    328 	 */
    329 	wp.updates.ajaxAlways = function( response ) {
    330 		if ( ! response.errorCode || 'unable_to_connect_to_filesystem' !== response.errorCode ) {
    331 			wp.updates.ajaxLocked = false;
    332 			wp.updates.queueChecker();
    333 		}
    334 
    335 		if ( 'undefined' !== typeof response.debug && window.console && window.console.log ) {
    336 			_.map( response.debug, function( message ) {
    337 				// Remove all HTML tags and write a message to the console.
    338 				window.console.log( wp.sanitize.stripTagsAndEncodeText( message ) );
    339 			} );
    340 		}
    341 	};
    342 
    343 	/**
    344 	 * Refreshes update counts everywhere on the screen.
    345 	 *
    346 	 * @since 4.7.0
    347 	 */
    348 	wp.updates.refreshCount = function() {
    349 		var $adminBarUpdates              = $( '#wp-admin-bar-updates' ),
    350 			$dashboardNavMenuUpdateCount  = $( 'a[href="update-core.php"] .update-plugins' ),
    351 			$pluginsNavMenuUpdateCount    = $( 'a[href="plugins.php"] .update-plugins' ),
    352 			$appearanceNavMenuUpdateCount = $( 'a[href="themes.php"] .update-plugins' ),
    353 			itemCount;
    354 
    355 		$adminBarUpdates.find( '.ab-item' ).removeAttr( 'title' );
    356 		$adminBarUpdates.find( '.ab-label' ).text( settings.totals.counts.total );
    357 
    358 		// Remove the update count from the toolbar if it's zero.
    359 		if ( 0 === settings.totals.counts.total ) {
    360 			$adminBarUpdates.find( '.ab-label' ).parents( 'li' ).remove();
    361 		}
    362 
    363 		// Update the "Updates" menu item.
    364 		$dashboardNavMenuUpdateCount.each( function( index, element ) {
    365 			element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.total );
    366 		} );
    367 		if ( settings.totals.counts.total > 0 ) {
    368 			$dashboardNavMenuUpdateCount.find( '.update-count' ).text( settings.totals.counts.total );
    369 		} else {
    370 			$dashboardNavMenuUpdateCount.remove();
    371 		}
    372 
    373 		// Update the "Plugins" menu item.
    374 		$pluginsNavMenuUpdateCount.each( function( index, element ) {
    375 			element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.plugins );
    376 		} );
    377 		if ( settings.totals.counts.total > 0 ) {
    378 			$pluginsNavMenuUpdateCount.find( '.plugin-count' ).text( settings.totals.counts.plugins );
    379 		} else {
    380 			$pluginsNavMenuUpdateCount.remove();
    381 		}
    382 
    383 		// Update the "Appearance" menu item.
    384 		$appearanceNavMenuUpdateCount.each( function( index, element ) {
    385 			element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.themes );
    386 		} );
    387 		if ( settings.totals.counts.total > 0 ) {
    388 			$appearanceNavMenuUpdateCount.find( '.theme-count' ).text( settings.totals.counts.themes );
    389 		} else {
    390 			$appearanceNavMenuUpdateCount.remove();
    391 		}
    392 
    393 		// Update list table filter navigation.
    394 		if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
    395 			itemCount = settings.totals.counts.plugins;
    396 		} else if ( 'themes' === pagenow || 'themes-network' === pagenow ) {
    397 			itemCount = settings.totals.counts.themes;
    398 		}
    399 
    400 		if ( itemCount > 0 ) {
    401 			$( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' );
    402 		} else {
    403 			$( '.subsubsub .upgrade' ).remove();
    404 			$( '.subsubsub li:last' ).html( function() { return $( this ).children(); } );
    405 		}
    406 	};
    407 
    408 	/**
    409 	 * Decrements the update counts throughout the various menus.
    410 	 *
    411 	 * This includes the toolbar, the "Updates" menu item and the menu items
    412 	 * for plugins and themes.
    413 	 *
    414 	 * @since 3.9.0
    415 	 *
    416 	 * @param {string} type The type of item that was updated or deleted.
    417 	 *                      Can be 'plugin', 'theme'.
    418 	 */
    419 	wp.updates.decrementCount = function( type ) {
    420 		settings.totals.counts.total = Math.max( --settings.totals.counts.total, 0 );
    421 
    422 		if ( 'plugin' === type ) {
    423 			settings.totals.counts.plugins = Math.max( --settings.totals.counts.plugins, 0 );
    424 		} else if ( 'theme' === type ) {
    425 			settings.totals.counts.themes = Math.max( --settings.totals.counts.themes, 0 );
    426 		}
    427 
    428 		wp.updates.refreshCount( type );
    429 	};
    430 
    431 	/**
    432 	 * Sends an Ajax request to the server to update a plugin.
    433 	 *
    434 	 * @since 4.2.0
    435 	 * @since 4.6.0 More accurately named `updatePlugin`.
    436 	 *
    437 	 * @param {Object}               args         Arguments.
    438 	 * @param {string}               args.plugin  Plugin basename.
    439 	 * @param {string}               args.slug    Plugin slug.
    440 	 * @param {updatePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.updatePluginSuccess
    441 	 * @param {updatePluginError=}   args.error   Optional. Error callback. Default: wp.updates.updatePluginError
    442 	 * @return {$.promise} A jQuery promise that represents the request,
    443 	 *                     decorated with an abort() method.
    444 	 */
    445 	wp.updates.updatePlugin = function( args ) {
    446 		var $updateRow, $card, $message, message,
    447 			$adminBarUpdates = $( '#wp-admin-bar-updates' );
    448 
    449 		args = _.extend( {
    450 			success: wp.updates.updatePluginSuccess,
    451 			error: wp.updates.updatePluginError
    452 		}, args );
    453 
    454 		if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
    455 			$updateRow = $( 'tr[data-plugin="' + args.plugin + '"]' );
    456 			$message   = $updateRow.find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
    457 			message    = sprintf(
    458 				/* translators: %s: Plugin name and version. */
    459  				_x( 'Updating %s...', 'plugin' ),
    460 				$updateRow.find( '.plugin-title strong' ).text()
    461 			);
    462 		} else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
    463 			$card    = $( '.plugin-card-' + args.slug );
    464 			$message = $card.find( '.update-now' ).addClass( 'updating-message' );
    465 			message    = sprintf(
    466 				/* translators: %s: Plugin name and version. */
    467  				_x( 'Updating %s...', 'plugin' ),
    468 				$message.data( 'name' )
    469 			);
    470 
    471 			// Remove previous error messages, if any.
    472 			$card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove();
    473 		}
    474 
    475 		$adminBarUpdates.addClass( 'spin' );
    476 
    477 		if ( $message.html() !== __( 'Updating...' ) ) {
    478 			$message.data( 'originaltext', $message.html() );
    479 		}
    480 
    481 		$message
    482 			.attr( 'aria-label', message )
    483 			.text( __( 'Updating...' ) );
    484 
    485 		$document.trigger( 'wp-plugin-updating', args );
    486 
    487 		return wp.updates.ajax( 'update-plugin', args );
    488 	};
    489 
    490 	/**
    491 	 * Updates the UI appropriately after a successful plugin update.
    492 	 *
    493 	 * @since 4.2.0
    494 	 * @since 4.6.0 More accurately named `updatePluginSuccess`.
    495 	 * @since 5.5.0 Auto-update "time to next update" text cleared.
    496 	 *
    497 	 * @param {Object} response            Response from the server.
    498 	 * @param {string} response.slug       Slug of the plugin to be updated.
    499 	 * @param {string} response.plugin     Basename of the plugin to be updated.
    500 	 * @param {string} response.pluginName Name of the plugin to be updated.
    501 	 * @param {string} response.oldVersion Old version of the plugin.
    502 	 * @param {string} response.newVersion New version of the plugin.
    503 	 */
    504 	wp.updates.updatePluginSuccess = function( response ) {
    505 		var $pluginRow, $updateMessage, newText,
    506 			$adminBarUpdates = $( '#wp-admin-bar-updates' );
    507 
    508 		if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
    509 			$pluginRow     = $( 'tr[data-plugin="' + response.plugin + '"]' )
    510 				.removeClass( 'update' )
    511 				.addClass( 'updated' );
    512 			$updateMessage = $pluginRow.find( '.update-message' )
    513 				.removeClass( 'updating-message notice-warning' )
    514 				.addClass( 'updated-message notice-success' ).find( 'p' );
    515 
    516 			// Update the version number in the row.
    517 			newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
    518 			$pluginRow.find( '.plugin-version-author-uri' ).html( newText );
    519 
    520 			// Clear the "time to next auto-update" text.
    521 			$pluginRow.find( '.auto-update-time' ).empty();
    522 		} else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
    523 			$updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' )
    524 				.removeClass( 'updating-message' )
    525 				.addClass( 'button-disabled updated-message' );
    526 		}
    527 
    528 		$adminBarUpdates.removeClass( 'spin' );
    529 
    530 		$updateMessage
    531 			.attr(
    532 				'aria-label',
    533 				sprintf(
    534 					/* translators: %s: Plugin name and version. */
    535 					_x( '%s updated!', 'plugin' ),
    536 					response.pluginName
    537 				)
    538 			)
    539 			.text( _x( 'Updated!', 'plugin' ) );
    540 
    541 		wp.a11y.speak( __( 'Update completed successfully.' ) );
    542 
    543 		wp.updates.decrementCount( 'plugin' );
    544 
    545 		$document.trigger( 'wp-plugin-update-success', response );
    546 	};
    547 
    548 	/**
    549 	 * Updates the UI appropriately after a failed plugin update.
    550 	 *
    551 	 * @since 4.2.0
    552 	 * @since 4.6.0 More accurately named `updatePluginError`.
    553 	 *
    554 	 * @param {Object}  response              Response from the server.
    555 	 * @param {string}  response.slug         Slug of the plugin to be updated.
    556 	 * @param {string}  response.plugin       Basename of the plugin to be updated.
    557 	 * @param {string=} response.pluginName   Optional. Name of the plugin to be updated.
    558 	 * @param {string}  response.errorCode    Error code for the error that occurred.
    559 	 * @param {string}  response.errorMessage The error that occurred.
    560 	 */
    561 	wp.updates.updatePluginError = function( response ) {
    562 		var $card, $message, errorMessage,
    563 			$adminBarUpdates = $( '#wp-admin-bar-updates' );
    564 
    565 		if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
    566 			return;
    567 		}
    568 
    569 		if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) {
    570 			return;
    571 		}
    572 
    573 		errorMessage = sprintf(
    574 			/* translators: %s: Error string for a failed update. */
    575 			__( 'Update failed: %s' ),
    576 			response.errorMessage
    577 		);
    578 
    579 		if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
    580 			if ( response.plugin ) {
    581 				$message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' );
    582 			} else {
    583 				$message = $( 'tr[data-slug="' + response.slug + '"]' ).find( '.update-message' );
    584 			}
    585 			$message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage );
    586 
    587 			if ( response.pluginName ) {
    588 				$message.find( 'p' )
    589 					.attr(
    590 						'aria-label',
    591 						sprintf(
    592 							/* translators: %s: Plugin name and version. */
    593 							_x( '%s update failed.', 'plugin' ),
    594 							response.pluginName
    595 						)
    596 					);
    597 			} else {
    598 				$message.find( 'p' ).removeAttr( 'aria-label' );
    599 			}
    600 		} else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
    601 			$card = $( '.plugin-card-' + response.slug )
    602 				.addClass( 'plugin-card-update-failed' )
    603 				.append( wp.updates.adminNotice( {
    604 					className: 'update-message notice-error notice-alt is-dismissible',
    605 					message:   errorMessage
    606 				} ) );
    607 
    608 			$card.find( '.update-now' )
    609 				.text(  __( 'Update failed.' ) )
    610 				.removeClass( 'updating-message' );
    611 
    612 			if ( response.pluginName ) {
    613 				$card.find( '.update-now' )
    614 					.attr(
    615 						'aria-label',
    616 						sprintf(
    617 							/* translators: %s: Plugin name and version. */
    618 							_x( '%s update failed.', 'plugin' ),
    619 							response.pluginName
    620 						)
    621 					);
    622 			} else {
    623 				$card.find( '.update-now' ).removeAttr( 'aria-label' );
    624 			}
    625 
    626 			$card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
    627 
    628 				// Use same delay as the total duration of the notice fadeTo + slideUp animation.
    629 				setTimeout( function() {
    630 					$card
    631 						.removeClass( 'plugin-card-update-failed' )
    632 						.find( '.column-name a' ).trigger( 'focus' );
    633 
    634 					$card.find( '.update-now' )
    635 						.attr( 'aria-label', false )
    636 						.text( __( 'Update Now' ) );
    637 				}, 200 );
    638 			} );
    639 		}
    640 
    641 		$adminBarUpdates.removeClass( 'spin' );
    642 
    643 		wp.a11y.speak( errorMessage, 'assertive' );
    644 
    645 		$document.trigger( 'wp-plugin-update-error', response );
    646 	};
    647 
    648 	/**
    649 	 * Sends an Ajax request to the server to install a plugin.
    650 	 *
    651 	 * @since 4.6.0
    652 	 *
    653 	 * @param {Object}                args         Arguments.
    654 	 * @param {string}                args.slug    Plugin identifier in the WordPress.org Plugin repository.
    655 	 * @param {installPluginSuccess=} args.success Optional. Success callback. Default: wp.updates.installPluginSuccess
    656 	 * @param {installPluginError=}   args.error   Optional. Error callback. Default: wp.updates.installPluginError
    657 	 * @return {$.promise} A jQuery promise that represents the request,
    658 	 *                     decorated with an abort() method.
    659 	 */
    660 	wp.updates.installPlugin = function( args ) {
    661 		var $card    = $( '.plugin-card-' + args.slug ),
    662 			$message = $card.find( '.install-now' );
    663 
    664 		args = _.extend( {
    665 			success: wp.updates.installPluginSuccess,
    666 			error: wp.updates.installPluginError
    667 		}, args );
    668 
    669 		if ( 'import' === pagenow ) {
    670 			$message = $( '[data-slug="' + args.slug + '"]' );
    671 		}
    672 
    673 		if ( $message.html() !== __( 'Installing...' ) ) {
    674 			$message.data( 'originaltext', $message.html() );
    675 		}
    676 
    677 		$message
    678 			.addClass( 'updating-message' )
    679 			.attr(
    680 				'aria-label',
    681 				sprintf(
    682 					/* translators: %s: Plugin name and version. */
    683 					_x( 'Installing %s...', 'plugin' ),
    684 					$message.data( 'name' )
    685 				)
    686 			)
    687 			.text( __( 'Installing...' ) );
    688 
    689 		wp.a11y.speak( __( 'Installing... please wait.' ) );
    690 
    691 		// Remove previous error messages, if any.
    692 		$card.removeClass( 'plugin-card-install-failed' ).find( '.notice.notice-error' ).remove();
    693 
    694 		$document.trigger( 'wp-plugin-installing', args );
    695 
    696 		return wp.updates.ajax( 'install-plugin', args );
    697 	};
    698 
    699 	/**
    700 	 * Updates the UI appropriately after a successful plugin install.
    701 	 *
    702 	 * @since 4.6.0
    703 	 *
    704 	 * @param {Object} response             Response from the server.
    705 	 * @param {string} response.slug        Slug of the installed plugin.
    706 	 * @param {string} response.pluginName  Name of the installed plugin.
    707 	 * @param {string} response.activateUrl URL to activate the just installed plugin.
    708 	 */
    709 	wp.updates.installPluginSuccess = function( response ) {
    710 		var $message = $( '.plugin-card-' + response.slug ).find( '.install-now' );
    711 
    712 		$message
    713 			.removeClass( 'updating-message' )
    714 			.addClass( 'updated-message installed button-disabled' )
    715 			.attr(
    716 				'aria-label',
    717 				sprintf(
    718 					/* translators: %s: Plugin name and version. */
    719 					_x( '%s installed!', 'plugin' ),
    720 					response.pluginName
    721 				)
    722 			)
    723 			.text( _x( 'Installed!', 'plugin' ) );
    724 
    725 		wp.a11y.speak( __( 'Installation completed successfully.' ) );
    726 
    727 		$document.trigger( 'wp-plugin-install-success', response );
    728 
    729 		if ( response.activateUrl ) {
    730 			setTimeout( function() {
    731 
    732 				// Transform the 'Install' button into an 'Activate' button.
    733 				$message.removeClass( 'install-now installed button-disabled updated-message' )
    734 					.addClass( 'activate-now button-primary' )
    735 					.attr( 'href', response.activateUrl );
    736 
    737 				if ( 'plugins-network' === pagenow ) {
    738 					$message
    739 						.attr(
    740 							'aria-label',
    741 							sprintf(
    742 								/* translators: %s: Plugin name. */
    743 								_x( 'Network Activate %s', 'plugin' ),
    744 								response.pluginName
    745 							)
    746 						)
    747 						.text( __( 'Network Activate' ) );
    748 				} else {
    749 					$message
    750 						.attr(
    751 							'aria-label',
    752 							sprintf(
    753 								/* translators: %s: Plugin name. */
    754 								_x( 'Activate %s', 'plugin' ),
    755 								response.pluginName
    756 							)
    757 						)
    758 						.text( __( 'Activate' ) );
    759 				}
    760 			}, 1000 );
    761 		}
    762 	};
    763 
    764 	/**
    765 	 * Updates the UI appropriately after a failed plugin install.
    766 	 *
    767 	 * @since 4.6.0
    768 	 *
    769 	 * @param {Object}  response              Response from the server.
    770 	 * @param {string}  response.slug         Slug of the plugin to be installed.
    771 	 * @param {string=} response.pluginName   Optional. Name of the plugin to be installed.
    772 	 * @param {string}  response.errorCode    Error code for the error that occurred.
    773 	 * @param {string}  response.errorMessage The error that occurred.
    774 	 */
    775 	wp.updates.installPluginError = function( response ) {
    776 		var $card   = $( '.plugin-card-' + response.slug ),
    777 			$button = $card.find( '.install-now' ),
    778 			errorMessage;
    779 
    780 		if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
    781 			return;
    782 		}
    783 
    784 		if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
    785 			return;
    786 		}
    787 
    788 		errorMessage = sprintf(
    789 			/* translators: %s: Error string for a failed installation. */
    790 			__( 'Installation failed: %s' ),
    791 			response.errorMessage
    792 		);
    793 
    794 		$card
    795 			.addClass( 'plugin-card-update-failed' )
    796 			.append( '<div class="notice notice-error notice-alt is-dismissible"><p>' + errorMessage + '</p></div>' );
    797 
    798 		$card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
    799 
    800 			// Use same delay as the total duration of the notice fadeTo + slideUp animation.
    801 			setTimeout( function() {
    802 				$card
    803 					.removeClass( 'plugin-card-update-failed' )
    804 					.find( '.column-name a' ).trigger( 'focus' );
    805 			}, 200 );
    806 		} );
    807 
    808 		$button
    809 			.removeClass( 'updating-message' ).addClass( 'button-disabled' )
    810 			.attr(
    811 				'aria-label',
    812 				sprintf(
    813 					/* translators: %s: Plugin name and version. */
    814 					_x( '%s installation failed', 'plugin' ),
    815 					$button.data( 'name' )
    816 				)
    817 			)
    818 			.text( __( 'Installation failed.' ) );
    819 
    820 		wp.a11y.speak( errorMessage, 'assertive' );
    821 
    822 		$document.trigger( 'wp-plugin-install-error', response );
    823 	};
    824 
    825 	/**
    826 	 * Updates the UI appropriately after a successful importer install.
    827 	 *
    828 	 * @since 4.6.0
    829 	 *
    830 	 * @param {Object} response             Response from the server.
    831 	 * @param {string} response.slug        Slug of the installed plugin.
    832 	 * @param {string} response.pluginName  Name of the installed plugin.
    833 	 * @param {string} response.activateUrl URL to activate the just installed plugin.
    834 	 */
    835 	wp.updates.installImporterSuccess = function( response ) {
    836 		wp.updates.addAdminNotice( {
    837 			id:        'install-success',
    838 			className: 'notice-success is-dismissible',
    839 			message:   sprintf(
    840 				/* translators: %s: Activation URL. */
    841 				__( 'Importer installed successfully. <a href="%s">Run importer</a>' ),
    842 				response.activateUrl + '&from=import'
    843 			)
    844 		} );
    845 
    846 		$( '[data-slug="' + response.slug + '"]' )
    847 			.removeClass( 'install-now updating-message' )
    848 			.addClass( 'activate-now' )
    849 			.attr({
    850 				'href': response.activateUrl + '&from=import',
    851 				'aria-label':sprintf(
    852 					/* translators: %s: Importer name. */
    853 					__( 'Run %s' ),
    854 					response.pluginName
    855 				)
    856 			})
    857 			.text( __( 'Run Importer' ) );
    858 
    859 		wp.a11y.speak( __( 'Installation completed successfully.' ) );
    860 
    861 		$document.trigger( 'wp-importer-install-success', response );
    862 	};
    863 
    864 	/**
    865 	 * Updates the UI appropriately after a failed importer install.
    866 	 *
    867 	 * @since 4.6.0
    868 	 *
    869 	 * @param {Object}  response              Response from the server.
    870 	 * @param {string}  response.slug         Slug of the plugin to be installed.
    871 	 * @param {string=} response.pluginName   Optional. Name of the plugin to be installed.
    872 	 * @param {string}  response.errorCode    Error code for the error that occurred.
    873 	 * @param {string}  response.errorMessage The error that occurred.
    874 	 */
    875 	wp.updates.installImporterError = function( response ) {
    876 		var errorMessage = sprintf(
    877 				/* translators: %s: Error string for a failed installation. */
    878 				__( 'Installation failed: %s' ),
    879 				response.errorMessage
    880 			),
    881 			$installLink = $( '[data-slug="' + response.slug + '"]' ),
    882 			pluginName = $installLink.data( 'name' );
    883 
    884 		if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
    885 			return;
    886 		}
    887 
    888 		if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
    889 			return;
    890 		}
    891 
    892 		wp.updates.addAdminNotice( {
    893 			id:        response.errorCode,
    894 			className: 'notice-error is-dismissible',
    895 			message:   errorMessage
    896 		} );
    897 
    898 		$installLink
    899 			.removeClass( 'updating-message' )
    900 			.attr(
    901 				'aria-label',
    902 				sprintf(
    903 					/* translators: %s: Plugin name. */
    904 					_x( 'Install %s now', 'plugin' ),
    905 					pluginName
    906 				)
    907 			)
    908 			.text( __( 'Install Now' ) );
    909 
    910 		wp.a11y.speak( errorMessage, 'assertive' );
    911 
    912 		$document.trigger( 'wp-importer-install-error', response );
    913 	};
    914 
    915 	/**
    916 	 * Sends an Ajax request to the server to delete a plugin.
    917 	 *
    918 	 * @since 4.6.0
    919 	 *
    920 	 * @param {Object}               args         Arguments.
    921 	 * @param {string}               args.plugin  Basename of the plugin to be deleted.
    922 	 * @param {string}               args.slug    Slug of the plugin to be deleted.
    923 	 * @param {deletePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.deletePluginSuccess
    924 	 * @param {deletePluginError=}   args.error   Optional. Error callback. Default: wp.updates.deletePluginError
    925 	 * @return {$.promise} A jQuery promise that represents the request,
    926 	 *                     decorated with an abort() method.
    927 	 */
    928 	wp.updates.deletePlugin = function( args ) {
    929 		var $link = $( '[data-plugin="' + args.plugin + '"]' ).find( '.row-actions a.delete' );
    930 
    931 		args = _.extend( {
    932 			success: wp.updates.deletePluginSuccess,
    933 			error: wp.updates.deletePluginError
    934 		}, args );
    935 
    936 		if ( $link.html() !== __( 'Deleting...' ) ) {
    937 			$link
    938 				.data( 'originaltext', $link.html() )
    939 				.text( __( 'Deleting...' ) );
    940 		}
    941 
    942 		wp.a11y.speak( __( 'Deleting...' ) );
    943 
    944 		$document.trigger( 'wp-plugin-deleting', args );
    945 
    946 		return wp.updates.ajax( 'delete-plugin', args );
    947 	};
    948 
    949 	/**
    950 	 * Updates the UI appropriately after a successful plugin deletion.
    951 	 *
    952 	 * @since 4.6.0
    953 	 *
    954 	 * @param {Object} response            Response from the server.
    955 	 * @param {string} response.slug       Slug of the plugin that was deleted.
    956 	 * @param {string} response.plugin     Base name of the plugin that was deleted.
    957 	 * @param {string} response.pluginName Name of the plugin that was deleted.
    958 	 */
    959 	wp.updates.deletePluginSuccess = function( response ) {
    960 
    961 		// Removes the plugin and updates rows.
    962 		$( '[data-plugin="' + response.plugin + '"]' ).css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
    963 			var $form            = $( '#bulk-action-form' ),
    964 				$views           = $( '.subsubsub' ),
    965 				$pluginRow       = $( this ),
    966 				columnCount      = $form.find( 'thead th:not(.hidden), thead td' ).length,
    967 				pluginDeletedRow = wp.template( 'item-deleted-row' ),
    968 				/**
    969 				 * Plugins Base names of plugins in their different states.
    970 				 *
    971 				 * @type {Object}
    972 				 */
    973 				plugins          = settings.plugins;
    974 
    975 			// Add a success message after deleting a plugin.
    976 			if ( ! $pluginRow.hasClass( 'plugin-update-tr' ) ) {
    977 				$pluginRow.after(
    978 					pluginDeletedRow( {
    979 						slug:    response.slug,
    980 						plugin:  response.plugin,
    981 						colspan: columnCount,
    982 						name:    response.pluginName
    983 					} )
    984 				);
    985 			}
    986 
    987 			$pluginRow.remove();
    988 
    989 			// Remove plugin from update count.
    990 			if ( -1 !== _.indexOf( plugins.upgrade, response.plugin ) ) {
    991 				plugins.upgrade = _.without( plugins.upgrade, response.plugin );
    992 				wp.updates.decrementCount( 'plugin' );
    993 			}
    994 
    995 			// Remove from views.
    996 			if ( -1 !== _.indexOf( plugins.inactive, response.plugin ) ) {
    997 				plugins.inactive = _.without( plugins.inactive, response.plugin );
    998 				if ( plugins.inactive.length ) {
    999 					$views.find( '.inactive .count' ).text( '(' + plugins.inactive.length + ')' );
   1000 				} else {
   1001 					$views.find( '.inactive' ).remove();
   1002 				}
   1003 			}
   1004 
   1005 			if ( -1 !== _.indexOf( plugins.active, response.plugin ) ) {
   1006 				plugins.active = _.without( plugins.active, response.plugin );
   1007 				if ( plugins.active.length ) {
   1008 					$views.find( '.active .count' ).text( '(' + plugins.active.length + ')' );
   1009 				} else {
   1010 					$views.find( '.active' ).remove();
   1011 				}
   1012 			}
   1013 
   1014 			if ( -1 !== _.indexOf( plugins.recently_activated, response.plugin ) ) {
   1015 				plugins.recently_activated = _.without( plugins.recently_activated, response.plugin );
   1016 				if ( plugins.recently_activated.length ) {
   1017 					$views.find( '.recently_activated .count' ).text( '(' + plugins.recently_activated.length + ')' );
   1018 				} else {
   1019 					$views.find( '.recently_activated' ).remove();
   1020 				}
   1021 			}
   1022 
   1023 			if ( -1 !== _.indexOf( plugins['auto-update-enabled'], response.plugin ) ) {
   1024 				plugins['auto-update-enabled'] = _.without( plugins['auto-update-enabled'], response.plugin );
   1025 				if ( plugins['auto-update-enabled'].length ) {
   1026 					$views.find( '.auto-update-enabled .count' ).text( '(' + plugins['auto-update-enabled'].length + ')' );
   1027 				} else {
   1028 					$views.find( '.auto-update-enabled' ).remove();
   1029 				}
   1030 			}
   1031 
   1032 			if ( -1 !== _.indexOf( plugins['auto-update-disabled'], response.plugin ) ) {
   1033 				plugins['auto-update-disabled'] = _.without( plugins['auto-update-disabled'], response.plugin );
   1034 				if ( plugins['auto-update-disabled'].length ) {
   1035 					$views.find( '.auto-update-disabled .count' ).text( '(' + plugins['auto-update-disabled'].length + ')' );
   1036 				} else {
   1037 					$views.find( '.auto-update-disabled' ).remove();
   1038 				}
   1039 			}
   1040 
   1041 			plugins.all = _.without( plugins.all, response.plugin );
   1042 
   1043 			if ( plugins.all.length ) {
   1044 				$views.find( '.all .count' ).text( '(' + plugins.all.length + ')' );
   1045 			} else {
   1046 				$form.find( '.tablenav' ).css( { visibility: 'hidden' } );
   1047 				$views.find( '.all' ).remove();
   1048 
   1049 				if ( ! $form.find( 'tr.no-items' ).length ) {
   1050 					$form.find( '#the-list' ).append( '<tr class="no-items"><td class="colspanchange" colspan="' + columnCount + '">' + __( 'No plugins are currently available.' ) + '</td></tr>' );
   1051 				}
   1052 			}
   1053 		} );
   1054 
   1055 		wp.a11y.speak( _x( 'Deleted!', 'plugin' ) );
   1056 
   1057 		$document.trigger( 'wp-plugin-delete-success', response );
   1058 	};
   1059 
   1060 	/**
   1061 	 * Updates the UI appropriately after a failed plugin deletion.
   1062 	 *
   1063 	 * @since 4.6.0
   1064 	 *
   1065 	 * @param {Object}  response              Response from the server.
   1066 	 * @param {string}  response.slug         Slug of the plugin to be deleted.
   1067 	 * @param {string}  response.plugin       Base name of the plugin to be deleted
   1068 	 * @param {string=} response.pluginName   Optional. Name of the plugin to be deleted.
   1069 	 * @param {string}  response.errorCode    Error code for the error that occurred.
   1070 	 * @param {string}  response.errorMessage The error that occurred.
   1071 	 */
   1072 	wp.updates.deletePluginError = function( response ) {
   1073 		var $plugin, $pluginUpdateRow,
   1074 			pluginUpdateRow  = wp.template( 'item-update-row' ),
   1075 			noticeContent    = wp.updates.adminNotice( {
   1076 				className: 'update-message notice-error notice-alt',
   1077 				message:   response.errorMessage
   1078 			} );
   1079 
   1080 		if ( response.plugin ) {
   1081 			$plugin          = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' );
   1082 			$pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' );
   1083 		} else {
   1084 			$plugin          = $( 'tr.inactive[data-slug="' + response.slug + '"]' );
   1085 			$pluginUpdateRow = $plugin.siblings( '[data-slug="' + response.slug + '"]' );
   1086 		}
   1087 
   1088 		if ( ! wp.updates.isValidResponse( response, 'delete' ) ) {
   1089 			return;
   1090 		}
   1091 
   1092 		if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) {
   1093 			return;
   1094 		}
   1095 
   1096 		// Add a plugin update row if it doesn't exist yet.
   1097 		if ( ! $pluginUpdateRow.length ) {
   1098 			$plugin.addClass( 'update' ).after(
   1099 				pluginUpdateRow( {
   1100 					slug:    response.slug,
   1101 					plugin:  response.plugin || response.slug,
   1102 					colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
   1103 					content: noticeContent
   1104 				} )
   1105 			);
   1106 		} else {
   1107 
   1108 			// Remove previous error messages, if any.
   1109 			$pluginUpdateRow.find( '.notice-error' ).remove();
   1110 
   1111 			$pluginUpdateRow.find( '.plugin-update' ).append( noticeContent );
   1112 		}
   1113 
   1114 		$document.trigger( 'wp-plugin-delete-error', response );
   1115 	};
   1116 
   1117 	/**
   1118 	 * Sends an Ajax request to the server to update a theme.
   1119 	 *
   1120 	 * @since 4.6.0
   1121 	 *
   1122 	 * @param {Object}              args         Arguments.
   1123 	 * @param {string}              args.slug    Theme stylesheet.
   1124 	 * @param {updateThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.updateThemeSuccess
   1125 	 * @param {updateThemeError=}   args.error   Optional. Error callback. Default: wp.updates.updateThemeError
   1126 	 * @return {$.promise} A jQuery promise that represents the request,
   1127 	 *                     decorated with an abort() method.
   1128 	 */
   1129 	wp.updates.updateTheme = function( args ) {
   1130 		var $notice;
   1131 
   1132 		args = _.extend( {
   1133 			success: wp.updates.updateThemeSuccess,
   1134 			error: wp.updates.updateThemeError
   1135 		}, args );
   1136 
   1137 		if ( 'themes-network' === pagenow ) {
   1138 			$notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
   1139 
   1140 		} else if ( 'customize' === pagenow ) {
   1141 
   1142 			// Update the theme details UI.
   1143 			$notice = $( '[data-slug="' + args.slug + '"].notice' ).removeClass( 'notice-large' );
   1144 
   1145 			$notice.find( 'h3' ).remove();
   1146 
   1147 			// Add the top-level UI, and update both.
   1148 			$notice = $notice.add( $( '#customize-control-installed_theme_' + args.slug ).find( '.update-message' ) );
   1149 			$notice = $notice.addClass( 'updating-message' ).find( 'p' );
   1150 
   1151 		} else {
   1152 			$notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' );
   1153 
   1154 			$notice.find( 'h3' ).remove();
   1155 
   1156 			$notice = $notice.add( $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ) );
   1157 			$notice = $notice.addClass( 'updating-message' ).find( 'p' );
   1158 		}
   1159 
   1160 		if ( $notice.html() !== __( 'Updating...' ) ) {
   1161 			$notice.data( 'originaltext', $notice.html() );
   1162 		}
   1163 
   1164 		wp.a11y.speak( __( 'Updating... please wait.' ) );
   1165 		$notice.text( __( 'Updating...' ) );
   1166 
   1167 		$document.trigger( 'wp-theme-updating', args );
   1168 
   1169 		return wp.updates.ajax( 'update-theme', args );
   1170 	};
   1171 
   1172 	/**
   1173 	 * Updates the UI appropriately after a successful theme update.
   1174 	 *
   1175 	 * @since 4.6.0
   1176 	 * @since 5.5.0 Auto-update "time to next update" text cleared.
   1177 	 *
   1178 	 * @param {Object} response
   1179 	 * @param {string} response.slug       Slug of the theme to be updated.
   1180 	 * @param {Object} response.theme      Updated theme.
   1181 	 * @param {string} response.oldVersion Old version of the theme.
   1182 	 * @param {string} response.newVersion New version of the theme.
   1183 	 */
   1184 	wp.updates.updateThemeSuccess = function( response ) {
   1185 		var isModalOpen    = $( 'body.modal-open' ).length,
   1186 			$theme         = $( '[data-slug="' + response.slug + '"]' ),
   1187 			updatedMessage = {
   1188 				className: 'updated-message notice-success notice-alt',
   1189 				message:   _x( 'Updated!', 'theme' )
   1190 			},
   1191 			$notice, newText;
   1192 
   1193 		if ( 'customize' === pagenow ) {
   1194 			$theme = $( '.updating-message' ).siblings( '.theme-name' );
   1195 
   1196 			if ( $theme.length ) {
   1197 
   1198 				// Update the version number in the row.
   1199 				newText = $theme.html().replace( response.oldVersion, response.newVersion );
   1200 				$theme.html( newText );
   1201 			}
   1202 
   1203 			$notice = $( '.theme-info .notice' ).add( wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' ).find( '.update-message' ) );
   1204 		} else if ( 'themes-network' === pagenow ) {
   1205 			$notice = $theme.find( '.update-message' );
   1206 
   1207 			// Update the version number in the row.
   1208 			newText = $theme.find( '.theme-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
   1209 			$theme.find( '.theme-version-author-uri' ).html( newText );
   1210 
   1211 			// Clear the "time to next auto-update" text.
   1212 			$theme.find( '.auto-update-time' ).empty();
   1213 		} else {
   1214 			$notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) );
   1215 
   1216 			// Focus on Customize button after updating.
   1217 			if ( isModalOpen ) {
   1218 				$( '.load-customize:visible' ).trigger( 'focus' );
   1219 				$( '.theme-info .theme-autoupdate' ).find( '.auto-update-time' ).empty();
   1220 			} else {
   1221 				$theme.find( '.load-customize' ).trigger( 'focus' );
   1222 			}
   1223 		}
   1224 
   1225 		wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) );
   1226 		wp.a11y.speak( __( 'Update completed successfully.' ) );
   1227 
   1228 		wp.updates.decrementCount( 'theme' );
   1229 
   1230 		$document.trigger( 'wp-theme-update-success', response );
   1231 
   1232 		// Show updated message after modal re-rendered.
   1233 		if ( isModalOpen && 'customize' !== pagenow ) {
   1234 			$( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) );
   1235 		}
   1236 	};
   1237 
   1238 	/**
   1239 	 * Updates the UI appropriately after a failed theme update.
   1240 	 *
   1241 	 * @since 4.6.0
   1242 	 *
   1243 	 * @param {Object} response              Response from the server.
   1244 	 * @param {string} response.slug         Slug of the theme to be updated.
   1245 	 * @param {string} response.errorCode    Error code for the error that occurred.
   1246 	 * @param {string} response.errorMessage The error that occurred.
   1247 	 */
   1248 	wp.updates.updateThemeError = function( response ) {
   1249 		var $theme       = $( '[data-slug="' + response.slug + '"]' ),
   1250 			errorMessage = sprintf(
   1251 				/* translators: %s: Error string for a failed update. */
   1252 				 __( 'Update failed: %s' ),
   1253 				response.errorMessage
   1254 			),
   1255 			$notice;
   1256 
   1257 		if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
   1258 			return;
   1259 		}
   1260 
   1261 		if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) {
   1262 			return;
   1263 		}
   1264 
   1265 		if ( 'customize' === pagenow ) {
   1266 			$theme = wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' );
   1267 		}
   1268 
   1269 		if ( 'themes-network' === pagenow ) {
   1270 			$notice = $theme.find( '.update-message ' );
   1271 		} else {
   1272 			$notice = $( '.theme-info .notice' ).add( $theme.find( '.notice' ) );
   1273 
   1274 			$( 'body.modal-open' ).length ? $( '.load-customize:visible' ).trigger( 'focus' ) : $theme.find( '.load-customize' ).trigger( 'focus');
   1275 		}
   1276 
   1277 		wp.updates.addAdminNotice( {
   1278 			selector:  $notice,
   1279 			className: 'update-message notice-error notice-alt is-dismissible',
   1280 			message:   errorMessage
   1281 		} );
   1282 
   1283 		wp.a11y.speak( errorMessage );
   1284 
   1285 		$document.trigger( 'wp-theme-update-error', response );
   1286 	};
   1287 
   1288 	/**
   1289 	 * Sends an Ajax request to the server to install a theme.
   1290 	 *
   1291 	 * @since 4.6.0
   1292 	 *
   1293 	 * @param {Object}               args
   1294 	 * @param {string}               args.slug    Theme stylesheet.
   1295 	 * @param {installThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.installThemeSuccess
   1296 	 * @param {installThemeError=}   args.error   Optional. Error callback. Default: wp.updates.installThemeError
   1297 	 * @return {$.promise} A jQuery promise that represents the request,
   1298 	 *                     decorated with an abort() method.
   1299 	 */
   1300 	wp.updates.installTheme = function( args ) {
   1301 		var $message = $( '.theme-install[data-slug="' + args.slug + '"]' );
   1302 
   1303 		args = _.extend( {
   1304 			success: wp.updates.installThemeSuccess,
   1305 			error: wp.updates.installThemeError
   1306 		}, args );
   1307 
   1308 		$message.addClass( 'updating-message' );
   1309 		$message.parents( '.theme' ).addClass( 'focus' );
   1310 		if ( $message.html() !== __( 'Installing...' ) ) {
   1311 			$message.data( 'originaltext', $message.html() );
   1312 		}
   1313 
   1314 		$message
   1315 			.attr(
   1316 				'aria-label',
   1317 				sprintf(
   1318 					/* translators: %s: Theme name and version. */
   1319 					_x( 'Installing %s...', 'theme' ),
   1320 					$message.data( 'name' )
   1321 				)
   1322 			)
   1323 			.text( __( 'Installing...' ) );
   1324 
   1325 		wp.a11y.speak( __( 'Installing... please wait.' ) );
   1326 
   1327 		// Remove previous error messages, if any.
   1328 		$( '.install-theme-info, [data-slug="' + args.slug + '"]' ).removeClass( 'theme-install-failed' ).find( '.notice.notice-error' ).remove();
   1329 
   1330 		$document.trigger( 'wp-theme-installing', args );
   1331 
   1332 		return wp.updates.ajax( 'install-theme', args );
   1333 	};
   1334 
   1335 	/**
   1336 	 * Updates the UI appropriately after a successful theme install.
   1337 	 *
   1338 	 * @since 4.6.0
   1339 	 *
   1340 	 * @param {Object} response              Response from the server.
   1341 	 * @param {string} response.slug         Slug of the theme to be installed.
   1342 	 * @param {string} response.customizeUrl URL to the Customizer for the just installed theme.
   1343 	 * @param {string} response.activateUrl  URL to activate the just installed theme.
   1344 	 */
   1345 	wp.updates.installThemeSuccess = function( response ) {
   1346 		var $card = $( '.wp-full-overlay-header, [data-slug=' + response.slug + ']' ),
   1347 			$message;
   1348 
   1349 		$document.trigger( 'wp-theme-install-success', response );
   1350 
   1351 		$message = $card.find( '.button-primary' )
   1352 			.removeClass( 'updating-message' )
   1353 			.addClass( 'updated-message disabled' )
   1354 			.attr(
   1355 				'aria-label',
   1356 				sprintf(
   1357 					/* translators: %s: Theme name and version. */
   1358 					_x( '%s installed!', 'theme' ),
   1359 					response.themeName
   1360 				)
   1361 			)
   1362 			.text( _x( 'Installed!', 'theme' ) );
   1363 
   1364 		wp.a11y.speak( __( 'Installation completed successfully.' ) );
   1365 
   1366 		setTimeout( function() {
   1367 
   1368 			if ( response.activateUrl ) {
   1369 
   1370 				// Transform the 'Install' button into an 'Activate' button.
   1371 				$message
   1372 					.attr( 'href', response.activateUrl )
   1373 					.removeClass( 'theme-install updated-message disabled' )
   1374 					.addClass( 'activate' );
   1375 
   1376 				if ( 'themes-network' === pagenow ) {
   1377 					$message
   1378 						.attr(
   1379 							'aria-label',
   1380 							sprintf(
   1381 								/* translators: %s: Theme name. */
   1382 								_x( 'Network Activate %s', 'theme' ),
   1383 								response.themeName
   1384 							)
   1385 						)
   1386 						.text( __( 'Network Enable' ) );
   1387 				} else {
   1388 					$message
   1389 						.attr(
   1390 							'aria-label',
   1391 							sprintf(
   1392 								/* translators: %s: Theme name. */
   1393 								_x( 'Activate %s', 'theme' ),
   1394 								response.themeName
   1395 							)
   1396 						)
   1397 						.text( __( 'Activate' ) );
   1398 				}
   1399 			}
   1400 
   1401 			if ( response.customizeUrl ) {
   1402 
   1403 				// Transform the 'Preview' button into a 'Live Preview' button.
   1404 				$message.siblings( '.preview' ).replaceWith( function () {
   1405 					return $( '<a>' )
   1406 						.attr( 'href', response.customizeUrl )
   1407 						.addClass( 'button load-customize' )
   1408 						.text( __( 'Live Preview' ) );
   1409 				} );
   1410 			}
   1411 		}, 1000 );
   1412 	};
   1413 
   1414 	/**
   1415 	 * Updates the UI appropriately after a failed theme install.
   1416 	 *
   1417 	 * @since 4.6.0
   1418 	 *
   1419 	 * @param {Object} response              Response from the server.
   1420 	 * @param {string} response.slug         Slug of the theme to be installed.
   1421 	 * @param {string} response.errorCode    Error code for the error that occurred.
   1422 	 * @param {string} response.errorMessage The error that occurred.
   1423 	 */
   1424 	wp.updates.installThemeError = function( response ) {
   1425 		var $card, $button,
   1426 			errorMessage = sprintf(
   1427 				/* translators: %s: Error string for a failed installation. */
   1428 				__( 'Installation failed: %s' ),
   1429 				response.errorMessage
   1430 			),
   1431 			$message     = wp.updates.adminNotice( {
   1432 				className: 'update-message notice-error notice-alt',
   1433 				message:   errorMessage
   1434 			} );
   1435 
   1436 		if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
   1437 			return;
   1438 		}
   1439 
   1440 		if ( wp.updates.maybeHandleCredentialError( response, 'install-theme' ) ) {
   1441 			return;
   1442 		}
   1443 
   1444 		if ( 'customize' === pagenow ) {
   1445 			if ( $document.find( 'body' ).hasClass( 'modal-open' ) ) {
   1446 				$button = $( '.theme-install[data-slug="' + response.slug + '"]' );
   1447 				$card   = $( '.theme-overlay .theme-info' ).prepend( $message );
   1448 			} else {
   1449 				$button = $( '.theme-install[data-slug="' + response.slug + '"]' );
   1450 				$card   = $button.closest( '.theme' ).addClass( 'theme-install-failed' ).append( $message );
   1451 			}
   1452 			wp.customize.notifications.remove( 'theme_installing' );
   1453 		} else {
   1454 			if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) {
   1455 				$button = $( '.theme-install[data-slug="' + response.slug + '"]' );
   1456 				$card   = $( '.install-theme-info' ).prepend( $message );
   1457 			} else {
   1458 				$card   = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message );
   1459 				$button = $card.find( '.theme-install' );
   1460 			}
   1461 		}
   1462 
   1463 		$button
   1464 			.removeClass( 'updating-message' )
   1465 			.attr(
   1466 				'aria-label',
   1467 				sprintf(
   1468 					/* translators: %s: Theme name and version. */
   1469 					_x( '%s installation failed', 'theme' ),
   1470 					$button.data( 'name' )
   1471 				)
   1472 			)
   1473 			.text( __( 'Installation failed.' ) );
   1474 
   1475 		wp.a11y.speak( errorMessage, 'assertive' );
   1476 
   1477 		$document.trigger( 'wp-theme-install-error', response );
   1478 	};
   1479 
   1480 	/**
   1481 	 * Sends an Ajax request to the server to delete a theme.
   1482 	 *
   1483 	 * @since 4.6.0
   1484 	 *
   1485 	 * @param {Object}              args
   1486 	 * @param {string}              args.slug    Theme stylesheet.
   1487 	 * @param {deleteThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.deleteThemeSuccess
   1488 	 * @param {deleteThemeError=}   args.error   Optional. Error callback. Default: wp.updates.deleteThemeError
   1489 	 * @return {$.promise} A jQuery promise that represents the request,
   1490 	 *                     decorated with an abort() method.
   1491 	 */
   1492 	wp.updates.deleteTheme = function( args ) {
   1493 		var $button;
   1494 
   1495 		if ( 'themes' === pagenow ) {
   1496 			$button = $( '.theme-actions .delete-theme' );
   1497 		} else if ( 'themes-network' === pagenow ) {
   1498 			$button = $( '[data-slug="' + args.slug + '"]' ).find( '.row-actions a.delete' );
   1499 		}
   1500 
   1501 		args = _.extend( {
   1502 			success: wp.updates.deleteThemeSuccess,
   1503 			error: wp.updates.deleteThemeError
   1504 		}, args );
   1505 
   1506 		if ( $button && $button.html() !== __( 'Deleting...' ) ) {
   1507 			$button
   1508 				.data( 'originaltext', $button.html() )
   1509 				.text( __( 'Deleting...' ) );
   1510 		}
   1511 
   1512 		wp.a11y.speak( __( 'Deleting...' ) );
   1513 
   1514 		// Remove previous error messages, if any.
   1515 		$( '.theme-info .update-message' ).remove();
   1516 
   1517 		$document.trigger( 'wp-theme-deleting', args );
   1518 
   1519 		return wp.updates.ajax( 'delete-theme', args );
   1520 	};
   1521 
   1522 	/**
   1523 	 * Updates the UI appropriately after a successful theme deletion.
   1524 	 *
   1525 	 * @since 4.6.0
   1526 	 *
   1527 	 * @param {Object} response      Response from the server.
   1528 	 * @param {string} response.slug Slug of the theme that was deleted.
   1529 	 */
   1530 	wp.updates.deleteThemeSuccess = function( response ) {
   1531 		var $themeRows = $( '[data-slug="' + response.slug + '"]' );
   1532 
   1533 		if ( 'themes-network' === pagenow ) {
   1534 
   1535 			// Removes the theme and updates rows.
   1536 			$themeRows.css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
   1537 				var $views     = $( '.subsubsub' ),
   1538 					$themeRow  = $( this ),
   1539 					themes     = settings.themes,
   1540 					deletedRow = wp.template( 'item-deleted-row' );
   1541 
   1542 				if ( ! $themeRow.hasClass( 'plugin-update-tr' ) ) {
   1543 					$themeRow.after(
   1544 						deletedRow( {
   1545 							slug:    response.slug,
   1546 							colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
   1547 							name:    $themeRow.find( '.theme-title strong' ).text()
   1548 						} )
   1549 					);
   1550 				}
   1551 
   1552 				$themeRow.remove();
   1553 
   1554 				// Remove theme from update count.
   1555 				if ( -1 !== _.indexOf( themes.upgrade, response.slug ) ) {
   1556 					themes.upgrade = _.without( themes.upgrade, response.slug );
   1557 					wp.updates.decrementCount( 'theme' );
   1558 				}
   1559 
   1560 				// Remove from views.
   1561 				if ( -1 !== _.indexOf( themes.disabled, response.slug ) ) {
   1562 					themes.disabled = _.without( themes.disabled, response.slug );
   1563 					if ( themes.disabled.length ) {
   1564 						$views.find( '.disabled .count' ).text( '(' + themes.disabled.length + ')' );
   1565 					} else {
   1566 						$views.find( '.disabled' ).remove();
   1567 					}
   1568 				}
   1569 
   1570 				if ( -1 !== _.indexOf( themes['auto-update-enabled'], response.slug ) ) {
   1571 					themes['auto-update-enabled'] = _.without( themes['auto-update-enabled'], response.slug );
   1572 					if ( themes['auto-update-enabled'].length ) {
   1573 						$views.find( '.auto-update-enabled .count' ).text( '(' + themes['auto-update-enabled'].length + ')' );
   1574 					} else {
   1575 						$views.find( '.auto-update-enabled' ).remove();
   1576 					}
   1577 				}
   1578 
   1579 				if ( -1 !== _.indexOf( themes['auto-update-disabled'], response.slug ) ) {
   1580 					themes['auto-update-disabled'] = _.without( themes['auto-update-disabled'], response.slug );
   1581 					if ( themes['auto-update-disabled'].length ) {
   1582 						$views.find( '.auto-update-disabled .count' ).text( '(' + themes['auto-update-disabled'].length + ')' );
   1583 					} else {
   1584 						$views.find( '.auto-update-disabled' ).remove();
   1585 					}
   1586 				}
   1587 
   1588 				themes.all = _.without( themes.all, response.slug );
   1589 
   1590 				// There is always at least one theme available.
   1591 				$views.find( '.all .count' ).text( '(' + themes.all.length + ')' );
   1592 			} );
   1593 		}
   1594 
   1595 		wp.a11y.speak( _x( 'Deleted!', 'theme' ) );
   1596 
   1597 		$document.trigger( 'wp-theme-delete-success', response );
   1598 	};
   1599 
   1600 	/**
   1601 	 * Updates the UI appropriately after a failed theme deletion.
   1602 	 *
   1603 	 * @since 4.6.0
   1604 	 *
   1605 	 * @param {Object} response              Response from the server.
   1606 	 * @param {string} response.slug         Slug of the theme to be deleted.
   1607 	 * @param {string} response.errorCode    Error code for the error that occurred.
   1608 	 * @param {string} response.errorMessage The error that occurred.
   1609 	 */
   1610 	wp.updates.deleteThemeError = function( response ) {
   1611 		var $themeRow    = $( 'tr.inactive[data-slug="' + response.slug + '"]' ),
   1612 			$button      = $( '.theme-actions .delete-theme' ),
   1613 			updateRow    = wp.template( 'item-update-row' ),
   1614 			$updateRow   = $themeRow.siblings( '#' + response.slug + '-update' ),
   1615 			errorMessage = sprintf(
   1616 				/* translators: %s: Error string for a failed deletion. */
   1617 				__( 'Deletion failed: %s' ),
   1618 				response.errorMessage
   1619 			),
   1620 			$message     = wp.updates.adminNotice( {
   1621 				className: 'update-message notice-error notice-alt',
   1622 				message:   errorMessage
   1623 			} );
   1624 
   1625 		if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) {
   1626 			return;
   1627 		}
   1628 
   1629 		if ( 'themes-network' === pagenow ) {
   1630 			if ( ! $updateRow.length ) {
   1631 				$themeRow.addClass( 'update' ).after(
   1632 					updateRow( {
   1633 						slug: response.slug,
   1634 						colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
   1635 						content: $message
   1636 					} )
   1637 				);
   1638 			} else {
   1639 				// Remove previous error messages, if any.
   1640 				$updateRow.find( '.notice-error' ).remove();
   1641 				$updateRow.find( '.plugin-update' ).append( $message );
   1642 			}
   1643 		} else {
   1644 			$( '.theme-info .theme-description' ).before( $message );
   1645 		}
   1646 
   1647 		$button.html( $button.data( 'originaltext' ) );
   1648 
   1649 		wp.a11y.speak( errorMessage, 'assertive' );
   1650 
   1651 		$document.trigger( 'wp-theme-delete-error', response );
   1652 	};
   1653 
   1654 	/**
   1655 	 * Adds the appropriate callback based on the type of action and the current page.
   1656 	 *
   1657 	 * @since 4.6.0
   1658 	 * @private
   1659 	 *
   1660 	 * @param {Object} data   Ajax payload.
   1661 	 * @param {string} action The type of request to perform.
   1662 	 * @return {Object} The Ajax payload with the appropriate callbacks.
   1663 	 */
   1664 	wp.updates._addCallbacks = function( data, action ) {
   1665 		if ( 'import' === pagenow && 'install-plugin' === action ) {
   1666 			data.success = wp.updates.installImporterSuccess;
   1667 			data.error   = wp.updates.installImporterError;
   1668 		}
   1669 
   1670 		return data;
   1671 	};
   1672 
   1673 	/**
   1674 	 * Pulls available jobs from the queue and runs them.
   1675 	 *
   1676 	 * @since 4.2.0
   1677 	 * @since 4.6.0 Can handle multiple job types.
   1678 	 */
   1679 	wp.updates.queueChecker = function() {
   1680 		var job;
   1681 
   1682 		if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) {
   1683 			return;
   1684 		}
   1685 
   1686 		job = wp.updates.queue.shift();
   1687 
   1688 		// Handle a queue job.
   1689 		switch ( job.action ) {
   1690 			case 'install-plugin':
   1691 				wp.updates.installPlugin( job.data );
   1692 				break;
   1693 
   1694 			case 'update-plugin':
   1695 				wp.updates.updatePlugin( job.data );
   1696 				break;
   1697 
   1698 			case 'delete-plugin':
   1699 				wp.updates.deletePlugin( job.data );
   1700 				break;
   1701 
   1702 			case 'install-theme':
   1703 				wp.updates.installTheme( job.data );
   1704 				break;
   1705 
   1706 			case 'update-theme':
   1707 				wp.updates.updateTheme( job.data );
   1708 				break;
   1709 
   1710 			case 'delete-theme':
   1711 				wp.updates.deleteTheme( job.data );
   1712 				break;
   1713 
   1714 			default:
   1715 				break;
   1716 		}
   1717 	};
   1718 
   1719 	/**
   1720 	 * Requests the users filesystem credentials if they aren't already known.
   1721 	 *
   1722 	 * @since 4.2.0
   1723 	 *
   1724 	 * @param {Event=} event Optional. Event interface.
   1725 	 */
   1726 	wp.updates.requestFilesystemCredentials = function( event ) {
   1727 		if ( false === wp.updates.filesystemCredentials.available ) {
   1728 			/*
   1729 			 * After exiting the credentials request modal,
   1730 			 * return the focus to the element triggering the request.
   1731 			 */
   1732 			if ( event && ! wp.updates.$elToReturnFocusToFromCredentialsModal ) {
   1733 				wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target );
   1734 			}
   1735 
   1736 			wp.updates.ajaxLocked = true;
   1737 			wp.updates.requestForCredentialsModalOpen();
   1738 		}
   1739 	};
   1740 
   1741 	/**
   1742 	 * Requests the users filesystem credentials if needed and there is no lock.
   1743 	 *
   1744 	 * @since 4.6.0
   1745 	 *
   1746 	 * @param {Event=} event Optional. Event interface.
   1747 	 */
   1748 	wp.updates.maybeRequestFilesystemCredentials = function( event ) {
   1749 		if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
   1750 			wp.updates.requestFilesystemCredentials( event );
   1751 		}
   1752 	};
   1753 
   1754 	/**
   1755 	 * Keydown handler for the request for credentials modal.
   1756 	 *
   1757 	 * Closes the modal when the escape key is pressed and
   1758 	 * constrains keyboard navigation to inside the modal.
   1759 	 *
   1760 	 * @since 4.2.0
   1761 	 *
   1762 	 * @param {Event} event Event interface.
   1763 	 */
   1764 	wp.updates.keydown = function( event ) {
   1765 		if ( 27 === event.keyCode ) {
   1766 			wp.updates.requestForCredentialsModalCancel();
   1767 		} else if ( 9 === event.keyCode ) {
   1768 
   1769 			// #upgrade button must always be the last focus-able element in the dialog.
   1770 			if ( 'upgrade' === event.target.id && ! event.shiftKey ) {
   1771 				$( '#hostname' ).trigger( 'focus' );
   1772 
   1773 				event.preventDefault();
   1774 			} else if ( 'hostname' === event.target.id && event.shiftKey ) {
   1775 				$( '#upgrade' ).trigger( 'focus' );
   1776 
   1777 				event.preventDefault();
   1778 			}
   1779 		}
   1780 	};
   1781 
   1782 	/**
   1783 	 * Opens the request for credentials modal.
   1784 	 *
   1785 	 * @since 4.2.0
   1786 	 */
   1787 	wp.updates.requestForCredentialsModalOpen = function() {
   1788 		var $modal = $( '#request-filesystem-credentials-dialog' );
   1789 
   1790 		$( 'body' ).addClass( 'modal-open' );
   1791 		$modal.show();
   1792 		$modal.find( 'input:enabled:first' ).trigger( 'focus' );
   1793 		$modal.on( 'keydown', wp.updates.keydown );
   1794 	};
   1795 
   1796 	/**
   1797 	 * Closes the request for credentials modal.
   1798 	 *
   1799 	 * @since 4.2.0
   1800 	 */
   1801 	wp.updates.requestForCredentialsModalClose = function() {
   1802 		$( '#request-filesystem-credentials-dialog' ).hide();
   1803 		$( 'body' ).removeClass( 'modal-open' );
   1804 
   1805 		if ( wp.updates.$elToReturnFocusToFromCredentialsModal ) {
   1806 			wp.updates.$elToReturnFocusToFromCredentialsModal.trigger( 'focus' );
   1807 		}
   1808 	};
   1809 
   1810 	/**
   1811 	 * Takes care of the steps that need to happen when the modal is canceled out.
   1812 	 *
   1813 	 * @since 4.2.0
   1814 	 * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions.
   1815 	 */
   1816 	wp.updates.requestForCredentialsModalCancel = function() {
   1817 
   1818 		// Not ajaxLocked and no queue means we already have cleared things up.
   1819 		if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) {
   1820 			return;
   1821 		}
   1822 
   1823 		_.each( wp.updates.queue, function( job ) {
   1824 			$document.trigger( 'credential-modal-cancel', job );
   1825 		} );
   1826 
   1827 		// Remove the lock, and clear the queue.
   1828 		wp.updates.ajaxLocked = false;
   1829 		wp.updates.queue = [];
   1830 
   1831 		wp.updates.requestForCredentialsModalClose();
   1832 	};
   1833 
   1834 	/**
   1835 	 * Displays an error message in the request for credentials form.
   1836 	 *
   1837 	 * @since 4.2.0
   1838 	 *
   1839 	 * @param {string} message Error message.
   1840 	 */
   1841 	wp.updates.showErrorInCredentialsForm = function( message ) {
   1842 		var $filesystemForm = $( '#request-filesystem-credentials-form' );
   1843 
   1844 		// Remove any existing error.
   1845 		$filesystemForm.find( '.notice' ).remove();
   1846 		$filesystemForm.find( '#request-filesystem-credentials-title' ).after( '<div class="notice notice-alt notice-error"><p>' + message + '</p></div>' );
   1847 	};
   1848 
   1849 	/**
   1850 	 * Handles credential errors and runs events that need to happen in that case.
   1851 	 *
   1852 	 * @since 4.2.0
   1853 	 *
   1854 	 * @param {Object} response Ajax response.
   1855 	 * @param {string} action   The type of request to perform.
   1856 	 */
   1857 	wp.updates.credentialError = function( response, action ) {
   1858 
   1859 		// Restore callbacks.
   1860 		response = wp.updates._addCallbacks( response, action );
   1861 
   1862 		wp.updates.queue.unshift( {
   1863 			action: action,
   1864 
   1865 			/*
   1866 			 * Not cool that we're depending on response for this data.
   1867 			 * This would feel more whole in a view all tied together.
   1868 			 */
   1869 			data: response
   1870 		} );
   1871 
   1872 		wp.updates.filesystemCredentials.available = false;
   1873 		wp.updates.showErrorInCredentialsForm( response.errorMessage );
   1874 		wp.updates.requestFilesystemCredentials();
   1875 	};
   1876 
   1877 	/**
   1878 	 * Handles credentials errors if it could not connect to the filesystem.
   1879 	 *
   1880 	 * @since 4.6.0
   1881 	 *
   1882 	 * @param {Object} response              Response from the server.
   1883 	 * @param {string} response.errorCode    Error code for the error that occurred.
   1884 	 * @param {string} response.errorMessage The error that occurred.
   1885 	 * @param {string} action                The type of request to perform.
   1886 	 * @return {boolean} Whether there is an error that needs to be handled or not.
   1887 	 */
   1888 	wp.updates.maybeHandleCredentialError = function( response, action ) {
   1889 		if ( wp.updates.shouldRequestFilesystemCredentials && response.errorCode && 'unable_to_connect_to_filesystem' === response.errorCode ) {
   1890 			wp.updates.credentialError( response, action );
   1891 			return true;
   1892 		}
   1893 
   1894 		return false;
   1895 	};
   1896 
   1897 	/**
   1898 	 * Validates an Ajax response to ensure it's a proper object.
   1899 	 *
   1900 	 * If the response deems to be invalid, an admin notice is being displayed.
   1901 	 *
   1902 	 * @param {(Object|string)} response              Response from the server.
   1903 	 * @param {function=}       response.always       Optional. Callback for when the Deferred is resolved or rejected.
   1904 	 * @param {string=}         response.statusText   Optional. Status message corresponding to the status code.
   1905 	 * @param {string=}         response.responseText Optional. Request response as text.
   1906 	 * @param {string}          action                Type of action the response is referring to. Can be 'delete',
   1907 	 *                                                'update' or 'install'.
   1908 	 */
   1909 	wp.updates.isValidResponse = function( response, action ) {
   1910 		var error = __( 'Something went wrong.' ),
   1911 			errorMessage;
   1912 
   1913 		// Make sure the response is a valid data object and not a Promise object.
   1914 		if ( _.isObject( response ) && ! _.isFunction( response.always ) ) {
   1915 			return true;
   1916 		}
   1917 
   1918 		if ( _.isString( response ) && '-1' === response ) {
   1919 			error = __( 'An error has occurred. Please reload the page and try again.' );
   1920 		} else if ( _.isString( response ) ) {
   1921 			error = response;
   1922 		} else if ( 'undefined' !== typeof response.readyState && 0 === response.readyState ) {
   1923 			error = __( 'Connection lost or the server is busy. Please try again later.' );
   1924 		} else if ( _.isString( response.responseText ) && '' !== response.responseText ) {
   1925 			error = response.responseText;
   1926 		} else if ( _.isString( response.statusText ) ) {
   1927 			error = response.statusText;
   1928 		}
   1929 
   1930 		switch ( action ) {
   1931 			case 'update':
   1932 				/* translators: %s: Error string for a failed update. */
   1933 				errorMessage = __( 'Update failed: %s' );
   1934 				break;
   1935 
   1936 			case 'install':
   1937 				/* translators: %s: Error string for a failed installation. */
   1938 				errorMessage = __( 'Installation failed: %s' );
   1939 				break;
   1940 
   1941 			case 'delete':
   1942 				/* translators: %s: Error string for a failed deletion. */
   1943 				errorMessage = __( 'Deletion failed: %s' );
   1944 				break;
   1945 		}
   1946 
   1947 		// Messages are escaped, remove HTML tags to make them more readable.
   1948 		error = error.replace( /<[\/a-z][^<>]*>/gi, '' );
   1949 		errorMessage = errorMessage.replace( '%s', error );
   1950 
   1951 		// Add admin notice.
   1952 		wp.updates.addAdminNotice( {
   1953 			id:        'unknown_error',
   1954 			className: 'notice-error is-dismissible',
   1955 			message:   _.escape( errorMessage )
   1956 		} );
   1957 
   1958 		// Remove the lock, and clear the queue.
   1959 		wp.updates.ajaxLocked = false;
   1960 		wp.updates.queue      = [];
   1961 
   1962 		// Change buttons of all running updates.
   1963 		$( '.button.updating-message' )
   1964 			.removeClass( 'updating-message' )
   1965 			.removeAttr( 'aria-label' )
   1966 			.prop( 'disabled', true )
   1967 			.text( __( 'Update failed.' ) );
   1968 
   1969 		$( '.updating-message:not(.button):not(.thickbox)' )
   1970 			.removeClass( 'updating-message notice-warning' )
   1971 			.addClass( 'notice-error' )
   1972 			.find( 'p' )
   1973 				.removeAttr( 'aria-label' )
   1974 				.text( errorMessage );
   1975 
   1976 		wp.a11y.speak( errorMessage, 'assertive' );
   1977 
   1978 		return false;
   1979 	};
   1980 
   1981 	/**
   1982 	 * Potentially adds an AYS to a user attempting to leave the page.
   1983 	 *
   1984 	 * If an update is on-going and a user attempts to leave the page,
   1985 	 * opens an "Are you sure?" alert.
   1986 	 *
   1987 	 * @since 4.2.0
   1988 	 */
   1989 	wp.updates.beforeunload = function() {
   1990 		if ( wp.updates.ajaxLocked ) {
   1991 			return __( 'Updates may not complete if you navigate away from this page.' );
   1992 		}
   1993 	};
   1994 
   1995 	$( function() {
   1996 		var $pluginFilter        = $( '#plugin-filter' ),
   1997 			$bulkActionForm      = $( '#bulk-action-form' ),
   1998 			$filesystemForm      = $( '#request-filesystem-credentials-form' ),
   1999 			$filesystemModal     = $( '#request-filesystem-credentials-dialog' ),
   2000 			$pluginSearch        = $( '.plugins-php .wp-filter-search' ),
   2001 			$pluginInstallSearch = $( '.plugin-install-php .wp-filter-search' );
   2002 
   2003 		settings = _.extend( settings, window._wpUpdatesItemCounts || {} );
   2004 
   2005 		if ( settings.totals ) {
   2006 			wp.updates.refreshCount();
   2007 		}
   2008 
   2009 		/*
   2010 		 * Whether a user needs to submit filesystem credentials.
   2011 		 *
   2012 		 * This is based on whether the form was output on the page server-side.
   2013 		 *
   2014 		 * @see {wp_print_request_filesystem_credentials_modal() in PHP}
   2015 		 */
   2016 		wp.updates.shouldRequestFilesystemCredentials = $filesystemModal.length > 0;
   2017 
   2018 		/**
   2019 		 * File system credentials form submit noop-er / handler.
   2020 		 *
   2021 		 * @since 4.2.0
   2022 		 */
   2023 		$filesystemModal.on( 'submit', 'form', function( event ) {
   2024 			event.preventDefault();
   2025 
   2026 			// Persist the credentials input by the user for the duration of the page load.
   2027 			wp.updates.filesystemCredentials.ftp.hostname       = $( '#hostname' ).val();
   2028 			wp.updates.filesystemCredentials.ftp.username       = $( '#username' ).val();
   2029 			wp.updates.filesystemCredentials.ftp.password       = $( '#password' ).val();
   2030 			wp.updates.filesystemCredentials.ftp.connectionType = $( 'input[name="connection_type"]:checked' ).val();
   2031 			wp.updates.filesystemCredentials.ssh.publicKey      = $( '#public_key' ).val();
   2032 			wp.updates.filesystemCredentials.ssh.privateKey     = $( '#private_key' ).val();
   2033 			wp.updates.filesystemCredentials.fsNonce            = $( '#_fs_nonce' ).val();
   2034 			wp.updates.filesystemCredentials.available          = true;
   2035 
   2036 			// Unlock and invoke the queue.
   2037 			wp.updates.ajaxLocked = false;
   2038 			wp.updates.queueChecker();
   2039 
   2040 			wp.updates.requestForCredentialsModalClose();
   2041 		} );
   2042 
   2043 		/**
   2044 		 * Closes the request credentials modal when clicking the 'Cancel' button or outside of the modal.
   2045 		 *
   2046 		 * @since 4.2.0
   2047 		 */
   2048 		$filesystemModal.on( 'click', '[data-js-action="close"], .notification-dialog-background', wp.updates.requestForCredentialsModalCancel );
   2049 
   2050 		/**
   2051 		 * Hide SSH fields when not selected.
   2052 		 *
   2053 		 * @since 4.2.0
   2054 		 */
   2055 		$filesystemForm.on( 'change', 'input[name="connection_type"]', function() {
   2056 			$( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) );
   2057 		} ).trigger( 'change' );
   2058 
   2059 		/**
   2060 		 * Handles events after the credential modal was closed.
   2061 		 *
   2062 		 * @since 4.6.0
   2063 		 *
   2064 		 * @param {Event}  event Event interface.
   2065 		 * @param {string} job   The install/update.delete request.
   2066 		 */
   2067 		$document.on( 'credential-modal-cancel', function( event, job ) {
   2068 			var $updatingMessage = $( '.updating-message' ),
   2069 				$message, originalText;
   2070 
   2071 			if ( 'import' === pagenow ) {
   2072 				$updatingMessage.removeClass( 'updating-message' );
   2073 			} else if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
   2074 				if ( 'update-plugin' === job.action ) {
   2075 					$message = $( 'tr[data-plugin="' + job.data.plugin + '"]' ).find( '.update-message' );
   2076 				} else if ( 'delete-plugin' === job.action ) {
   2077 					$message = $( '[data-plugin="' + job.data.plugin + '"]' ).find( '.row-actions a.delete' );
   2078 				}
   2079 			} else if ( 'themes' === pagenow || 'themes-network' === pagenow ) {
   2080 				if ( 'update-theme' === job.action ) {
   2081 					$message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.update-message' );
   2082 				} else if ( 'delete-theme' === job.action && 'themes-network' === pagenow ) {
   2083 					$message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.row-actions a.delete' );
   2084 				} else if ( 'delete-theme' === job.action && 'themes' === pagenow ) {
   2085 					$message = $( '.theme-actions .delete-theme' );
   2086 				}
   2087 			} else {
   2088 				$message = $updatingMessage;
   2089 			}
   2090 
   2091 			if ( $message && $message.hasClass( 'updating-message' ) ) {
   2092 				originalText = $message.data( 'originaltext' );
   2093 
   2094 				if ( 'undefined' === typeof originalText ) {
   2095 					originalText = $( '<p>' ).html( $message.find( 'p' ).data( 'originaltext' ) );
   2096 				}
   2097 
   2098 				$message
   2099 					.removeClass( 'updating-message' )
   2100 					.html( originalText );
   2101 
   2102 				if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
   2103 					if ( 'update-plugin' === job.action ) {
   2104 						$message.attr(
   2105 							'aria-label',
   2106 							sprintf(
   2107 								/* translators: %s: Plugin name and version. */
   2108 								_x( 'Update %s now', 'plugin' ),
   2109 								$message.data( 'name' )
   2110 							)
   2111 						);
   2112 					} else if ( 'install-plugin' === job.action ) {
   2113 						$message.attr(
   2114 							'aria-label',
   2115 							sprintf(
   2116 								/* translators: %s: Plugin name. */
   2117 								_x( 'Install %s now', 'plugin' ),
   2118 								$message.data( 'name' )
   2119 							)
   2120 						);
   2121 					}
   2122 				}
   2123 			}
   2124 
   2125 			wp.a11y.speak( __( 'Update canceled.' ) );
   2126 		} );
   2127 
   2128 		/**
   2129 		 * Click handler for plugin updates in List Table view.
   2130 		 *
   2131 		 * @since 4.2.0
   2132 		 *
   2133 		 * @param {Event} event Event interface.
   2134 		 */
   2135 		$bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) {
   2136 			var $message   = $( event.target ),
   2137 				$pluginRow = $message.parents( 'tr' );
   2138 
   2139 			event.preventDefault();
   2140 
   2141 			if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
   2142 				return;
   2143 			}
   2144 
   2145 			wp.updates.maybeRequestFilesystemCredentials( event );
   2146 
   2147 			// Return the user to the input box of the plugin's table row after closing the modal.
   2148 			wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' );
   2149 			wp.updates.updatePlugin( {
   2150 				plugin: $pluginRow.data( 'plugin' ),
   2151 				slug:   $pluginRow.data( 'slug' )
   2152 			} );
   2153 		} );
   2154 
   2155 		/**
   2156 		 * Click handler for plugin updates in plugin install view.
   2157 		 *
   2158 		 * @since 4.2.0
   2159 		 *
   2160 		 * @param {Event} event Event interface.
   2161 		 */
   2162 		$pluginFilter.on( 'click', '.update-now', function( event ) {
   2163 			var $button = $( event.target );
   2164 			event.preventDefault();
   2165 
   2166 			if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
   2167 				return;
   2168 			}
   2169 
   2170 			wp.updates.maybeRequestFilesystemCredentials( event );
   2171 
   2172 			wp.updates.updatePlugin( {
   2173 				plugin: $button.data( 'plugin' ),
   2174 				slug:   $button.data( 'slug' )
   2175 			} );
   2176 		} );
   2177 
   2178 		/**
   2179 		 * Click handler for plugin installs in plugin install view.
   2180 		 *
   2181 		 * @since 4.6.0
   2182 		 *
   2183 		 * @param {Event} event Event interface.
   2184 		 */
   2185 		$pluginFilter.on( 'click', '.install-now', function( event ) {
   2186 			var $button = $( event.target );
   2187 			event.preventDefault();
   2188 
   2189 			if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
   2190 				return;
   2191 			}
   2192 
   2193 			if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
   2194 				wp.updates.requestFilesystemCredentials( event );
   2195 
   2196 				$document.on( 'credential-modal-cancel', function() {
   2197 					var $message = $( '.install-now.updating-message' );
   2198 
   2199 					$message
   2200 						.removeClass( 'updating-message' )
   2201 						.text( __( 'Install Now' ) );
   2202 
   2203 					wp.a11y.speak( __( 'Update canceled.' ) );
   2204 				} );
   2205 			}
   2206 
   2207 			wp.updates.installPlugin( {
   2208 				slug: $button.data( 'slug' )
   2209 			} );
   2210 		} );
   2211 
   2212 		/**
   2213 		 * Click handler for importer plugins installs in the Import screen.
   2214 		 *
   2215 		 * @since 4.6.0
   2216 		 *
   2217 		 * @param {Event} event Event interface.
   2218 		 */
   2219 		$document.on( 'click', '.importer-item .install-now', function( event ) {
   2220 			var $button = $( event.target ),
   2221 				pluginName = $( this ).data( 'name' );
   2222 
   2223 			event.preventDefault();
   2224 
   2225 			if ( $button.hasClass( 'updating-message' ) ) {
   2226 				return;
   2227 			}
   2228 
   2229 			if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
   2230 				wp.updates.requestFilesystemCredentials( event );
   2231 
   2232 				$document.on( 'credential-modal-cancel', function() {
   2233 
   2234 					$button
   2235 						.removeClass( 'updating-message' )
   2236 						.attr(
   2237 							'aria-label',
   2238 							sprintf(
   2239 								/* translators: %s: Plugin name. */
   2240 								_x( 'Install %s now', 'plugin' ),
   2241 								pluginName
   2242 							)
   2243 						)
   2244 						.text( __( 'Install Now' ) );
   2245 
   2246 					wp.a11y.speak( __( 'Update canceled.' ) );
   2247 				} );
   2248 			}
   2249 
   2250 			wp.updates.installPlugin( {
   2251 				slug:    $button.data( 'slug' ),
   2252 				pagenow: pagenow,
   2253 				success: wp.updates.installImporterSuccess,
   2254 				error:   wp.updates.installImporterError
   2255 			} );
   2256 		} );
   2257 
   2258 		/**
   2259 		 * Click handler for plugin deletions.
   2260 		 *
   2261 		 * @since 4.6.0
   2262 		 *
   2263 		 * @param {Event} event Event interface.
   2264 		 */
   2265 		$bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) {
   2266 			var $pluginRow = $( event.target ).parents( 'tr' ),
   2267 				confirmMessage;
   2268 
   2269 			if ( $pluginRow.hasClass( 'is-uninstallable' ) ) {
   2270 				confirmMessage = sprintf(
   2271 					/* translators: %s: Plugin name. */
   2272 					__( 'Are you sure you want to delete %s and its data?' ),
   2273 					$pluginRow.find( '.plugin-title strong' ).text()
   2274 				);
   2275 			} else {
   2276 				confirmMessage = sprintf(
   2277 					/* translators: %s: Plugin name. */
   2278 					__( 'Are you sure you want to delete %s?' ),
   2279 					$pluginRow.find( '.plugin-title strong' ).text()
   2280 				);
   2281 			}
   2282 
   2283 			event.preventDefault();
   2284 
   2285 			if ( ! window.confirm( confirmMessage ) ) {
   2286 				return;
   2287 			}
   2288 
   2289 			wp.updates.maybeRequestFilesystemCredentials( event );
   2290 
   2291 			wp.updates.deletePlugin( {
   2292 				plugin: $pluginRow.data( 'plugin' ),
   2293 				slug:   $pluginRow.data( 'slug' )
   2294 			} );
   2295 
   2296 		} );
   2297 
   2298 		/**
   2299 		 * Click handler for theme updates.
   2300 		 *
   2301 		 * @since 4.6.0
   2302 		 *
   2303 		 * @param {Event} event Event interface.
   2304 		 */
   2305 		$document.on( 'click', '.themes-php.network-admin .update-link', function( event ) {
   2306 			var $message  = $( event.target ),
   2307 				$themeRow = $message.parents( 'tr' );
   2308 
   2309 			event.preventDefault();
   2310 
   2311 			if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
   2312 				return;
   2313 			}
   2314 
   2315 			wp.updates.maybeRequestFilesystemCredentials( event );
   2316 
   2317 			// Return the user to the input box of the theme's table row after closing the modal.
   2318 			wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' );
   2319 			wp.updates.updateTheme( {
   2320 				slug: $themeRow.data( 'slug' )
   2321 			} );
   2322 		} );
   2323 
   2324 		/**
   2325 		 * Click handler for theme deletions.
   2326 		 *
   2327 		 * @since 4.6.0
   2328 		 *
   2329 		 * @param {Event} event Event interface.
   2330 		 */
   2331 		$document.on( 'click', '.themes-php.network-admin a.delete', function( event ) {
   2332 			var $themeRow = $( event.target ).parents( 'tr' ),
   2333 				confirmMessage = sprintf(
   2334 					/* translators: %s: Theme name. */
   2335 					__( 'Are you sure you want to delete %s?' ),
   2336 					$themeRow.find( '.theme-title strong' ).text()
   2337 				);
   2338 
   2339 			event.preventDefault();
   2340 
   2341 			if ( ! window.confirm( confirmMessage ) ) {
   2342 				return;
   2343 			}
   2344 
   2345 			wp.updates.maybeRequestFilesystemCredentials( event );
   2346 
   2347 			wp.updates.deleteTheme( {
   2348 				slug: $themeRow.data( 'slug' )
   2349 			} );
   2350 		} );
   2351 
   2352 		/**
   2353 		 * Bulk action handler for plugins and themes.
   2354 		 *
   2355 		 * Handles both deletions and updates.
   2356 		 *
   2357 		 * @since 4.6.0
   2358 		 *
   2359 		 * @param {Event} event Event interface.
   2360 		 */
   2361 		$bulkActionForm.on( 'click', '[type="submit"]:not([name="clear-recent-list"])', function( event ) {
   2362 			var bulkAction    = $( event.target ).siblings( 'select' ).val(),
   2363 				itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ),
   2364 				success       = 0,
   2365 				error         = 0,
   2366 				errorMessages = [],
   2367 				type, action;
   2368 
   2369 			// Determine which type of item we're dealing with.
   2370 			switch ( pagenow ) {
   2371 				case 'plugins':
   2372 				case 'plugins-network':
   2373 					type = 'plugin';
   2374 					break;
   2375 
   2376 				case 'themes-network':
   2377 					type = 'theme';
   2378 					break;
   2379 
   2380 				default:
   2381 					return;
   2382 			}
   2383 
   2384 			// Bail if there were no items selected.
   2385 			if ( ! itemsSelected.length ) {
   2386 				event.preventDefault();
   2387 				$( 'html, body' ).animate( { scrollTop: 0 } );
   2388 
   2389 				return wp.updates.addAdminNotice( {
   2390 					id:        'no-items-selected',
   2391 					className: 'notice-error is-dismissible',
   2392 					message:   __( 'Please select at least one item to perform this action on.' )
   2393 				} );
   2394 			}
   2395 
   2396 			// Determine the type of request we're dealing with.
   2397 			switch ( bulkAction ) {
   2398 				case 'update-selected':
   2399 					action = bulkAction.replace( 'selected', type );
   2400 					break;
   2401 
   2402 				case 'delete-selected':
   2403 					var confirmMessage = 'plugin' === type ?
   2404 						__( 'Are you sure you want to delete the selected plugins and their data?' ) :
   2405 						__( 'Caution: These themes may be active on other sites in the network. Are you sure you want to proceed?' );
   2406 
   2407 					if ( ! window.confirm( confirmMessage ) ) {
   2408 						event.preventDefault();
   2409 						return;
   2410 					}
   2411 
   2412 					action = bulkAction.replace( 'selected', type );
   2413 					break;
   2414 
   2415 				default:
   2416 					return;
   2417 			}
   2418 
   2419 			wp.updates.maybeRequestFilesystemCredentials( event );
   2420 
   2421 			event.preventDefault();
   2422 
   2423 			// Un-check the bulk checkboxes.
   2424 			$bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false );
   2425 
   2426 			$document.trigger( 'wp-' + type + '-bulk-' + bulkAction, itemsSelected );
   2427 
   2428 			// Find all the checkboxes which have been checked.
   2429 			itemsSelected.each( function( index, element ) {
   2430 				var $checkbox = $( element ),
   2431 					$itemRow = $checkbox.parents( 'tr' );
   2432 
   2433 				// Only add update-able items to the update queue.
   2434 				if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) {
   2435 
   2436 					// Un-check the box.
   2437 					$checkbox.prop( 'checked', false );
   2438 					return;
   2439 				}
   2440 
   2441 				// Add it to the queue.
   2442 				wp.updates.queue.push( {
   2443 					action: action,
   2444 					data:   {
   2445 						plugin: $itemRow.data( 'plugin' ),
   2446 						slug:   $itemRow.data( 'slug' )
   2447 					}
   2448 				} );
   2449 			} );
   2450 
   2451 			// Display bulk notification for updates of any kind.
   2452 			$document.on( 'wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error', function( event, response ) {
   2453 				var $itemRow = $( '[data-slug="' + response.slug + '"]' ),
   2454 					$bulkActionNotice, itemName;
   2455 
   2456 				if ( 'wp-' + response.update + '-update-success' === event.type ) {
   2457 					success++;
   2458 				} else {
   2459 					itemName = response.pluginName ? response.pluginName : $itemRow.find( '.column-primary strong' ).text();
   2460 
   2461 					error++;
   2462 					errorMessages.push( itemName + ': ' + response.errorMessage );
   2463 				}
   2464 
   2465 				$itemRow.find( 'input[name="checked[]"]:checked' ).prop( 'checked', false );
   2466 
   2467 				wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' );
   2468 
   2469 				wp.updates.addAdminNotice( {
   2470 					id:            'bulk-action-notice',
   2471 					className:     'bulk-action-notice',
   2472 					successes:     success,
   2473 					errors:        error,
   2474 					errorMessages: errorMessages,
   2475 					type:          response.update
   2476 				} );
   2477 
   2478 				$bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() {
   2479 					// $( this ) is the clicked button, no need to get it again.
   2480 					$( this )
   2481 						.toggleClass( 'bulk-action-errors-collapsed' )
   2482 						.attr( 'aria-expanded', ! $( this ).hasClass( 'bulk-action-errors-collapsed' ) );
   2483 					// Show the errors list.
   2484 					$bulkActionNotice.find( '.bulk-action-errors' ).toggleClass( 'hidden' );
   2485 				} );
   2486 
   2487 				if ( error > 0 && ! wp.updates.queue.length ) {
   2488 					$( 'html, body' ).animate( { scrollTop: 0 } );
   2489 				}
   2490 			} );
   2491 
   2492 			// Reset admin notice template after #bulk-action-notice was added.
   2493 			$document.on( 'wp-updates-notice-added', function() {
   2494 				wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
   2495 			} );
   2496 
   2497 			// Check the queue, now that the event handlers have been added.
   2498 			wp.updates.queueChecker();
   2499 		} );
   2500 
   2501 		if ( $pluginInstallSearch.length ) {
   2502 			$pluginInstallSearch.attr( 'aria-describedby', 'live-search-desc' );
   2503 		}
   2504 
   2505 		/**
   2506 		 * Handles changes to the plugin search box on the new-plugin page,
   2507 		 * searching the repository dynamically.
   2508 		 *
   2509 		 * @since 4.6.0
   2510 		 */
   2511 		$pluginInstallSearch.on( 'keyup input', _.debounce( function( event, eventtype ) {
   2512 			var $searchTab = $( '.plugin-install-search' ), data, searchLocation;
   2513 
   2514 			data = {
   2515 				_ajax_nonce: wp.updates.ajaxNonce,
   2516 				s:           event.target.value,
   2517 				tab:         'search',
   2518 				type:        $( '#typeselector' ).val(),
   2519 				pagenow:     pagenow
   2520 			};
   2521 			searchLocation = location.href.split( '?' )[ 0 ] + '?' + $.param( _.omit( data, [ '_ajax_nonce', 'pagenow' ] ) );
   2522 
   2523 			// Clear on escape.
   2524 			if ( 'keyup' === event.type && 27 === event.which ) {
   2525 				event.target.value = '';
   2526 			}
   2527 
   2528 			if ( wp.updates.searchTerm === data.s && 'typechange' !== eventtype ) {
   2529 				return;
   2530 			} else {
   2531 				$pluginFilter.empty();
   2532 				wp.updates.searchTerm = data.s;
   2533 			}
   2534 
   2535 			if ( window.history && window.history.replaceState ) {
   2536 				window.history.replaceState( null, '', searchLocation );
   2537 			}
   2538 
   2539 			if ( ! $searchTab.length ) {
   2540 				$searchTab = $( '<li class="plugin-install-search" />' )
   2541 					.append( $( '<a />', {
   2542 						'class': 'current',
   2543 						'href': searchLocation,
   2544 						'text': __( 'Search Results' )
   2545 					} ) );
   2546 
   2547 				$( '.wp-filter .filter-links .current' )
   2548 					.removeClass( 'current' )
   2549 					.parents( '.filter-links' )
   2550 					.prepend( $searchTab );
   2551 
   2552 				$pluginFilter.prev( 'p' ).remove();
   2553 				$( '.plugins-popular-tags-wrapper' ).remove();
   2554 			}
   2555 
   2556 			if ( 'undefined' !== typeof wp.updates.searchRequest ) {
   2557 				wp.updates.searchRequest.abort();
   2558 			}
   2559 			$( 'body' ).addClass( 'loading-content' );
   2560 
   2561 			wp.updates.searchRequest = wp.ajax.post( 'search-install-plugins', data ).done( function( response ) {
   2562 				$( 'body' ).removeClass( 'loading-content' );
   2563 				$pluginFilter.append( response.items );
   2564 				delete wp.updates.searchRequest;
   2565 
   2566 				if ( 0 === response.count ) {
   2567 					wp.a11y.speak( __( 'You do not appear to have any plugins available at this time.' ) );
   2568 				} else {
   2569 					wp.a11y.speak(
   2570 						sprintf(
   2571 							/* translators: %s: Number of plugins. */
   2572 							__( 'Number of plugins found: %d' ),
   2573 							response.count
   2574 						)
   2575 					);
   2576 				}
   2577 			} );
   2578 		}, 1000 ) );
   2579 
   2580 		if ( $pluginSearch.length ) {
   2581 			$pluginSearch.attr( 'aria-describedby', 'live-search-desc' );
   2582 		}
   2583 
   2584 		/**
   2585 		 * Handles changes to the plugin search box on the Installed Plugins screen,
   2586 		 * searching the plugin list dynamically.
   2587 		 *
   2588 		 * @since 4.6.0
   2589 		 */
   2590 		$pluginSearch.on( 'keyup input', _.debounce( function( event ) {
   2591 			var data = {
   2592 				_ajax_nonce:   wp.updates.ajaxNonce,
   2593 				s:             event.target.value,
   2594 				pagenow:       pagenow,
   2595 				plugin_status: 'all'
   2596 			},
   2597 			queryArgs;
   2598 
   2599 			// Clear on escape.
   2600 			if ( 'keyup' === event.type && 27 === event.which ) {
   2601 				event.target.value = '';
   2602 			}
   2603 
   2604 			if ( wp.updates.searchTerm === data.s ) {
   2605 				return;
   2606 			} else {
   2607 				wp.updates.searchTerm = data.s;
   2608 			}
   2609 
   2610 			queryArgs = _.object( _.compact( _.map( location.search.slice( 1 ).split( '&' ), function( item ) {
   2611 				if ( item ) return item.split( '=' );
   2612 			} ) ) );
   2613 
   2614 			data.plugin_status = queryArgs.plugin_status || 'all';
   2615 
   2616 			if ( window.history && window.history.replaceState ) {
   2617 				window.history.replaceState( null, '', location.href.split( '?' )[ 0 ] + '?s=' + data.s + '&plugin_status=' + data.plugin_status );
   2618 			}
   2619 
   2620 			if ( 'undefined' !== typeof wp.updates.searchRequest ) {
   2621 				wp.updates.searchRequest.abort();
   2622 			}
   2623 
   2624 			$bulkActionForm.empty();
   2625 			$( 'body' ).addClass( 'loading-content' );
   2626 			$( '.subsubsub .current' ).removeClass( 'current' );
   2627 
   2628 			wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) {
   2629 
   2630 				// Can we just ditch this whole subtitle business?
   2631 				var $subTitle    = $( '<span />' ).addClass( 'subtitle' ).html(
   2632 					sprintf(
   2633 						/* translators: %s: Search query. */
   2634 						__( 'Search results for: %s' ),
   2635 						'<strong>' + _.escape( data.s ) + '</strong>'
   2636 					) ),
   2637 					$oldSubTitle = $( '.wrap .subtitle' );
   2638 
   2639 				if ( ! data.s.length ) {
   2640 					$oldSubTitle.remove();
   2641 					$( '.subsubsub .' + data.plugin_status + ' a' ).addClass( 'current' );
   2642 				} else if ( $oldSubTitle.length ) {
   2643 					$oldSubTitle.replaceWith( $subTitle );
   2644 				} else {
   2645 					$( '.wp-header-end' ).before( $subTitle );
   2646 				}
   2647 
   2648 				$( 'body' ).removeClass( 'loading-content' );
   2649 				$bulkActionForm.append( response.items );
   2650 				delete wp.updates.searchRequest;
   2651 
   2652 				if ( 0 === response.count ) {
   2653 					wp.a11y.speak( __( 'No plugins found. Try a different search.'  ) );
   2654 				} else {
   2655 					wp.a11y.speak(
   2656 						sprintf(
   2657 							/* translators: %s: Number of plugins. */
   2658 							__( 'Number of plugins found: %d' ),
   2659 							response.count
   2660 						)
   2661 					);
   2662 				}
   2663 			} );
   2664 		}, 500 ) );
   2665 
   2666 		/**
   2667 		 * Trigger a search event when the search form gets submitted.
   2668 		 *
   2669 		 * @since 4.6.0
   2670 		 */
   2671 		$document.on( 'submit', '.search-plugins', function( event ) {
   2672 			event.preventDefault();
   2673 
   2674 			$( 'input.wp-filter-search' ).trigger( 'input' );
   2675 		} );
   2676 
   2677 		/**
   2678 		 * Trigger a search event when the "Try Again" button is clicked.
   2679 		 *
   2680 		 * @since 4.9.0
   2681 		 */
   2682 		$document.on( 'click', '.try-again', function( event ) {
   2683 			event.preventDefault();
   2684 			$pluginInstallSearch.trigger( 'input' );
   2685 		} );
   2686 
   2687 		/**
   2688 		 * Trigger a search event when the search type gets changed.
   2689 		 *
   2690 		 * @since 4.6.0
   2691 		 */
   2692 		$( '#typeselector' ).on( 'change', function() {
   2693 			var $search = $( 'input[name="s"]' );
   2694 
   2695 			if ( $search.val().length ) {
   2696 				$search.trigger( 'input', 'typechange' );
   2697 			}
   2698 		} );
   2699 
   2700 		/**
   2701 		 * Click handler for updating a plugin from the details modal on `plugin-install.php`.
   2702 		 *
   2703 		 * @since 4.2.0
   2704 		 *
   2705 		 * @param {Event} event Event interface.
   2706 		 */
   2707 		$( '#plugin_update_from_iframe' ).on( 'click', function( event ) {
   2708 			var target = window.parent === window ? null : window.parent,
   2709 				update;
   2710 
   2711 			$.support.postMessage = !! window.postMessage;
   2712 
   2713 			if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'update-core.php' ) ) {
   2714 				return;
   2715 			}
   2716 
   2717 			event.preventDefault();
   2718 
   2719 			update = {
   2720 				action: 'update-plugin',
   2721 				data:   {
   2722 					plugin: $( this ).data( 'plugin' ),
   2723 					slug:   $( this ).data( 'slug' )
   2724 				}
   2725 			};
   2726 
   2727 			target.postMessage( JSON.stringify( update ), window.location.origin );
   2728 		} );
   2729 
   2730 		/**
   2731 		 * Click handler for installing a plugin from the details modal on `plugin-install.php`.
   2732 		 *
   2733 		 * @since 4.6.0
   2734 		 *
   2735 		 * @param {Event} event Event interface.
   2736 		 */
   2737 		$( '#plugin_install_from_iframe' ).on( 'click', function( event ) {
   2738 			var target = window.parent === window ? null : window.parent,
   2739 				install;
   2740 
   2741 			$.support.postMessage = !! window.postMessage;
   2742 
   2743 			if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'index.php' ) ) {
   2744 				return;
   2745 			}
   2746 
   2747 			event.preventDefault();
   2748 
   2749 			install = {
   2750 				action: 'install-plugin',
   2751 				data:   {
   2752 					slug: $( this ).data( 'slug' )
   2753 				}
   2754 			};
   2755 
   2756 			target.postMessage( JSON.stringify( install ), window.location.origin );
   2757 		} );
   2758 
   2759 		/**
   2760 		 * Handles postMessage events.
   2761 		 *
   2762 		 * @since 4.2.0
   2763 		 * @since 4.6.0 Switched `update-plugin` action to use the queue.
   2764 		 *
   2765 		 * @param {Event} event Event interface.
   2766 		 */
   2767 		$( window ).on( 'message', function( event ) {
   2768 			var originalEvent  = event.originalEvent,
   2769 				expectedOrigin = document.location.protocol + '//' + document.location.host,
   2770 				message;
   2771 
   2772 			if ( originalEvent.origin !== expectedOrigin ) {
   2773 				return;
   2774 			}
   2775 
   2776 			try {
   2777 				message = JSON.parse( originalEvent.data );
   2778 			} catch ( e ) {
   2779 				return;
   2780 			}
   2781 
   2782 			if ( ! message || 'undefined' === typeof message.action ) {
   2783 				return;
   2784 			}
   2785 
   2786 			switch ( message.action ) {
   2787 
   2788 				// Called from `wp-admin/includes/class-wp-upgrader-skins.php`.
   2789 				case 'decrementUpdateCount':
   2790 					/** @property {string} message.upgradeType */
   2791 					wp.updates.decrementCount( message.upgradeType );
   2792 					break;
   2793 
   2794 				case 'install-plugin':
   2795 				case 'update-plugin':
   2796 					/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
   2797 					window.tb_remove();
   2798 					/* jscs:enable */
   2799 
   2800 					message.data = wp.updates._addCallbacks( message.data, message.action );
   2801 
   2802 					wp.updates.queue.push( message );
   2803 					wp.updates.queueChecker();
   2804 					break;
   2805 			}
   2806 		} );
   2807 
   2808 		/**
   2809 		 * Adds a callback to display a warning before leaving the page.
   2810 		 *
   2811 		 * @since 4.2.0
   2812 		 */
   2813 		$( window ).on( 'beforeunload', wp.updates.beforeunload );
   2814 
   2815 		/**
   2816 		 * Prevents the page form scrolling when activating auto-updates with the Spacebar key.
   2817 		 *
   2818 		 * @since 5.5.0
   2819 		 */
   2820 		$document.on( 'keydown', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) {
   2821 			if ( 32 === event.which ) {
   2822 				event.preventDefault();
   2823 			}
   2824 		} );
   2825 
   2826 		/**
   2827 		 * Click and keyup handler for enabling and disabling plugin and theme auto-updates.
   2828 		 *
   2829 		 * These controls can be either links or buttons. When JavaScript is enabled,
   2830 		 * we want them to behave like buttons. An ARIA role `button` is added via
   2831 		 * the JavaScript that targets elements with the CSS class `aria-button-if-js`.
   2832 		 *
   2833 		 * @since 5.5.0
   2834 		 */
   2835 		$document.on( 'click keyup', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) {
   2836 			var data, asset, type, $parent,
   2837 				$toggler = $( this ),
   2838 				action = $toggler.attr( 'data-wp-action' ),
   2839 				$label = $toggler.find( '.label' );
   2840 
   2841 			if ( 'keyup' === event.type && 32 !== event.which ) {
   2842 				return;
   2843 			}
   2844 
   2845 			if ( 'themes' !== pagenow ) {
   2846 				$parent = $toggler.closest( '.column-auto-updates' );
   2847 			} else {
   2848 				$parent = $toggler.closest( '.theme-autoupdate' );
   2849 			}
   2850 
   2851 			event.preventDefault();
   2852 
   2853 			// Prevent multiple simultaneous requests.
   2854 			if ( $toggler.attr( 'data-doing-ajax' ) === 'yes' ) {
   2855 				return;
   2856 			}
   2857 
   2858 			$toggler.attr( 'data-doing-ajax', 'yes' );
   2859 
   2860 			switch ( pagenow ) {
   2861 				case 'plugins':
   2862 				case 'plugins-network':
   2863 					type = 'plugin';
   2864 					asset = $toggler.closest( 'tr' ).attr( 'data-plugin' );
   2865 					break;
   2866 				case 'themes-network':
   2867 					type = 'theme';
   2868 					asset = $toggler.closest( 'tr' ).attr( 'data-slug' );
   2869 					break;
   2870 				case 'themes':
   2871 					type = 'theme';
   2872 					asset = $toggler.attr( 'data-slug' );
   2873 					break;
   2874 			}
   2875 
   2876 			// Clear any previous errors.
   2877 			$parent.find( '.notice.notice-error' ).addClass( 'hidden' );
   2878 
   2879 			// Show loading status.
   2880 			if ( 'enable' === action ) {
   2881 				$label.text( __( 'Enabling...' ) );
   2882 			} else {
   2883 				$label.text( __( 'Disabling...' ) );
   2884 			}
   2885 
   2886 			$toggler.find( '.dashicons-update' ).removeClass( 'hidden' );
   2887 
   2888 			data = {
   2889 				action: 'toggle-auto-updates',
   2890 				_ajax_nonce: settings.ajax_nonce,
   2891 				state: action,
   2892 				type: type,
   2893 				asset: asset
   2894 			};
   2895 
   2896 			$.post( window.ajaxurl, data )
   2897 				.done( function( response ) {
   2898 					var $enabled, $disabled, enabledNumber, disabledNumber, errorMessage,
   2899 						href = $toggler.attr( 'href' );
   2900 
   2901 					if ( ! response.success ) {
   2902 						// if WP returns 0 for response (which can happen in a few cases),
   2903 						// output the general error message since we won't have response.data.error.
   2904 						if ( response.data && response.data.error ) {
   2905 							errorMessage = response.data.error;
   2906 						} else {
   2907 							errorMessage = __( 'The request could not be completed.' );
   2908 						}
   2909 
   2910 						$parent.find( '.notice.notice-error' ).removeClass( 'hidden' ).find( 'p' ).text( errorMessage );
   2911 						wp.a11y.speak( errorMessage, 'assertive' );
   2912 						return;
   2913 					}
   2914 
   2915 					// Update the counts in the enabled/disabled views if on a screen
   2916 					// with a list table.
   2917 					if ( 'themes' !== pagenow ) {
   2918 						$enabled       = $( '.auto-update-enabled span' );
   2919 						$disabled      = $( '.auto-update-disabled span' );
   2920 						enabledNumber  = parseInt( $enabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0;
   2921 						disabledNumber = parseInt( $disabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0;
   2922 
   2923 						switch ( action ) {
   2924 							case 'enable':
   2925 								++enabledNumber;
   2926 								--disabledNumber;
   2927 								break;
   2928 							case 'disable':
   2929 								--enabledNumber;
   2930 								++disabledNumber;
   2931 								break;
   2932 						}
   2933 
   2934 						enabledNumber = Math.max( 0, enabledNumber );
   2935 						disabledNumber = Math.max( 0, disabledNumber );
   2936 
   2937 						$enabled.text( '(' + enabledNumber + ')' );
   2938 						$disabled.text( '(' + disabledNumber + ')' );
   2939 					}
   2940 
   2941 					if ( 'enable' === action ) {
   2942 						// The toggler control can be either a link or a button.
   2943 						if ( $toggler[ 0 ].hasAttribute( 'href' ) ) {
   2944 							href = href.replace( 'action=enable-auto-update', 'action=disable-auto-update' );
   2945 							$toggler.attr( 'href', href );
   2946 						}
   2947 						$toggler.attr( 'data-wp-action', 'disable' );
   2948 
   2949 						$label.text( __( 'Disable auto-updates' ) );
   2950 						$parent.find( '.auto-update-time' ).removeClass( 'hidden' );
   2951 						wp.a11y.speak( __( 'Auto-updates enabled' ) );
   2952 					} else {
   2953 						// The toggler control can be either a link or a button.
   2954 						if ( $toggler[ 0 ].hasAttribute( 'href' ) ) {
   2955 							href = href.replace( 'action=disable-auto-update', 'action=enable-auto-update' );
   2956 							$toggler.attr( 'href', href );
   2957 						}
   2958 						$toggler.attr( 'data-wp-action', 'enable' );
   2959 
   2960 						$label.text( __( 'Enable auto-updates' ) );
   2961 						$parent.find( '.auto-update-time' ).addClass( 'hidden' );
   2962 						wp.a11y.speak( __( 'Auto-updates disabled' ) );
   2963 					}
   2964 
   2965 					$document.trigger( 'wp-auto-update-setting-changed', { state: action, type: type, asset: asset } );
   2966 				} )
   2967 				.fail( function() {
   2968 					$parent.find( '.notice.notice-error' )
   2969 						.removeClass( 'hidden' )
   2970 						.find( 'p' )
   2971 						.text( __( 'The request could not be completed.' ) );
   2972 
   2973 					wp.a11y.speak( __( 'The request could not be completed.' ), 'assertive' );
   2974 				} )
   2975 				.always( function() {
   2976 					$toggler.removeAttr( 'data-doing-ajax' ).find( '.dashicons-update' ).addClass( 'hidden' );
   2977 				} );
   2978 			}
   2979 		);
   2980 	} );
   2981 })( jQuery, window.wp, window._wpUpdatesSettings );