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