400842ebd462426846e0063dd991c6a309f36d66
[autoinstalls/wordpress.git] / wp-admin / js / updates.js
1 /**
2  * Functions for ajaxified updates, deletions and installs inside the WordPress admin.
3  *
4  * @version 4.2.0
5  *
6  * @package WordPress
7  * @subpackage Administration
8  */
9
10 /* global pagenow */
11
12 /**
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.
30  */
31 (function( $, wp, settings ) {
32         var $document = $( document );
33
34         wp = wp || {};
35
36         /**
37          * The WP Updates object.
38          *
39          * @since 4.2.0
40          *
41          * @type {object}
42          */
43         wp.updates = {};
44
45         /**
46          * User nonce for ajax calls.
47          *
48          * @since 4.2.0
49          *
50          * @type {string}
51          */
52         wp.updates.ajaxNonce = settings.ajax_nonce;
53
54         /**
55          * Localized strings.
56          *
57          * @since 4.2.0
58          *
59          * @type {object}
60          */
61         wp.updates.l10n = settings.l10n;
62
63         /**
64          * Current search term.
65          *
66          * @since 4.6.0
67          *
68          * @type {string}
69          */
70         wp.updates.searchTerm = '';
71
72         /**
73          * Whether filesystem credentials need to be requested from the user.
74          *
75          * @since 4.2.0
76          *
77          * @type {bool}
78          */
79         wp.updates.shouldRequestFilesystemCredentials = false;
80
81         /**
82          * Filesystem credentials to be packaged along with the request.
83          *
84          * @since 4.2.0
85          * @since 4.6.0 Added `available` property to indicate whether credentials have been provided.
86          *
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.
98          *                                                         Default 'false'.
99          */
100         wp.updates.filesystemCredentials = {
101                 ftp:       {
102                         host:           '',
103                         username:       '',
104                         password:       '',
105                         connectionType: ''
106                 },
107                 ssh:       {
108                         publicKey:  '',
109                         privateKey: ''
110                 },
111                 available: false
112         };
113
114         /**
115          * Whether we're waiting for an Ajax request to complete.
116          *
117          * @since 4.2.0
118          * @since 4.6.0 More accurately named `ajaxLocked`.
119          *
120          * @type {bool}
121          */
122         wp.updates.ajaxLocked = false;
123
124         /**
125          * Admin notice template.
126          *
127          * @since 4.6.0
128          *
129          * @type {function} A function that lazily-compiles the template requested.
130          */
131         wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
132
133         /**
134          * Update queue.
135          *
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.
138          *
139          * @since 4.2.0
140          * @since 4.6.0 More accurately named `queue`.
141          *
142          * @type {Array.object}
143          */
144         wp.updates.queue = [];
145
146         /**
147          * Holds a jQuery reference to return focus to when exiting the request credentials modal.
148          *
149          * @since 4.2.0
150          *
151          * @type {jQuery}
152          */
153         wp.updates.$elToReturnFocusToFromCredentialsModal = undefined;
154
155         /**
156          * Adds or updates an admin notice.
157          *
158          * @since 4.6.0
159          *
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.
168          *
169          */
170         wp.updates.addAdminNotice = function( data ) {
171                 var $notice = $( data.selector ), $adminNotice;
172
173                 delete data.selector;
174                 $adminNotice = wp.updates.adminNotice( data );
175
176                 // Check if this admin notice already exists.
177                 if ( ! $notice.length ) {
178                         $notice = $( '#' + data.id );
179                 }
180
181                 if ( $notice.length ) {
182                         $notice.replaceWith( $adminNotice );
183                 } else {
184                         $( '.wrap' ).find( '> h1' ).after( $adminNotice );
185                 }
186
187                 $document.trigger( 'wp-updates-notice-added' );
188         };
189
190         /**
191          * Handles Ajax requests to WordPress.
192          *
193          * @since 4.6.0
194          *
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.
199          */
200         wp.updates.ajax = function( action, data ) {
201                 var options = {};
202
203                 if ( wp.updates.ajaxLocked ) {
204                         wp.updates.queue.push( {
205                                 action: action,
206                                 data:   data
207                         } );
208
209                         // Return a Deferred object so callbacks can always be registered.
210                         return $.Deferred();
211                 }
212
213                 wp.updates.ajaxLocked = true;
214
215                 if ( data.success ) {
216                         options.success = data.success;
217                         delete data.success;
218                 }
219
220                 if ( data.error ) {
221                         options.error = data.error;
222                         delete data.error;
223                 }
224
225                 options.data = _.extend( data, {
226                         action:          action,
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
234                 } );
235
236                 return wp.ajax.send( options ).always( wp.updates.ajaxAlways );
237         };
238
239         /**
240          * Actions performed after every Ajax request.
241          *
242          * @since 4.6.0
243          *
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.
247          */
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();
252                 }
253
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() );
257                         } );
258                 }
259         };
260
261         /**
262          * Refreshes update counts everywhere on the screen.
263          *
264          * @since 4.7.0
265          */
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' ),
271                         itemCount;
272
273                 $adminBarUpdates.find( '.ab-item' ).removeAttr( 'title' );
274                 $adminBarUpdates.find( '.ab-label' ).text( settings.totals.counts.total );
275
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();
279                 }
280
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 );
284                 } );
285                 if ( settings.totals.counts.total > 0 ) {
286                         $dashboardNavMenuUpdateCount.find( '.update-count' ).text( settings.totals.counts.total );
287                 } else {
288                         $dashboardNavMenuUpdateCount.remove();
289                 }
290
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 );
294                 } );
295                 if ( settings.totals.counts.total > 0 ) {
296                         $pluginsNavMenuUpdateCount.find( '.plugin-count' ).text( settings.totals.counts.plugins );
297                 } else {
298                         $pluginsNavMenuUpdateCount.remove();
299                 }
300
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 );
304                 } );
305                 if ( settings.totals.counts.total > 0 ) {
306                         $appearanceNavMenuUpdateCount.find( '.theme-count' ).text( settings.totals.counts.themes );
307                 } else {
308                         $appearanceNavMenuUpdateCount.remove();
309                 }
310
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;
316                 }
317
318                 if ( itemCount > 0 ) {
319                         $( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' );
320                 } else {
321                         $( '.subsubsub .upgrade' ).remove();
322                 }
323         };
324
325         /**
326          * Decrements the update counts throughout the various menus.
327          *
328          * This includes the toolbar, the "Updates" menu item and the menu items
329          * for plugins and themes.
330          *
331          * @since 3.9.0
332          *
333          * @param {string} type The type of item that was updated or deleted.
334          *                      Can be 'plugin', 'theme'.
335          */
336         wp.updates.decrementCount = function( type ) {
337                 settings.totals.counts.total = Math.max( --settings.totals.counts.total, 0 );
338
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 );
343                 }
344
345                 wp.updates.refreshCount( type );
346         };
347
348         /**
349          * Sends an Ajax request to the server to update a plugin.
350          *
351          * @since 4.2.0
352          * @since 4.6.0 More accurately named `updatePlugin`.
353          *
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.
361          */
362         wp.updates.updatePlugin = function( args ) {
363                 var $updateRow, $card, $message, message;
364
365                 args = _.extend( {
366                         success: wp.updates.updatePluginSuccess,
367                         error: wp.updates.updatePluginError
368                 }, args );
369
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' ) );
378
379                         // Remove previous error messages, if any.
380                         $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove();
381                 }
382
383                 if ( $message.html() !== wp.updates.l10n.updating ) {
384                         $message.data( 'originaltext', $message.html() );
385                 }
386
387                 $message
388                         .attr( 'aria-label', message )
389                         .text( wp.updates.l10n.updating );
390
391                 $document.trigger( 'wp-plugin-updating', args );
392
393                 return wp.updates.ajax( 'update-plugin', args );
394         };
395
396         /**
397          * Updates the UI appropriately after a successful plugin update.
398          *
399          * @since 4.2.0
400          * @since 4.6.0 More accurately named `updatePluginSuccess`.
401          *
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.
409          */
410         wp.updates.updatePluginSuccess = function( response ) {
411                 var $pluginRow, $updateMessage, newText;
412
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' );
420
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' );
428                 }
429
430                 $updateMessage
431                         .attr( 'aria-label', wp.updates.l10n.updatedLabel.replace( '%s', response.pluginName ) )
432                         .text( wp.updates.l10n.updated );
433
434                 wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' );
435
436                 wp.updates.decrementCount( 'plugin' );
437
438                 $document.trigger( 'wp-plugin-update-success', response );
439         };
440
441         /**
442          * Updates the UI appropriately after a failed plugin update.
443          *
444          * @since 4.2.0
445          * @since 4.6.0 More accurately named `updatePluginError`.
446          *
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.
454          */
455         wp.updates.updatePluginError = function( response ) {
456                 var $card, $message, errorMessage;
457
458                 if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
459                         return;
460                 }
461
462                 if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) {
463                         return;
464                 }
465
466                 errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage );
467
468                 if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
469                         if ( response.plugin ) {
470                                 $message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' );
471                         } else {
472                                 $message = $( 'tr[data-slug="' + response.slug + '"]' ).find( '.update-message' );
473                         }
474                         $message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage );
475
476                         if ( response.pluginName ) {
477                                 $message.find( 'p' )
478                                         .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) );
479                         } else {
480                                 $message.find( 'p' ).removeAttr( 'aria-label' );
481                         }
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
488                                 } ) );
489
490                         $card.find( '.update-now' )
491                                 .text( wp.updates.l10n.updateFailedShort ).removeClass( 'updating-message' );
492
493                         if ( response.pluginName ) {
494                                 $card.find( '.update-now' )
495                                         .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) );
496                         } else {
497                                 $card.find( '.update-now' ).removeAttr( 'aria-label' );
498                         }
499
500                         $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
501
502                                 // Use same delay as the total duration of the notice fadeTo + slideUp animation.
503                                 setTimeout( function() {
504                                         $card
505                                                 .removeClass( 'plugin-card-update-failed' )
506                                                 .find( '.column-name a' ).focus();
507
508                                         $card.find( '.update-now' )
509                                                 .attr( 'aria-label', false )
510                                                 .text( wp.updates.l10n.updateNow );
511                                 }, 200 );
512                         } );
513                 }
514
515                 wp.a11y.speak( errorMessage, 'assertive' );
516
517                 $document.trigger( 'wp-plugin-update-error', response );
518         };
519
520         /**
521          * Sends an Ajax request to the server to install a plugin.
522          *
523          * @since 4.6.0
524          *
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.
531          */
532         wp.updates.installPlugin = function( args ) {
533                 var $card    = $( '.plugin-card-' + args.slug ),
534                         $message = $card.find( '.install-now' );
535
536                 args = _.extend( {
537                         success: wp.updates.installPluginSuccess,
538                         error: wp.updates.installPluginError
539                 }, args );
540
541                 if ( 'import' === pagenow ) {
542                         $message = $( '[data-slug="' + args.slug + '"]' );
543                 }
544
545                 if ( $message.html() !== wp.updates.l10n.installing ) {
546                         $message.data( 'originaltext', $message.html() );
547                 }
548
549                 $message
550                         .addClass( 'updating-message' )
551                         .attr( 'aria-label', wp.updates.l10n.pluginInstallingLabel.replace( '%s', $message.data( 'name' ) ) )
552                         .text( wp.updates.l10n.installing );
553
554                 wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' );
555
556                 // Remove previous error messages, if any.
557                 $card.removeClass( 'plugin-card-install-failed' ).find( '.notice.notice-error' ).remove();
558
559                 $document.trigger( 'wp-plugin-installing', args );
560
561                 return wp.updates.ajax( 'install-plugin', args );
562         };
563
564         /**
565          * Updates the UI appropriately after a successful plugin install.
566          *
567          * @since 4.6.0
568          *
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.
574          */
575         wp.updates.installPluginSuccess = function( response ) {
576                 var $message = $( '.plugin-card-' + response.slug ).find( '.install-now' );
577
578                 $message
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 );
583
584                 wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
585
586                 $document.trigger( 'wp-plugin-install-success', response );
587
588                 if ( response.activateUrl ) {
589                         setTimeout( function() {
590
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 );
596                         }, 1000 );
597                 }
598         };
599
600         /**
601          * Updates the UI appropriately after a failed plugin install.
602          *
603          * @since 4.6.0
604          *
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.
611          */
612         wp.updates.installPluginError = function( response ) {
613                 var $card   = $( '.plugin-card-' + response.slug ),
614                         $button = $card.find( '.install-now' ),
615                         errorMessage;
616
617                 if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
618                         return;
619                 }
620
621                 if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
622                         return;
623                 }
624
625                 errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage );
626
627                 $card
628                         .addClass( 'plugin-card-update-failed' )
629                         .append( '<div class="notice notice-error notice-alt is-dismissible"><p>' + errorMessage + '</p></div>' );
630
631                 $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
632
633                         // Use same delay as the total duration of the notice fadeTo + slideUp animation.
634                         setTimeout( function() {
635                                 $card
636                                         .removeClass( 'plugin-card-update-failed' )
637                                         .find( '.column-name a' ).focus();
638                         }, 200 );
639                 } );
640
641                 $button
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 );
645
646                 wp.a11y.speak( errorMessage, 'assertive' );
647
648                 $document.trigger( 'wp-plugin-install-error', response );
649         };
650
651         /**
652          * Updates the UI appropriately after a successful importer install.
653          *
654          * @since 4.6.0
655          *
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.
661          */
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' )
667                 } );
668
669                 $( '[data-slug="' + response.slug + '"]' )
670                         .removeClass( 'install-now updating-message' )
671                         .addClass( 'activate-now' )
672                         .attr({
673                                 'href': response.activateUrl + '&from=import',
674                                 'aria-label': wp.updates.l10n.activateImporterLabel.replace( '%s', response.pluginName )
675                         })
676                         .text( wp.updates.l10n.activateImporter );
677
678                 wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
679
680                 $document.trigger( 'wp-importer-install-success', response );
681         };
682
683         /**
684          * Updates the UI appropriately after a failed importer install.
685          *
686          * @since 4.6.0
687          *
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.
694          */
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' );
699
700                 if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
701                         return;
702                 }
703
704                 if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
705                         return;
706                 }
707
708                 wp.updates.addAdminNotice( {
709                         id:        response.errorCode,
710                         className: 'notice-error is-dismissible',
711                         message:   errorMessage
712                 } );
713
714                 $installLink
715                         .removeClass( 'updating-message' )
716                         .text( wp.updates.l10n.installNow )
717                         .attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', pluginName ) );
718
719                 wp.a11y.speak( errorMessage, 'assertive' );
720
721                 $document.trigger( 'wp-importer-install-error', response );
722         };
723
724         /**
725          * Sends an Ajax request to the server to delete a plugin.
726          *
727          * @since 4.6.0
728          *
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.
736          */
737         wp.updates.deletePlugin = function( args ) {
738                 var $link = $( '[data-plugin="' + args.plugin + '"]' ).find( '.row-actions a.delete' );
739
740                 args = _.extend( {
741                         success: wp.updates.deletePluginSuccess,
742                         error: wp.updates.deletePluginError
743                 }, args );
744
745                 if ( $link.html() !== wp.updates.l10n.deleting ) {
746                         $link
747                                 .data( 'originaltext', $link.html() )
748                                 .text( wp.updates.l10n.deleting );
749                 }
750
751                 wp.a11y.speak( wp.updates.l10n.deleting, 'polite' );
752
753                 $document.trigger( 'wp-plugin-deleting', args );
754
755                 return wp.updates.ajax( 'delete-plugin', args );
756         };
757
758         /**
759          * Updates the UI appropriately after a successful plugin deletion.
760          *
761          * @since 4.6.0
762          *
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.
768          */
769         wp.updates.deletePluginSuccess = function( response ) {
770
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;
780
781                         // Add a success message after deleting a plugin.
782                         if ( ! $pluginRow.hasClass( 'plugin-update-tr' ) ) {
783                                 $pluginRow.after(
784                                         pluginDeletedRow( {
785                                                 slug:    response.slug,
786                                                 plugin:  response.plugin,
787                                                 colspan: columnCount,
788                                                 name:    response.pluginName
789                                         } )
790                                 );
791                         }
792
793                         $pluginRow.remove();
794
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' );
799                         }
800
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 + ')' );
806                                 } else {
807                                         $views.find( '.inactive' ).remove();
808                                 }
809                         }
810
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 + ')' );
815                                 } else {
816                                         $views.find( '.active' ).remove();
817                                 }
818                         }
819
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 + ')' );
824                                 } else {
825                                         $views.find( '.recently_activated' ).remove();
826                                 }
827                         }
828
829                         plugins.all = _.without( plugins.all, response.plugin );
830
831                         if ( plugins.all.length ) {
832                                 $views.find( '.all .count' ).text( '(' + plugins.all.length + ')' );
833                         } else {
834                                 $form.find( '.tablenav' ).css( { visibility: 'hidden' } );
835                                 $views.find( '.all' ).remove();
836
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>' );
839                                 }
840                         }
841                 } );
842
843                 wp.a11y.speak( wp.updates.l10n.deleted, 'polite' );
844
845                 $document.trigger( 'wp-plugin-delete-success', response );
846         };
847
848         /**
849          * Updates the UI appropriately after a failed plugin deletion.
850          *
851          * @since 4.6.0
852          *
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.
860          */
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
867                         } );
868
869                 if ( response.plugin ) {
870                         $plugin          = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' );
871                         $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' );
872                 } else {
873                         $plugin          = $( 'tr.inactive[data-slug="' + response.slug + '"]' );
874                         $pluginUpdateRow = $plugin.siblings( '[data-slug="' + response.slug + '"]' );
875                 }
876
877                 if ( ! wp.updates.isValidResponse( response, 'delete' ) ) {
878                         return;
879                 }
880
881                 if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) {
882                         return;
883                 }
884
885                 // Add a plugin update row if it doesn't exist yet.
886                 if ( ! $pluginUpdateRow.length ) {
887                         $plugin.addClass( 'update' ).after(
888                                 pluginUpdateRow( {
889                                         slug:    response.slug,
890                                         plugin:  response.plugin || response.slug,
891                                         colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
892                                         content: noticeContent
893                                 } )
894                         );
895                 } else {
896
897                         // Remove previous error messages, if any.
898                         $pluginUpdateRow.find( '.notice-error' ).remove();
899
900                         $pluginUpdateRow.find( '.plugin-update' ).append( noticeContent );
901                 }
902
903                 $document.trigger( 'wp-plugin-delete-error', response );
904         };
905
906         /**
907          * Sends an Ajax request to the server to update a theme.
908          *
909          * @since 4.6.0
910          *
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.
917          */
918         wp.updates.updateTheme = function( args ) {
919                 var $notice;
920
921                 args = _.extend( {
922                         success: wp.updates.updateThemeSuccess,
923                         error: wp.updates.updateThemeError
924                 }, args );
925
926                 if ( 'themes-network' === pagenow ) {
927                         $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
928
929                 } else {
930                         $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' );
931
932                         $notice.find( 'h3' ).remove();
933
934                         $notice = $notice.add( $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ) );
935                         $notice = $notice.addClass( 'updating-message' ).find( 'p' );
936                 }
937
938                 if ( $notice.html() !== wp.updates.l10n.updating ) {
939                         $notice.data( 'originaltext', $notice.html() );
940                 }
941
942                 wp.a11y.speak( wp.updates.l10n.updatingMsg, 'polite' );
943                 $notice.text( wp.updates.l10n.updating );
944
945                 $document.trigger( 'wp-theme-updating', args );
946
947                 return wp.updates.ajax( 'update-theme', args );
948         };
949
950         /**
951          * Updates the UI appropriately after a successful theme update.
952          *
953          * @since 4.6.0
954          *
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.
961          */
962         wp.updates.updateThemeSuccess = function( response ) {
963                 var isModalOpen    = $( 'body.modal-open' ).length,
964                         $theme         = $( '[data-slug="' + response.slug + '"]' ),
965                         updatedMessage = {
966                                 className: 'updated-message notice-success notice-alt',
967                                 message:   wp.updates.l10n.updated
968                         },
969                         $notice, newText;
970
971                 if ( 'themes-network' === pagenow ) {
972                         $notice = $theme.find( '.update-message' );
973
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 );
977                 } else {
978                         $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) );
979
980                         // Focus on Customize button after updating.
981                         if ( isModalOpen ) {
982                                 $( '.load-customize:visible' ).focus();
983                         } else {
984                                 $theme.find( '.load-customize' ).focus();
985                         }
986                 }
987
988                 wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) );
989                 wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' );
990
991                 wp.updates.decrementCount( 'theme' );
992
993                 $document.trigger( 'wp-theme-update-success', response );
994
995                 // Show updated message after modal re-rendered.
996                 if ( isModalOpen ) {
997                         $( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) );
998                 }
999         };
1000
1001         /**
1002          * Updates the UI appropriately after a failed theme update.
1003          *
1004          * @since 4.6.0
1005          *
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.
1011          */
1012         wp.updates.updateThemeError = function( response ) {
1013                 var $theme       = $( '[data-slug="' + response.slug + '"]' ),
1014                         errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage ),
1015                         $notice;
1016
1017                 if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
1018                         return;
1019                 }
1020
1021                 if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) {
1022                         return;
1023                 }
1024
1025                 if ( 'themes-network' === pagenow ) {
1026                         $notice = $theme.find( '.update-message ' );
1027                 } else {
1028                         $notice = $( '.theme-info .notice' ).add( $theme.find( '.notice' ) );
1029
1030                         $( 'body.modal-open' ).length ? $( '.load-customize:visible' ).focus() : $theme.find( '.load-customize' ).focus();
1031                 }
1032
1033                 wp.updates.addAdminNotice( {
1034                         selector:  $notice,
1035                         className: 'update-message notice-error notice-alt is-dismissible',
1036                         message:   errorMessage
1037                 } );
1038
1039                 wp.a11y.speak( errorMessage, 'polite' );
1040
1041                 $document.trigger( 'wp-theme-update-error', response );
1042         };
1043
1044         /**
1045          * Sends an Ajax request to the server to install a theme.
1046          *
1047          * @since 4.6.0
1048          *
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.
1055          */
1056         wp.updates.installTheme = function( args ) {
1057                 var $message = $( '.theme-install[data-slug="' + args.slug + '"]' );
1058
1059                 args = _.extend( {
1060                         success: wp.updates.installThemeSuccess,
1061                         error: wp.updates.installThemeError
1062                 }, args );
1063
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() );
1068                 }
1069
1070                 $message
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' );
1074
1075                 // Remove previous error messages, if any.
1076                 $( '.install-theme-info, [data-slug="' + args.slug + '"]' ).removeClass( 'theme-install-failed' ).find( '.notice.notice-error' ).remove();
1077
1078                 $document.trigger( 'wp-theme-installing', args );
1079
1080                 return wp.updates.ajax( 'install-theme', args );
1081         };
1082
1083         /**
1084          * Updates the UI appropriately after a successful theme install.
1085          *
1086          * @since 4.6.0
1087          *
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.
1093          */
1094         wp.updates.installThemeSuccess = function( response ) {
1095                 var $card = $( '.wp-full-overlay-header, [data-slug=' + response.slug + ']' ),
1096                         $message;
1097
1098                 $document.trigger( 'wp-theme-install-success', response );
1099
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 );
1105
1106                 wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
1107
1108                 setTimeout( function() {
1109
1110                         if ( response.activateUrl ) {
1111
1112                                 // Transform the 'Install' button into an 'Activate' button.
1113                                 $message
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 );
1119                         }
1120
1121                         if ( response.customizeUrl ) {
1122
1123                                 // Transform the 'Preview' button into a 'Live Preview' button.
1124                                 $message.siblings( '.preview' ).replaceWith( function () {
1125                                         return $( '<a>' )
1126                                                 .attr( 'href', response.customizeUrl )
1127                                                 .addClass( 'button load-customize' )
1128                                                 .text( wp.updates.l10n.livePreview );
1129                                 } );
1130                         }
1131                 }, 1000 );
1132         };
1133
1134         /**
1135          * Updates the UI appropriately after a failed theme install.
1136          *
1137          * @since 4.6.0
1138          *
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.
1144          */
1145         wp.updates.installThemeError = function( response ) {
1146                 var $card, $button,
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
1151                         } );
1152
1153                 if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
1154                         return;
1155                 }
1156
1157                 if ( wp.updates.maybeHandleCredentialError( response, 'install-theme' ) ) {
1158                         return;
1159                 }
1160
1161                 if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) {
1162                         $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
1163                         $card   = $( '.install-theme-info' ).prepend( $message );
1164                 } else {
1165                         $card   = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message );
1166                         $button = $card.find( '.theme-install' );
1167                 }
1168
1169                 $button
1170                         .removeClass( 'updating-message' )
1171                         .attr( 'aria-label', wp.updates.l10n.themeInstallFailedLabel.replace( '%s', $button.data( 'name' ) ) )
1172                         .text( wp.updates.l10n.installFailedShort );
1173
1174                 wp.a11y.speak( errorMessage, 'assertive' );
1175
1176                 $document.trigger( 'wp-theme-install-error', response );
1177         };
1178
1179         /**
1180          * Sends an Ajax request to the server to install a theme.
1181          *
1182          * @since 4.6.0
1183          *
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.
1190          */
1191         wp.updates.deleteTheme = function( args ) {
1192                 var $button;
1193
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' );
1198                 }
1199
1200                 args = _.extend( {
1201                         success: wp.updates.deleteThemeSuccess,
1202                         error: wp.updates.deleteThemeError
1203                 }, args );
1204
1205                 if ( $button && $button.html() !== wp.updates.l10n.deleting ) {
1206                         $button
1207                                 .data( 'originaltext', $button.html() )
1208                                 .text( wp.updates.l10n.deleting );
1209                 }
1210
1211                 wp.a11y.speak( wp.updates.l10n.deleting, 'polite' );
1212
1213                 // Remove previous error messages, if any.
1214                 $( '.theme-info .update-message' ).remove();
1215
1216                 $document.trigger( 'wp-theme-deleting', args );
1217
1218                 return wp.updates.ajax( 'delete-theme', args );
1219         };
1220
1221         /**
1222          * Updates the UI appropriately after a successful theme deletion.
1223          *
1224          * @since 4.6.0
1225          *
1226          * @typedef {object} deleteThemeSuccess
1227          * @param {object} response      Response from the server.
1228          * @param {string} response.slug Slug of the theme that was deleted.
1229          */
1230         wp.updates.deleteThemeSuccess = function( response ) {
1231                 var $themeRows = $( '[data-slug="' + response.slug + '"]' );
1232
1233                 if ( 'themes-network' === pagenow ) {
1234
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' );
1241
1242                                 if ( ! $themeRow.hasClass( 'plugin-update-tr' ) ) {
1243                                         $themeRow.after(
1244                                                 deletedRow( {
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()
1248                                                 } )
1249                                         );
1250                                 }
1251
1252                                 $themeRow.remove();
1253
1254                                 // Remove theme from update count.
1255                                 if ( $themeRow.hasClass( 'update' ) ) {
1256                                         totals.upgrade--;
1257                                         wp.updates.decrementCount( 'theme' );
1258                                 }
1259
1260                                 // Remove from views.
1261                                 if ( $themeRow.hasClass( 'inactive' ) ) {
1262                                         totals.disabled--;
1263                                         if ( totals.disabled ) {
1264                                                 $views.find( '.disabled .count' ).text( '(' + totals.disabled + ')' );
1265                                         } else {
1266                                                 $views.find( '.disabled' ).remove();
1267                                         }
1268                                 }
1269
1270                                 // There is always at least one theme available.
1271                                 $views.find( '.all .count' ).text( '(' + --totals.all + ')' );
1272                         } );
1273                 }
1274
1275                 wp.a11y.speak( wp.updates.l10n.deleted, 'polite' );
1276
1277                 $document.trigger( 'wp-theme-delete-success', response );
1278         };
1279
1280         /**
1281          * Updates the UI appropriately after a failed theme deletion.
1282          *
1283          * @since 4.6.0
1284          *
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.
1290          */
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
1300                         } );
1301
1302                 if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) {
1303                         return;
1304                 }
1305
1306                 if ( 'themes-network' === pagenow ) {
1307                         if ( ! $updateRow.length ) {
1308                                 $themeRow.addClass( 'update' ).after(
1309                                         updateRow( {
1310                                                 slug: response.slug,
1311                                                 colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
1312                                                 content: $message
1313                                         } )
1314                                 );
1315                         } else {
1316                                 // Remove previous error messages, if any.
1317                                 $updateRow.find( '.notice-error' ).remove();
1318                                 $updateRow.find( '.plugin-update' ).append( $message );
1319                         }
1320                 } else {
1321                         $( '.theme-info .theme-description' ).before( $message );
1322                 }
1323
1324                 $button.html( $button.data( 'originaltext' ) );
1325
1326                 wp.a11y.speak( errorMessage, 'assertive' );
1327
1328                 $document.trigger( 'wp-theme-delete-error', response );
1329         };
1330
1331         /**
1332          * Adds the appropriate callback based on the type of action and the current page.
1333          *
1334          * @since 4.6.0
1335          * @private
1336          *
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.
1340          */
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;
1345                 }
1346
1347                 return data;
1348         };
1349
1350         /**
1351          * Pulls available jobs from the queue and runs them.
1352          *
1353          * @since 4.2.0
1354          * @since 4.6.0 Can handle multiple job types.
1355          */
1356         wp.updates.queueChecker = function() {
1357                 var job;
1358
1359                 if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) {
1360                         return;
1361                 }
1362
1363                 job = wp.updates.queue.shift();
1364
1365                 // Handle a queue job.
1366                 switch ( job.action ) {
1367                         case 'install-plugin':
1368                                 wp.updates.installPlugin( job.data );
1369                                 break;
1370
1371                         case 'update-plugin':
1372                                 wp.updates.updatePlugin( job.data );
1373                                 break;
1374
1375                         case 'delete-plugin':
1376                                 wp.updates.deletePlugin( job.data );
1377                                 break;
1378
1379                         case 'install-theme':
1380                                 wp.updates.installTheme( job.data );
1381                                 break;
1382
1383                         case 'update-theme':
1384                                 wp.updates.updateTheme( job.data );
1385                                 break;
1386
1387                         case 'delete-theme':
1388                                 wp.updates.deleteTheme( job.data );
1389                                 break;
1390
1391                         default:
1392                                 break;
1393                 }
1394         };
1395
1396         /**
1397          * Requests the users filesystem credentials if they aren't already known.
1398          *
1399          * @since 4.2.0
1400          *
1401          * @param {Event=} event Optional. Event interface.
1402          */
1403         wp.updates.requestFilesystemCredentials = function( event ) {
1404                 if ( false === wp.updates.filesystemCredentials.available ) {
1405                         /*
1406                          * After exiting the credentials request modal,
1407                          * return the focus to the element triggering the request.
1408                          */
1409                         if ( event && ! wp.updates.$elToReturnFocusToFromCredentialsModal ) {
1410                                 wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target );
1411                         }
1412
1413                         wp.updates.ajaxLocked = true;
1414                         wp.updates.requestForCredentialsModalOpen();
1415                 }
1416         };
1417
1418         /**
1419          * Requests the users filesystem credentials if needed and there is no lock.
1420          *
1421          * @since 4.6.0
1422          *
1423          * @param {Event=} event Optional. Event interface.
1424          */
1425         wp.updates.maybeRequestFilesystemCredentials = function( event ) {
1426                 if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
1427                         wp.updates.requestFilesystemCredentials( event );
1428                 }
1429         };
1430
1431         /**
1432          * Keydown handler for the request for credentials modal.
1433          *
1434          * Closes the modal when the escape key is pressed and
1435          * constrains keyboard navigation to inside the modal.
1436          *
1437          * @since 4.2.0
1438          *
1439          * @param {Event} event Event interface.
1440          */
1441         wp.updates.keydown = function( event ) {
1442                 if ( 27 === event.keyCode ) {
1443                         wp.updates.requestForCredentialsModalCancel();
1444                 } else if ( 9 === event.keyCode ) {
1445
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();
1449
1450                                 event.preventDefault();
1451                         } else if ( 'hostname' === event.target.id && event.shiftKey ) {
1452                                 $( '#upgrade' ).focus();
1453
1454                                 event.preventDefault();
1455                         }
1456                 }
1457         };
1458
1459         /**
1460          * Opens the request for credentials modal.
1461          *
1462          * @since 4.2.0
1463          */
1464         wp.updates.requestForCredentialsModalOpen = function() {
1465                 var $modal = $( '#request-filesystem-credentials-dialog' );
1466
1467                 $( 'body' ).addClass( 'modal-open' );
1468                 $modal.show();
1469                 $modal.find( 'input:enabled:first' ).focus();
1470                 $modal.on( 'keydown', wp.updates.keydown );
1471         };
1472
1473         /**
1474          * Closes the request for credentials modal.
1475          *
1476          * @since 4.2.0
1477          */
1478         wp.updates.requestForCredentialsModalClose = function() {
1479                 $( '#request-filesystem-credentials-dialog' ).hide();
1480                 $( 'body' ).removeClass( 'modal-open' );
1481
1482                 if ( wp.updates.$elToReturnFocusToFromCredentialsModal ) {
1483                         wp.updates.$elToReturnFocusToFromCredentialsModal.focus();
1484                 }
1485         };
1486
1487         /**
1488          * Takes care of the steps that need to happen when the modal is canceled out.
1489          *
1490          * @since 4.2.0
1491          * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions.
1492          */
1493         wp.updates.requestForCredentialsModalCancel = function() {
1494
1495                 // Not ajaxLocked and no queue means we already have cleared things up.
1496                 if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) {
1497                         return;
1498                 }
1499
1500                 _.each( wp.updates.queue, function( job ) {
1501                         $document.trigger( 'credential-modal-cancel', job );
1502                 } );
1503
1504                 // Remove the lock, and clear the queue.
1505                 wp.updates.ajaxLocked = false;
1506                 wp.updates.queue = [];
1507
1508                 wp.updates.requestForCredentialsModalClose();
1509         };
1510
1511         /**
1512          * Displays an error message in the request for credentials form.
1513          *
1514          * @since 4.2.0
1515          *
1516          * @param {string} message Error message.
1517          */
1518         wp.updates.showErrorInCredentialsForm = function( message ) {
1519                 var $modal = $( '#request-filesystem-credentials-form' );
1520
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>' );
1524         };
1525
1526         /**
1527          * Handles credential errors and runs events that need to happen in that case.
1528          *
1529          * @since 4.2.0
1530          *
1531          * @param {object} response Ajax response.
1532          * @param {string} action   The type of request to perform.
1533          */
1534         wp.updates.credentialError = function( response, action ) {
1535
1536                 // Restore callbacks.
1537                 response = wp.updates._addCallbacks( response, action );
1538
1539                 wp.updates.queue.unshift( {
1540                         action: action,
1541
1542                         /*
1543                          * Not cool that we're depending on response for this data.
1544                          * This would feel more whole in a view all tied together.
1545                          */
1546                         data: response
1547                 } );
1548
1549                 wp.updates.filesystemCredentials.available = false;
1550                 wp.updates.showErrorInCredentialsForm( response.errorMessage );
1551                 wp.updates.requestFilesystemCredentials();
1552         };
1553
1554         /**
1555          * Handles credentials errors if it could not connect to the filesystem.
1556          *
1557          * @since 4.6.0
1558          *
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.
1565          */
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 );
1569                         return true;
1570                 }
1571
1572                 return false;
1573         };
1574
1575         /**
1576          * Validates an AJAX response to ensure it's a proper object.
1577          *
1578          * If the response deems to be invalid, an admin notice is being displayed.
1579          *
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'.
1586          */
1587         wp.updates.isValidResponse = function( response, action ) {
1588                 var error = wp.updates.l10n.unknownError,
1589                     errorMessage;
1590
1591                 // Make sure the response is a valid data object and not a Promise object.
1592                 if ( _.isObject( response ) && ! _.isFunction( response.always ) ) {
1593                         return true;
1594                 }
1595
1596                 if ( _.isString( response ) && '-1' === response ) {
1597                         error = wp.updates.l10n.nonceError;
1598                 } else if ( _.isString( response ) ) {
1599                         error = 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;
1606                 }
1607
1608                 switch ( action ) {
1609                         case 'update':
1610                                 errorMessage = wp.updates.l10n.updateFailed;
1611                                 break;
1612
1613                         case 'install':
1614                                 errorMessage = wp.updates.l10n.installFailed;
1615                                 break;
1616
1617                         case 'delete':
1618                                 errorMessage = wp.updates.l10n.deleteFailed;
1619                                 break;
1620                 }
1621
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 );
1625
1626                 // Add admin notice.
1627                 wp.updates.addAdminNotice( {
1628                         id:        'unknown_error',
1629                         className: 'notice-error is-dismissible',
1630                         message:   _.escape( errorMessage )
1631                 } );
1632
1633                 // Remove the lock, and clear the queue.
1634                 wp.updates.ajaxLocked = false;
1635                 wp.updates.queue      = [];
1636
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 );
1643
1644                 $( '.updating-message:not(.button):not(.thickbox)' )
1645                         .removeClass( 'updating-message notice-warning' )
1646                         .addClass( 'notice-error' )
1647                         .find( 'p' )
1648                                 .removeAttr( 'aria-label' )
1649                                 .text( errorMessage );
1650
1651                 wp.a11y.speak( errorMessage, 'assertive' );
1652
1653                 return false;
1654         };
1655
1656         /**
1657          * Potentially adds an AYS to a user attempting to leave the page.
1658          *
1659          * If an update is on-going and a user attempts to leave the page,
1660          * opens an "Are you sure?" alert.
1661          *
1662          * @since 4.2.0
1663          */
1664         wp.updates.beforeunload = function() {
1665                 if ( wp.updates.ajaxLocked ) {
1666                         return wp.updates.l10n.beforeunload;
1667                 }
1668         };
1669
1670         $( function() {
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' );
1676
1677                 settings = _.extend( settings, window._wpUpdatesItemCounts || {} );
1678
1679                 if ( settings.totals ) {
1680                         wp.updates.refreshCount();
1681                 }
1682
1683                 /*
1684                  * Whether a user needs to submit filesystem credentials.
1685                  *
1686                  * This is based on whether the form was output on the page server-side.
1687                  *
1688                  * @see {wp_print_request_filesystem_credentials_modal() in PHP}
1689                  */
1690                 wp.updates.shouldRequestFilesystemCredentials = $filesystemModal.length > 0;
1691
1692                 /**
1693                  * File system credentials form submit noop-er / handler.
1694                  *
1695                  * @since 4.2.0
1696                  */
1697                 $filesystemModal.on( 'submit', 'form', function( event ) {
1698                         event.preventDefault();
1699
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;
1708
1709                         // Unlock and invoke the queue.
1710                         wp.updates.ajaxLocked = false;
1711                         wp.updates.queueChecker();
1712
1713                         wp.updates.requestForCredentialsModalClose();
1714                 } );
1715
1716                 /**
1717                  * Closes the request credentials modal when clicking the 'Cancel' button or outside of the modal.
1718                  *
1719                  * @since 4.2.0
1720                  */
1721                 $filesystemModal.on( 'click', '[data-js-action="close"], .notification-dialog-background', wp.updates.requestForCredentialsModalCancel );
1722
1723                 /**
1724                  * Hide SSH fields when not selected.
1725                  *
1726                  * @since 4.2.0
1727                  */
1728                 $filesystemModal.on( 'change', 'input[name="connection_type"]', function() {
1729                         $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) );
1730                 } ).change();
1731
1732                 /**
1733                  * Handles events after the credential modal was closed.
1734                  *
1735                  * @since 4.6.0
1736                  *
1737                  * @param {Event}  event Event interface.
1738                  * @param {string} job   The install/update.delete request.
1739                  */
1740                 $document.on( 'credential-modal-cancel', function( event, job ) {
1741                         var $updatingMessage = $( '.updating-message' ),
1742                                 $message, originalText;
1743
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' );
1751                                 }
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' );
1759                                 }
1760                         } else {
1761                                 $message = $updatingMessage;
1762                         }
1763
1764                         if ( $message && $message.hasClass( 'updating-message' ) ) {
1765                                 originalText = $message.data( 'originaltext' );
1766
1767                                 if ( 'undefined' === typeof originalText ) {
1768                                         originalText = $( '<p>' ).html( $message.find( 'p' ).data( 'originaltext' ) );
1769                                 }
1770
1771                                 $message
1772                                         .removeClass( 'updating-message' )
1773                                         .html( originalText );
1774
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' ) ) );
1780                                         }
1781                                 }
1782                         }
1783
1784                         wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
1785                 } );
1786
1787                 /**
1788                  * Click handler for plugin updates in List Table view.
1789                  *
1790                  * @since 4.2.0
1791                  *
1792                  * @param {Event} event Event interface.
1793                  */
1794                 $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) {
1795                         var $message   = $( event.target ),
1796                                 $pluginRow = $message.parents( 'tr' );
1797
1798                         event.preventDefault();
1799
1800                         if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
1801                                 return;
1802                         }
1803
1804                         wp.updates.maybeRequestFilesystemCredentials( event );
1805
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' )
1811                         } );
1812                 } );
1813
1814                 /**
1815                  * Click handler for plugin updates in plugin install view.
1816                  *
1817                  * @since 4.2.0
1818                  *
1819                  * @param {Event} event Event interface.
1820                  */
1821                 $pluginFilter.on( 'click', '.update-now', function( event ) {
1822                         var $button = $( event.target );
1823                         event.preventDefault();
1824
1825                         if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
1826                                 return;
1827                         }
1828
1829                         wp.updates.maybeRequestFilesystemCredentials( event );
1830
1831                         wp.updates.updatePlugin( {
1832                                 plugin: $button.data( 'plugin' ),
1833                                 slug:   $button.data( 'slug' )
1834                         } );
1835                 } );
1836
1837                 /**
1838                  * Click handler for plugin installs in plugin install view.
1839                  *
1840                  * @since 4.6.0
1841                  *
1842                  * @param {Event} event Event interface.
1843                  */
1844                 $pluginFilter.on( 'click', '.install-now', function( event ) {
1845                         var $button = $( event.target );
1846                         event.preventDefault();
1847
1848                         if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
1849                                 return;
1850                         }
1851
1852                         if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
1853                                 wp.updates.requestFilesystemCredentials( event );
1854
1855                                 $document.on( 'credential-modal-cancel', function() {
1856                                         var $message = $( '.install-now.updating-message' );
1857
1858                                         $message
1859                                                 .removeClass( 'updating-message' )
1860                                                 .text( wp.updates.l10n.installNow );
1861
1862                                         wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
1863                                 } );
1864                         }
1865
1866                         wp.updates.installPlugin( {
1867                                 slug: $button.data( 'slug' )
1868                         } );
1869                 } );
1870
1871                 /**
1872                  * Click handler for importer plugins installs in the Import screen.
1873                  *
1874                  * @since 4.6.0
1875                  *
1876                  * @param {Event} event Event interface.
1877                  */
1878                 $document.on( 'click', '.importer-item .install-now', function( event ) {
1879                         var $button = $( event.target ),
1880                                 pluginName = $( this ).data( 'name' );
1881
1882                         event.preventDefault();
1883
1884                         if ( $button.hasClass( 'updating-message' ) ) {
1885                                 return;
1886                         }
1887
1888                         if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
1889                                 wp.updates.requestFilesystemCredentials( event );
1890
1891                                 $document.on( 'credential-modal-cancel', function() {
1892
1893                                         $button
1894                                                 .removeClass( 'updating-message' )
1895                                                 .text( wp.updates.l10n.installNow )
1896                                                 .attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', pluginName ) );
1897
1898                                         wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
1899                                 } );
1900                         }
1901
1902                         wp.updates.installPlugin( {
1903                                 slug:    $button.data( 'slug' ),
1904                                 pagenow: pagenow,
1905                                 success: wp.updates.installImporterSuccess,
1906                                 error:   wp.updates.installImporterError
1907                         } );
1908                 } );
1909
1910                 /**
1911                  * Click handler for plugin deletions.
1912                  *
1913                  * @since 4.6.0
1914                  *
1915                  * @param {Event} event Event interface.
1916                  */
1917                 $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) {
1918                         var $pluginRow = $( event.target ).parents( 'tr' );
1919
1920                         event.preventDefault();
1921
1922                         if ( ! window.confirm( wp.updates.l10n.aysDeleteUninstall.replace( '%s', $pluginRow.find( '.plugin-title strong' ).text() ) ) ) {
1923                                 return;
1924                         }
1925
1926                         wp.updates.maybeRequestFilesystemCredentials( event );
1927
1928                         wp.updates.deletePlugin( {
1929                                 plugin: $pluginRow.data( 'plugin' ),
1930                                 slug:   $pluginRow.data( 'slug' )
1931                         } );
1932
1933                 } );
1934
1935                 /**
1936                  * Click handler for theme updates.
1937                  *
1938                  * @since 4.6.0
1939                  *
1940                  * @param {Event} event Event interface.
1941                  */
1942                 $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) {
1943                         var $message  = $( event.target ),
1944                                 $themeRow = $message.parents( 'tr' );
1945
1946                         event.preventDefault();
1947
1948                         if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
1949                                 return;
1950                         }
1951
1952                         wp.updates.maybeRequestFilesystemCredentials( event );
1953
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' )
1958                         } );
1959                 } );
1960
1961                 /**
1962                  * Click handler for theme deletions.
1963                  *
1964                  * @since 4.6.0
1965                  *
1966                  * @param {Event} event Event interface.
1967                  */
1968                 $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) {
1969                         var $themeRow = $( event.target ).parents( 'tr' );
1970
1971                         event.preventDefault();
1972
1973                         if ( ! window.confirm( wp.updates.l10n.aysDelete.replace( '%s', $themeRow.find( '.theme-title strong' ).text() ) ) ) {
1974                                 return;
1975                         }
1976
1977                         wp.updates.maybeRequestFilesystemCredentials( event );
1978
1979                         wp.updates.deleteTheme( {
1980                                 slug: $themeRow.data( 'slug' )
1981                         } );
1982                 } );
1983
1984                 /**
1985                  * Bulk action handler for plugins and themes.
1986                  *
1987                  * Handles both deletions and updates.
1988                  *
1989                  * @since 4.6.0
1990                  *
1991                  * @param {Event} event Event interface.
1992                  */
1993                 $bulkActionForm.on( 'click', '[type="submit"]', function( event ) {
1994                         var bulkAction    = $( event.target ).siblings( 'select' ).val(),
1995                                 itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ),
1996                                 success       = 0,
1997                                 error         = 0,
1998                                 errorMessages = [],
1999                                 type, action;
2000
2001                         // Determine which type of item we're dealing with.
2002                         switch ( pagenow ) {
2003                                 case 'plugins':
2004                                 case 'plugins-network':
2005                                         type = 'plugin';
2006                                         break;
2007
2008                                 case 'themes-network':
2009                                         type = 'theme';
2010                                         break;
2011
2012                                 default:
2013                                         return;
2014                         }
2015
2016                         // Bail if there were no items selected.
2017                         if ( ! itemsSelected.length ) {
2018                                 event.preventDefault();
2019                                 $( 'html, body' ).animate( { scrollTop: 0 } );
2020
2021                                 return wp.updates.addAdminNotice( {
2022                                         id:        'no-items-selected',
2023                                         className: 'notice-error is-dismissible',
2024                                         message:   wp.updates.l10n.noItemsSelected
2025                                 } );
2026                         }
2027
2028                         // Determine the type of request we're dealing with.
2029                         switch ( bulkAction ) {
2030                                 case 'update-selected':
2031                                         action = bulkAction.replace( 'selected', type );
2032                                         break;
2033
2034                                 case 'delete-selected':
2035                                         if ( ! window.confirm( 'plugin' === type ? wp.updates.l10n.aysBulkDelete : wp.updates.l10n.aysBulkDeleteThemes ) ) {
2036                                                 event.preventDefault();
2037                                                 return;
2038                                         }
2039
2040                                         action = bulkAction.replace( 'selected', type );
2041                                         break;
2042
2043                                 default:
2044                                         return;
2045                         }
2046
2047                         wp.updates.maybeRequestFilesystemCredentials( event );
2048
2049                         event.preventDefault();
2050
2051                         // Un-check the bulk checkboxes.
2052                         $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false );
2053
2054                         $document.trigger( 'wp-' + type + '-bulk-' + bulkAction, itemsSelected );
2055
2056                         // Find all the checkboxes which have been checked.
2057                         itemsSelected.each( function( index, element ) {
2058                                 var $checkbox = $( element ),
2059                                         $itemRow = $checkbox.parents( 'tr' );
2060
2061                                 // Only add update-able items to the update queue.
2062                                 if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) {
2063
2064                                         // Un-check the box.
2065                                         $checkbox.prop( 'checked', false );
2066                                         return;
2067                                 }
2068
2069                                 // Add it to the queue.
2070                                 wp.updates.queue.push( {
2071                                         action: action,
2072                                         data:   {
2073                                                 plugin: $itemRow.data( 'plugin' ),
2074                                                 slug:   $itemRow.data( 'slug' )
2075                                         }
2076                                 } );
2077                         } );
2078
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;
2083
2084                                 if ( 'wp-' + response.update + '-update-success' === event.type ) {
2085                                         success++;
2086                                 } else {
2087                                         itemName = response.pluginName ? response.pluginName : $itemRow.find( '.column-primary strong' ).text();
2088
2089                                         error++;
2090                                         errorMessages.push( itemName + ': ' + response.errorMessage );
2091                                 }
2092
2093                                 $itemRow.find( 'input[name="checked[]"]:checked' ).prop( 'checked', false );
2094
2095                                 wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' );
2096
2097                                 wp.updates.addAdminNotice( {
2098                                         id:            'bulk-action-notice',
2099                                         className:     'bulk-action-notice',
2100                                         successes:     success,
2101                                         errors:        error,
2102                                         errorMessages: errorMessages,
2103                                         type:          response.update
2104                                 } );
2105
2106                                 $bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() {
2107                                         // $( this ) is the clicked button, no need to get it again.
2108                                         $( this )
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' );
2113                                 } );
2114
2115                                 if ( error > 0 && ! wp.updates.queue.length ) {
2116                                         $( 'html, body' ).animate( { scrollTop: 0 } );
2117                                 }
2118                         } );
2119
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' );
2123                         } );
2124
2125                         // Check the queue, now that the event handlers have been added.
2126                         wp.updates.queueChecker();
2127                 } );
2128
2129                 if ( $pluginInstallSearch.length ) {
2130                         $pluginInstallSearch.attr( 'aria-describedby', 'live-search-desc' );
2131                 }
2132
2133                 /**
2134                  * Handles changes to the plugin search box on the new-plugin page,
2135                  * searching the repository dynamically.
2136                  *
2137                  * @since 4.6.0
2138                  */
2139                 $pluginInstallSearch.on( 'keyup input', _.debounce( function( event, eventtype ) {
2140                         var $searchTab = $( '.plugin-install-search' ), data, searchLocation;
2141
2142                         data = {
2143                                 _ajax_nonce: wp.updates.ajaxNonce,
2144                                 s:           event.target.value,
2145                                 tab:         'search',
2146                                 type:        $( '#typeselector' ).val(),
2147                                 pagenow:     pagenow
2148                         };
2149                         searchLocation = location.href.split( '?' )[ 0 ] + '?' + $.param( _.omit( data, [ '_ajax_nonce', 'pagenow' ] ) );
2150
2151                         // Clear on escape.
2152                         if ( 'keyup' === event.type && 27 === event.which ) {
2153                                 event.target.value = '';
2154                         }
2155
2156                         if ( wp.updates.searchTerm === data.s && 'typechange' !== eventtype ) {
2157                                 return;
2158                         } else {
2159                                 $pluginFilter.empty();
2160                                 wp.updates.searchTerm = data.s;
2161                         }
2162
2163                         if ( window.history && window.history.replaceState ) {
2164                                 window.history.replaceState( null, '', searchLocation );
2165                         }
2166
2167                         if ( ! $searchTab.length ) {
2168                                 $searchTab = $( '<li class="plugin-install-search" />' )
2169                                         .append( $( '<a />', {
2170                                                 'class': 'current',
2171                                                 'href': searchLocation,
2172                                                 'text': wp.updates.l10n.searchResultsLabel
2173                                         } ) );
2174
2175                                 $( '.wp-filter .filter-links .current' )
2176                                         .removeClass( 'current' )
2177                                         .parents( '.filter-links' )
2178                                         .prepend( $searchTab );
2179
2180                                 $pluginFilter.prev( 'p' ).remove();
2181                                 $( '.plugins-popular-tags-wrapper' ).remove();
2182                         }
2183
2184                         if ( 'undefined' !== typeof wp.updates.searchRequest ) {
2185                                 wp.updates.searchRequest.abort();
2186                         }
2187                         $( 'body' ).addClass( 'loading-content' );
2188
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;
2193
2194                                 if ( 0 === response.count ) {
2195                                         wp.a11y.speak( wp.updates.l10n.noPluginsFound );
2196                                 } else {
2197                                         wp.a11y.speak( wp.updates.l10n.pluginsFound.replace( '%d', response.count ) );
2198                                 }
2199                         } );
2200                 }, 500 ) );
2201
2202                 if ( $pluginSearch.length ) {
2203                         $pluginSearch.attr( 'aria-describedby', 'live-search-desc' );
2204                 }
2205
2206                 /**
2207                  * Handles changes to the plugin search box on the Installed Plugins screen,
2208                  * searching the plugin list dynamically.
2209                  *
2210                  * @since 4.6.0
2211                  */
2212                 $pluginSearch.on( 'keyup input', _.debounce( function( event ) {
2213                         var data = {
2214                                 _ajax_nonce:   wp.updates.ajaxNonce,
2215                                 s:             event.target.value,
2216                                 pagenow:       pagenow,
2217                                 plugin_status: 'all'
2218                         },
2219                         queryArgs;
2220
2221                         // Clear on escape.
2222                         if ( 'keyup' === event.type && 27 === event.which ) {
2223                                 event.target.value = '';
2224                         }
2225
2226                         if ( wp.updates.searchTerm === data.s ) {
2227                                 return;
2228                         } else {
2229                                 wp.updates.searchTerm = data.s;
2230                         }
2231
2232                         queryArgs = _.object( _.compact( _.map( location.search.slice( 1 ).split( '&' ), function( item ) {
2233                                 if ( item ) return item.split( '=' );
2234                         } ) ) );
2235
2236                         data.plugin_status = queryArgs.plugin_status || 'all';
2237
2238                         if ( window.history && window.history.replaceState ) {
2239                                 window.history.replaceState( null, '', location.href.split( '?' )[ 0 ] + '?s=' + data.s + '&plugin_status=' + data.plugin_status );
2240                         }
2241
2242                         if ( 'undefined' !== typeof wp.updates.searchRequest ) {
2243                                 wp.updates.searchRequest.abort();
2244                         }
2245
2246                         $bulkActionForm.empty();
2247                         $( 'body' ).addClass( 'loading-content' );
2248                         $( '.subsubsub .current' ).removeClass( 'current' );
2249
2250                         wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) {
2251
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' );
2255
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 );
2261                                 } else {
2262                                         $( '.wrap h1' ).append( $subTitle );
2263                                 }
2264
2265                                 $( 'body' ).removeClass( 'loading-content' );
2266                                 $bulkActionForm.append( response.items );
2267                                 delete wp.updates.searchRequest;
2268
2269                                 if ( 0 === response.count ) {
2270                                         wp.a11y.speak( wp.updates.l10n.noPluginsFound );
2271                                 } else {
2272                                         wp.a11y.speak( wp.updates.l10n.pluginsFound.replace( '%d', response.count ) );
2273                                 }
2274                         } );
2275                 }, 500 ) );
2276
2277                 /**
2278                  * Trigger a search event when the search form gets submitted.
2279                  *
2280                  * @since 4.6.0
2281                  */
2282                 $document.on( 'submit', '.search-plugins', function( event ) {
2283                         event.preventDefault();
2284
2285                         $( 'input.wp-filter-search' ).trigger( 'input' );
2286                 } );
2287
2288                 /**
2289                  * Trigger a search event when the search type gets changed.
2290                  *
2291                  * @since 4.6.0
2292                  */
2293                 $( '#typeselector' ).on( 'change', function() {
2294                         var $search = $( 'input[name="s"]' );
2295
2296                         if ( $search.val().length ) {
2297                                 $search.trigger( 'input', 'typechange' );
2298                         }
2299                 } );
2300
2301                 /**
2302                  * Click handler for updating a plugin from the details modal on `plugin-install.php`.
2303                  *
2304                  * @since 4.2.0
2305                  *
2306                  * @param {Event} event Event interface.
2307                  */
2308                 $( '#plugin_update_from_iframe' ).on( 'click', function( event ) {
2309                         var target = window.parent === window ? null : window.parent,
2310                                 update;
2311
2312                         $.support.postMessage = !! window.postMessage;
2313
2314                         if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'update-core.php' ) ) {
2315                                 return;
2316                         }
2317
2318                         event.preventDefault();
2319
2320                         update = {
2321                                 action: 'update-plugin',
2322                                 data:   {
2323                                         plugin: $( this ).data( 'plugin' ),
2324                                         slug:   $( this ).data( 'slug' )
2325                                 }
2326                         };
2327
2328                         target.postMessage( JSON.stringify( update ), window.location.origin );
2329                 } );
2330
2331                 /**
2332                  * Click handler for installing a plugin from the details modal on `plugin-install.php`.
2333                  *
2334                  * @since 4.6.0
2335                  *
2336                  * @param {Event} event Event interface.
2337                  */
2338                 $( '#plugin_install_from_iframe' ).on( 'click', function( event ) {
2339                         var target = window.parent === window ? null : window.parent,
2340                                 install;
2341
2342                         $.support.postMessage = !! window.postMessage;
2343
2344                         if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'index.php' ) ) {
2345                                 return;
2346                         }
2347
2348                         event.preventDefault();
2349
2350                         install = {
2351                                 action: 'install-plugin',
2352                                 data:   {
2353                                         slug: $( this ).data( 'slug' )
2354                                 }
2355                         };
2356
2357                         target.postMessage( JSON.stringify( install ), window.location.origin );
2358                 } );
2359
2360                 /**
2361                  * Handles postMessage events.
2362                  *
2363                  * @since 4.2.0
2364                  * @since 4.6.0 Switched `update-plugin` action to use the queue.
2365                  *
2366                  * @param {Event} event Event interface.
2367                  */
2368                 $( window ).on( 'message', function( event ) {
2369                         var originalEvent  = event.originalEvent,
2370                                 expectedOrigin = document.location.protocol + '//' + document.location.hostname,
2371                                 message;
2372
2373                         if ( originalEvent.origin !== expectedOrigin ) {
2374                                 return;
2375                         }
2376
2377                         try {
2378                                 message = $.parseJSON( originalEvent.data );
2379                         } catch ( e ) {
2380                                 return;
2381                         }
2382
2383                         if ( 'undefined' === typeof message.action ) {
2384                                 return;
2385                         }
2386
2387                         switch ( message.action ) {
2388
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 );
2393                                         break;
2394
2395                                 case 'install-plugin':
2396                                 case 'update-plugin':
2397                                         /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
2398                                         window.tb_remove();
2399                                         /* jscs:enable */
2400
2401                                         message.data = wp.updates._addCallbacks( message.data, message.action );
2402
2403                                         wp.updates.queue.push( message );
2404                                         wp.updates.queueChecker();
2405                                         break;
2406                         }
2407                 } );
2408
2409                 /**
2410                  * Adds a callback to display a warning before leaving the page.
2411                  *
2412                  * @since 4.2.0
2413                  */
2414                 $( window ).on( 'beforeunload', wp.updates.beforeunload );
2415         } );
2416 })( jQuery, window.wp, window._wpUpdatesSettings );