[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

/wp-admin/js/ -> updates.js (source)

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


Generated: Sat Sep 21 01:00:03 2019 Cross-referenced by PHPXref 0.7.1