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.totals Plugin/theme status information or null.
25 * @param {number} settings.totals.all Amount of all plugins or themes.
26 * @param {number} settings.totals.upgrade Amount of plugins or themes with updates available.
27 * @param {number} settings.totals.disabled Amount of disabled themes.
29 (function( $, wp, settings ) {
30 var $document = $( document );
35 * The WP Updates object.
44 * User nonce for ajax calls.
50 wp.updates.ajaxNonce = settings.ajax_nonce;
59 wp.updates.l10n = settings.l10n;
62 * Current search term.
68 wp.updates.searchTerm = '';
71 * Whether filesystem credentials need to be requested from the user.
77 wp.updates.shouldRequestFilesystemCredentials = false;
80 * Filesystem credentials to be packaged along with the request.
83 * @since 4.6.0 Added `available` property to indicate whether credentials have been provided.
85 * @type {object} filesystemCredentials Holds filesystem credentials.
86 * @type {object} filesystemCredentials.ftp Holds FTP credentials.
87 * @type {string} filesystemCredentials.ftp.host FTP host. Default empty string.
88 * @type {string} filesystemCredentials.ftp.username FTP user name. Default empty string.
89 * @type {string} filesystemCredentials.ftp.password FTP password. Default empty string.
90 * @type {string} filesystemCredentials.ftp.connectionType Type of FTP connection. 'ssh', 'ftp', or 'ftps'.
91 * Default empty string.
92 * @type {object} filesystemCredentials.ssh Holds SSH credentials.
93 * @type {string} filesystemCredentials.ssh.publicKey The public key. Default empty string.
94 * @type {string} filesystemCredentials.ssh.privateKey The private key. Default empty string.
95 * @type {bool} filesystemCredentials.available Whether filesystem credentials have been provided.
98 wp.updates.filesystemCredentials = {
113 * Whether we're waiting for an Ajax request to complete.
116 * @since 4.6.0 More accurately named `ajaxLocked`.
120 wp.updates.ajaxLocked = false;
123 * Admin notice template.
127 * @type {function} A function that lazily-compiles the template requested.
129 wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
134 * If the user tries to update a plugin while an update is
135 * already happening, it can be placed in this queue to perform later.
138 * @since 4.6.0 More accurately named `queue`.
140 * @type {Array.object}
142 wp.updates.queue = [];
145 * Holds a jQuery reference to return focus to when exiting the request credentials modal.
151 wp.updates.$elToReturnFocusToFromCredentialsModal = undefined;
154 * Adds or updates an admin notice.
158 * @param {object} data
159 * @param {*=} data.selector Optional. Selector of an element to be replaced with the admin notice.
160 * @param {string=} data.id Optional. Unique id that will be used as the notice's id attribute.
161 * @param {string=} data.className Optional. Class names that will be used in the admin notice.
162 * @param {string=} data.message Optional. The message displayed in the notice.
163 * @param {number=} data.successes Optional. The amount of successful operations.
164 * @param {number=} data.errors Optional. The amount of failed operations.
165 * @param {Array=} data.errorMessages Optional. Error messages of failed operations.
168 wp.updates.addAdminNotice = function( data ) {
169 var $notice = $( data.selector ), $adminNotice;
171 delete data.selector;
172 $adminNotice = wp.updates.adminNotice( data );
174 // Check if this admin notice already exists.
175 if ( ! $notice.length ) {
176 $notice = $( '#' + data.id );
179 if ( $notice.length ) {
180 $notice.replaceWith( $adminNotice );
182 $( '.wrap' ).find( '> h1' ).after( $adminNotice );
185 $document.trigger( 'wp-updates-notice-added' );
189 * Handles Ajax requests to WordPress.
193 * @param {string} action The type of Ajax request ('update-plugin', 'install-theme', etc).
194 * @param {object} data Data that needs to be passed to the ajax callback.
195 * @return {$.promise} A jQuery promise that represents the request,
196 * decorated with an abort() method.
198 wp.updates.ajax = function( action, data ) {
201 if ( wp.updates.ajaxLocked ) {
202 wp.updates.queue.push( {
207 // Return a Deferred object so callbacks can always be registered.
211 wp.updates.ajaxLocked = true;
213 if ( data.success ) {
214 options.success = data.success;
219 options.error = data.error;
223 options.data = _.extend( data, {
225 _ajax_nonce: wp.updates.ajaxNonce,
226 username: wp.updates.filesystemCredentials.ftp.username,
227 password: wp.updates.filesystemCredentials.ftp.password,
228 hostname: wp.updates.filesystemCredentials.ftp.hostname,
229 connection_type: wp.updates.filesystemCredentials.ftp.connectionType,
230 public_key: wp.updates.filesystemCredentials.ssh.publicKey,
231 private_key: wp.updates.filesystemCredentials.ssh.privateKey
234 return wp.ajax.send( options ).always( wp.updates.ajaxAlways );
238 * Actions performed after every Ajax request.
242 * @param {object} response
243 * @param {array=} response.debug Optional. Debug information.
244 * @param {string=} response.errorCode Optional. Error code for an error that occurred.
246 wp.updates.ajaxAlways = function( response ) {
247 if ( ! response.errorCode || 'unable_to_connect_to_filesystem' !== response.errorCode ) {
248 wp.updates.ajaxLocked = false;
249 wp.updates.queueChecker();
252 if ( 'undefined' !== typeof response.debug && window.console && window.console.log ) {
253 _.map( response.debug, function( message ) {
254 window.console.log( $( '<p />' ).html( message ).text() );
260 * Decrements the update counts throughout the various menus.
262 * This includes the toolbar, the "Updates" menu item and the menu items
263 * for plugins and themes.
267 * @param {string} type The type of item that was updated or deleted.
268 * Can be 'plugin', 'theme'.
270 wp.updates.decrementCount = function( type ) {
271 var $adminBarUpdates = $( '#wp-admin-bar-updates' ),
272 $dashboardNavMenuUpdateCount = $( 'a[href="update-core.php"] .update-plugins' ),
273 count = $adminBarUpdates.find( '.ab-label' ).text(),
274 $menuItem, $itemCount, itemCount;
276 count = parseInt( count, 10 ) - 1;
278 if ( count < 0 || isNaN( count ) ) {
282 $adminBarUpdates.find( '.ab-item' ).removeAttr( 'title' );
283 $adminBarUpdates.find( '.ab-label' ).text( count );
285 // Remove the update count from the toolbar if it's zero.
287 $adminBarUpdates.find( '.ab-label' ).parents( 'li' ).remove();
290 // Update the "Updates" menu item.
291 $dashboardNavMenuUpdateCount.each( function( index, element ) {
292 element.className = element.className.replace( /count-\d+/, 'count-' + count );
295 $dashboardNavMenuUpdateCount.removeAttr( 'title' );
296 $dashboardNavMenuUpdateCount.find( '.update-count' ).text( count );
298 if ( 'plugin' === type ) {
299 $menuItem = $( '#menu-plugins' );
300 $itemCount = $menuItem.find( '.plugin-count' );
301 } else if ( 'theme' === type ) {
302 $menuItem = $( '#menu-appearance' );
303 $itemCount = $menuItem.find( '.theme-count' );
306 // Decrement the counter of the other menu items.
308 itemCount = $itemCount.eq( 0 ).text();
309 itemCount = parseInt( itemCount, 10 ) - 1;
312 if ( itemCount < 0 || isNaN( itemCount ) ) {
316 if ( itemCount > 0 ) {
317 $( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' );
319 $itemCount.text( itemCount );
320 $menuItem.find( '.update-plugins' ).each( function( index, element ) {
321 element.className = element.className.replace( /count-\d+/, 'count-' + itemCount );
324 $( '.subsubsub .upgrade' ).remove();
325 $menuItem.find( '.update-plugins' ).remove();
330 * Sends an Ajax request to the server to update a plugin.
333 * @since 4.6.0 More accurately named `updatePlugin`.
335 * @param {object} args Arguments.
336 * @param {string} args.plugin Plugin basename.
337 * @param {string} args.slug Plugin slug.
338 * @param {updatePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.updatePluginSuccess
339 * @param {updatePluginError=} args.error Optional. Error callback. Default: wp.updates.updatePluginError
340 * @return {$.promise} A jQuery promise that represents the request,
341 * decorated with an abort() method.
343 wp.updates.updatePlugin = function( args ) {
344 var $updateRow, $card, $message, message;
347 success: wp.updates.updatePluginSuccess,
348 error: wp.updates.updatePluginError
351 if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
352 $updateRow = $( 'tr[data-plugin="' + args.plugin + '"]' );
353 $message = $updateRow.find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
354 message = wp.updates.l10n.updatingLabel.replace( '%s', $updateRow.find( '.plugin-title strong' ).text() );
355 } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
356 $card = $( '.plugin-card-' + args.slug );
357 $message = $card.find( '.update-now' ).addClass( 'updating-message' );
358 message = wp.updates.l10n.updatingLabel.replace( '%s', $message.data( 'name' ) );
360 // Remove previous error messages, if any.
361 $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove();
364 if ( $message.html() !== wp.updates.l10n.updating ) {
365 $message.data( 'originaltext', $message.html() );
369 .attr( 'aria-label', message )
370 .text( wp.updates.l10n.updating );
372 $document.trigger( 'wp-plugin-updating', args );
374 return wp.updates.ajax( 'update-plugin', args );
378 * Updates the UI appropriately after a successful plugin update.
381 * @since 4.6.0 More accurately named `updatePluginSuccess`.
383 * @typedef {object} updatePluginSuccess
384 * @param {object} response Response from the server.
385 * @param {string} response.slug Slug of the plugin to be updated.
386 * @param {string} response.plugin Basename of the plugin to be updated.
387 * @param {string} response.pluginName Name of the plugin to be updated.
388 * @param {string} response.oldVersion Old version of the plugin.
389 * @param {string} response.newVersion New version of the plugin.
391 wp.updates.updatePluginSuccess = function( response ) {
392 var $pluginRow, $updateMessage, newText;
394 if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
395 $pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' )
396 .removeClass( 'update' )
397 .addClass( 'updated' );
398 $updateMessage = $pluginRow.find( '.update-message' )
399 .removeClass( 'updating-message notice-warning' )
400 .addClass( 'updated-message notice-success' ).find( 'p' );
402 // Update the version number in the row.
403 newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
404 $pluginRow.find( '.plugin-version-author-uri' ).html( newText );
405 } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
406 $updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' )
407 .removeClass( 'updating-message' )
408 .addClass( 'button-disabled updated-message' );
412 .attr( 'aria-label', wp.updates.l10n.updatedLabel.replace( '%s', response.pluginName ) )
413 .text( wp.updates.l10n.updated );
415 wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' );
417 wp.updates.decrementCount( 'plugin' );
419 $document.trigger( 'wp-plugin-update-success', response );
423 * Updates the UI appropriately after a failed plugin update.
426 * @since 4.6.0 More accurately named `updatePluginError`.
428 * @typedef {object} updatePluginError
429 * @param {object} response Response from the server.
430 * @param {string} response.slug Slug of the plugin to be updated.
431 * @param {string} response.plugin Basename of the plugin to be updated.
432 * @param {string=} response.pluginName Optional. Name of the plugin to be updated.
433 * @param {string} response.errorCode Error code for the error that occurred.
434 * @param {string} response.errorMessage The error that occurred.
436 wp.updates.updatePluginError = function( response ) {
437 var $card, $message, errorMessage;
439 if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
443 if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) {
447 errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage );
449 if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
450 if ( response.plugin ) {
451 $message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' );
453 $message = $( 'tr[data-slug="' + response.slug + '"]' ).find( '.update-message' );
455 $message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage );
457 if ( response.pluginName ) {
459 .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) );
461 $message.find( 'p' ).removeAttr( 'aria-label' );
463 } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
464 $card = $( '.plugin-card-' + response.slug )
465 .addClass( 'plugin-card-update-failed' )
466 .append( wp.updates.adminNotice( {
467 className: 'update-message notice-error notice-alt is-dismissible',
468 message: errorMessage
471 $card.find( '.update-now' )
472 .text( wp.updates.l10n.updateFailedShort ).removeClass( 'updating-message' );
474 if ( response.pluginName ) {
475 $card.find( '.update-now' )
476 .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) );
478 $card.find( '.update-now' ).removeAttr( 'aria-label' );
481 $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
483 // Use same delay as the total duration of the notice fadeTo + slideUp animation.
484 setTimeout( function() {
486 .removeClass( 'plugin-card-update-failed' )
487 .find( '.column-name a' ).focus();
489 $card.find( '.update-now' )
490 .attr( 'aria-label', false )
491 .text( wp.updates.l10n.updateNow );
496 wp.a11y.speak( errorMessage, 'assertive' );
498 $document.trigger( 'wp-plugin-update-error', response );
502 * Sends an Ajax request to the server to install a plugin.
506 * @param {object} args Arguments.
507 * @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository.
508 * @param {installPluginSuccess=} args.success Optional. Success callback. Default: wp.updates.installPluginSuccess
509 * @param {installPluginError=} args.error Optional. Error callback. Default: wp.updates.installPluginError
510 * @return {$.promise} A jQuery promise that represents the request,
511 * decorated with an abort() method.
513 wp.updates.installPlugin = function( args ) {
514 var $card = $( '.plugin-card-' + args.slug ),
515 $message = $card.find( '.install-now' );
518 success: wp.updates.installPluginSuccess,
519 error: wp.updates.installPluginError
522 if ( 'import' === pagenow ) {
523 $message = $( '[data-slug="' + args.slug + '"]' );
526 if ( $message.html() !== wp.updates.l10n.installing ) {
527 $message.data( 'originaltext', $message.html() );
531 .addClass( 'updating-message' )
532 .attr( 'aria-label', wp.updates.l10n.pluginInstallingLabel.replace( '%s', $message.data( 'name' ) ) )
533 .text( wp.updates.l10n.installing );
535 wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' );
537 // Remove previous error messages, if any.
538 $card.removeClass( 'plugin-card-install-failed' ).find( '.notice.notice-error' ).remove();
540 $document.trigger( 'wp-plugin-installing', args );
542 return wp.updates.ajax( 'install-plugin', args );
546 * Updates the UI appropriately after a successful plugin install.
550 * @typedef {object} installPluginSuccess
551 * @param {object} response Response from the server.
552 * @param {string} response.slug Slug of the installed plugin.
553 * @param {string} response.pluginName Name of the installed plugin.
554 * @param {string} response.activateUrl URL to activate the just installed plugin.
556 wp.updates.installPluginSuccess = function( response ) {
557 var $message = $( '.plugin-card-' + response.slug ).find( '.install-now' );
560 .removeClass( 'updating-message' )
561 .addClass( 'updated-message installed button-disabled' )
562 .attr( 'aria-label', wp.updates.l10n.pluginInstalledLabel.replace( '%s', response.pluginName ) )
563 .text( wp.updates.l10n.installed );
565 wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
567 $document.trigger( 'wp-plugin-install-success', response );
569 if ( response.activateUrl ) {
570 setTimeout( function() {
572 // Transform the 'Install' button into an 'Activate' button.
573 $message.removeClass( 'install-now installed button-disabled updated-message' ).addClass( 'activate-now button-primary' )
574 .attr( 'href', response.activateUrl )
575 .attr( 'aria-label', wp.updates.l10n.activatePluginLabel.replace( '%s', response.pluginName ) )
576 .text( wp.updates.l10n.activatePlugin );
582 * Updates the UI appropriately after a failed plugin install.
586 * @typedef {object} installPluginError
587 * @param {object} response Response from the server.
588 * @param {string} response.slug Slug of the plugin to be installed.
589 * @param {string=} response.pluginName Optional. Name of the plugin to be installed.
590 * @param {string} response.errorCode Error code for the error that occurred.
591 * @param {string} response.errorMessage The error that occurred.
593 wp.updates.installPluginError = function( response ) {
594 var $card = $( '.plugin-card-' + response.slug ),
595 $button = $card.find( '.install-now' ),
598 if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
602 if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
606 errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage );
609 .addClass( 'plugin-card-update-failed' )
610 .append( '<div class="notice notice-error notice-alt is-dismissible"><p>' + errorMessage + '</p></div>' );
612 $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
614 // Use same delay as the total duration of the notice fadeTo + slideUp animation.
615 setTimeout( function() {
617 .removeClass( 'plugin-card-update-failed' )
618 .find( '.column-name a' ).focus();
623 .removeClass( 'updating-message' ).addClass( 'button-disabled' )
624 .attr( 'aria-label', wp.updates.l10n.pluginInstallFailedLabel.replace( '%s', $button.data( 'name' ) ) )
625 .text( wp.updates.l10n.installFailedShort );
627 wp.a11y.speak( errorMessage, 'assertive' );
629 $document.trigger( 'wp-plugin-install-error', response );
633 * Updates the UI appropriately after a successful importer install.
637 * @typedef {object} installImporterSuccess
638 * @param {object} response Response from the server.
639 * @param {string} response.slug Slug of the installed plugin.
640 * @param {string} response.pluginName Name of the installed plugin.
641 * @param {string} response.activateUrl URL to activate the just installed plugin.
643 wp.updates.installImporterSuccess = function( response ) {
644 wp.updates.addAdminNotice( {
645 id: 'install-success',
646 className: 'notice-success is-dismissible',
647 message: wp.updates.l10n.importerInstalledMsg.replace( '%s', response.activateUrl + '&from=import' )
650 $( '[data-slug="' + response.slug + '"]' )
651 .removeClass( 'install-now updating-message' )
652 .addClass( 'activate-now' )
654 'href': response.activateUrl + '&from=import',
655 'aria-label': wp.updates.l10n.activateImporterLabel.replace( '%s', response.pluginName )
657 .text( wp.updates.l10n.activateImporter );
659 wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
661 $document.trigger( 'wp-importer-install-success', response );
665 * Updates the UI appropriately after a failed importer install.
669 * @typedef {object} installImporterError
670 * @param {object} response Response from the server.
671 * @param {string} response.slug Slug of the plugin to be installed.
672 * @param {string=} response.pluginName Optional. Name of the plugin to be installed.
673 * @param {string} response.errorCode Error code for the error that occurred.
674 * @param {string} response.errorMessage The error that occurred.
676 wp.updates.installImporterError = function( response ) {
677 var errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage ),
678 $installLink = $( '[data-slug="' + response.slug + '"]' ),
679 pluginName = $installLink.data( 'name' );
681 if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
685 if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
689 wp.updates.addAdminNotice( {
690 id: response.errorCode,
691 className: 'notice-error is-dismissible',
692 message: errorMessage
696 .removeClass( 'updating-message' )
697 .text( wp.updates.l10n.installNow )
698 .attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', pluginName ) );
700 wp.a11y.speak( errorMessage, 'assertive' );
702 $document.trigger( 'wp-importer-install-error', response );
706 * Sends an Ajax request to the server to delete a plugin.
710 * @param {object} args Arguments.
711 * @param {string} args.plugin Basename of the plugin to be deleted.
712 * @param {string} args.slug Slug of the plugin to be deleted.
713 * @param {deletePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.deletePluginSuccess
714 * @param {deletePluginError=} args.error Optional. Error callback. Default: wp.updates.deletePluginError
715 * @return {$.promise} A jQuery promise that represents the request,
716 * decorated with an abort() method.
718 wp.updates.deletePlugin = function( args ) {
719 var $link = $( '[data-plugin="' + args.plugin + '"]' ).find( '.row-actions a.delete' );
722 success: wp.updates.deletePluginSuccess,
723 error: wp.updates.deletePluginError
726 if ( $link.html() !== wp.updates.l10n.deleting ) {
728 .data( 'originaltext', $link.html() )
729 .text( wp.updates.l10n.deleting );
732 wp.a11y.speak( wp.updates.l10n.deleting, 'polite' );
734 $document.trigger( 'wp-plugin-deleting', args );
736 return wp.updates.ajax( 'delete-plugin', args );
740 * Updates the UI appropriately after a successful plugin deletion.
744 * @typedef {object} deletePluginSuccess
745 * @param {object} response Response from the server.
746 * @param {string} response.slug Slug of the plugin that was deleted.
747 * @param {string} response.plugin Base name of the plugin that was deleted.
748 * @param {string} response.pluginName Name of the plugin that was deleted.
750 wp.updates.deletePluginSuccess = function( response ) {
752 // Removes the plugin and updates rows.
753 $( '[data-plugin="' + response.plugin + '"]' ).css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
754 var $form = $( '#bulk-action-form' ),
755 $views = $( '.subsubsub' ),
756 $pluginRow = $( this ),
757 columnCount = $form.find( 'thead th:not(.hidden), thead td' ).length,
758 pluginDeletedRow = wp.template( 'item-deleted-row' ),
759 /** @type {object} plugins Base names of plugins in their different states. */
760 plugins = settings.plugins;
762 // Add a success message after deleting a plugin.
763 if ( ! $pluginRow.hasClass( 'plugin-update-tr' ) ) {
767 plugin: response.plugin,
768 colspan: columnCount,
769 name: response.pluginName
776 // Remove plugin from update count.
777 if ( -1 !== _.indexOf( plugins.upgrade, response.plugin ) ) {
778 plugins.upgrade = _.without( plugins.upgrade, response.plugin );
779 wp.updates.decrementCount( 'plugin' );
782 // Remove from views.
783 if ( -1 !== _.indexOf( plugins.inactive, response.plugin ) ) {
784 plugins.inactive = _.without( plugins.inactive, response.plugin );
785 if ( plugins.inactive.length ) {
786 $views.find( '.inactive .count' ).text( '(' + plugins.inactive.length + ')' );
788 $views.find( '.inactive' ).remove();
792 if ( -1 !== _.indexOf( plugins.active, response.plugin ) ) {
793 plugins.active = _.without( plugins.active, response.plugin );
794 if ( plugins.active.length ) {
795 $views.find( '.active .count' ).text( '(' + plugins.active.length + ')' );
797 $views.find( '.active' ).remove();
801 if ( -1 !== _.indexOf( plugins.recently_activated, response.plugin ) ) {
802 plugins.recently_activated = _.without( plugins.recently_activated, response.plugin );
803 if ( plugins.recently_activated.length ) {
804 $views.find( '.recently_activated .count' ).text( '(' + plugins.recently_activated.length + ')' );
806 $views.find( '.recently_activated' ).remove();
810 plugins.all = _.without( plugins.all, response.plugin );
812 if ( plugins.all.length ) {
813 $views.find( '.all .count' ).text( '(' + plugins.all.length + ')' );
815 $form.find( '.tablenav' ).css( { visibility: 'hidden' } );
816 $views.find( '.all' ).remove();
818 if ( ! $form.find( 'tr.no-items' ).length ) {
819 $form.find( '#the-list' ).append( '<tr class="no-items"><td class="colspanchange" colspan="' + columnCount + '">' + wp.updates.l10n.noPlugins + '</td></tr>' );
824 wp.a11y.speak( wp.updates.l10n.deleted, 'polite' );
826 $document.trigger( 'wp-plugin-delete-success', response );
830 * Updates the UI appropriately after a failed plugin deletion.
834 * @typedef {object} deletePluginError
835 * @param {object} response Response from the server.
836 * @param {string} response.slug Slug of the plugin to be deleted.
837 * @param {string} response.plugin Base name of the plugin to be deleted
838 * @param {string=} response.pluginName Optional. Name of the plugin to be deleted.
839 * @param {string} response.errorCode Error code for the error that occurred.
840 * @param {string} response.errorMessage The error that occurred.
842 wp.updates.deletePluginError = function( response ) {
843 var $plugin, $pluginUpdateRow,
844 pluginUpdateRow = wp.template( 'item-update-row' ),
845 noticeContent = wp.updates.adminNotice( {
846 className: 'update-message notice-error notice-alt',
847 message: response.errorMessage
850 if ( response.plugin ) {
851 $plugin = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' );
852 $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' );
854 $plugin = $( 'tr.inactive[data-slug="' + response.slug + '"]' );
855 $pluginUpdateRow = $plugin.siblings( '[data-slug="' + response.slug + '"]' );
858 if ( ! wp.updates.isValidResponse( response, 'delete' ) ) {
862 if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) {
866 // Add a plugin update row if it doesn't exist yet.
867 if ( ! $pluginUpdateRow.length ) {
868 $plugin.addClass( 'update' ).after(
871 plugin: response.plugin || response.slug,
872 colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
873 content: noticeContent
878 // Remove previous error messages, if any.
879 $pluginUpdateRow.find( '.notice-error' ).remove();
881 $pluginUpdateRow.find( '.plugin-update' ).append( noticeContent );
884 $document.trigger( 'wp-plugin-delete-error', response );
888 * Sends an Ajax request to the server to update a theme.
892 * @param {object} args Arguments.
893 * @param {string} args.slug Theme stylesheet.
894 * @param {updateThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.updateThemeSuccess
895 * @param {updateThemeError=} args.error Optional. Error callback. Default: wp.updates.updateThemeError
896 * @return {$.promise} A jQuery promise that represents the request,
897 * decorated with an abort() method.
899 wp.updates.updateTheme = function( args ) {
903 success: wp.updates.updateThemeSuccess,
904 error: wp.updates.updateThemeError
907 if ( 'themes-network' === pagenow ) {
908 $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
911 $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' );
913 $notice.find( 'h3' ).remove();
915 $notice = $notice.add( $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ) );
916 $notice = $notice.addClass( 'updating-message' ).find( 'p' );
919 if ( $notice.html() !== wp.updates.l10n.updating ) {
920 $notice.data( 'originaltext', $notice.html() );
923 wp.a11y.speak( wp.updates.l10n.updatingMsg, 'polite' );
924 $notice.text( wp.updates.l10n.updating );
926 $document.trigger( 'wp-theme-updating', args );
928 return wp.updates.ajax( 'update-theme', args );
932 * Updates the UI appropriately after a successful theme update.
936 * @typedef {object} updateThemeSuccess
937 * @param {object} response
938 * @param {string} response.slug Slug of the theme to be updated.
939 * @param {object} response.theme Updated theme.
940 * @param {string} response.oldVersion Old version of the theme.
941 * @param {string} response.newVersion New version of the theme.
943 wp.updates.updateThemeSuccess = function( response ) {
944 var isModalOpen = $( 'body.modal-open' ).length,
945 $theme = $( '[data-slug="' + response.slug + '"]' ),
947 className: 'updated-message notice-success notice-alt',
948 message: wp.updates.l10n.updated
952 if ( 'themes-network' === pagenow ) {
953 $notice = $theme.find( '.update-message' );
955 // Update the version number in the row.
956 newText = $theme.find( '.theme-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
957 $theme.find( '.theme-version-author-uri' ).html( newText );
959 $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) );
961 // Focus on Customize button after updating.
963 $( '.load-customize:visible' ).focus();
965 $theme.find( '.load-customize' ).focus();
969 wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) );
970 wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' );
972 wp.updates.decrementCount( 'theme' );
974 $document.trigger( 'wp-theme-update-success', response );
976 // Show updated message after modal re-rendered.
978 $( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) );
983 * Updates the UI appropriately after a failed theme update.
987 * @typedef {object} updateThemeError
988 * @param {object} response Response from the server.
989 * @param {string} response.slug Slug of the theme to be updated.
990 * @param {string} response.errorCode Error code for the error that occurred.
991 * @param {string} response.errorMessage The error that occurred.
993 wp.updates.updateThemeError = function( response ) {
994 var $theme = $( '[data-slug="' + response.slug + '"]' ),
995 errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage ),
998 if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
1002 if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) {
1006 if ( 'themes-network' === pagenow ) {
1007 $notice = $theme.find( '.update-message ' );
1009 $notice = $( '.theme-info .notice' ).add( $theme.find( '.notice' ) );
1011 $( 'body.modal-open' ).length ? $( '.load-customize:visible' ).focus() : $theme.find( '.load-customize' ).focus();
1014 wp.updates.addAdminNotice( {
1016 className: 'update-message notice-error notice-alt is-dismissible',
1017 message: errorMessage
1020 wp.a11y.speak( errorMessage, 'polite' );
1022 $document.trigger( 'wp-theme-update-error', response );
1026 * Sends an Ajax request to the server to install a theme.
1030 * @param {object} args
1031 * @param {string} args.slug Theme stylesheet.
1032 * @param {installThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.installThemeSuccess
1033 * @param {installThemeError=} args.error Optional. Error callback. Default: wp.updates.installThemeError
1034 * @return {$.promise} A jQuery promise that represents the request,
1035 * decorated with an abort() method.
1037 wp.updates.installTheme = function( args ) {
1038 var $message = $( '.theme-install[data-slug="' + args.slug + '"]' );
1041 success: wp.updates.installThemeSuccess,
1042 error: wp.updates.installThemeError
1045 $message.addClass( 'updating-message' );
1046 $message.parents( '.theme' ).addClass( 'focus' );
1047 if ( $message.html() !== wp.updates.l10n.installing ) {
1048 $message.data( 'originaltext', $message.html() );
1052 .text( wp.updates.l10n.installing )
1053 .attr( 'aria-label', wp.updates.l10n.themeInstallingLabel.replace( '%s', $message.data( 'name' ) ) );
1054 wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' );
1056 // Remove previous error messages, if any.
1057 $( '.install-theme-info, [data-slug="' + args.slug + '"]' ).removeClass( 'theme-install-failed' ).find( '.notice.notice-error' ).remove();
1059 $document.trigger( 'wp-theme-installing', args );
1061 return wp.updates.ajax( 'install-theme', args );
1065 * Updates the UI appropriately after a successful theme install.
1069 * @typedef {object} installThemeSuccess
1070 * @param {object} response Response from the server.
1071 * @param {string} response.slug Slug of the theme to be installed.
1072 * @param {string} response.customizeUrl URL to the Customizer for the just installed theme.
1073 * @param {string} response.activateUrl URL to activate the just installed theme.
1075 wp.updates.installThemeSuccess = function( response ) {
1076 var $card = $( '.wp-full-overlay-header, [data-slug=' + response.slug + ']' ),
1079 $document.trigger( 'wp-theme-install-success', response );
1081 $message = $card.find( '.button-primary' )
1082 .removeClass( 'updating-message' )
1083 .addClass( 'updated-message disabled' )
1084 .attr( 'aria-label', wp.updates.l10n.themeInstalledLabel.replace( '%s', response.themeName ) )
1085 .text( wp.updates.l10n.installed );
1087 wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
1089 setTimeout( function() {
1091 if ( response.activateUrl ) {
1093 // Transform the 'Install' button into an 'Activate' button.
1095 .attr( 'href', response.activateUrl )
1096 .removeClass( 'theme-install updated-message disabled' )
1097 .addClass( 'activate' )
1098 .attr( 'aria-label', wp.updates.l10n.activateThemeLabel.replace( '%s', response.themeName ) )
1099 .text( wp.updates.l10n.activateTheme );
1102 if ( response.customizeUrl ) {
1104 // Transform the 'Preview' button into a 'Live Preview' button.
1105 $message.siblings( '.preview' ).replaceWith( function () {
1107 .attr( 'href', response.customizeUrl )
1108 .addClass( 'button button-secondary load-customize' )
1109 .text( wp.updates.l10n.livePreview );
1116 * Updates the UI appropriately after a failed theme install.
1120 * @typedef {object} installThemeError
1121 * @param {object} response Response from the server.
1122 * @param {string} response.slug Slug of the theme to be installed.
1123 * @param {string} response.errorCode Error code for the error that occurred.
1124 * @param {string} response.errorMessage The error that occurred.
1126 wp.updates.installThemeError = function( response ) {
1128 errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage ),
1129 $message = wp.updates.adminNotice( {
1130 className: 'update-message notice-error notice-alt',
1131 message: errorMessage
1134 if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
1138 if ( wp.updates.maybeHandleCredentialError( response, 'install-theme' ) ) {
1142 if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) {
1143 $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
1144 $card = $( '.install-theme-info' ).prepend( $message );
1146 $card = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message );
1147 $button = $card.find( '.theme-install' );
1151 .removeClass( 'updating-message' )
1152 .attr( 'aria-label', wp.updates.l10n.themeInstallFailedLabel.replace( '%s', $button.data( 'name' ) ) )
1153 .text( wp.updates.l10n.installFailedShort );
1155 wp.a11y.speak( errorMessage, 'assertive' );
1157 $document.trigger( 'wp-theme-install-error', response );
1161 * Sends an Ajax request to the server to install a theme.
1165 * @param {object} args
1166 * @param {string} args.slug Theme stylesheet.
1167 * @param {deleteThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.deleteThemeSuccess
1168 * @param {deleteThemeError=} args.error Optional. Error callback. Default: wp.updates.deleteThemeError
1169 * @return {$.promise} A jQuery promise that represents the request,
1170 * decorated with an abort() method.
1172 wp.updates.deleteTheme = function( args ) {
1175 if ( 'themes' === pagenow ) {
1176 $button = $( '.theme-actions .delete-theme' );
1177 } else if ( 'themes-network' === pagenow ) {
1178 $button = $( '[data-slug="' + args.slug + '"]' ).find( '.row-actions a.delete' );
1182 success: wp.updates.deleteThemeSuccess,
1183 error: wp.updates.deleteThemeError
1186 if ( $button && $button.html() !== wp.updates.l10n.deleting ) {
1188 .data( 'originaltext', $button.html() )
1189 .text( wp.updates.l10n.deleting );
1192 wp.a11y.speak( wp.updates.l10n.deleting, 'polite' );
1194 // Remove previous error messages, if any.
1195 $( '.theme-info .update-message' ).remove();
1197 $document.trigger( 'wp-theme-deleting', args );
1199 return wp.updates.ajax( 'delete-theme', args );
1203 * Updates the UI appropriately after a successful theme deletion.
1207 * @typedef {object} deleteThemeSuccess
1208 * @param {object} response Response from the server.
1209 * @param {string} response.slug Slug of the theme that was deleted.
1211 wp.updates.deleteThemeSuccess = function( response ) {
1212 var $themeRows = $( '[data-slug="' + response.slug + '"]' );
1214 if ( 'themes-network' === pagenow ) {
1216 // Removes the theme and updates rows.
1217 $themeRows.css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
1218 var $views = $( '.subsubsub' ),
1219 $themeRow = $( this ),
1220 totals = settings.totals,
1221 deletedRow = wp.template( 'item-deleted-row' );
1223 if ( ! $themeRow.hasClass( 'plugin-update-tr' ) ) {
1226 slug: response.slug,
1227 colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
1228 name: $themeRow.find( '.theme-title strong' ).text()
1235 // Remove theme from update count.
1236 if ( $themeRow.hasClass( 'update' ) ) {
1238 wp.updates.decrementCount( 'theme' );
1241 // Remove from views.
1242 if ( $themeRow.hasClass( 'inactive' ) ) {
1244 if ( totals.disabled ) {
1245 $views.find( '.disabled .count' ).text( '(' + totals.disabled + ')' );
1247 $views.find( '.disabled' ).remove();
1251 // There is always at least one theme available.
1252 $views.find( '.all .count' ).text( '(' + --totals.all + ')' );
1256 wp.a11y.speak( wp.updates.l10n.deleted, 'polite' );
1258 $document.trigger( 'wp-theme-delete-success', response );
1262 * Updates the UI appropriately after a failed theme deletion.
1266 * @typedef {object} deleteThemeError
1267 * @param {object} response Response from the server.
1268 * @param {string} response.slug Slug of the theme to be deleted.
1269 * @param {string} response.errorCode Error code for the error that occurred.
1270 * @param {string} response.errorMessage The error that occurred.
1272 wp.updates.deleteThemeError = function( response ) {
1273 var $themeRow = $( 'tr.inactive[data-slug="' + response.slug + '"]' ),
1274 $button = $( '.theme-actions .delete-theme' ),
1275 updateRow = wp.template( 'item-update-row' ),
1276 $updateRow = $themeRow.siblings( '#' + response.slug + '-update' ),
1277 errorMessage = wp.updates.l10n.deleteFailed.replace( '%s', response.errorMessage ),
1278 $message = wp.updates.adminNotice( {
1279 className: 'update-message notice-error notice-alt',
1280 message: errorMessage
1283 if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) {
1287 if ( 'themes-network' === pagenow ) {
1288 if ( ! $updateRow.length ) {
1289 $themeRow.addClass( 'update' ).after(
1291 slug: response.slug,
1292 colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
1297 // Remove previous error messages, if any.
1298 $updateRow.find( '.notice-error' ).remove();
1299 $updateRow.find( '.plugin-update' ).append( $message );
1302 $( '.theme-info .theme-description' ).before( $message );
1305 $button.html( $button.data( 'originaltext' ) );
1307 wp.a11y.speak( errorMessage, 'assertive' );
1309 $document.trigger( 'wp-theme-delete-error', response );
1313 * Adds the appropriate callback based on the type of action and the current page.
1318 * @param {object} data AJAX payload.
1319 * @param {string} action The type of request to perform.
1320 * @return {object} The AJAX payload with the appropriate callbacks.
1322 wp.updates._addCallbacks = function( data, action ) {
1323 if ( 'import' === pagenow && 'install-plugin' === action ) {
1324 data.success = wp.updates.installImporterSuccess;
1325 data.error = wp.updates.installImporterError;
1332 * Pulls available jobs from the queue and runs them.
1335 * @since 4.6.0 Can handle multiple job types.
1337 wp.updates.queueChecker = function() {
1340 if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) {
1344 job = wp.updates.queue.shift();
1346 // Handle a queue job.
1347 switch ( job.action ) {
1348 case 'install-plugin':
1349 wp.updates.installPlugin( job.data );
1352 case 'update-plugin':
1353 wp.updates.updatePlugin( job.data );
1356 case 'delete-plugin':
1357 wp.updates.deletePlugin( job.data );
1360 case 'install-theme':
1361 wp.updates.installTheme( job.data );
1364 case 'update-theme':
1365 wp.updates.updateTheme( job.data );
1368 case 'delete-theme':
1369 wp.updates.deleteTheme( job.data );
1378 * Requests the users filesystem credentials if they aren't already known.
1382 * @param {Event=} event Optional. Event interface.
1384 wp.updates.requestFilesystemCredentials = function( event ) {
1385 if ( false === wp.updates.filesystemCredentials.available ) {
1387 * After exiting the credentials request modal,
1388 * return the focus to the element triggering the request.
1390 if ( event && ! wp.updates.$elToReturnFocusToFromCredentialsModal ) {
1391 wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target );
1394 wp.updates.ajaxLocked = true;
1395 wp.updates.requestForCredentialsModalOpen();
1400 * Requests the users filesystem credentials if needed and there is no lock.
1404 * @param {Event=} event Optional. Event interface.
1406 wp.updates.maybeRequestFilesystemCredentials = function( event ) {
1407 if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
1408 wp.updates.requestFilesystemCredentials( event );
1413 * Keydown handler for the request for credentials modal.
1415 * Closes the modal when the escape key is pressed and
1416 * constrains keyboard navigation to inside the modal.
1420 * @param {Event} event Event interface.
1422 wp.updates.keydown = function( event ) {
1423 if ( 27 === event.keyCode ) {
1424 wp.updates.requestForCredentialsModalCancel();
1425 } else if ( 9 === event.keyCode ) {
1427 // #upgrade button must always be the last focus-able element in the dialog.
1428 if ( 'upgrade' === event.target.id && ! event.shiftKey ) {
1429 $( '#hostname' ).focus();
1431 event.preventDefault();
1432 } else if ( 'hostname' === event.target.id && event.shiftKey ) {
1433 $( '#upgrade' ).focus();
1435 event.preventDefault();
1441 * Opens the request for credentials modal.
1445 wp.updates.requestForCredentialsModalOpen = function() {
1446 var $modal = $( '#request-filesystem-credentials-dialog' );
1448 $( 'body' ).addClass( 'modal-open' );
1450 $modal.find( 'input:enabled:first' ).focus();
1451 $modal.on( 'keydown', wp.updates.keydown );
1455 * Closes the request for credentials modal.
1459 wp.updates.requestForCredentialsModalClose = function() {
1460 $( '#request-filesystem-credentials-dialog' ).hide();
1461 $( 'body' ).removeClass( 'modal-open' );
1463 if ( wp.updates.$elToReturnFocusToFromCredentialsModal ) {
1464 wp.updates.$elToReturnFocusToFromCredentialsModal.focus();
1469 * Takes care of the steps that need to happen when the modal is canceled out.
1472 * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions.
1474 wp.updates.requestForCredentialsModalCancel = function() {
1476 // Not ajaxLocked and no queue means we already have cleared things up.
1477 if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) {
1481 _.each( wp.updates.queue, function( job ) {
1482 $document.trigger( 'credential-modal-cancel', job );
1485 // Remove the lock, and clear the queue.
1486 wp.updates.ajaxLocked = false;
1487 wp.updates.queue = [];
1489 wp.updates.requestForCredentialsModalClose();
1493 * Displays an error message in the request for credentials form.
1497 * @param {string} message Error message.
1499 wp.updates.showErrorInCredentialsForm = function( message ) {
1500 var $modal = $( '#request-filesystem-credentials-form' );
1502 // Remove any existing error.
1503 $modal.find( '.notice' ).remove();
1504 $modal.find( '#request-filesystem-credentials-title' ).after( '<div class="notice notice-alt notice-error"><p>' + message + '</p></div>' );
1508 * Handles credential errors and runs events that need to happen in that case.
1512 * @param {object} response Ajax response.
1513 * @param {string} action The type of request to perform.
1515 wp.updates.credentialError = function( response, action ) {
1517 // Restore callbacks.
1518 response = wp.updates._addCallbacks( response, action );
1520 wp.updates.queue.unshift( {
1524 * Not cool that we're depending on response for this data.
1525 * This would feel more whole in a view all tied together.
1530 wp.updates.filesystemCredentials.available = false;
1531 wp.updates.showErrorInCredentialsForm( response.errorMessage );
1532 wp.updates.requestFilesystemCredentials();
1536 * Handles credentials errors if it could not connect to the filesystem.
1540 * @typedef {object} maybeHandleCredentialError
1541 * @param {object} response Response from the server.
1542 * @param {string} response.errorCode Error code for the error that occurred.
1543 * @param {string} response.errorMessage The error that occurred.
1544 * @param {string} action The type of request to perform.
1545 * @returns {boolean} Whether there is an error that needs to be handled or not.
1547 wp.updates.maybeHandleCredentialError = function( response, action ) {
1548 if ( wp.updates.shouldRequestFilesystemCredentials && response.errorCode && 'unable_to_connect_to_filesystem' === response.errorCode ) {
1549 wp.updates.credentialError( response, action );
1557 * Validates an AJAX response to ensure it's a proper object.
1559 * If the response deems to be invalid, an admin notice is being displayed.
1561 * @param {(object|string)} response Response from the server.
1562 * @param {function=} response.always Optional. Callback for when the Deferred is resolved or rejected.
1563 * @param {string=} response.statusText Optional. Status message corresponding to the status code.
1564 * @param {string=} response.responseText Optional. Request response as text.
1565 * @param {string} action Type of action the response is referring to. Can be 'delete',
1566 * 'update' or 'install'.
1568 wp.updates.isValidResponse = function( response, action ) {
1569 var error = wp.updates.l10n.unknownError,
1572 // Make sure the response is a valid data object and not a Promise object.
1573 if ( _.isObject( response ) && ! _.isFunction( response.always ) ) {
1577 if ( _.isString( response ) && '-1' === response ) {
1578 error = wp.updates.l10n.nonceError;
1579 } else if ( _.isString( response ) ) {
1581 } else if ( 'undefined' !== typeof response.readyState && 0 === response.readyState ) {
1582 error = wp.updates.l10n.connectionError;
1583 } else if ( _.isString( response.responseText ) && '' !== response.responseText ) {
1584 error = response.responseText;
1585 } else if ( _.isString( response.statusText ) ) {
1586 error = response.statusText;
1591 errorMessage = wp.updates.l10n.updateFailed;
1595 errorMessage = wp.updates.l10n.installFailed;
1599 errorMessage = wp.updates.l10n.deleteFailed;
1603 // Messages are escaped, remove HTML tags to make them more readable.
1604 error = error.replace( /<[\/a-z][^<>]*>/gi, '' );
1605 errorMessage = errorMessage.replace( '%s', error );
1607 // Add admin notice.
1608 wp.updates.addAdminNotice( {
1609 id: 'unknown_error',
1610 className: 'notice-error is-dismissible',
1611 message: _.escape( errorMessage )
1614 // Remove the lock, and clear the queue.
1615 wp.updates.ajaxLocked = false;
1616 wp.updates.queue = [];
1618 // Change buttons of all running updates.
1619 $( '.button.updating-message' )
1620 .removeClass( 'updating-message' )
1621 .removeAttr( 'aria-label' )
1622 .prop( 'disabled', true )
1623 .text( wp.updates.l10n.updateFailedShort );
1625 $( '.updating-message:not(.button):not(.thickbox)' )
1626 .removeClass( 'updating-message notice-warning' )
1627 .addClass( 'notice-error' )
1629 .removeAttr( 'aria-label' )
1630 .text( errorMessage );
1632 wp.a11y.speak( errorMessage, 'assertive' );
1638 * Potentially adds an AYS to a user attempting to leave the page.
1640 * If an update is on-going and a user attempts to leave the page,
1641 * opens an "Are you sure?" alert.
1645 wp.updates.beforeunload = function() {
1646 if ( wp.updates.ajaxLocked ) {
1647 return wp.updates.l10n.beforeunload;
1652 var $pluginFilter = $( '#plugin-filter' ),
1653 $bulkActionForm = $( '#bulk-action-form' ),
1654 $filesystemModal = $( '#request-filesystem-credentials-dialog' ),
1655 $pluginSearch = $( '.plugins-php .wp-filter-search' ),
1656 $pluginInstallSearch = $( '.plugin-install-php .wp-filter-search' );
1659 * Whether a user needs to submit filesystem credentials.
1661 * This is based on whether the form was output on the page server-side.
1663 * @see {wp_print_request_filesystem_credentials_modal() in PHP}
1665 wp.updates.shouldRequestFilesystemCredentials = $filesystemModal.length > 0;
1668 * File system credentials form submit noop-er / handler.
1672 $filesystemModal.on( 'submit', 'form', function( event ) {
1673 event.preventDefault();
1675 // Persist the credentials input by the user for the duration of the page load.
1676 wp.updates.filesystemCredentials.ftp.hostname = $( '#hostname' ).val();
1677 wp.updates.filesystemCredentials.ftp.username = $( '#username' ).val();
1678 wp.updates.filesystemCredentials.ftp.password = $( '#password' ).val();
1679 wp.updates.filesystemCredentials.ftp.connectionType = $( 'input[name="connection_type"]:checked' ).val();
1680 wp.updates.filesystemCredentials.ssh.publicKey = $( '#public_key' ).val();
1681 wp.updates.filesystemCredentials.ssh.privateKey = $( '#private_key' ).val();
1682 wp.updates.filesystemCredentials.available = true;
1684 // Unlock and invoke the queue.
1685 wp.updates.ajaxLocked = false;
1686 wp.updates.queueChecker();
1688 wp.updates.requestForCredentialsModalClose();
1692 * Closes the request credentials modal when clicking the 'Cancel' button or outside of the modal.
1696 $filesystemModal.on( 'click', '[data-js-action="close"], .notification-dialog-background', wp.updates.requestForCredentialsModalCancel );
1699 * Hide SSH fields when not selected.
1703 $filesystemModal.on( 'change', 'input[name="connection_type"]', function() {
1704 $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) );
1708 * Handles events after the credential modal was closed.
1712 * @param {Event} event Event interface.
1713 * @param {string} job The install/update.delete request.
1715 $document.on( 'credential-modal-cancel', function( event, job ) {
1716 var $updatingMessage = $( '.updating-message' ),
1717 $message, originalText;
1719 if ( 'import' === pagenow ) {
1720 $updatingMessage.removeClass( 'updating-message' );
1721 } else if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
1722 if ( 'update-plugin' === job.action ) {
1723 $message = $( 'tr[data-plugin="' + job.data.plugin + '"]' ).find( '.update-message' );
1724 } else if ( 'delete-plugin' === job.action ) {
1725 $message = $( '[data-plugin="' + job.data.plugin + '"]' ).find( '.row-actions a.delete' );
1727 } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) {
1728 if ( 'update-theme' === job.action ) {
1729 $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.update-message' );
1730 } else if ( 'delete-theme' === job.action && 'themes-network' === pagenow ) {
1731 $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.row-actions a.delete' );
1732 } else if ( 'delete-theme' === job.action && 'themes' === pagenow ) {
1733 $message = $( '.theme-actions .delete-theme' );
1736 $message = $updatingMessage;
1739 if ( $message && $message.hasClass( 'updating-message' ) ) {
1740 originalText = $message.data( 'originaltext' );
1742 if ( 'undefined' === typeof originalText ) {
1743 originalText = $( '<p>' ).html( $message.find( 'p' ).data( 'originaltext' ) );
1747 .removeClass( 'updating-message' )
1748 .html( originalText );
1750 if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
1751 if ( 'update-plugin' === job.action ) {
1752 $message.attr( 'aria-label', wp.updates.l10n.updateNowLabel.replace( '%s', $message.data( 'name' ) ) );
1753 } else if ( 'install-plugin' === job.action ) {
1754 $message.attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', $message.data( 'name' ) ) );
1759 wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
1763 * Click handler for plugin updates in List Table view.
1767 * @param {Event} event Event interface.
1769 $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) {
1770 var $message = $( event.target ),
1771 $pluginRow = $message.parents( 'tr' );
1773 event.preventDefault();
1775 if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
1779 wp.updates.maybeRequestFilesystemCredentials( event );
1781 // Return the user to the input box of the plugin's table row after closing the modal.
1782 wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' );
1783 wp.updates.updatePlugin( {
1784 plugin: $pluginRow.data( 'plugin' ),
1785 slug: $pluginRow.data( 'slug' )
1790 * Click handler for plugin updates in plugin install view.
1794 * @param {Event} event Event interface.
1796 $pluginFilter.on( 'click', '.update-now', function( event ) {
1797 var $button = $( event.target );
1798 event.preventDefault();
1800 if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
1804 wp.updates.maybeRequestFilesystemCredentials( event );
1806 wp.updates.updatePlugin( {
1807 plugin: $button.data( 'plugin' ),
1808 slug: $button.data( 'slug' )
1813 * Click handler for plugin installs in plugin install view.
1817 * @param {Event} event Event interface.
1819 $pluginFilter.on( 'click', '.install-now', function( event ) {
1820 var $button = $( event.target );
1821 event.preventDefault();
1823 if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
1827 if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
1828 wp.updates.requestFilesystemCredentials( event );
1830 $document.on( 'credential-modal-cancel', function() {
1831 var $message = $( '.install-now.updating-message' );
1834 .removeClass( 'updating-message' )
1835 .text( wp.updates.l10n.installNow );
1837 wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
1841 wp.updates.installPlugin( {
1842 slug: $button.data( 'slug' )
1847 * Click handler for importer plugins installs in the Import screen.
1851 * @param {Event} event Event interface.
1853 $document.on( 'click', '.importer-item .install-now', function( event ) {
1854 var $button = $( event.target ),
1855 pluginName = $( this ).data( 'name' );
1857 event.preventDefault();
1859 if ( $button.hasClass( 'updating-message' ) ) {
1863 if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
1864 wp.updates.requestFilesystemCredentials( event );
1866 $document.on( 'credential-modal-cancel', function() {
1869 .removeClass( 'updating-message' )
1870 .text( wp.updates.l10n.installNow )
1871 .attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', pluginName ) );
1873 wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
1877 wp.updates.installPlugin( {
1878 slug: $button.data( 'slug' ),
1879 success: wp.updates.installImporterSuccess,
1880 error: wp.updates.installImporterError
1885 * Click handler for plugin deletions.
1889 * @param {Event} event Event interface.
1891 $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) {
1892 var $pluginRow = $( event.target ).parents( 'tr' );
1894 event.preventDefault();
1896 if ( ! window.confirm( wp.updates.l10n.aysDeleteUninstall.replace( '%s', $pluginRow.find( '.plugin-title strong' ).text() ) ) ) {
1900 wp.updates.maybeRequestFilesystemCredentials( event );
1902 wp.updates.deletePlugin( {
1903 plugin: $pluginRow.data( 'plugin' ),
1904 slug: $pluginRow.data( 'slug' )
1910 * Click handler for theme updates.
1914 * @param {Event} event Event interface.
1916 $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) {
1917 var $message = $( event.target ),
1918 $themeRow = $message.parents( 'tr' );
1920 event.preventDefault();
1922 if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
1926 wp.updates.maybeRequestFilesystemCredentials( event );
1928 // Return the user to the input box of the theme's table row after closing the modal.
1929 wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' );
1930 wp.updates.updateTheme( {
1931 slug: $themeRow.data( 'slug' )
1936 * Click handler for theme deletions.
1940 * @param {Event} event Event interface.
1942 $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) {
1943 var $themeRow = $( event.target ).parents( 'tr' );
1945 event.preventDefault();
1947 if ( ! window.confirm( wp.updates.l10n.aysDelete.replace( '%s', $themeRow.find( '.theme-title strong' ).text() ) ) ) {
1951 wp.updates.maybeRequestFilesystemCredentials( event );
1953 wp.updates.deleteTheme( {
1954 slug: $themeRow.data( 'slug' )
1959 * Bulk action handler for plugins and themes.
1961 * Handles both deletions and updates.
1965 * @param {Event} event Event interface.
1967 $bulkActionForm.on( 'click', '[type="submit"]', function( event ) {
1968 var bulkAction = $( event.target ).siblings( 'select' ).val(),
1969 itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ),
1975 // Determine which type of item we're dealing with.
1976 switch ( pagenow ) {
1978 case 'plugins-network':
1982 case 'themes-network':
1990 // Bail if there were no items selected.
1991 if ( ! itemsSelected.length ) {
1992 event.preventDefault();
1993 $( 'html, body' ).animate( { scrollTop: 0 } );
1995 return wp.updates.addAdminNotice( {
1996 id: 'no-items-selected',
1997 className: 'notice-error is-dismissible',
1998 message: wp.updates.l10n.noItemsSelected
2002 // Determine the type of request we're dealing with.
2003 switch ( bulkAction ) {
2004 case 'update-selected':
2005 action = bulkAction.replace( 'selected', type );
2008 case 'delete-selected':
2009 if ( ! window.confirm( 'plugin' === type ? wp.updates.l10n.aysBulkDelete : wp.updates.l10n.aysBulkDeleteThemes ) ) {
2010 event.preventDefault();
2014 action = bulkAction.replace( 'selected', type );
2021 wp.updates.maybeRequestFilesystemCredentials( event );
2023 event.preventDefault();
2025 // Un-check the bulk checkboxes.
2026 $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false );
2028 $document.trigger( 'wp-' + type + '-bulk-' + bulkAction, itemsSelected );
2030 // Find all the checkboxes which have been checked.
2031 itemsSelected.each( function( index, element ) {
2032 var $checkbox = $( element ),
2033 $itemRow = $checkbox.parents( 'tr' );
2035 // Only add update-able items to the update queue.
2036 if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) {
2038 // Un-check the box.
2039 $checkbox.prop( 'checked', false );
2043 // Add it to the queue.
2044 wp.updates.queue.push( {
2047 plugin: $itemRow.data( 'plugin' ),
2048 slug: $itemRow.data( 'slug' )
2053 // Display bulk notification for updates of any kind.
2054 $document.on( 'wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error', function( event, response ) {
2055 var $itemRow = $( '[data-slug="' + response.slug + '"]' ),
2056 $bulkActionNotice, itemName;
2058 if ( 'wp-' + response.update + '-update-success' === event.type ) {
2061 itemName = response.pluginName ? response.pluginName : $itemRow.find( '.column-primary strong' ).text();
2064 errorMessages.push( itemName + ': ' + response.errorMessage );
2067 $itemRow.find( 'input[name="checked[]"]:checked' ).prop( 'checked', false );
2069 wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' );
2071 wp.updates.addAdminNotice( {
2072 id: 'bulk-action-notice',
2073 className: 'bulk-action-notice',
2076 errorMessages: errorMessages,
2077 type: response.update
2080 $bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() {
2081 // $( this ) is the clicked button, no need to get it again.
2083 .toggleClass( 'bulk-action-errors-collapsed' )
2084 .attr( 'aria-expanded', ! $( this ).hasClass( 'bulk-action-errors-collapsed' ) );
2085 // Show the errors list.
2086 $bulkActionNotice.find( '.bulk-action-errors' ).toggleClass( 'hidden' );
2089 if ( error > 0 && ! wp.updates.queue.length ) {
2090 $( 'html, body' ).animate( { scrollTop: 0 } );
2094 // Reset admin notice template after #bulk-action-notice was added.
2095 $document.on( 'wp-updates-notice-added', function() {
2096 wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
2099 // Check the queue, now that the event handlers have been added.
2100 wp.updates.queueChecker();
2103 if ( $pluginInstallSearch.length ) {
2104 $pluginInstallSearch.attr( 'aria-describedby', 'live-search-desc' );
2108 * Handles changes to the plugin search box on the new-plugin page,
2109 * searching the repository dynamically.
2113 $pluginInstallSearch.on( 'keyup input', _.debounce( function( event, eventtype ) {
2114 var $searchTab = $( '.plugin-install-search' ), data, searchLocation;
2117 _ajax_nonce: wp.updates.ajaxNonce,
2118 s: event.target.value,
2120 type: $( '#typeselector' ).val(),
2123 searchLocation = location.href.split( '?' )[ 0 ] + '?' + $.param( _.omit( data, [ '_ajax_nonce', 'pagenow' ] ) );
2126 if ( 'keyup' === event.type && 27 === event.which ) {
2127 event.target.value = '';
2130 if ( wp.updates.searchTerm === data.s && 'typechange' !== eventtype ) {
2133 $pluginFilter.empty();
2134 wp.updates.searchTerm = data.s;
2137 if ( window.history && window.history.replaceState ) {
2138 window.history.replaceState( null, '', searchLocation );
2141 if ( ! $searchTab.length ) {
2142 $searchTab = $( '<li class="plugin-install-search" />' )
2143 .append( $( '<a />', {
2145 'href': searchLocation,
2146 'text': wp.updates.l10n.searchResultsLabel
2149 $( '.wp-filter .filter-links .current' )
2150 .removeClass( 'current' )
2151 .parents( '.filter-links' )
2152 .prepend( $searchTab );
2154 $pluginFilter.prev( 'p' ).remove();
2155 $( '.plugins-popular-tags-wrapper' ).remove();
2158 if ( 'undefined' !== typeof wp.updates.searchRequest ) {
2159 wp.updates.searchRequest.abort();
2161 $( 'body' ).addClass( 'loading-content' );
2163 wp.updates.searchRequest = wp.ajax.post( 'search-install-plugins', data ).done( function( response ) {
2164 $( 'body' ).removeClass( 'loading-content' );
2165 $pluginFilter.append( response.items );
2166 delete wp.updates.searchRequest;
2168 if ( 0 === response.count ) {
2169 wp.a11y.speak( wp.updates.l10n.noPluginsFound );
2171 wp.a11y.speak( wp.updates.l10n.pluginsFound.replace( '%d', response.count ) );
2176 if ( $pluginSearch.length ) {
2177 $pluginSearch.attr( 'aria-describedby', 'live-search-desc' );
2181 * Handles changes to the plugin search box on the Installed Plugins screen,
2182 * searching the plugin list dynamically.
2186 $pluginSearch.on( 'keyup input', _.debounce( function( event ) {
2188 _ajax_nonce: wp.updates.ajaxNonce,
2189 s: event.target.value,
2194 if ( 'keyup' === event.type && 27 === event.which ) {
2195 event.target.value = '';
2198 if ( wp.updates.searchTerm === data.s ) {
2201 wp.updates.searchTerm = data.s;
2204 if ( window.history && window.history.replaceState ) {
2205 window.history.replaceState( null, '', location.href.split( '?' )[ 0 ] + '?s=' + data.s );
2208 if ( 'undefined' !== typeof wp.updates.searchRequest ) {
2209 wp.updates.searchRequest.abort();
2212 $bulkActionForm.empty();
2213 $( 'body' ).addClass( 'loading-content' );
2215 wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) {
2217 // Can we just ditch this whole subtitle business?
2218 var $subTitle = $( '<span />' ).addClass( 'subtitle' ).html( wp.updates.l10n.searchResults.replace( '%s', _.escape( data.s ) ) ),
2219 $oldSubTitle = $( '.wrap .subtitle' );
2221 if ( ! data.s.length ) {
2222 $oldSubTitle.remove();
2223 } else if ( $oldSubTitle.length ) {
2224 $oldSubTitle.replaceWith( $subTitle );
2226 $( '.wrap h1' ).append( $subTitle );
2229 $( 'body' ).removeClass( 'loading-content' );
2230 $bulkActionForm.append( response.items );
2231 delete wp.updates.searchRequest;
2233 if ( 0 === response.count ) {
2234 wp.a11y.speak( wp.updates.l10n.noPluginsFound );
2236 wp.a11y.speak( wp.updates.l10n.pluginsFound.replace( '%d', response.count ) );
2242 * Trigger a search event when the search form gets submitted.
2246 $document.on( 'submit', '.search-plugins', function( event ) {
2247 event.preventDefault();
2249 $( 'input.wp-filter-search' ).trigger( 'input' );
2253 * Trigger a search event when the search type gets changed.
2257 $( '#typeselector' ).on( 'change', function() {
2258 var $search = $( 'input[name="s"]' );
2260 if ( $search.val().length ) {
2261 $search.trigger( 'input', 'typechange' );
2266 * Click handler for updating a plugin from the details modal on `plugin-install.php`.
2270 * @param {Event} event Event interface.
2272 $( '#plugin_update_from_iframe' ).on( 'click', function( event ) {
2273 var target = window.parent === window ? null : window.parent,
2276 $.support.postMessage = !! window.postMessage;
2278 if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'update-core.php' ) ) {
2282 event.preventDefault();
2285 action: 'update-plugin',
2287 plugin: $( this ).data( 'plugin' ),
2288 slug: $( this ).data( 'slug' )
2292 target.postMessage( JSON.stringify( update ), window.location.origin );
2296 * Click handler for installing a plugin from the details modal on `plugin-install.php`.
2300 * @param {Event} event Event interface.
2302 $( '#plugin_install_from_iframe' ).on( 'click', function( event ) {
2303 var target = window.parent === window ? null : window.parent,
2306 $.support.postMessage = !! window.postMessage;
2308 if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'index.php' ) ) {
2312 event.preventDefault();
2315 action: 'install-plugin',
2317 slug: $( this ).data( 'slug' )
2321 target.postMessage( JSON.stringify( install ), window.location.origin );
2325 * Handles postMessage events.
2328 * @since 4.6.0 Switched `update-plugin` action to use the queue.
2330 * @param {Event} event Event interface.
2332 $( window ).on( 'message', function( event ) {
2333 var originalEvent = event.originalEvent,
2334 expectedOrigin = document.location.protocol + '//' + document.location.hostname,
2337 if ( originalEvent.origin !== expectedOrigin ) {
2342 message = $.parseJSON( originalEvent.data );
2347 if ( 'undefined' === typeof message.action ) {
2351 switch ( message.action ) {
2353 // Called from `wp-admin/includes/class-wp-upgrader-skins.php`.
2354 case 'decrementUpdateCount':
2355 /** @property {string} message.upgradeType */
2356 wp.updates.decrementCount( message.upgradeType );
2359 case 'install-plugin':
2360 case 'update-plugin':
2361 /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
2365 message.data = wp.updates._addCallbacks( message.data, message.action );
2367 wp.updates.queue.push( message );
2368 wp.updates.queueChecker();
2374 * Adds a callback to display a warning before leaving the page.
2378 $( window ).on( 'beforeunload', wp.updates.beforeunload );
2380 })( jQuery, window.wp, _.extend( window._wpUpdatesSettings, window._wpUpdatesItemCounts || {} ) );