2 * Functions for ajaxified updates, deletions and installs inside the WordPress admin.
7 * @subpackage Administration
13 * @param {jQuery} $ jQuery object.
14 * @param {object} wp WP object.
15 * @param {object} settings WP Updates settings.
16 * @param {string} settings.ajax_nonce AJAX nonce.
17 * @param {object} settings.l10n Translation strings.
18 * @param {object=} settings.plugins Base names of plugins in their different states.
19 * @param {Array} settings.plugins.all Base names of all plugins.
20 * @param {Array} settings.plugins.active Base names of active plugins.
21 * @param {Array} settings.plugins.inactive Base names of inactive plugins.
22 * @param {Array} settings.plugins.upgrade Base names of plugins with updates available.
23 * @param {Array} settings.plugins.recently_activated Base names of recently activated plugins.
24 * @param {object=} settings.themes Plugin/theme status information or null.
25 * @param {number} settings.themes.all Amount of all themes.
26 * @param {number} settings.themes.upgrade Amount of themes with updates available.
27 * @param {number} settings.themes.disabled Amount of disabled themes.
28 * @param {object=} settings.totals Combined information for available update counts.
29 * @param {number} settings.totals.count Holds the amount of available updates.
31 (function( $, wp, settings ) {
32 var $document = $( document );
37 * The WP Updates object.
46 * User nonce for ajax calls.
52 wp.updates.ajaxNonce = settings.ajax_nonce;
61 wp.updates.l10n = settings.l10n;
64 * Current search term.
70 wp.updates.searchTerm = '';
73 * Whether filesystem credentials need to be requested from the user.
79 wp.updates.shouldRequestFilesystemCredentials = false;
82 * Filesystem credentials to be packaged along with the request.
85 * @since 4.6.0 Added `available` property to indicate whether credentials have been provided.
87 * @type {object} filesystemCredentials Holds filesystem credentials.
88 * @type {object} filesystemCredentials.ftp Holds FTP credentials.
89 * @type {string} filesystemCredentials.ftp.host FTP host. Default empty string.
90 * @type {string} filesystemCredentials.ftp.username FTP user name. Default empty string.
91 * @type {string} filesystemCredentials.ftp.password FTP password. Default empty string.
92 * @type {string} filesystemCredentials.ftp.connectionType Type of FTP connection. 'ssh', 'ftp', or 'ftps'.
93 * Default empty string.
94 * @type {object} filesystemCredentials.ssh Holds SSH credentials.
95 * @type {string} filesystemCredentials.ssh.publicKey The public key. Default empty string.
96 * @type {string} filesystemCredentials.ssh.privateKey The private key. Default empty string.
97 * @type {bool} filesystemCredentials.available Whether filesystem credentials have been provided.
100 wp.updates.filesystemCredentials = {
115 * Whether we're waiting for an Ajax request to complete.
118 * @since 4.6.0 More accurately named `ajaxLocked`.
122 wp.updates.ajaxLocked = false;
125 * Admin notice template.
129 * @type {function} A function that lazily-compiles the template requested.
131 wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
136 * If the user tries to update a plugin while an update is
137 * already happening, it can be placed in this queue to perform later.
140 * @since 4.6.0 More accurately named `queue`.
142 * @type {Array.object}
144 wp.updates.queue = [];
147 * Holds a jQuery reference to return focus to when exiting the request credentials modal.
153 wp.updates.$elToReturnFocusToFromCredentialsModal = undefined;
156 * Adds or updates an admin notice.
160 * @param {object} data
161 * @param {*=} data.selector Optional. Selector of an element to be replaced with the admin notice.
162 * @param {string=} data.id Optional. Unique id that will be used as the notice's id attribute.
163 * @param {string=} data.className Optional. Class names that will be used in the admin notice.
164 * @param {string=} data.message Optional. The message displayed in the notice.
165 * @param {number=} data.successes Optional. The amount of successful operations.
166 * @param {number=} data.errors Optional. The amount of failed operations.
167 * @param {Array=} data.errorMessages Optional. Error messages of failed operations.
170 wp.updates.addAdminNotice = function( data ) {
171 var $notice = $( data.selector ), $adminNotice;
173 delete data.selector;
174 $adminNotice = wp.updates.adminNotice( data );
176 // Check if this admin notice already exists.
177 if ( ! $notice.length ) {
178 $notice = $( '#' + data.id );
181 if ( $notice.length ) {
182 $notice.replaceWith( $adminNotice );
184 $( '.wrap' ).find( '> h1' ).after( $adminNotice );
187 $document.trigger( 'wp-updates-notice-added' );
191 * Handles Ajax requests to WordPress.
195 * @param {string} action The type of Ajax request ('update-plugin', 'install-theme', etc).
196 * @param {object} data Data that needs to be passed to the ajax callback.
197 * @return {$.promise} A jQuery promise that represents the request,
198 * decorated with an abort() method.
200 wp.updates.ajax = function( action, data ) {
203 if ( wp.updates.ajaxLocked ) {
204 wp.updates.queue.push( {
209 // Return a Deferred object so callbacks can always be registered.
213 wp.updates.ajaxLocked = true;
215 if ( data.success ) {
216 options.success = data.success;
221 options.error = data.error;
225 options.data = _.extend( data, {
227 _ajax_nonce: wp.updates.ajaxNonce,
228 username: wp.updates.filesystemCredentials.ftp.username,
229 password: wp.updates.filesystemCredentials.ftp.password,
230 hostname: wp.updates.filesystemCredentials.ftp.hostname,
231 connection_type: wp.updates.filesystemCredentials.ftp.connectionType,
232 public_key: wp.updates.filesystemCredentials.ssh.publicKey,
233 private_key: wp.updates.filesystemCredentials.ssh.privateKey
236 return wp.ajax.send( options ).always( wp.updates.ajaxAlways );
240 * Actions performed after every Ajax request.
244 * @param {object} response
245 * @param {array=} response.debug Optional. Debug information.
246 * @param {string=} response.errorCode Optional. Error code for an error that occurred.
248 wp.updates.ajaxAlways = function( response ) {
249 if ( ! response.errorCode || 'unable_to_connect_to_filesystem' !== response.errorCode ) {
250 wp.updates.ajaxLocked = false;
251 wp.updates.queueChecker();
254 if ( 'undefined' !== typeof response.debug && window.console && window.console.log ) {
255 _.map( response.debug, function( message ) {
256 window.console.log( $( '<p />' ).html( message ).text() );
262 * Refreshes update counts everywhere on the screen.
266 wp.updates.refreshCount = function() {
267 var $adminBarUpdates = $( '#wp-admin-bar-updates' ),
268 $dashboardNavMenuUpdateCount = $( 'a[href="update-core.php"] .update-plugins' ),
269 $pluginsNavMenuUpdateCount = $( 'a[href="plugins.php"] .update-plugins' ),
270 $appearanceNavMenuUpdateCount = $( 'a[href="themes.php"] .update-plugins' ),
273 $adminBarUpdates.find( '.ab-item' ).removeAttr( 'title' );
274 $adminBarUpdates.find( '.ab-label' ).text( settings.totals.counts.total );
276 // Remove the update count from the toolbar if it's zero.
277 if ( 0 === settings.totals.counts.total ) {
278 $adminBarUpdates.find( '.ab-label' ).parents( 'li' ).remove();
281 // Update the "Updates" menu item.
282 $dashboardNavMenuUpdateCount.each( function( index, element ) {
283 element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.total );
285 if ( settings.totals.counts.total > 0 ) {
286 $dashboardNavMenuUpdateCount.find( '.update-count' ).text( settings.totals.counts.total );
288 $dashboardNavMenuUpdateCount.remove();
291 // Update the "Plugins" menu item.
292 $pluginsNavMenuUpdateCount.each( function( index, element ) {
293 element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.plugins );
295 if ( settings.totals.counts.total > 0 ) {
296 $pluginsNavMenuUpdateCount.find( '.plugin-count' ).text( settings.totals.counts.plugins );
298 $pluginsNavMenuUpdateCount.remove();
301 // Update the "Appearance" menu item.
302 $appearanceNavMenuUpdateCount.each( function( index, element ) {
303 element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.themes );
305 if ( settings.totals.counts.total > 0 ) {
306 $appearanceNavMenuUpdateCount.find( '.theme-count' ).text( settings.totals.counts.themes );
308 $appearanceNavMenuUpdateCount.remove();
311 // Update list table filter navigation.
312 if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
313 itemCount = settings.totals.counts.plugins;
314 } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) {
315 itemCount = settings.totals.counts.themes;
318 if ( itemCount > 0 ) {
319 $( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' );
321 $( '.subsubsub .upgrade' ).remove();
326 * Decrements the update counts throughout the various menus.
328 * This includes the toolbar, the "Updates" menu item and the menu items
329 * for plugins and themes.
333 * @param {string} type The type of item that was updated or deleted.
334 * Can be 'plugin', 'theme'.
336 wp.updates.decrementCount = function( type ) {
337 settings.totals.counts.total = Math.max( --settings.totals.counts.total, 0 );
339 if ( 'plugin' === type ) {
340 settings.totals.counts.plugins = Math.max( --settings.totals.counts.plugins, 0 );
341 } else if ( 'theme' === type ) {
342 settings.totals.counts.themes = Math.max( --settings.totals.counts.themes, 0 );
345 wp.updates.refreshCount( type );
349 * Sends an Ajax request to the server to update a plugin.
352 * @since 4.6.0 More accurately named `updatePlugin`.
354 * @param {object} args Arguments.
355 * @param {string} args.plugin Plugin basename.
356 * @param {string} args.slug Plugin slug.
357 * @param {updatePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.updatePluginSuccess
358 * @param {updatePluginError=} args.error Optional. Error callback. Default: wp.updates.updatePluginError
359 * @return {$.promise} A jQuery promise that represents the request,
360 * decorated with an abort() method.
362 wp.updates.updatePlugin = function( args ) {
363 var $updateRow, $card, $message, message;
366 success: wp.updates.updatePluginSuccess,
367 error: wp.updates.updatePluginError
370 if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
371 $updateRow = $( 'tr[data-plugin="' + args.plugin + '"]' );
372 $message = $updateRow.find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
373 message = wp.updates.l10n.updatingLabel.replace( '%s', $updateRow.find( '.plugin-title strong' ).text() );
374 } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
375 $card = $( '.plugin-card-' + args.slug );
376 $message = $card.find( '.update-now' ).addClass( 'updating-message' );
377 message = wp.updates.l10n.updatingLabel.replace( '%s', $message.data( 'name' ) );
379 // Remove previous error messages, if any.
380 $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove();
383 if ( $message.html() !== wp.updates.l10n.updating ) {
384 $message.data( 'originaltext', $message.html() );
388 .attr( 'aria-label', message )
389 .text( wp.updates.l10n.updating );
391 $document.trigger( 'wp-plugin-updating', args );
393 return wp.updates.ajax( 'update-plugin', args );
397 * Updates the UI appropriately after a successful plugin update.
400 * @since 4.6.0 More accurately named `updatePluginSuccess`.
402 * @typedef {object} updatePluginSuccess
403 * @param {object} response Response from the server.
404 * @param {string} response.slug Slug of the plugin to be updated.
405 * @param {string} response.plugin Basename of the plugin to be updated.
406 * @param {string} response.pluginName Name of the plugin to be updated.
407 * @param {string} response.oldVersion Old version of the plugin.
408 * @param {string} response.newVersion New version of the plugin.
410 wp.updates.updatePluginSuccess = function( response ) {
411 var $pluginRow, $updateMessage, newText;
413 if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
414 $pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' )
415 .removeClass( 'update' )
416 .addClass( 'updated' );
417 $updateMessage = $pluginRow.find( '.update-message' )
418 .removeClass( 'updating-message notice-warning' )
419 .addClass( 'updated-message notice-success' ).find( 'p' );
421 // Update the version number in the row.
422 newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
423 $pluginRow.find( '.plugin-version-author-uri' ).html( newText );
424 } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
425 $updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' )
426 .removeClass( 'updating-message' )
427 .addClass( 'button-disabled updated-message' );
431 .attr( 'aria-label', wp.updates.l10n.updatedLabel.replace( '%s', response.pluginName ) )
432 .text( wp.updates.l10n.updated );
434 wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' );
436 wp.updates.decrementCount( 'plugin' );
438 $document.trigger( 'wp-plugin-update-success', response );
442 * Updates the UI appropriately after a failed plugin update.
445 * @since 4.6.0 More accurately named `updatePluginError`.
447 * @typedef {object} updatePluginError
448 * @param {object} response Response from the server.
449 * @param {string} response.slug Slug of the plugin to be updated.
450 * @param {string} response.plugin Basename of the plugin to be updated.
451 * @param {string=} response.pluginName Optional. Name of the plugin to be updated.
452 * @param {string} response.errorCode Error code for the error that occurred.
453 * @param {string} response.errorMessage The error that occurred.
455 wp.updates.updatePluginError = function( response ) {
456 var $card, $message, errorMessage;
458 if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
462 if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) {
466 errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage );
468 if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
469 if ( response.plugin ) {
470 $message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' );
472 $message = $( 'tr[data-slug="' + response.slug + '"]' ).find( '.update-message' );
474 $message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage );
476 if ( response.pluginName ) {
478 .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) );
480 $message.find( 'p' ).removeAttr( 'aria-label' );
482 } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
483 $card = $( '.plugin-card-' + response.slug )
484 .addClass( 'plugin-card-update-failed' )
485 .append( wp.updates.adminNotice( {
486 className: 'update-message notice-error notice-alt is-dismissible',
487 message: errorMessage
490 $card.find( '.update-now' )
491 .text( wp.updates.l10n.updateFailedShort ).removeClass( 'updating-message' );
493 if ( response.pluginName ) {
494 $card.find( '.update-now' )
495 .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) );
497 $card.find( '.update-now' ).removeAttr( 'aria-label' );
500 $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
502 // Use same delay as the total duration of the notice fadeTo + slideUp animation.
503 setTimeout( function() {
505 .removeClass( 'plugin-card-update-failed' )
506 .find( '.column-name a' ).focus();
508 $card.find( '.update-now' )
509 .attr( 'aria-label', false )
510 .text( wp.updates.l10n.updateNow );
515 wp.a11y.speak( errorMessage, 'assertive' );
517 $document.trigger( 'wp-plugin-update-error', response );
521 * Sends an Ajax request to the server to install a plugin.
525 * @param {object} args Arguments.
526 * @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository.
527 * @param {installPluginSuccess=} args.success Optional. Success callback. Default: wp.updates.installPluginSuccess
528 * @param {installPluginError=} args.error Optional. Error callback. Default: wp.updates.installPluginError
529 * @return {$.promise} A jQuery promise that represents the request,
530 * decorated with an abort() method.
532 wp.updates.installPlugin = function( args ) {
533 var $card = $( '.plugin-card-' + args.slug ),
534 $message = $card.find( '.install-now' );
537 success: wp.updates.installPluginSuccess,
538 error: wp.updates.installPluginError
541 if ( 'import' === pagenow ) {
542 $message = $( '[data-slug="' + args.slug + '"]' );
545 if ( $message.html() !== wp.updates.l10n.installing ) {
546 $message.data( 'originaltext', $message.html() );
550 .addClass( 'updating-message' )
551 .attr( 'aria-label', wp.updates.l10n.pluginInstallingLabel.replace( '%s', $message.data( 'name' ) ) )
552 .text( wp.updates.l10n.installing );
554 wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' );
556 // Remove previous error messages, if any.
557 $card.removeClass( 'plugin-card-install-failed' ).find( '.notice.notice-error' ).remove();
559 $document.trigger( 'wp-plugin-installing', args );
561 return wp.updates.ajax( 'install-plugin', args );
565 * Updates the UI appropriately after a successful plugin install.
569 * @typedef {object} installPluginSuccess
570 * @param {object} response Response from the server.
571 * @param {string} response.slug Slug of the installed plugin.
572 * @param {string} response.pluginName Name of the installed plugin.
573 * @param {string} response.activateUrl URL to activate the just installed plugin.
575 wp.updates.installPluginSuccess = function( response ) {
576 var $message = $( '.plugin-card-' + response.slug ).find( '.install-now' );
579 .removeClass( 'updating-message' )
580 .addClass( 'updated-message installed button-disabled' )
581 .attr( 'aria-label', wp.updates.l10n.pluginInstalledLabel.replace( '%s', response.pluginName ) )
582 .text( wp.updates.l10n.installed );
584 wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
586 $document.trigger( 'wp-plugin-install-success', response );
588 if ( response.activateUrl ) {
589 setTimeout( function() {
591 // Transform the 'Install' button into an 'Activate' button.
592 $message.removeClass( 'install-now installed button-disabled updated-message' ).addClass( 'activate-now button-primary' )
593 .attr( 'href', response.activateUrl )
594 .attr( 'aria-label', wp.updates.l10n.activatePluginLabel.replace( '%s', response.pluginName ) )
595 .text( wp.updates.l10n.activatePlugin );
601 * Updates the UI appropriately after a failed plugin install.
605 * @typedef {object} installPluginError
606 * @param {object} response Response from the server.
607 * @param {string} response.slug Slug of the plugin to be installed.
608 * @param {string=} response.pluginName Optional. Name of the plugin to be installed.
609 * @param {string} response.errorCode Error code for the error that occurred.
610 * @param {string} response.errorMessage The error that occurred.
612 wp.updates.installPluginError = function( response ) {
613 var $card = $( '.plugin-card-' + response.slug ),
614 $button = $card.find( '.install-now' ),
617 if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
621 if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
625 errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage );
628 .addClass( 'plugin-card-update-failed' )
629 .append( '<div class="notice notice-error notice-alt is-dismissible"><p>' + errorMessage + '</p></div>' );
631 $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
633 // Use same delay as the total duration of the notice fadeTo + slideUp animation.
634 setTimeout( function() {
636 .removeClass( 'plugin-card-update-failed' )
637 .find( '.column-name a' ).focus();
642 .removeClass( 'updating-message' ).addClass( 'button-disabled' )
643 .attr( 'aria-label', wp.updates.l10n.pluginInstallFailedLabel.replace( '%s', $button.data( 'name' ) ) )
644 .text( wp.updates.l10n.installFailedShort );
646 wp.a11y.speak( errorMessage, 'assertive' );
648 $document.trigger( 'wp-plugin-install-error', response );
652 * Updates the UI appropriately after a successful importer install.
656 * @typedef {object} installImporterSuccess
657 * @param {object} response Response from the server.
658 * @param {string} response.slug Slug of the installed plugin.
659 * @param {string} response.pluginName Name of the installed plugin.
660 * @param {string} response.activateUrl URL to activate the just installed plugin.
662 wp.updates.installImporterSuccess = function( response ) {
663 wp.updates.addAdminNotice( {
664 id: 'install-success',
665 className: 'notice-success is-dismissible',
666 message: wp.updates.l10n.importerInstalledMsg.replace( '%s', response.activateUrl + '&from=import' )
669 $( '[data-slug="' + response.slug + '"]' )
670 .removeClass( 'install-now updating-message' )
671 .addClass( 'activate-now' )
673 'href': response.activateUrl + '&from=import',
674 'aria-label': wp.updates.l10n.activateImporterLabel.replace( '%s', response.pluginName )
676 .text( wp.updates.l10n.activateImporter );
678 wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
680 $document.trigger( 'wp-importer-install-success', response );
684 * Updates the UI appropriately after a failed importer install.
688 * @typedef {object} installImporterError
689 * @param {object} response Response from the server.
690 * @param {string} response.slug Slug of the plugin to be installed.
691 * @param {string=} response.pluginName Optional. Name of the plugin to be installed.
692 * @param {string} response.errorCode Error code for the error that occurred.
693 * @param {string} response.errorMessage The error that occurred.
695 wp.updates.installImporterError = function( response ) {
696 var errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage ),
697 $installLink = $( '[data-slug="' + response.slug + '"]' ),
698 pluginName = $installLink.data( 'name' );
700 if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
704 if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
708 wp.updates.addAdminNotice( {
709 id: response.errorCode,
710 className: 'notice-error is-dismissible',
711 message: errorMessage
715 .removeClass( 'updating-message' )
716 .text( wp.updates.l10n.installNow )
717 .attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', pluginName ) );
719 wp.a11y.speak( errorMessage, 'assertive' );
721 $document.trigger( 'wp-importer-install-error', response );
725 * Sends an Ajax request to the server to delete a plugin.
729 * @param {object} args Arguments.
730 * @param {string} args.plugin Basename of the plugin to be deleted.
731 * @param {string} args.slug Slug of the plugin to be deleted.
732 * @param {deletePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.deletePluginSuccess
733 * @param {deletePluginError=} args.error Optional. Error callback. Default: wp.updates.deletePluginError
734 * @return {$.promise} A jQuery promise that represents the request,
735 * decorated with an abort() method.
737 wp.updates.deletePlugin = function( args ) {
738 var $link = $( '[data-plugin="' + args.plugin + '"]' ).find( '.row-actions a.delete' );
741 success: wp.updates.deletePluginSuccess,
742 error: wp.updates.deletePluginError
745 if ( $link.html() !== wp.updates.l10n.deleting ) {
747 .data( 'originaltext', $link.html() )
748 .text( wp.updates.l10n.deleting );
751 wp.a11y.speak( wp.updates.l10n.deleting, 'polite' );
753 $document.trigger( 'wp-plugin-deleting', args );
755 return wp.updates.ajax( 'delete-plugin', args );
759 * Updates the UI appropriately after a successful plugin deletion.
763 * @typedef {object} deletePluginSuccess
764 * @param {object} response Response from the server.
765 * @param {string} response.slug Slug of the plugin that was deleted.
766 * @param {string} response.plugin Base name of the plugin that was deleted.
767 * @param {string} response.pluginName Name of the plugin that was deleted.
769 wp.updates.deletePluginSuccess = function( response ) {
771 // Removes the plugin and updates rows.
772 $( '[data-plugin="' + response.plugin + '"]' ).css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
773 var $form = $( '#bulk-action-form' ),
774 $views = $( '.subsubsub' ),
775 $pluginRow = $( this ),
776 columnCount = $form.find( 'thead th:not(.hidden), thead td' ).length,
777 pluginDeletedRow = wp.template( 'item-deleted-row' ),
778 /** @type {object} plugins Base names of plugins in their different states. */
779 plugins = settings.plugins;
781 // Add a success message after deleting a plugin.
782 if ( ! $pluginRow.hasClass( 'plugin-update-tr' ) ) {
786 plugin: response.plugin,
787 colspan: columnCount,
788 name: response.pluginName
795 // Remove plugin from update count.
796 if ( -1 !== _.indexOf( plugins.upgrade, response.plugin ) ) {
797 plugins.upgrade = _.without( plugins.upgrade, response.plugin );
798 wp.updates.decrementCount( 'plugin' );
801 // Remove from views.
802 if ( -1 !== _.indexOf( plugins.inactive, response.plugin ) ) {
803 plugins.inactive = _.without( plugins.inactive, response.plugin );
804 if ( plugins.inactive.length ) {
805 $views.find( '.inactive .count' ).text( '(' + plugins.inactive.length + ')' );
807 $views.find( '.inactive' ).remove();
811 if ( -1 !== _.indexOf( plugins.active, response.plugin ) ) {
812 plugins.active = _.without( plugins.active, response.plugin );
813 if ( plugins.active.length ) {
814 $views.find( '.active .count' ).text( '(' + plugins.active.length + ')' );
816 $views.find( '.active' ).remove();
820 if ( -1 !== _.indexOf( plugins.recently_activated, response.plugin ) ) {
821 plugins.recently_activated = _.without( plugins.recently_activated, response.plugin );
822 if ( plugins.recently_activated.length ) {
823 $views.find( '.recently_activated .count' ).text( '(' + plugins.recently_activated.length + ')' );
825 $views.find( '.recently_activated' ).remove();
829 plugins.all = _.without( plugins.all, response.plugin );
831 if ( plugins.all.length ) {
832 $views.find( '.all .count' ).text( '(' + plugins.all.length + ')' );
834 $form.find( '.tablenav' ).css( { visibility: 'hidden' } );
835 $views.find( '.all' ).remove();
837 if ( ! $form.find( 'tr.no-items' ).length ) {
838 $form.find( '#the-list' ).append( '<tr class="no-items"><td class="colspanchange" colspan="' + columnCount + '">' + wp.updates.l10n.noPlugins + '</td></tr>' );
843 wp.a11y.speak( wp.updates.l10n.deleted, 'polite' );
845 $document.trigger( 'wp-plugin-delete-success', response );
849 * Updates the UI appropriately after a failed plugin deletion.
853 * @typedef {object} deletePluginError
854 * @param {object} response Response from the server.
855 * @param {string} response.slug Slug of the plugin to be deleted.
856 * @param {string} response.plugin Base name of the plugin to be deleted
857 * @param {string=} response.pluginName Optional. Name of the plugin to be deleted.
858 * @param {string} response.errorCode Error code for the error that occurred.
859 * @param {string} response.errorMessage The error that occurred.
861 wp.updates.deletePluginError = function( response ) {
862 var $plugin, $pluginUpdateRow,
863 pluginUpdateRow = wp.template( 'item-update-row' ),
864 noticeContent = wp.updates.adminNotice( {
865 className: 'update-message notice-error notice-alt',
866 message: response.errorMessage
869 if ( response.plugin ) {
870 $plugin = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' );
871 $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' );
873 $plugin = $( 'tr.inactive[data-slug="' + response.slug + '"]' );
874 $pluginUpdateRow = $plugin.siblings( '[data-slug="' + response.slug + '"]' );
877 if ( ! wp.updates.isValidResponse( response, 'delete' ) ) {
881 if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) {
885 // Add a plugin update row if it doesn't exist yet.
886 if ( ! $pluginUpdateRow.length ) {
887 $plugin.addClass( 'update' ).after(
890 plugin: response.plugin || response.slug,
891 colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
892 content: noticeContent
897 // Remove previous error messages, if any.
898 $pluginUpdateRow.find( '.notice-error' ).remove();
900 $pluginUpdateRow.find( '.plugin-update' ).append( noticeContent );
903 $document.trigger( 'wp-plugin-delete-error', response );
907 * Sends an Ajax request to the server to update a theme.
911 * @param {object} args Arguments.
912 * @param {string} args.slug Theme stylesheet.
913 * @param {updateThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.updateThemeSuccess
914 * @param {updateThemeError=} args.error Optional. Error callback. Default: wp.updates.updateThemeError
915 * @return {$.promise} A jQuery promise that represents the request,
916 * decorated with an abort() method.
918 wp.updates.updateTheme = function( args ) {
922 success: wp.updates.updateThemeSuccess,
923 error: wp.updates.updateThemeError
926 if ( 'themes-network' === pagenow ) {
927 $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
930 $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' );
932 $notice.find( 'h3' ).remove();
934 $notice = $notice.add( $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ) );
935 $notice = $notice.addClass( 'updating-message' ).find( 'p' );
938 if ( $notice.html() !== wp.updates.l10n.updating ) {
939 $notice.data( 'originaltext', $notice.html() );
942 wp.a11y.speak( wp.updates.l10n.updatingMsg, 'polite' );
943 $notice.text( wp.updates.l10n.updating );
945 $document.trigger( 'wp-theme-updating', args );
947 return wp.updates.ajax( 'update-theme', args );
951 * Updates the UI appropriately after a successful theme update.
955 * @typedef {object} updateThemeSuccess
956 * @param {object} response
957 * @param {string} response.slug Slug of the theme to be updated.
958 * @param {object} response.theme Updated theme.
959 * @param {string} response.oldVersion Old version of the theme.
960 * @param {string} response.newVersion New version of the theme.
962 wp.updates.updateThemeSuccess = function( response ) {
963 var isModalOpen = $( 'body.modal-open' ).length,
964 $theme = $( '[data-slug="' + response.slug + '"]' ),
966 className: 'updated-message notice-success notice-alt',
967 message: wp.updates.l10n.updated
971 if ( 'themes-network' === pagenow ) {
972 $notice = $theme.find( '.update-message' );
974 // Update the version number in the row.
975 newText = $theme.find( '.theme-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
976 $theme.find( '.theme-version-author-uri' ).html( newText );
978 $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) );
980 // Focus on Customize button after updating.
982 $( '.load-customize:visible' ).focus();
984 $theme.find( '.load-customize' ).focus();
988 wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) );
989 wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' );
991 wp.updates.decrementCount( 'theme' );
993 $document.trigger( 'wp-theme-update-success', response );
995 // Show updated message after modal re-rendered.
997 $( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) );
1002 * Updates the UI appropriately after a failed theme update.
1006 * @typedef {object} updateThemeError
1007 * @param {object} response Response from the server.
1008 * @param {string} response.slug Slug of the theme to be updated.
1009 * @param {string} response.errorCode Error code for the error that occurred.
1010 * @param {string} response.errorMessage The error that occurred.
1012 wp.updates.updateThemeError = function( response ) {
1013 var $theme = $( '[data-slug="' + response.slug + '"]' ),
1014 errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage ),
1017 if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
1021 if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) {
1025 if ( 'themes-network' === pagenow ) {
1026 $notice = $theme.find( '.update-message ' );
1028 $notice = $( '.theme-info .notice' ).add( $theme.find( '.notice' ) );
1030 $( 'body.modal-open' ).length ? $( '.load-customize:visible' ).focus() : $theme.find( '.load-customize' ).focus();
1033 wp.updates.addAdminNotice( {
1035 className: 'update-message notice-error notice-alt is-dismissible',
1036 message: errorMessage
1039 wp.a11y.speak( errorMessage, 'polite' );
1041 $document.trigger( 'wp-theme-update-error', response );
1045 * Sends an Ajax request to the server to install a theme.
1049 * @param {object} args
1050 * @param {string} args.slug Theme stylesheet.
1051 * @param {installThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.installThemeSuccess
1052 * @param {installThemeError=} args.error Optional. Error callback. Default: wp.updates.installThemeError
1053 * @return {$.promise} A jQuery promise that represents the request,
1054 * decorated with an abort() method.
1056 wp.updates.installTheme = function( args ) {
1057 var $message = $( '.theme-install[data-slug="' + args.slug + '"]' );
1060 success: wp.updates.installThemeSuccess,
1061 error: wp.updates.installThemeError
1064 $message.addClass( 'updating-message' );
1065 $message.parents( '.theme' ).addClass( 'focus' );
1066 if ( $message.html() !== wp.updates.l10n.installing ) {
1067 $message.data( 'originaltext', $message.html() );
1071 .text( wp.updates.l10n.installing )
1072 .attr( 'aria-label', wp.updates.l10n.themeInstallingLabel.replace( '%s', $message.data( 'name' ) ) );
1073 wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' );
1075 // Remove previous error messages, if any.
1076 $( '.install-theme-info, [data-slug="' + args.slug + '"]' ).removeClass( 'theme-install-failed' ).find( '.notice.notice-error' ).remove();
1078 $document.trigger( 'wp-theme-installing', args );
1080 return wp.updates.ajax( 'install-theme', args );
1084 * Updates the UI appropriately after a successful theme install.
1088 * @typedef {object} installThemeSuccess
1089 * @param {object} response Response from the server.
1090 * @param {string} response.slug Slug of the theme to be installed.
1091 * @param {string} response.customizeUrl URL to the Customizer for the just installed theme.
1092 * @param {string} response.activateUrl URL to activate the just installed theme.
1094 wp.updates.installThemeSuccess = function( response ) {
1095 var $card = $( '.wp-full-overlay-header, [data-slug=' + response.slug + ']' ),
1098 $document.trigger( 'wp-theme-install-success', response );
1100 $message = $card.find( '.button-primary' )
1101 .removeClass( 'updating-message' )
1102 .addClass( 'updated-message disabled' )
1103 .attr( 'aria-label', wp.updates.l10n.themeInstalledLabel.replace( '%s', response.themeName ) )
1104 .text( wp.updates.l10n.installed );
1106 wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
1108 setTimeout( function() {
1110 if ( response.activateUrl ) {
1112 // Transform the 'Install' button into an 'Activate' button.
1114 .attr( 'href', response.activateUrl )
1115 .removeClass( 'theme-install updated-message disabled' )
1116 .addClass( 'activate' )
1117 .attr( 'aria-label', wp.updates.l10n.activateThemeLabel.replace( '%s', response.themeName ) )
1118 .text( wp.updates.l10n.activateTheme );
1121 if ( response.customizeUrl ) {
1123 // Transform the 'Preview' button into a 'Live Preview' button.
1124 $message.siblings( '.preview' ).replaceWith( function () {
1126 .attr( 'href', response.customizeUrl )
1127 .addClass( 'button load-customize' )
1128 .text( wp.updates.l10n.livePreview );
1135 * Updates the UI appropriately after a failed theme install.
1139 * @typedef {object} installThemeError
1140 * @param {object} response Response from the server.
1141 * @param {string} response.slug Slug of the theme to be installed.
1142 * @param {string} response.errorCode Error code for the error that occurred.
1143 * @param {string} response.errorMessage The error that occurred.
1145 wp.updates.installThemeError = function( response ) {
1147 errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage ),
1148 $message = wp.updates.adminNotice( {
1149 className: 'update-message notice-error notice-alt',
1150 message: errorMessage
1153 if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
1157 if ( wp.updates.maybeHandleCredentialError( response, 'install-theme' ) ) {
1161 if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) {
1162 $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
1163 $card = $( '.install-theme-info' ).prepend( $message );
1165 $card = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message );
1166 $button = $card.find( '.theme-install' );
1170 .removeClass( 'updating-message' )
1171 .attr( 'aria-label', wp.updates.l10n.themeInstallFailedLabel.replace( '%s', $button.data( 'name' ) ) )
1172 .text( wp.updates.l10n.installFailedShort );
1174 wp.a11y.speak( errorMessage, 'assertive' );
1176 $document.trigger( 'wp-theme-install-error', response );
1180 * Sends an Ajax request to the server to install a theme.
1184 * @param {object} args
1185 * @param {string} args.slug Theme stylesheet.
1186 * @param {deleteThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.deleteThemeSuccess
1187 * @param {deleteThemeError=} args.error Optional. Error callback. Default: wp.updates.deleteThemeError
1188 * @return {$.promise} A jQuery promise that represents the request,
1189 * decorated with an abort() method.
1191 wp.updates.deleteTheme = function( args ) {
1194 if ( 'themes' === pagenow ) {
1195 $button = $( '.theme-actions .delete-theme' );
1196 } else if ( 'themes-network' === pagenow ) {
1197 $button = $( '[data-slug="' + args.slug + '"]' ).find( '.row-actions a.delete' );
1201 success: wp.updates.deleteThemeSuccess,
1202 error: wp.updates.deleteThemeError
1205 if ( $button && $button.html() !== wp.updates.l10n.deleting ) {
1207 .data( 'originaltext', $button.html() )
1208 .text( wp.updates.l10n.deleting );
1211 wp.a11y.speak( wp.updates.l10n.deleting, 'polite' );
1213 // Remove previous error messages, if any.
1214 $( '.theme-info .update-message' ).remove();
1216 $document.trigger( 'wp-theme-deleting', args );
1218 return wp.updates.ajax( 'delete-theme', args );
1222 * Updates the UI appropriately after a successful theme deletion.
1226 * @typedef {object} deleteThemeSuccess
1227 * @param {object} response Response from the server.
1228 * @param {string} response.slug Slug of the theme that was deleted.
1230 wp.updates.deleteThemeSuccess = function( response ) {
1231 var $themeRows = $( '[data-slug="' + response.slug + '"]' );
1233 if ( 'themes-network' === pagenow ) {
1235 // Removes the theme and updates rows.
1236 $themeRows.css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
1237 var $views = $( '.subsubsub' ),
1238 $themeRow = $( this ),
1239 totals = settings.themes,
1240 deletedRow = wp.template( 'item-deleted-row' );
1242 if ( ! $themeRow.hasClass( 'plugin-update-tr' ) ) {
1245 slug: response.slug,
1246 colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
1247 name: $themeRow.find( '.theme-title strong' ).text()
1254 // Remove theme from update count.
1255 if ( $themeRow.hasClass( 'update' ) ) {
1257 wp.updates.decrementCount( 'theme' );
1260 // Remove from views.
1261 if ( $themeRow.hasClass( 'inactive' ) ) {
1263 if ( totals.disabled ) {
1264 $views.find( '.disabled .count' ).text( '(' + totals.disabled + ')' );
1266 $views.find( '.disabled' ).remove();
1270 // There is always at least one theme available.
1271 $views.find( '.all .count' ).text( '(' + --totals.all + ')' );
1275 wp.a11y.speak( wp.updates.l10n.deleted, 'polite' );
1277 $document.trigger( 'wp-theme-delete-success', response );
1281 * Updates the UI appropriately after a failed theme deletion.
1285 * @typedef {object} deleteThemeError
1286 * @param {object} response Response from the server.
1287 * @param {string} response.slug Slug of the theme to be deleted.
1288 * @param {string} response.errorCode Error code for the error that occurred.
1289 * @param {string} response.errorMessage The error that occurred.
1291 wp.updates.deleteThemeError = function( response ) {
1292 var $themeRow = $( 'tr.inactive[data-slug="' + response.slug + '"]' ),
1293 $button = $( '.theme-actions .delete-theme' ),
1294 updateRow = wp.template( 'item-update-row' ),
1295 $updateRow = $themeRow.siblings( '#' + response.slug + '-update' ),
1296 errorMessage = wp.updates.l10n.deleteFailed.replace( '%s', response.errorMessage ),
1297 $message = wp.updates.adminNotice( {
1298 className: 'update-message notice-error notice-alt',
1299 message: errorMessage
1302 if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) {
1306 if ( 'themes-network' === pagenow ) {
1307 if ( ! $updateRow.length ) {
1308 $themeRow.addClass( 'update' ).after(
1310 slug: response.slug,
1311 colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
1316 // Remove previous error messages, if any.
1317 $updateRow.find( '.notice-error' ).remove();
1318 $updateRow.find( '.plugin-update' ).append( $message );
1321 $( '.theme-info .theme-description' ).before( $message );
1324 $button.html( $button.data( 'originaltext' ) );
1326 wp.a11y.speak( errorMessage, 'assertive' );
1328 $document.trigger( 'wp-theme-delete-error', response );
1332 * Adds the appropriate callback based on the type of action and the current page.
1337 * @param {object} data AJAX payload.
1338 * @param {string} action The type of request to perform.
1339 * @return {object} The AJAX payload with the appropriate callbacks.
1341 wp.updates._addCallbacks = function( data, action ) {
1342 if ( 'import' === pagenow && 'install-plugin' === action ) {
1343 data.success = wp.updates.installImporterSuccess;
1344 data.error = wp.updates.installImporterError;
1351 * Pulls available jobs from the queue and runs them.
1354 * @since 4.6.0 Can handle multiple job types.
1356 wp.updates.queueChecker = function() {
1359 if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) {
1363 job = wp.updates.queue.shift();
1365 // Handle a queue job.
1366 switch ( job.action ) {
1367 case 'install-plugin':
1368 wp.updates.installPlugin( job.data );
1371 case 'update-plugin':
1372 wp.updates.updatePlugin( job.data );
1375 case 'delete-plugin':
1376 wp.updates.deletePlugin( job.data );
1379 case 'install-theme':
1380 wp.updates.installTheme( job.data );
1383 case 'update-theme':
1384 wp.updates.updateTheme( job.data );
1387 case 'delete-theme':
1388 wp.updates.deleteTheme( job.data );
1397 * Requests the users filesystem credentials if they aren't already known.
1401 * @param {Event=} event Optional. Event interface.
1403 wp.updates.requestFilesystemCredentials = function( event ) {
1404 if ( false === wp.updates.filesystemCredentials.available ) {
1406 * After exiting the credentials request modal,
1407 * return the focus to the element triggering the request.
1409 if ( event && ! wp.updates.$elToReturnFocusToFromCredentialsModal ) {
1410 wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target );
1413 wp.updates.ajaxLocked = true;
1414 wp.updates.requestForCredentialsModalOpen();
1419 * Requests the users filesystem credentials if needed and there is no lock.
1423 * @param {Event=} event Optional. Event interface.
1425 wp.updates.maybeRequestFilesystemCredentials = function( event ) {
1426 if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
1427 wp.updates.requestFilesystemCredentials( event );
1432 * Keydown handler for the request for credentials modal.
1434 * Closes the modal when the escape key is pressed and
1435 * constrains keyboard navigation to inside the modal.
1439 * @param {Event} event Event interface.
1441 wp.updates.keydown = function( event ) {
1442 if ( 27 === event.keyCode ) {
1443 wp.updates.requestForCredentialsModalCancel();
1444 } else if ( 9 === event.keyCode ) {
1446 // #upgrade button must always be the last focus-able element in the dialog.
1447 if ( 'upgrade' === event.target.id && ! event.shiftKey ) {
1448 $( '#hostname' ).focus();
1450 event.preventDefault();
1451 } else if ( 'hostname' === event.target.id && event.shiftKey ) {
1452 $( '#upgrade' ).focus();
1454 event.preventDefault();
1460 * Opens the request for credentials modal.
1464 wp.updates.requestForCredentialsModalOpen = function() {
1465 var $modal = $( '#request-filesystem-credentials-dialog' );
1467 $( 'body' ).addClass( 'modal-open' );
1469 $modal.find( 'input:enabled:first' ).focus();
1470 $modal.on( 'keydown', wp.updates.keydown );
1474 * Closes the request for credentials modal.
1478 wp.updates.requestForCredentialsModalClose = function() {
1479 $( '#request-filesystem-credentials-dialog' ).hide();
1480 $( 'body' ).removeClass( 'modal-open' );
1482 if ( wp.updates.$elToReturnFocusToFromCredentialsModal ) {
1483 wp.updates.$elToReturnFocusToFromCredentialsModal.focus();
1488 * Takes care of the steps that need to happen when the modal is canceled out.
1491 * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions.
1493 wp.updates.requestForCredentialsModalCancel = function() {
1495 // Not ajaxLocked and no queue means we already have cleared things up.
1496 if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) {
1500 _.each( wp.updates.queue, function( job ) {
1501 $document.trigger( 'credential-modal-cancel', job );
1504 // Remove the lock, and clear the queue.
1505 wp.updates.ajaxLocked = false;
1506 wp.updates.queue = [];
1508 wp.updates.requestForCredentialsModalClose();
1512 * Displays an error message in the request for credentials form.
1516 * @param {string} message Error message.
1518 wp.updates.showErrorInCredentialsForm = function( message ) {
1519 var $modal = $( '#request-filesystem-credentials-form' );
1521 // Remove any existing error.
1522 $modal.find( '.notice' ).remove();
1523 $modal.find( '#request-filesystem-credentials-title' ).after( '<div class="notice notice-alt notice-error"><p>' + message + '</p></div>' );
1527 * Handles credential errors and runs events that need to happen in that case.
1531 * @param {object} response Ajax response.
1532 * @param {string} action The type of request to perform.
1534 wp.updates.credentialError = function( response, action ) {
1536 // Restore callbacks.
1537 response = wp.updates._addCallbacks( response, action );
1539 wp.updates.queue.unshift( {
1543 * Not cool that we're depending on response for this data.
1544 * This would feel more whole in a view all tied together.
1549 wp.updates.filesystemCredentials.available = false;
1550 wp.updates.showErrorInCredentialsForm( response.errorMessage );
1551 wp.updates.requestFilesystemCredentials();
1555 * Handles credentials errors if it could not connect to the filesystem.
1559 * @typedef {object} maybeHandleCredentialError
1560 * @param {object} response Response from the server.
1561 * @param {string} response.errorCode Error code for the error that occurred.
1562 * @param {string} response.errorMessage The error that occurred.
1563 * @param {string} action The type of request to perform.
1564 * @returns {boolean} Whether there is an error that needs to be handled or not.
1566 wp.updates.maybeHandleCredentialError = function( response, action ) {
1567 if ( wp.updates.shouldRequestFilesystemCredentials && response.errorCode && 'unable_to_connect_to_filesystem' === response.errorCode ) {
1568 wp.updates.credentialError( response, action );
1576 * Validates an AJAX response to ensure it's a proper object.
1578 * If the response deems to be invalid, an admin notice is being displayed.
1580 * @param {(object|string)} response Response from the server.
1581 * @param {function=} response.always Optional. Callback for when the Deferred is resolved or rejected.
1582 * @param {string=} response.statusText Optional. Status message corresponding to the status code.
1583 * @param {string=} response.responseText Optional. Request response as text.
1584 * @param {string} action Type of action the response is referring to. Can be 'delete',
1585 * 'update' or 'install'.
1587 wp.updates.isValidResponse = function( response, action ) {
1588 var error = wp.updates.l10n.unknownError,
1591 // Make sure the response is a valid data object and not a Promise object.
1592 if ( _.isObject( response ) && ! _.isFunction( response.always ) ) {
1596 if ( _.isString( response ) && '-1' === response ) {
1597 error = wp.updates.l10n.nonceError;
1598 } else if ( _.isString( response ) ) {
1600 } else if ( 'undefined' !== typeof response.readyState && 0 === response.readyState ) {
1601 error = wp.updates.l10n.connectionError;
1602 } else if ( _.isString( response.responseText ) && '' !== response.responseText ) {
1603 error = response.responseText;
1604 } else if ( _.isString( response.statusText ) ) {
1605 error = response.statusText;
1610 errorMessage = wp.updates.l10n.updateFailed;
1614 errorMessage = wp.updates.l10n.installFailed;
1618 errorMessage = wp.updates.l10n.deleteFailed;
1622 // Messages are escaped, remove HTML tags to make them more readable.
1623 error = error.replace( /<[\/a-z][^<>]*>/gi, '' );
1624 errorMessage = errorMessage.replace( '%s', error );
1626 // Add admin notice.
1627 wp.updates.addAdminNotice( {
1628 id: 'unknown_error',
1629 className: 'notice-error is-dismissible',
1630 message: _.escape( errorMessage )
1633 // Remove the lock, and clear the queue.
1634 wp.updates.ajaxLocked = false;
1635 wp.updates.queue = [];
1637 // Change buttons of all running updates.
1638 $( '.button.updating-message' )
1639 .removeClass( 'updating-message' )
1640 .removeAttr( 'aria-label' )
1641 .prop( 'disabled', true )
1642 .text( wp.updates.l10n.updateFailedShort );
1644 $( '.updating-message:not(.button):not(.thickbox)' )
1645 .removeClass( 'updating-message notice-warning' )
1646 .addClass( 'notice-error' )
1648 .removeAttr( 'aria-label' )
1649 .text( errorMessage );
1651 wp.a11y.speak( errorMessage, 'assertive' );
1657 * Potentially adds an AYS to a user attempting to leave the page.
1659 * If an update is on-going and a user attempts to leave the page,
1660 * opens an "Are you sure?" alert.
1664 wp.updates.beforeunload = function() {
1665 if ( wp.updates.ajaxLocked ) {
1666 return wp.updates.l10n.beforeunload;
1671 var $pluginFilter = $( '#plugin-filter' ),
1672 $bulkActionForm = $( '#bulk-action-form' ),
1673 $filesystemModal = $( '#request-filesystem-credentials-dialog' ),
1674 $pluginSearch = $( '.plugins-php .wp-filter-search' ),
1675 $pluginInstallSearch = $( '.plugin-install-php .wp-filter-search' );
1677 settings = _.extend( settings, window._wpUpdatesItemCounts || {} );
1679 if ( settings.totals ) {
1680 wp.updates.refreshCount();
1684 * Whether a user needs to submit filesystem credentials.
1686 * This is based on whether the form was output on the page server-side.
1688 * @see {wp_print_request_filesystem_credentials_modal() in PHP}
1690 wp.updates.shouldRequestFilesystemCredentials = $filesystemModal.length > 0;
1693 * File system credentials form submit noop-er / handler.
1697 $filesystemModal.on( 'submit', 'form', function( event ) {
1698 event.preventDefault();
1700 // Persist the credentials input by the user for the duration of the page load.
1701 wp.updates.filesystemCredentials.ftp.hostname = $( '#hostname' ).val();
1702 wp.updates.filesystemCredentials.ftp.username = $( '#username' ).val();
1703 wp.updates.filesystemCredentials.ftp.password = $( '#password' ).val();
1704 wp.updates.filesystemCredentials.ftp.connectionType = $( 'input[name="connection_type"]:checked' ).val();
1705 wp.updates.filesystemCredentials.ssh.publicKey = $( '#public_key' ).val();
1706 wp.updates.filesystemCredentials.ssh.privateKey = $( '#private_key' ).val();
1707 wp.updates.filesystemCredentials.available = true;
1709 // Unlock and invoke the queue.
1710 wp.updates.ajaxLocked = false;
1711 wp.updates.queueChecker();
1713 wp.updates.requestForCredentialsModalClose();
1717 * Closes the request credentials modal when clicking the 'Cancel' button or outside of the modal.
1721 $filesystemModal.on( 'click', '[data-js-action="close"], .notification-dialog-background', wp.updates.requestForCredentialsModalCancel );
1724 * Hide SSH fields when not selected.
1728 $filesystemModal.on( 'change', 'input[name="connection_type"]', function() {
1729 $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) );
1733 * Handles events after the credential modal was closed.
1737 * @param {Event} event Event interface.
1738 * @param {string} job The install/update.delete request.
1740 $document.on( 'credential-modal-cancel', function( event, job ) {
1741 var $updatingMessage = $( '.updating-message' ),
1742 $message, originalText;
1744 if ( 'import' === pagenow ) {
1745 $updatingMessage.removeClass( 'updating-message' );
1746 } else if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
1747 if ( 'update-plugin' === job.action ) {
1748 $message = $( 'tr[data-plugin="' + job.data.plugin + '"]' ).find( '.update-message' );
1749 } else if ( 'delete-plugin' === job.action ) {
1750 $message = $( '[data-plugin="' + job.data.plugin + '"]' ).find( '.row-actions a.delete' );
1752 } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) {
1753 if ( 'update-theme' === job.action ) {
1754 $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.update-message' );
1755 } else if ( 'delete-theme' === job.action && 'themes-network' === pagenow ) {
1756 $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.row-actions a.delete' );
1757 } else if ( 'delete-theme' === job.action && 'themes' === pagenow ) {
1758 $message = $( '.theme-actions .delete-theme' );
1761 $message = $updatingMessage;
1764 if ( $message && $message.hasClass( 'updating-message' ) ) {
1765 originalText = $message.data( 'originaltext' );
1767 if ( 'undefined' === typeof originalText ) {
1768 originalText = $( '<p>' ).html( $message.find( 'p' ).data( 'originaltext' ) );
1772 .removeClass( 'updating-message' )
1773 .html( originalText );
1775 if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
1776 if ( 'update-plugin' === job.action ) {
1777 $message.attr( 'aria-label', wp.updates.l10n.updateNowLabel.replace( '%s', $message.data( 'name' ) ) );
1778 } else if ( 'install-plugin' === job.action ) {
1779 $message.attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', $message.data( 'name' ) ) );
1784 wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
1788 * Click handler for plugin updates in List Table view.
1792 * @param {Event} event Event interface.
1794 $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) {
1795 var $message = $( event.target ),
1796 $pluginRow = $message.parents( 'tr' );
1798 event.preventDefault();
1800 if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
1804 wp.updates.maybeRequestFilesystemCredentials( event );
1806 // Return the user to the input box of the plugin's table row after closing the modal.
1807 wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' );
1808 wp.updates.updatePlugin( {
1809 plugin: $pluginRow.data( 'plugin' ),
1810 slug: $pluginRow.data( 'slug' )
1815 * Click handler for plugin updates in plugin install view.
1819 * @param {Event} event Event interface.
1821 $pluginFilter.on( 'click', '.update-now', function( event ) {
1822 var $button = $( event.target );
1823 event.preventDefault();
1825 if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
1829 wp.updates.maybeRequestFilesystemCredentials( event );
1831 wp.updates.updatePlugin( {
1832 plugin: $button.data( 'plugin' ),
1833 slug: $button.data( 'slug' )
1838 * Click handler for plugin installs in plugin install view.
1842 * @param {Event} event Event interface.
1844 $pluginFilter.on( 'click', '.install-now', function( event ) {
1845 var $button = $( event.target );
1846 event.preventDefault();
1848 if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
1852 if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
1853 wp.updates.requestFilesystemCredentials( event );
1855 $document.on( 'credential-modal-cancel', function() {
1856 var $message = $( '.install-now.updating-message' );
1859 .removeClass( 'updating-message' )
1860 .text( wp.updates.l10n.installNow );
1862 wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
1866 wp.updates.installPlugin( {
1867 slug: $button.data( 'slug' )
1872 * Click handler for importer plugins installs in the Import screen.
1876 * @param {Event} event Event interface.
1878 $document.on( 'click', '.importer-item .install-now', function( event ) {
1879 var $button = $( event.target ),
1880 pluginName = $( this ).data( 'name' );
1882 event.preventDefault();
1884 if ( $button.hasClass( 'updating-message' ) ) {
1888 if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
1889 wp.updates.requestFilesystemCredentials( event );
1891 $document.on( 'credential-modal-cancel', function() {
1894 .removeClass( 'updating-message' )
1895 .text( wp.updates.l10n.installNow )
1896 .attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', pluginName ) );
1898 wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
1902 wp.updates.installPlugin( {
1903 slug: $button.data( 'slug' ),
1905 success: wp.updates.installImporterSuccess,
1906 error: wp.updates.installImporterError
1911 * Click handler for plugin deletions.
1915 * @param {Event} event Event interface.
1917 $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) {
1918 var $pluginRow = $( event.target ).parents( 'tr' );
1920 event.preventDefault();
1922 if ( ! window.confirm( wp.updates.l10n.aysDeleteUninstall.replace( '%s', $pluginRow.find( '.plugin-title strong' ).text() ) ) ) {
1926 wp.updates.maybeRequestFilesystemCredentials( event );
1928 wp.updates.deletePlugin( {
1929 plugin: $pluginRow.data( 'plugin' ),
1930 slug: $pluginRow.data( 'slug' )
1936 * Click handler for theme updates.
1940 * @param {Event} event Event interface.
1942 $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) {
1943 var $message = $( event.target ),
1944 $themeRow = $message.parents( 'tr' );
1946 event.preventDefault();
1948 if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
1952 wp.updates.maybeRequestFilesystemCredentials( event );
1954 // Return the user to the input box of the theme's table row after closing the modal.
1955 wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' );
1956 wp.updates.updateTheme( {
1957 slug: $themeRow.data( 'slug' )
1962 * Click handler for theme deletions.
1966 * @param {Event} event Event interface.
1968 $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) {
1969 var $themeRow = $( event.target ).parents( 'tr' );
1971 event.preventDefault();
1973 if ( ! window.confirm( wp.updates.l10n.aysDelete.replace( '%s', $themeRow.find( '.theme-title strong' ).text() ) ) ) {
1977 wp.updates.maybeRequestFilesystemCredentials( event );
1979 wp.updates.deleteTheme( {
1980 slug: $themeRow.data( 'slug' )
1985 * Bulk action handler for plugins and themes.
1987 * Handles both deletions and updates.
1991 * @param {Event} event Event interface.
1993 $bulkActionForm.on( 'click', '[type="submit"]', function( event ) {
1994 var bulkAction = $( event.target ).siblings( 'select' ).val(),
1995 itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ),
2001 // Determine which type of item we're dealing with.
2002 switch ( pagenow ) {
2004 case 'plugins-network':
2008 case 'themes-network':
2016 // Bail if there were no items selected.
2017 if ( ! itemsSelected.length ) {
2018 event.preventDefault();
2019 $( 'html, body' ).animate( { scrollTop: 0 } );
2021 return wp.updates.addAdminNotice( {
2022 id: 'no-items-selected',
2023 className: 'notice-error is-dismissible',
2024 message: wp.updates.l10n.noItemsSelected
2028 // Determine the type of request we're dealing with.
2029 switch ( bulkAction ) {
2030 case 'update-selected':
2031 action = bulkAction.replace( 'selected', type );
2034 case 'delete-selected':
2035 if ( ! window.confirm( 'plugin' === type ? wp.updates.l10n.aysBulkDelete : wp.updates.l10n.aysBulkDeleteThemes ) ) {
2036 event.preventDefault();
2040 action = bulkAction.replace( 'selected', type );
2047 wp.updates.maybeRequestFilesystemCredentials( event );
2049 event.preventDefault();
2051 // Un-check the bulk checkboxes.
2052 $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false );
2054 $document.trigger( 'wp-' + type + '-bulk-' + bulkAction, itemsSelected );
2056 // Find all the checkboxes which have been checked.
2057 itemsSelected.each( function( index, element ) {
2058 var $checkbox = $( element ),
2059 $itemRow = $checkbox.parents( 'tr' );
2061 // Only add update-able items to the update queue.
2062 if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) {
2064 // Un-check the box.
2065 $checkbox.prop( 'checked', false );
2069 // Add it to the queue.
2070 wp.updates.queue.push( {
2073 plugin: $itemRow.data( 'plugin' ),
2074 slug: $itemRow.data( 'slug' )
2079 // Display bulk notification for updates of any kind.
2080 $document.on( 'wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error', function( event, response ) {
2081 var $itemRow = $( '[data-slug="' + response.slug + '"]' ),
2082 $bulkActionNotice, itemName;
2084 if ( 'wp-' + response.update + '-update-success' === event.type ) {
2087 itemName = response.pluginName ? response.pluginName : $itemRow.find( '.column-primary strong' ).text();
2090 errorMessages.push( itemName + ': ' + response.errorMessage );
2093 $itemRow.find( 'input[name="checked[]"]:checked' ).prop( 'checked', false );
2095 wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' );
2097 wp.updates.addAdminNotice( {
2098 id: 'bulk-action-notice',
2099 className: 'bulk-action-notice',
2102 errorMessages: errorMessages,
2103 type: response.update
2106 $bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() {
2107 // $( this ) is the clicked button, no need to get it again.
2109 .toggleClass( 'bulk-action-errors-collapsed' )
2110 .attr( 'aria-expanded', ! $( this ).hasClass( 'bulk-action-errors-collapsed' ) );
2111 // Show the errors list.
2112 $bulkActionNotice.find( '.bulk-action-errors' ).toggleClass( 'hidden' );
2115 if ( error > 0 && ! wp.updates.queue.length ) {
2116 $( 'html, body' ).animate( { scrollTop: 0 } );
2120 // Reset admin notice template after #bulk-action-notice was added.
2121 $document.on( 'wp-updates-notice-added', function() {
2122 wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
2125 // Check the queue, now that the event handlers have been added.
2126 wp.updates.queueChecker();
2129 if ( $pluginInstallSearch.length ) {
2130 $pluginInstallSearch.attr( 'aria-describedby', 'live-search-desc' );
2134 * Handles changes to the plugin search box on the new-plugin page,
2135 * searching the repository dynamically.
2139 $pluginInstallSearch.on( 'keyup input', _.debounce( function( event, eventtype ) {
2140 var $searchTab = $( '.plugin-install-search' ), data, searchLocation;
2143 _ajax_nonce: wp.updates.ajaxNonce,
2144 s: event.target.value,
2146 type: $( '#typeselector' ).val(),
2149 searchLocation = location.href.split( '?' )[ 0 ] + '?' + $.param( _.omit( data, [ '_ajax_nonce', 'pagenow' ] ) );
2152 if ( 'keyup' === event.type && 27 === event.which ) {
2153 event.target.value = '';
2156 if ( wp.updates.searchTerm === data.s && 'typechange' !== eventtype ) {
2159 $pluginFilter.empty();
2160 wp.updates.searchTerm = data.s;
2163 if ( window.history && window.history.replaceState ) {
2164 window.history.replaceState( null, '', searchLocation );
2167 if ( ! $searchTab.length ) {
2168 $searchTab = $( '<li class="plugin-install-search" />' )
2169 .append( $( '<a />', {
2171 'href': searchLocation,
2172 'text': wp.updates.l10n.searchResultsLabel
2175 $( '.wp-filter .filter-links .current' )
2176 .removeClass( 'current' )
2177 .parents( '.filter-links' )
2178 .prepend( $searchTab );
2180 $pluginFilter.prev( 'p' ).remove();
2181 $( '.plugins-popular-tags-wrapper' ).remove();
2184 if ( 'undefined' !== typeof wp.updates.searchRequest ) {
2185 wp.updates.searchRequest.abort();
2187 $( 'body' ).addClass( 'loading-content' );
2189 wp.updates.searchRequest = wp.ajax.post( 'search-install-plugins', data ).done( function( response ) {
2190 $( 'body' ).removeClass( 'loading-content' );
2191 $pluginFilter.append( response.items );
2192 delete wp.updates.searchRequest;
2194 if ( 0 === response.count ) {
2195 wp.a11y.speak( wp.updates.l10n.noPluginsFound );
2197 wp.a11y.speak( wp.updates.l10n.pluginsFound.replace( '%d', response.count ) );
2202 if ( $pluginSearch.length ) {
2203 $pluginSearch.attr( 'aria-describedby', 'live-search-desc' );
2207 * Handles changes to the plugin search box on the Installed Plugins screen,
2208 * searching the plugin list dynamically.
2212 $pluginSearch.on( 'keyup input', _.debounce( function( event ) {
2214 _ajax_nonce: wp.updates.ajaxNonce,
2215 s: event.target.value,
2217 plugin_status: 'all'
2222 if ( 'keyup' === event.type && 27 === event.which ) {
2223 event.target.value = '';
2226 if ( wp.updates.searchTerm === data.s ) {
2229 wp.updates.searchTerm = data.s;
2232 queryArgs = _.object( _.compact( _.map( location.search.slice( 1 ).split( '&' ), function( item ) {
2233 if ( item ) return item.split( '=' );
2236 data.plugin_status = queryArgs.plugin_status || 'all';
2238 if ( window.history && window.history.replaceState ) {
2239 window.history.replaceState( null, '', location.href.split( '?' )[ 0 ] + '?s=' + data.s + '&plugin_status=' + data.plugin_status );
2242 if ( 'undefined' !== typeof wp.updates.searchRequest ) {
2243 wp.updates.searchRequest.abort();
2246 $bulkActionForm.empty();
2247 $( 'body' ).addClass( 'loading-content' );
2248 $( '.subsubsub .current' ).removeClass( 'current' );
2250 wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) {
2252 // Can we just ditch this whole subtitle business?
2253 var $subTitle = $( '<span />' ).addClass( 'subtitle' ).html( wp.updates.l10n.searchResults.replace( '%s', _.escape( data.s ) ) ),
2254 $oldSubTitle = $( '.wrap .subtitle' );
2256 if ( ! data.s.length ) {
2257 $oldSubTitle.remove();
2258 $( '.subsubsub .' + data.plugin_status + ' a' ).addClass( 'current' );
2259 } else if ( $oldSubTitle.length ) {
2260 $oldSubTitle.replaceWith( $subTitle );
2262 $( '.wrap h1' ).append( $subTitle );
2265 $( 'body' ).removeClass( 'loading-content' );
2266 $bulkActionForm.append( response.items );
2267 delete wp.updates.searchRequest;
2269 if ( 0 === response.count ) {
2270 wp.a11y.speak( wp.updates.l10n.noPluginsFound );
2272 wp.a11y.speak( wp.updates.l10n.pluginsFound.replace( '%d', response.count ) );
2278 * Trigger a search event when the search form gets submitted.
2282 $document.on( 'submit', '.search-plugins', function( event ) {
2283 event.preventDefault();
2285 $( 'input.wp-filter-search' ).trigger( 'input' );
2289 * Trigger a search event when the search type gets changed.
2293 $( '#typeselector' ).on( 'change', function() {
2294 var $search = $( 'input[name="s"]' );
2296 if ( $search.val().length ) {
2297 $search.trigger( 'input', 'typechange' );
2302 * Click handler for updating a plugin from the details modal on `plugin-install.php`.
2306 * @param {Event} event Event interface.
2308 $( '#plugin_update_from_iframe' ).on( 'click', function( event ) {
2309 var target = window.parent === window ? null : window.parent,
2312 $.support.postMessage = !! window.postMessage;
2314 if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'update-core.php' ) ) {
2318 event.preventDefault();
2321 action: 'update-plugin',
2323 plugin: $( this ).data( 'plugin' ),
2324 slug: $( this ).data( 'slug' )
2328 target.postMessage( JSON.stringify( update ), window.location.origin );
2332 * Click handler for installing a plugin from the details modal on `plugin-install.php`.
2336 * @param {Event} event Event interface.
2338 $( '#plugin_install_from_iframe' ).on( 'click', function( event ) {
2339 var target = window.parent === window ? null : window.parent,
2342 $.support.postMessage = !! window.postMessage;
2344 if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'index.php' ) ) {
2348 event.preventDefault();
2351 action: 'install-plugin',
2353 slug: $( this ).data( 'slug' )
2357 target.postMessage( JSON.stringify( install ), window.location.origin );
2361 * Handles postMessage events.
2364 * @since 4.6.0 Switched `update-plugin` action to use the queue.
2366 * @param {Event} event Event interface.
2368 $( window ).on( 'message', function( event ) {
2369 var originalEvent = event.originalEvent,
2370 expectedOrigin = document.location.protocol + '//' + document.location.hostname,
2373 if ( originalEvent.origin !== expectedOrigin ) {
2378 message = $.parseJSON( originalEvent.data );
2383 if ( 'undefined' === typeof message.action ) {
2387 switch ( message.action ) {
2389 // Called from `wp-admin/includes/class-wp-upgrader-skins.php`.
2390 case 'decrementUpdateCount':
2391 /** @property {string} message.upgradeType */
2392 wp.updates.decrementCount( message.upgradeType );
2395 case 'install-plugin':
2396 case 'update-plugin':
2397 /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
2401 message.data = wp.updates._addCallbacks( message.data, message.action );
2403 wp.updates.queue.push( message );
2404 wp.updates.queueChecker();
2410 * Adds a callback to display a warning before leaving the page.
2414 $( window ).on( 'beforeunload', wp.updates.beforeunload );
2416 })( jQuery, window.wp, window._wpUpdatesSettings );