[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

   1  /**
   2   * @output wp-admin/js/updates.js
   3   */
   4  
   5  /* global pagenow, _wpThemeSettings */
   6  
   7  /**
   8   * Provides functions for ajaxified updates, deletions and installs inside the WordPress admin.
   9   *
  10   * @version 4.2.0
  11   *
  12   * @param {JQueryStatic} $                                        The jQuery object.
  13   * @param {Object}       wp                                       The WordPress global object.
  14   * @param {Object}       settings                                 WP Updates settings.
  15   * @param {string}       settings.ajax_nonce                      Ajax nonce.
  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 {Array}        settings.plugins['auto-update-enabled']  Base names of plugins set to auto-update.
  23   * @param {Array}        settings.plugins['auto-update-disabled'] Base names of plugins set to not auto-update.
  24   * @param {Object}       settings.themes                          Slugs of themes in their different states.
  25   * @param {Array}        settings.themes.all                      Slugs of all themes.
  26   * @param {Array}        settings.themes.upgrade                  Slugs of themes with updates available.
  27   * @param {Array}        settings.themes.disabled                 Slugs of disabled themes.
  28   * @param {Array}        settings.themes['auto-update-enabled']   Slugs of themes set to auto-update.
  29   * @param {Array}        settings.themes['auto-update-disabled']  Slugs of themes set to not auto-update.
  30   * @param {Object}       settings.totals                          Combined information for available update counts.
  31   * @param {number}       settings.totals.count                    Holds the amount of available updates.
  32   */
  33  (function( $, wp, settings ) {
  34      var $document = $( document ),
  35          __ = wp.i18n.__,
  36          _x = wp.i18n._x,
  37          _n = wp.i18n._n,
  38          _nx = wp.i18n._nx,
  39          sprintf = wp.i18n.sprintf;
  40  
  41      wp = wp || {};
  42  
  43      /**
  44       * The WP Updates object.
  45       *
  46       * @since 4.2.0
  47       *
  48       * @namespace wp.updates
  49       */
  50      wp.updates = {};
  51  
  52      /**
  53       * Removed in 5.5.0, needed for back-compatibility.
  54       *
  55       * @since 4.2.0
  56       * @deprecated 5.5.0
  57       *
  58       * @type {Object}
  59       */
  60      wp.updates.l10n = {
  61          searchResults: '',
  62          searchResultsLabel: '',
  63          noPlugins: '',
  64          noItemsSelected: '',
  65          updating: '',
  66          pluginUpdated: '',
  67          themeUpdated: '',
  68          update: '',
  69          updateNow: '',
  70          pluginUpdateNowLabel: '',
  71          updateFailedShort: '',
  72          updateFailed: '',
  73          pluginUpdatingLabel: '',
  74          pluginUpdatedLabel: '',
  75          pluginUpdateFailedLabel: '',
  76          updatingMsg: '',
  77          updatedMsg: '',
  78          updateCancel: '',
  79          beforeunload: '',
  80          installNow: '',
  81          pluginInstallNowLabel: '',
  82          installing: '',
  83          pluginInstalled: '',
  84          themeInstalled: '',
  85          installFailedShort: '',
  86          installFailed: '',
  87          pluginInstallingLabel: '',
  88          themeInstallingLabel: '',
  89          pluginInstalledLabel: '',
  90          themeInstalledLabel: '',
  91          pluginInstallFailedLabel: '',
  92          themeInstallFailedLabel: '',
  93          installingMsg: '',
  94          installedMsg: '',
  95          importerInstalledMsg: '',
  96          aysDelete: '',
  97          aysDeleteUninstall: '',
  98          aysBulkDelete: '',
  99          aysBulkDeleteThemes: '',
 100          deleting: '',
 101          deleteFailed: '',
 102          pluginDeleted: '',
 103          themeDeleted: '',
 104          livePreview: '',
 105          activatePlugin: '',
 106          activateTheme: '',
 107          activatePluginLabel: '',
 108          activateThemeLabel: '',
 109          activateImporter: '',
 110          activateImporterLabel: '',
 111          unknownError: '',
 112          connectionError: '',
 113          nonceError: '',
 114          pluginsFound: '',
 115          noPluginsFound: '',
 116          autoUpdatesEnable: '',
 117          autoUpdatesEnabling: '',
 118          autoUpdatesEnabled: '',
 119          autoUpdatesDisable: '',
 120          autoUpdatesDisabling: '',
 121          autoUpdatesDisabled: '',
 122          autoUpdatesError: ''
 123      };
 124  
 125      wp.updates.l10n = window.wp.deprecateL10nObject( 'wp.updates.l10n', wp.updates.l10n, '5.5.0' );
 126  
 127      /**
 128       * User nonce for ajax calls.
 129       *
 130       * @since 4.2.0
 131       *
 132       * @type {string}
 133       */
 134      wp.updates.ajaxNonce = settings.ajax_nonce;
 135  
 136      /**
 137       * Current search term.
 138       *
 139       * @since 4.6.0
 140       *
 141       * @type {string}
 142       */
 143      wp.updates.searchTerm = '';
 144  
 145      /**
 146       * Minimum number of characters before an ajax search is fired.
 147       *
 148       * @since 6.7.0
 149       *
 150       * @type {number}
 151       */
 152      wp.updates.searchMinCharacters = 2;
 153  
 154      /**
 155       * Whether filesystem credentials need to be requested from the user.
 156       *
 157       * @since 4.2.0
 158       *
 159       * @type {boolean}
 160       */
 161      wp.updates.shouldRequestFilesystemCredentials = false;
 162  
 163      /**
 164       * Filesystem credentials to be packaged along with the request.
 165       *
 166       * @since 4.2.0
 167       * @since 4.6.0 Added `available` property to indicate whether credentials have been provided.
 168       *
 169       * @type {Object}
 170       * @property {Object}  filesystemCredentials.ftp                Holds FTP credentials.
 171       * @property {string}  filesystemCredentials.ftp.host           FTP host. Default empty string.
 172       * @property {string}  filesystemCredentials.ftp.username       FTP user name. Default empty string.
 173       * @property {string}  filesystemCredentials.ftp.password       FTP password. Default empty string.
 174       * @property {string}  filesystemCredentials.ftp.connectionType Type of FTP connection. 'ssh', 'ftp', or 'ftps'.
 175       *                                                              Default empty string.
 176       * @property {Object}  filesystemCredentials.ssh                Holds SSH credentials.
 177       * @property {string}  filesystemCredentials.ssh.publicKey      The public key. Default empty string.
 178       * @property {string}  filesystemCredentials.ssh.privateKey     The private key. Default empty string.
 179       * @property {string}  filesystemCredentials.fsNonce            Filesystem credentials form nonce.
 180       * @property {boolean} filesystemCredentials.available          Whether filesystem credentials have been provided.
 181       *                                                              Default 'false'.
 182       */
 183      wp.updates.filesystemCredentials = {
 184          ftp:       {
 185              host:           '',
 186              username:       '',
 187              password:       '',
 188              connectionType: ''
 189          },
 190          ssh:       {
 191              publicKey:  '',
 192              privateKey: ''
 193          },
 194          fsNonce: '',
 195          available: false
 196      };
 197  
 198      /**
 199       * Whether we're waiting for an Ajax request to complete.
 200       *
 201       * @since 4.2.0
 202       * @since 4.6.0 More accurately named `ajaxLocked`.
 203       *
 204       * @type {boolean}
 205       */
 206      wp.updates.ajaxLocked = false;
 207  
 208      /**
 209       * Admin notice template.
 210       *
 211       * @since 4.6.0
 212       *
 213       * @type {Function}
 214       */
 215      wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
 216  
 217      /**
 218       * Update queue.
 219       *
 220       * If the user tries to update a plugin while an update is
 221       * already happening, it can be placed in this queue to perform later.
 222       *
 223       * @since 4.2.0
 224       * @since 4.6.0 More accurately named `queue`.
 225       *
 226       * @type {Array.object}
 227       */
 228      wp.updates.queue = [];
 229  
 230      /**
 231       * Holds a jQuery reference to return focus to when exiting the request credentials modal.
 232       *
 233       * @since 4.2.0
 234       *
 235       * @type {jQuery}
 236       */
 237      wp.updates.$elToReturnFocusToFromCredentialsModal = undefined;
 238  
 239      /**
 240       * Adds or updates an admin notice.
 241       *
 242       * @since 4.6.0
 243       *
 244       * @param {Object} data                 The data for the admin notice.
 245       * @param {*}      [data.selector]      Optional. Selector of an element to be replaced with the admin notice.
 246       * @param {string} [data.id]            Optional. Unique id that will be used as the notice's id attribute.
 247       * @param {string} [data.className]     Optional. Class names that will be used in the admin notice.
 248       * @param {string} [data.message]       Optional. The message displayed in the notice.
 249       * @param {number} [data.successes]     Optional. The amount of successful operations.
 250       * @param {number} [data.errors]        Optional. The amount of failed operations.
 251       * @param {Array}  [data.errorMessages] Optional. Error messages of failed operations.
 252       *
 253       */
 254      wp.updates.addAdminNotice = function( data ) {
 255          var $notice = $( data.selector ),
 256              $headerEnd = $( '.wp-header-end' ),
 257              $adminNotice;
 258  
 259          delete data.selector;
 260          $adminNotice = wp.updates.adminNotice( data );
 261  
 262          // Check if this admin notice already exists.
 263          if ( ! $notice.length ) {
 264              $notice = $( '#' + data.id );
 265          }
 266  
 267          if ( $notice.length ) {
 268              $notice.replaceWith( $adminNotice );
 269          } else if ( $headerEnd.length ) {
 270              $headerEnd.after( $adminNotice );
 271          } else {
 272              if ( 'customize' === pagenow ) {
 273                  $( '.customize-themes-notifications' ).append( $adminNotice );
 274              } else {
 275                  $( '.wrap' ).find( '> h1' ).after( $adminNotice );
 276              }
 277          }
 278  
 279          $document.trigger( 'wp-updates-notice-added' );
 280      };
 281  
 282      /**
 283       * Handles Ajax requests to WordPress.
 284       *
 285       * @since 4.6.0
 286       *
 287       * @param {string} action The type of Ajax request ('update-plugin', 'install-theme', etc).
 288       * @param {Object} data   Data that needs to be passed to the ajax callback.
 289       * @return {$.promise}    A jQuery promise that represents the request,
 290       *                        decorated with an abort() method.
 291       */
 292      wp.updates.ajax = function( action, data ) {
 293          var options = {};
 294  
 295          if ( wp.updates.ajaxLocked ) {
 296              wp.updates.queue.push( {
 297                  action: action,
 298                  data:   data
 299              } );
 300  
 301              // Return a Deferred object so callbacks can always be registered.
 302              return $.Deferred();
 303          }
 304  
 305          wp.updates.ajaxLocked = true;
 306  
 307          if ( data.success ) {
 308              options.success = data.success;
 309              delete data.success;
 310          }
 311  
 312          if ( data.error ) {
 313              options.error = data.error;
 314              delete data.error;
 315          }
 316  
 317          options.data = _.extend( data, {
 318              action:          action,
 319              _ajax_nonce:     wp.updates.ajaxNonce,
 320              _fs_nonce:       wp.updates.filesystemCredentials.fsNonce,
 321              username:        wp.updates.filesystemCredentials.ftp.username,
 322              password:        wp.updates.filesystemCredentials.ftp.password,
 323              hostname:        wp.updates.filesystemCredentials.ftp.hostname,
 324              connection_type: wp.updates.filesystemCredentials.ftp.connectionType,
 325              public_key:      wp.updates.filesystemCredentials.ssh.publicKey,
 326              private_key:     wp.updates.filesystemCredentials.ssh.privateKey
 327          } );
 328  
 329          return wp.ajax.send( options ).always( wp.updates.ajaxAlways );
 330      };
 331  
 332      /**
 333       * Actions performed after every Ajax request.
 334       *
 335       * @since 4.6.0
 336       *
 337       * @param {Object} response             The response object from the Ajax request.
 338       * @param {Array}  [response.debug]     Optional. Debug information.
 339       * @param {string} [response.errorCode] Optional. Error code for an error that occurred.
 340       */
 341      wp.updates.ajaxAlways = function( response ) {
 342          if ( ! response.errorCode || 'unable_to_connect_to_filesystem' !== response.errorCode ) {
 343              wp.updates.ajaxLocked = false;
 344              wp.updates.queueChecker();
 345          }
 346  
 347          if ( 'undefined' !== typeof response.debug && window.console && window.console.log ) {
 348              _.map( response.debug, function( message ) {
 349                  // Remove all HTML tags and write a message to the console.
 350                  window.console.log( wp.sanitize.stripTagsAndEncodeText( message ) );
 351              } );
 352          }
 353      };
 354  
 355      /**
 356       * Refreshes update counts everywhere on the screen.
 357       *
 358       * @since 4.7.0
 359       */
 360      wp.updates.refreshCount = function() {
 361          var $adminBarUpdates              = $( '#wp-admin-bar-updates' ),
 362              $dashboardNavMenuUpdateCount  = $( 'a[href="update-core.php"] .update-plugins' ),
 363              $pluginsNavMenuUpdateCount    = $( 'a[href="plugins.php"] .update-plugins' ),
 364              $appearanceNavMenuUpdateCount = $( 'a[href="themes.php"] .update-plugins' ),
 365              itemCount;
 366  
 367          $adminBarUpdates.find( '.ab-label' ).text( settings.totals.counts.total );
 368          $adminBarUpdates.find( '.updates-available-text' ).text(
 369              sprintf(
 370                  /* translators: %s: Total number of updates available. */
 371                  _n( '%s update available', '%s updates available', settings.totals.counts.total ),
 372                  settings.totals.counts.total
 373              )
 374          );
 375  
 376          // Remove the update count from the toolbar if it's zero.
 377          if ( 0 === settings.totals.counts.total ) {
 378              $adminBarUpdates.find( '.ab-label' ).parents( 'li' ).remove();
 379          }
 380  
 381          // Update the "Updates" menu item.
 382          $dashboardNavMenuUpdateCount.each( function( index, element ) {
 383              element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.total );
 384          } );
 385          if ( settings.totals.counts.total > 0 ) {
 386              $dashboardNavMenuUpdateCount.find( '.update-count' ).text( settings.totals.counts.total );
 387          } else {
 388              $dashboardNavMenuUpdateCount.remove();
 389          }
 390  
 391          // Update the "Plugins" menu item.
 392          $pluginsNavMenuUpdateCount.each( function( index, element ) {
 393              element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.plugins );
 394          } );
 395          if ( settings.totals.counts.total > 0 ) {
 396              $pluginsNavMenuUpdateCount.find( '.plugin-count' ).text( settings.totals.counts.plugins );
 397          } else {
 398              $pluginsNavMenuUpdateCount.remove();
 399          }
 400  
 401          // Update the "Appearance" menu item.
 402          $appearanceNavMenuUpdateCount.each( function( index, element ) {
 403              element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.themes );
 404          } );
 405          if ( settings.totals.counts.total > 0 ) {
 406              $appearanceNavMenuUpdateCount.find( '.theme-count' ).text( settings.totals.counts.themes );
 407          } else {
 408              $appearanceNavMenuUpdateCount.remove();
 409          }
 410  
 411          // Update list table filter navigation.
 412          if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
 413              itemCount = settings.totals.counts.plugins;
 414          } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) {
 415              itemCount = settings.totals.counts.themes;
 416          }
 417  
 418          if ( itemCount > 0 ) {
 419              $( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' );
 420          } else {
 421              $( '.subsubsub .upgrade' ).remove();
 422              $( '.subsubsub li:last' ).html( function() { return $( this ).children(); } );
 423          }
 424      };
 425  
 426      /**
 427       * Sends a message from a modal to the main screen to update buttons in plugin cards.
 428       *
 429       * @since 6.5.0
 430       *
 431       * @param {Object} data                 An object of data to use for the button.
 432       * @param {string} data.slug            The plugin's slug.
 433       * @param {string} data.text            The text to use for the button.
 434       * @param {string} data.ariaLabel       The value for the button's aria-label attribute. An empty string removes the attribute.
 435       * @param {string} [data.status]        Optional. An identifier for the status.
 436       * @param {string} [data.removeClasses] Optional. A space-separated list of classes to remove from the button.
 437       * @param {string} [data.addClasses]    Optional. A space-separated list of classes to add to the button.
 438       * @param {string} [data.href]          Optional. The button's URL.
 439       * @param {string} [data.pluginName]    Optional. The plugin's name.
 440       * @param {string} [data.plugin]        Optional. The plugin file, relative to the plugins directory.
 441       */
 442      wp.updates.setCardButtonStatus = function( data ) {
 443          var target = window.parent === window ? null : window.parent;
 444  
 445          $.support.postMessage = !! window.postMessage;
 446          if ( false !== $.support.postMessage && null !== target && -1 === window.parent.location.pathname.indexOf( 'index.php' ) ) {
 447              target.postMessage( JSON.stringify( data ), window.location.origin );
 448          }
 449      };
 450  
 451      /**
 452       * Decrements the update counts throughout the various menus.
 453       *
 454       * This includes the toolbar, the "Updates" menu item and the menu items
 455       * for plugins and themes.
 456       *
 457       * @since 3.9.0
 458       *
 459       * @param {string} type The type of item that was updated or deleted.
 460       *                      Can be 'plugin', 'theme'.
 461       */
 462      wp.updates.decrementCount = function( type ) {
 463          settings.totals.counts.total = Math.max( --settings.totals.counts.total, 0 );
 464  
 465          if ( 'plugin' === type ) {
 466              settings.totals.counts.plugins = Math.max( --settings.totals.counts.plugins, 0 );
 467          } else if ( 'theme' === type ) {
 468              settings.totals.counts.themes = Math.max( --settings.totals.counts.themes, 0 );
 469          }
 470  
 471          wp.updates.refreshCount( type );
 472      };
 473  
 474      /**
 475       * Sends an Ajax request to the server to update a plugin.
 476       *
 477       * @since 4.2.0
 478       * @since 4.6.0 More accurately named `updatePlugin`.
 479       *
 480       * @param {Object}              args           Arguments.
 481       * @param {string}              args.plugin    Plugin basename.
 482       * @param {string}              args.slug      Plugin slug.
 483       * @param {updatePluginSuccess} [args.success] Optional. Success callback. Default: wp.updates.updatePluginSuccess
 484       * @param {updatePluginError}   [args.error]   Optional. Error callback. Default: wp.updates.updatePluginError
 485       * @return {$.promise} A jQuery promise that represents the request,
 486       *                     decorated with an abort() method.
 487       */
 488      wp.updates.updatePlugin = function( args ) {
 489          var $updateRow, $card, $message, message,
 490              $adminBarUpdates = $( '#wp-admin-bar-updates' ),
 491              buttonText = __( 'Updating...' ),
 492              isPluginInstall = 'plugin-install' === pagenow || 'plugin-install-network' === pagenow;
 493  
 494          args = _.extend( {
 495              success: wp.updates.updatePluginSuccess,
 496              error: wp.updates.updatePluginError
 497          }, args );
 498  
 499          if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
 500              $updateRow = $( 'tr[data-plugin="' + args.plugin + '"]' );
 501              $message   = $updateRow.find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
 502              message    = sprintf(
 503                  /* translators: %s: Plugin name and version. */
 504                   _x( 'Updating %s...', 'plugin' ),
 505                  $updateRow.find( '.plugin-title strong' ).text()
 506              );
 507          } else if ( isPluginInstall ) {
 508              $card    = $( '.plugin-card-' + args.slug + ', #plugin-information-footer' );
 509              $message = $card.find( '.update-now' ).addClass( 'updating-message' );
 510              message    = sprintf(
 511                  /* translators: %s: Plugin name and version. */
 512                   _x( 'Updating %s...', 'plugin' ),
 513                  $message.data( 'name' )
 514              );
 515  
 516              // Remove previous error messages, if any.
 517              $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove();
 518          }
 519  
 520          $adminBarUpdates.addClass( 'spin' );
 521  
 522          if ( $message.html() !== __( 'Updating...' ) ) {
 523              $message.data( 'originaltext', $message.html() );
 524          }
 525  
 526          $message
 527              .attr( 'aria-label', message )
 528              .text( buttonText );
 529  
 530          $document.trigger( 'wp-plugin-updating', args );
 531  
 532          if ( isPluginInstall && 'plugin-information-footer' === $card.attr( 'id' ) ) {
 533              wp.updates.setCardButtonStatus(
 534                  {
 535                      status: 'updating-plugin',
 536                      slug: args.slug,
 537                      addClasses: 'updating-message',
 538                      text: buttonText,
 539                      ariaLabel: message
 540                  }
 541              );
 542          }
 543  
 544          return wp.updates.ajax( 'update-plugin', args );
 545      };
 546  
 547      /**
 548       * Updates the UI appropriately after a successful plugin update.
 549       *
 550       * @since 4.2.0
 551       * @since 4.6.0 More accurately named `updatePluginSuccess`.
 552       * @since 5.5.0 Auto-update "time to next update" text cleared.
 553       *
 554       * @param {Object} response            Response from the server.
 555       * @param {string} response.slug       Slug of the plugin to be updated.
 556       * @param {string} response.plugin     Basename of the plugin to be updated.
 557       * @param {string} response.pluginName Name of the plugin to be updated.
 558       * @param {string} response.oldVersion Old version of the plugin.
 559       * @param {string} response.newVersion New version of the plugin.
 560       */
 561      wp.updates.updatePluginSuccess = function( response ) {
 562          var $pluginRow, $updateMessage, newText,
 563              $adminBarUpdates = $( '#wp-admin-bar-updates' ),
 564              buttonText = _x( 'Updated!', 'plugin' ),
 565              ariaLabel = sprintf(
 566                  /* translators: %s: Plugin name and version. */
 567                  _x( '%s updated!', 'plugin' ),
 568                  response.pluginName
 569              );
 570  
 571          if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
 572              $pluginRow     = $( 'tr[data-plugin="' + response.plugin + '"]' )
 573                  .removeClass( 'update is-enqueued' )
 574                  .addClass( 'updated' );
 575              $updateMessage = $pluginRow.find( '.update-message' )
 576                  .removeClass( 'updating-message notice-warning' )
 577                  .addClass( 'updated-message notice-success' ).find( 'p' );
 578  
 579              // Update the version number in the row.
 580              newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
 581              $pluginRow.find( '.plugin-version-author-uri' ).html( newText );
 582  
 583              // Clear the "time to next auto-update" text.
 584              $pluginRow.find( '.auto-update-time' ).empty();
 585          } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
 586              $updateMessage = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.update-now' )
 587                  .removeClass( 'updating-message' )
 588                  .addClass( 'button-disabled updated-message' );
 589          }
 590  
 591          $adminBarUpdates.removeClass( 'spin' );
 592  
 593          $updateMessage
 594              .attr( 'aria-label', ariaLabel )
 595              .text( buttonText );
 596  
 597          wp.a11y.speak( __( 'Update completed successfully.' ) );
 598  
 599          if ( 'plugin_install_from_iframe' !== $updateMessage.attr( 'id' ) ) {
 600              wp.updates.decrementCount( 'plugin' );
 601          } else {
 602              wp.updates.setCardButtonStatus(
 603                  {
 604                      status: 'updated-plugin',
 605                      slug: response.slug,
 606                      removeClasses: 'updating-message',
 607                      addClasses: 'button-disabled updated-message',
 608                      text: buttonText,
 609                      ariaLabel: ariaLabel
 610                  }
 611              );
 612          }
 613  
 614          $document.trigger( 'wp-plugin-update-success', response );
 615      };
 616  
 617      /**
 618       * Updates the UI appropriately after a failed plugin update.
 619       *
 620       * @since 4.2.0
 621       * @since 4.6.0 More accurately named `updatePluginError`.
 622       *
 623       * @param {Object} response              Response from the server.
 624       * @param {string} response.slug         Slug of the plugin to be updated.
 625       * @param {string} response.plugin       Basename of the plugin to be updated.
 626       * @param {string} response.pluginName   Optional. Name of the plugin to be updated.
 627       * @param {string} response.errorCode    Error code for the error that occurred.
 628       * @param {string} response.errorMessage The error that occurred.
 629       */
 630      wp.updates.updatePluginError = function( response ) {
 631          var $pluginRow, $card, $message, errorMessage, buttonText, ariaLabel,
 632              $adminBarUpdates = $( '#wp-admin-bar-updates' );
 633  
 634          if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
 635              return;
 636          }
 637  
 638          if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) {
 639              return;
 640          }
 641  
 642          errorMessage = sprintf(
 643              /* translators: %s: Error string for a failed update. */
 644              __( 'Update failed: %s' ),
 645              response.errorMessage
 646          );
 647  
 648          if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
 649              $pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' ).removeClass( 'is-enqueued' );
 650  
 651              if ( response.plugin ) {
 652                  $message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' );
 653              } else {
 654                  $message = $( 'tr[data-slug="' + response.slug + '"]' ).find( '.update-message' );
 655              }
 656              $message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage );
 657  
 658              if ( response.pluginName ) {
 659                  $message.find( 'p' )
 660                      .attr(
 661                          'aria-label',
 662                          sprintf(
 663                              /* translators: %s: Plugin name and version. */
 664                              _x( '%s update failed.', 'plugin' ),
 665                              response.pluginName
 666                          )
 667                      );
 668              } else {
 669                  $message.find( 'p' ).removeAttr( 'aria-label' );
 670              }
 671          } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
 672              buttonText = __( 'Update failed.' );
 673  
 674              $card = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' )
 675                  .append( wp.updates.adminNotice( {
 676                      className: 'update-message notice-error notice-alt is-dismissible',
 677                      message:   errorMessage
 678                  } ) );
 679  
 680              if ( $card.hasClass( 'plugin-card-' + response.slug ) ) {
 681                  $card.addClass( 'plugin-card-update-failed' );
 682              }
 683  
 684              $card.find( '.update-now' )
 685                  .text( buttonText )
 686                  .removeClass( 'updating-message' );
 687  
 688              if ( response.pluginName ) {
 689                  ariaLabel = sprintf(
 690                      /* translators: %s: Plugin name and version. */
 691                      _x( '%s update failed.', 'plugin' ),
 692                      response.pluginName
 693                  );
 694  
 695                  $card.find( '.update-now' ).attr( 'aria-label', ariaLabel );
 696              } else {
 697                  ariaLabel = '';
 698                  $card.find( '.update-now' ).removeAttr( 'aria-label' );
 699              }
 700  
 701              $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
 702  
 703                  // Use same delay as the total duration of the notice fadeTo + slideUp animation.
 704                  setTimeout( function() {
 705                      $card
 706                          .removeClass( 'plugin-card-update-failed' )
 707                          .find( '.column-name a' ).trigger( 'focus' );
 708  
 709                      $card.find( '.update-now' )
 710                          .attr( 'aria-label', false )
 711                          .text( __( 'Update Now' ) );
 712                  }, 200 );
 713              } );
 714          }
 715  
 716          $adminBarUpdates.removeClass( 'spin' );
 717  
 718          wp.a11y.speak( errorMessage, 'assertive' );
 719  
 720          if ( 'plugin-information-footer' === $card.attr('id' ) ) {
 721              wp.updates.setCardButtonStatus(
 722                  {
 723                      status: 'plugin-update-failed',
 724                      slug: response.slug,
 725                      removeClasses: 'updating-message',
 726                      text: buttonText,
 727                      ariaLabel: ariaLabel
 728                  }
 729              );
 730          }
 731  
 732          $document.trigger( 'wp-plugin-update-error', response );
 733      };
 734  
 735      /**
 736       * Sends an Ajax request to the server to install a plugin.
 737       *
 738       * @since 4.6.0
 739       *
 740       * @param {Object}               args         Arguments.
 741       * @param {string}               args.slug    Plugin identifier in the WordPress.org Plugin repository.
 742       * @param {installPluginSuccess} args.success Optional. Success callback. Default: wp.updates.installPluginSuccess
 743       * @param {installPluginError}   args.error   Optional. Error callback. Default: wp.updates.installPluginError
 744       * @return {$.promise} A jQuery promise that represents the request,
 745       *                     decorated with an abort() method.
 746       */
 747      wp.updates.installPlugin = function( args ) {
 748          var $card    = $( '.plugin-card-' + args.slug + ', #plugin-information-footer' ),
 749              $message = $card.find( '.install-now' ),
 750              buttonText = __( 'Installing...' ),
 751              ariaLabel;
 752  
 753          args = _.extend( {
 754              success: wp.updates.installPluginSuccess,
 755              error: wp.updates.installPluginError
 756          }, args );
 757  
 758          if ( 'import' === pagenow ) {
 759              $message = $( '[data-slug="' + args.slug + '"]' );
 760          }
 761  
 762          if ( $message.html() !== __( 'Installing...' ) ) {
 763              $message.data( 'originaltext', $message.html() );
 764          }
 765  
 766          ariaLabel = sprintf(
 767              /* translators: %s: Plugin name and version. */
 768              _x( 'Installing %s...', 'plugin' ),
 769              $message.data( 'name' )
 770          );
 771  
 772          $message
 773              .addClass( 'updating-message' )
 774              .attr( 'aria-label', ariaLabel )
 775              .text( buttonText );
 776  
 777          wp.a11y.speak( __( 'Installing... please wait.' ) );
 778  
 779          // Remove previous error messages, if any.
 780          $card.removeClass( 'plugin-card-install-failed' ).find( '.notice.notice-error' ).remove();
 781  
 782          $document.trigger( 'wp-plugin-installing', args );
 783  
 784          if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
 785              wp.updates.setCardButtonStatus(
 786                  {
 787                      status: 'installing-plugin',
 788                      slug: args.slug,
 789                      addClasses: 'updating-message',
 790                      text: buttonText,
 791                      ariaLabel: ariaLabel
 792                  }
 793              );
 794          }
 795  
 796          return wp.updates.ajax( 'install-plugin', args );
 797      };
 798  
 799      /**
 800       * Updates the UI appropriately after a successful plugin install.
 801       *
 802       * @since 4.6.0
 803       *
 804       * @param {Object} response             Response from the server.
 805       * @param {string} response.slug        Slug of the installed plugin.
 806       * @param {string} response.pluginName  Name of the installed plugin.
 807       * @param {string} response.activateUrl URL to activate the just installed plugin.
 808       */
 809      wp.updates.installPluginSuccess = function( response ) {
 810          var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.install-now' ),
 811              buttonText = _x( 'Installed!', 'plugin' ),
 812              ariaLabel = sprintf(
 813                  /* translators: %s: Plugin name and version. */
 814                  _x( '%s installed!', 'plugin' ),
 815                  response.pluginName
 816              );
 817  
 818          $message
 819              .removeClass( 'updating-message' )
 820              .addClass( 'updated-message installed button-disabled' )
 821              .attr( 'aria-label', ariaLabel )
 822              .text( buttonText );
 823  
 824          wp.a11y.speak( __( 'Installation completed successfully.' ) );
 825  
 826          $document.trigger( 'wp-plugin-install-success', response );
 827  
 828          if ( response.activateUrl ) {
 829              setTimeout( function() {
 830                  wp.updates.checkPluginDependencies( {
 831                      slug: response.slug
 832                  } );
 833              }, 1000 );
 834          }
 835  
 836          if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
 837              wp.updates.setCardButtonStatus(
 838                  {
 839                      status: 'installed-plugin',
 840                      slug: response.slug,
 841                      removeClasses: 'updating-message',
 842                      addClasses: 'updated-message installed button-disabled',
 843                      text: buttonText,
 844                      ariaLabel: ariaLabel
 845                  }
 846              );
 847          }
 848      };
 849  
 850      /**
 851       * Updates the UI appropriately after a failed plugin install.
 852       *
 853       * @since 4.6.0
 854       *
 855       * @param {Object} response              Response from the server.
 856       * @param {string} response.slug         Slug of the plugin to be installed.
 857       * @param {string} response.pluginName   Optional. Name of the plugin to be installed.
 858       * @param {string} response.errorCode    Error code for the error that occurred.
 859       * @param {string} response.errorMessage The error that occurred.
 860       */
 861      wp.updates.installPluginError = function( response ) {
 862          var $card   = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ),
 863              $button = $card.find( '.install-now' ),
 864              buttonText = __( 'Installation failed.' ),
 865              ariaLabel = sprintf(
 866                  /* translators: %s: Plugin name and version. */
 867                  _x( '%s installation failed', 'plugin' ),
 868                  $button.data( 'name' )
 869              ),
 870              errorMessage;
 871  
 872          if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
 873              return;
 874          }
 875  
 876          if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
 877              return;
 878          }
 879  
 880          errorMessage = sprintf(
 881              /* translators: %s: Error string for a failed installation. */
 882              __( 'Installation failed: %s' ),
 883              response.errorMessage
 884          );
 885  
 886          $card
 887              .addClass( 'plugin-card-update-failed' )
 888              .append( '<div class="notice notice-error notice-alt is-dismissible" role="alert"><p>' + errorMessage + '</p></div>' );
 889  
 890          $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
 891  
 892              // Use same delay as the total duration of the notice fadeTo + slideUp animation.
 893              setTimeout( function() {
 894                  $card
 895                      .removeClass( 'plugin-card-update-failed' )
 896                      .find( '.column-name a' ).trigger( 'focus' );
 897              }, 200 );
 898          } );
 899  
 900          $button
 901              .removeClass( 'updating-message' ).addClass( 'button-disabled' )
 902              .attr( 'aria-label', ariaLabel )
 903              .text( buttonText );
 904  
 905          wp.a11y.speak( errorMessage, 'assertive' );
 906  
 907          wp.updates.setCardButtonStatus(
 908              {
 909                  status: 'plugin-install-failed',
 910                  slug: response.slug,
 911                  removeClasses: 'updating-message',
 912                  addClasses: 'button-disabled',
 913                  text: buttonText,
 914                  ariaLabel: ariaLabel
 915              }
 916          );
 917  
 918          $document.trigger( 'wp-plugin-install-error', response );
 919      };
 920  
 921      /**
 922       * Sends an Ajax request to the server to check a plugin's dependencies.
 923       *
 924       * @since 6.5.0
 925       *
 926       * @param {Object}                         args         Arguments.
 927       * @param {string}                         args.slug    Plugin identifier in the WordPress.org Plugin repository.
 928       * @param {checkPluginDependenciesSuccess} args.success Optional. Success callback. Default: wp.updates.checkPluginDependenciesSuccess
 929       * @param {checkPluginDependenciesError}   args.error   Optional. Error callback. Default: wp.updates.checkPluginDependenciesError
 930       * @return {$.promise} A jQuery promise that represents the request,
 931       *                     decorated with an abort() method.
 932       */
 933      wp.updates.checkPluginDependencies = function( args ) {
 934          args = _.extend( {
 935              success: wp.updates.checkPluginDependenciesSuccess,
 936              error: wp.updates.checkPluginDependenciesError
 937          }, args );
 938  
 939          wp.a11y.speak( __( 'Checking plugin dependencies... please wait.' ) );
 940          $document.trigger( 'wp-checking-plugin-dependencies', args );
 941  
 942          return wp.updates.ajax( 'check_plugin_dependencies', args );
 943      };
 944  
 945      /**
 946       * Updates the UI appropriately after a successful plugin dependencies check.
 947       *
 948       * @since 6.5.0
 949       *
 950       * @param {Object} response             Response from the server.
 951       * @param {string} response.slug        Slug of the checked plugin.
 952       * @param {string} response.pluginName  Name of the checked plugin.
 953       * @param {string} response.plugin      The plugin file, relative to the plugins directory.
 954       * @param {string} response.activateUrl URL to activate the just checked plugin.
 955       */
 956      wp.updates.checkPluginDependenciesSuccess = function( response ) {
 957          var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.install-now' ),
 958              buttonText, ariaLabel;
 959  
 960          // Transform the 'Install' button into an 'Activate' button.
 961          $message
 962              .removeClass( 'install-now installed button-disabled updated-message' )
 963              .addClass( 'activate-now button-primary' )
 964              .attr( 'href', response.activateUrl );
 965  
 966          wp.a11y.speak( __( 'Plugin dependencies check completed successfully.' ) );
 967          $document.trigger( 'wp-check-plugin-dependencies-success', response );
 968  
 969          if ( 'plugins-network' === pagenow || 'plugin-install-network' === pagenow ) {
 970              buttonText = _x( 'Network Activate', 'plugin' );
 971              ariaLabel  = sprintf(
 972                  /* translators: %s: Plugin name. */
 973                  _x( 'Network Activate %s', 'plugin' ),
 974                  response.pluginName
 975              );
 976  
 977              $message
 978                  .attr( 'aria-label', ariaLabel )
 979                  .text( buttonText );
 980          } else {
 981              buttonText = _x( 'Activate', 'plugin' );
 982              ariaLabel = sprintf(
 983                  /* translators: %s: Plugin name. */
 984                  _x( 'Activate %s', 'plugin' ),
 985                  response.pluginName
 986              );
 987  
 988              $message
 989                  .attr( 'aria-label', ariaLabel )
 990                  .attr( 'data-name', response.pluginName )
 991                  .attr( 'data-slug', response.slug )
 992                  .attr( 'data-plugin', response.plugin )
 993                  .text( buttonText );
 994          }
 995  
 996          if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
 997              wp.updates.setCardButtonStatus(
 998                  {
 999                      status: 'dependencies-check-success',
1000                      slug: response.slug,
1001                      removeClasses: 'install-now installed button-disabled updated-message',
1002                      addClasses: 'activate-now button-primary',
1003                      text: buttonText,
1004                      ariaLabel: ariaLabel,
1005                      pluginName: response.pluginName,
1006                      plugin: response.plugin,
1007                      href: response.activateUrl
1008                  }
1009              );
1010          }
1011      };
1012  
1013      /**
1014       * Updates the UI appropriately after a failed plugin dependencies check.
1015       *
1016       * @since 6.5.0
1017       *
1018       * @param {Object} response              Response from the server.
1019       * @param {string} response.slug         Slug of the plugin to be checked.
1020       * @param {string} response.pluginName   Optional. Name of the plugin to be checked.
1021       * @param {string} response.errorCode    Error code for the error that occurred.
1022       * @param {string} response.errorMessage The error that occurred.
1023       */
1024      wp.updates.checkPluginDependenciesError = function( response ) {
1025          var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.install-now' ),
1026              buttonText = _x( 'Activate', 'plugin' ),
1027              ariaLabel = sprintf(
1028                  /* translators: 1: Plugin name, 2. The reason the plugin cannot be activated. */
1029                  _x( 'Cannot activate %1$s. %2$s', 'plugin' ),
1030                  response.pluginName,
1031                  response.errorMessage
1032              ),
1033              errorMessage;
1034  
1035          if ( ! wp.updates.isValidResponse( response, 'check-dependencies' ) ) {
1036              return;
1037          }
1038  
1039          errorMessage = sprintf(
1040              /* translators: %s: Error string for a failed activation. */
1041              __( 'Activation failed: %s' ),
1042              response.errorMessage
1043          );
1044  
1045          wp.a11y.speak( errorMessage, 'assertive' );
1046          $document.trigger( 'wp-check-plugin-dependencies-error', response );
1047  
1048          $message
1049              .removeClass( 'install-now installed updated-message' )
1050              .addClass( 'activate-now button-primary' )
1051              .attr( 'aria-label', ariaLabel )
1052              .text( buttonText );
1053  
1054          if ( 'plugin-information-footer' === $message.parent().attr('id' ) ) {
1055              wp.updates.setCardButtonStatus(
1056                  {
1057                      status: 'dependencies-check-failed',
1058                      slug: response.slug,
1059                      removeClasses: 'install-now installed updated-message',
1060                      addClasses: 'activate-now button-primary',
1061                      text: buttonText,
1062                      ariaLabel: ariaLabel
1063                  }
1064              );
1065          }
1066      };
1067  
1068      /**
1069       * Sends an Ajax request to the server to activate a plugin.
1070       *
1071       * @since 6.5.0
1072       *
1073       * @param {Object}                args         Arguments.
1074       * @param {string}                args.name    The name of the plugin.
1075       * @param {string}                args.slug    Plugin identifier in the WordPress.org Plugin repository.
1076       * @param {string}                args.plugin  The plugin file, relative to the plugins directory.
1077       * @param {activatePluginSuccess} args.success Optional. Success callback. Default: wp.updates.activatePluginSuccess
1078       * @param {activatePluginError}   args.error   Optional. Error callback. Default: wp.updates.activatePluginError
1079       * @return {$.promise} A jQuery promise that represents the request,
1080       *                     decorated with an abort() method.
1081       */
1082      wp.updates.activatePlugin = function( args ) {
1083          var $message = $( '.plugin-card-' + args.slug + ', #plugin-information-footer' ).find( '.activate-now, .activating-message' );
1084  
1085          args = _.extend( {
1086              success: wp.updates.activatePluginSuccess,
1087              error: wp.updates.activatePluginError
1088          }, args );
1089  
1090          wp.a11y.speak( __( 'Activating... please wait.' ) );
1091          $document.trigger( 'wp-activating-plugin', args );
1092  
1093          if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
1094              wp.updates.setCardButtonStatus(
1095                  {
1096                      status: 'activating-plugin',
1097                      slug: args.slug,
1098                      removeClasses: 'installed updated-message button-primary',
1099                      addClasses: 'activating-message',
1100                      text: __( 'Activating...' ),
1101                      ariaLabel: sprintf(
1102                          /* translators: %s: Plugin name. */
1103                          _x( 'Activating %s', 'plugin' ),
1104                          args.name
1105                      )
1106                  }
1107              );
1108          }
1109  
1110          return wp.updates.ajax( 'activate-plugin', args );
1111      };
1112  
1113      /**
1114       * Updates the UI appropriately after a successful plugin activation.
1115       *
1116       * @since 6.5.0
1117       *
1118       * @param {Object} response            Response from the server.
1119       * @param {string} response.slug       Slug of the activated plugin.
1120       * @param {string} response.pluginName Name of the activated plugin.
1121       * @param {string} response.plugin     The plugin file, relative to the plugins directory.
1122       */
1123      wp.updates.activatePluginSuccess = function( response ) {
1124          var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.activating-message' ),
1125              buttonText = _x( 'Activated!', 'plugin' ),
1126              ariaLabel = sprintf(
1127                  /* translators: %s: The plugin name. */
1128                  '%s activated successfully.',
1129                  response.pluginName
1130              );
1131  
1132          wp.a11y.speak( __( 'Activation completed successfully.' ) );
1133          $document.trigger( 'wp-plugin-activate-success', response );
1134  
1135          $message
1136              .removeClass( 'activating-message' )
1137              .addClass( 'activated-message button-disabled' )
1138              .attr( 'aria-label', ariaLabel )
1139              .text( buttonText );
1140  
1141          if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
1142              wp.updates.setCardButtonStatus(
1143                  {
1144                      status: 'activated-plugin',
1145                      slug: response.slug,
1146                      removeClasses: 'activating-message',
1147                      addClasses: 'activated-message button-disabled',
1148                      text: buttonText,
1149                      ariaLabel: ariaLabel
1150                  }
1151              );
1152          }
1153  
1154          setTimeout( function() {
1155              $message.removeClass( 'activated-message' )
1156              .text( _x( 'Active', 'plugin' ) );
1157  
1158              if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
1159                  wp.updates.setCardButtonStatus(
1160                      {
1161                          status: 'plugin-active',
1162                          slug: response.slug,
1163                          removeClasses: 'activated-message',
1164                          text: _x( 'Active', 'plugin' ),
1165                          ariaLabel: sprintf(
1166                              /* translators: %s: The plugin name. */
1167                              '%s is active.',
1168                              response.pluginName
1169                          )
1170                      }
1171                  );
1172              }
1173          }, 1000 );
1174      };
1175  
1176      /**
1177       * Updates the UI appropriately after a failed plugin activation.
1178       *
1179       * @since 6.5.0
1180       *
1181       * @param {Object} response              Response from the server.
1182       * @param {string} response.slug         Slug of the plugin to be activated.
1183       * @param {string} response.pluginName   Optional. Name of the plugin to be activated.
1184       * @param {string} response.errorCode    Error code for the error that occurred.
1185       * @param {string} response.errorMessage The error that occurred.
1186       */
1187      wp.updates.activatePluginError = function( response ) {
1188          var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.activating-message' ),
1189              buttonText = __( 'Activation failed.' ),
1190              ariaLabel = sprintf(
1191                  /* translators: %s: Plugin name. */
1192                  _x( '%s activation failed', 'plugin' ),
1193                  response.pluginName
1194              ),
1195              errorMessage;
1196  
1197          if ( ! wp.updates.isValidResponse( response, 'activate' ) ) {
1198              return;
1199          }
1200  
1201          errorMessage = sprintf(
1202              /* translators: %s: Error string for a failed activation. */
1203              __( 'Activation failed: %s' ),
1204              response.errorMessage
1205          );
1206  
1207          wp.a11y.speak( errorMessage, 'assertive' );
1208          $document.trigger( 'wp-plugin-activate-error', response );
1209  
1210          $message
1211              .removeClass( 'install-now installed activating-message' )
1212              .addClass( 'button-disabled' )
1213              .attr( 'aria-label', ariaLabel )
1214              .text( buttonText );
1215  
1216          if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) {
1217              wp.updates.setCardButtonStatus(
1218                  {
1219                      status: 'plugin-activation-failed',
1220                      slug: response.slug,
1221                      removeClasses: 'install-now installed activating-message',
1222                      addClasses: 'button-disabled',
1223                      text: buttonText,
1224                      ariaLabel: ariaLabel
1225                  }
1226              );
1227          }
1228      };
1229  
1230      /**
1231       * Updates the UI appropriately after a successful importer install.
1232       *
1233       * @since 4.6.0
1234       *
1235       * @param {Object} response             Response from the server.
1236       * @param {string} response.slug        Slug of the installed plugin.
1237       * @param {string} response.pluginName  Name of the installed plugin.
1238       * @param {string} response.activateUrl URL to activate the just installed plugin.
1239       */
1240      wp.updates.installImporterSuccess = function( response ) {
1241          wp.updates.addAdminNotice( {
1242              id:        'install-success',
1243              className: 'notice-success is-dismissible',
1244              message:   sprintf(
1245                  /* translators: %s: Activation URL. */
1246                  __( 'Importer installed successfully. <a href="%s">Run importer</a>' ),
1247                  response.activateUrl + '&from=import'
1248              )
1249          } );
1250  
1251          $( '[data-slug="' + response.slug + '"]' )
1252              .removeClass( 'install-now updating-message' )
1253              .addClass( 'activate-now' )
1254              .attr({
1255                  'href': response.activateUrl + '&from=import',
1256                  'aria-label':sprintf(
1257                      /* translators: %s: Importer name. */
1258                      __( 'Run %s' ),
1259                      response.pluginName
1260                  )
1261              })
1262              .text( __( 'Run Importer' ) );
1263  
1264          wp.a11y.speak( __( 'Installation completed successfully.' ) );
1265  
1266          $document.trigger( 'wp-importer-install-success', response );
1267      };
1268  
1269      /**
1270       * Updates the UI appropriately after a failed importer install.
1271       *
1272       * @since 4.6.0
1273       *
1274       * @param {Object} response              Response from the server.
1275       * @param {string} response.slug         Slug of the plugin to be installed.
1276       * @param {string} response.pluginName   Optional. Name of the plugin to be installed.
1277       * @param {string} response.errorCode    Error code for the error that occurred.
1278       * @param {string} response.errorMessage The error that occurred.
1279       */
1280      wp.updates.installImporterError = function( response ) {
1281          var errorMessage = sprintf(
1282                  /* translators: %s: Error string for a failed installation. */
1283                  __( 'Installation failed: %s' ),
1284                  response.errorMessage
1285              ),
1286              $installLink = $( '[data-slug="' + response.slug + '"]' ),
1287              pluginName = $installLink.data( 'name' );
1288  
1289          if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
1290              return;
1291          }
1292  
1293          if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
1294              return;
1295          }
1296  
1297          wp.updates.addAdminNotice( {
1298              id:        response.errorCode,
1299              className: 'notice-error is-dismissible',
1300              message:   errorMessage
1301          } );
1302  
1303          $installLink
1304              .removeClass( 'updating-message' )
1305              .attr(
1306                  'aria-label',
1307                  sprintf(
1308                      /* translators: %s: Plugin name. */
1309                      _x( 'Install %s now', 'plugin' ),
1310                      pluginName
1311                  )
1312              )
1313              .text( _x( 'Install Now', 'plugin' ) );
1314  
1315          wp.a11y.speak( errorMessage, 'assertive' );
1316  
1317          $document.trigger( 'wp-importer-install-error', response );
1318      };
1319  
1320      /**
1321       * Sends an Ajax request to the server to delete a plugin.
1322       *
1323       * @since 4.6.0
1324       *
1325       * @param {Object}              args         Arguments.
1326       * @param {string}              args.plugin  Basename of the plugin to be deleted.
1327       * @param {string}              args.slug    Slug of the plugin to be deleted.
1328       * @param {deletePluginSuccess} args.success Optional. Success callback. Default: wp.updates.deletePluginSuccess
1329       * @param {deletePluginError}   args.error   Optional. Error callback. Default: wp.updates.deletePluginError
1330       * @return {$.promise} A jQuery promise that represents the request,
1331       *                     decorated with an abort() method.
1332       */
1333      wp.updates.deletePlugin = function( args ) {
1334          var $link = $( '[data-plugin="' + args.plugin + '"]' ).find( '.row-actions a.delete' );
1335  
1336          args = _.extend( {
1337              success: wp.updates.deletePluginSuccess,
1338              error: wp.updates.deletePluginError
1339          }, args );
1340  
1341          if ( $link.html() !== __( 'Deleting...' ) ) {
1342              $link
1343                  .data( 'originaltext', $link.html() )
1344                  .text( __( 'Deleting...' ) );
1345          }
1346  
1347          wp.a11y.speak( __( 'Deleting...' ) );
1348  
1349          $document.trigger( 'wp-plugin-deleting', args );
1350  
1351          return wp.updates.ajax( 'delete-plugin', args );
1352      };
1353  
1354      /**
1355       * Updates the UI appropriately after a successful plugin deletion.
1356       *
1357       * @since 4.6.0
1358       *
1359       * @param {Object} response            Response from the server.
1360       * @param {string} response.slug       Slug of the plugin that was deleted.
1361       * @param {string} response.plugin     Base name of the plugin that was deleted.
1362       * @param {string} response.pluginName Name of the plugin that was deleted.
1363       */
1364      wp.updates.deletePluginSuccess = function( response ) {
1365  
1366          // Removes the plugin and updates rows.
1367          $( '[data-plugin="' + response.plugin + '"]' ).css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
1368              var $form            = $( '#bulk-action-form' ),
1369                  $views           = $( '.subsubsub' ),
1370                  $pluginRow       = $( this ),
1371                  $currentView     = $views.find( '[aria-current="page"]' ),
1372                  $itemsCount      = $( '.displaying-num' ),
1373                  columnCount      = $form.find( 'thead th:not(.hidden), thead td' ).length,
1374                  pluginDeletedRow = wp.template( 'item-deleted-row' ),
1375                  /**
1376                   * Plugins Base names of plugins in their different states.
1377                   *
1378                   * @type {Object}
1379                   */
1380                  plugins          = settings.plugins,
1381                  remainingCount;
1382  
1383              // Add a success message after deleting a plugin.
1384              if ( ! $pluginRow.hasClass( 'plugin-update-tr' ) ) {
1385                  $pluginRow.after(
1386                      pluginDeletedRow( {
1387                          slug:    response.slug,
1388                          plugin:  response.plugin,
1389                          colspan: columnCount,
1390                          name:    response.pluginName
1391                      } )
1392                  );
1393              }
1394  
1395              $pluginRow.remove();
1396  
1397              // Remove plugin from update count.
1398              if ( -1 !== _.indexOf( plugins.upgrade, response.plugin ) ) {
1399                  plugins.upgrade = _.without( plugins.upgrade, response.plugin );
1400                  wp.updates.decrementCount( 'plugin' );
1401              }
1402  
1403              // Remove from views.
1404              if ( -1 !== _.indexOf( plugins.inactive, response.plugin ) ) {
1405                  plugins.inactive = _.without( plugins.inactive, response.plugin );
1406                  if ( plugins.inactive.length ) {
1407                      $views.find( '.inactive .count' ).text( '(' + plugins.inactive.length + ')' );
1408                  } else {
1409                      $views.find( '.inactive' ).remove();
1410                  }
1411              }
1412  
1413              if ( -1 !== _.indexOf( plugins.active, response.plugin ) ) {
1414                  plugins.active = _.without( plugins.active, response.plugin );
1415                  if ( plugins.active.length ) {
1416                      $views.find( '.active .count' ).text( '(' + plugins.active.length + ')' );
1417                  } else {
1418                      $views.find( '.active' ).remove();
1419                  }
1420              }
1421  
1422              if ( -1 !== _.indexOf( plugins.recently_activated, response.plugin ) ) {
1423                  plugins.recently_activated = _.without( plugins.recently_activated, response.plugin );
1424                  if ( plugins.recently_activated.length ) {
1425                      $views.find( '.recently_activated .count' ).text( '(' + plugins.recently_activated.length + ')' );
1426                  } else {
1427                      $views.find( '.recently_activated' ).remove();
1428                  }
1429              }
1430  
1431              if ( -1 !== _.indexOf( plugins['auto-update-enabled'], response.plugin ) ) {
1432                  plugins['auto-update-enabled'] = _.without( plugins['auto-update-enabled'], response.plugin );
1433                  if ( plugins['auto-update-enabled'].length ) {
1434                      $views.find( '.auto-update-enabled .count' ).text( '(' + plugins['auto-update-enabled'].length + ')' );
1435                  } else {
1436                      $views.find( '.auto-update-enabled' ).remove();
1437                  }
1438              }
1439  
1440              if ( -1 !== _.indexOf( plugins['auto-update-disabled'], response.plugin ) ) {
1441                  plugins['auto-update-disabled'] = _.without( plugins['auto-update-disabled'], response.plugin );
1442                  if ( plugins['auto-update-disabled'].length ) {
1443                      $views.find( '.auto-update-disabled .count' ).text( '(' + plugins['auto-update-disabled'].length + ')' );
1444                  } else {
1445                      $views.find( '.auto-update-disabled' ).remove();
1446                  }
1447              }
1448  
1449              plugins.all = _.without( plugins.all, response.plugin );
1450  
1451              if ( plugins.all.length ) {
1452                  $views.find( '.all .count' ).text( '(' + plugins.all.length + ')' );
1453              } else {
1454                  $form.find( '.tablenav' ).css( { visibility: 'hidden' } );
1455                  $views.find( '.all' ).remove();
1456  
1457                  if ( ! $form.find( 'tr.no-items' ).length ) {
1458                      $form.find( '#the-list' ).append( '<tr class="no-items"><td class="colspanchange" colspan="' + columnCount + '">' + __( 'No plugins are currently available.' ) + '</td></tr>' );
1459                  }
1460              }
1461  
1462              if ( $itemsCount.length && $currentView.length ) {
1463                  remainingCount = plugins[ $currentView.parent( 'li' ).attr('class') ].length;
1464                  $itemsCount.text(
1465                      sprintf(
1466                          /* translators: %s: The remaining number of plugins. */
1467                          _nx( '%s item', '%s items', remainingCount, 'plugin/plugins'  ),
1468                          remainingCount
1469                      )
1470                  );
1471              }
1472          } );
1473  
1474          wp.a11y.speak( _x( 'Deleted!', 'plugin' ) );
1475  
1476          $document.trigger( 'wp-plugin-delete-success', response );
1477      };
1478  
1479      /**
1480       * Updates the UI appropriately after a failed plugin deletion.
1481       *
1482       * @since 4.6.0
1483       *
1484       * @param {Object} response              Response from the server.
1485       * @param {string} response.slug         Slug of the plugin to be deleted.
1486       * @param {string} response.plugin       Base name of the plugin to be deleted
1487       * @param {string} response.pluginName   Optional. Name of the plugin to be deleted.
1488       * @param {string} response.errorCode    Error code for the error that occurred.
1489       * @param {string} response.errorMessage The error that occurred.
1490       */
1491      wp.updates.deletePluginError = function( response ) {
1492          var $plugin, $pluginUpdateRow,
1493              pluginUpdateRow  = wp.template( 'item-update-row' ),
1494              noticeContent    = wp.updates.adminNotice( {
1495                  className: 'update-message notice-error notice-alt',
1496                  message:   response.errorMessage
1497              } );
1498  
1499          if ( response.plugin ) {
1500              $plugin          = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' );
1501              $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' );
1502          } else {
1503              $plugin          = $( 'tr.inactive[data-slug="' + response.slug + '"]' );
1504              $pluginUpdateRow = $plugin.siblings( '[data-slug="' + response.slug + '"]' );
1505          }
1506  
1507          if ( ! wp.updates.isValidResponse( response, 'delete' ) ) {
1508              return;
1509          }
1510  
1511          if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) {
1512              return;
1513          }
1514  
1515          // Add a plugin update row if it doesn't exist yet.
1516          if ( ! $pluginUpdateRow.length ) {
1517              $plugin.addClass( 'update' ).after(
1518                  pluginUpdateRow( {
1519                      slug:    response.slug,
1520                      plugin:  response.plugin || response.slug,
1521                      colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
1522                      content: noticeContent
1523                  } )
1524              );
1525          } else {
1526  
1527              // Remove previous error messages, if any.
1528              $pluginUpdateRow.find( '.notice-error' ).remove();
1529  
1530              $pluginUpdateRow.find( '.plugin-update' ).append( noticeContent );
1531          }
1532  
1533          $document.trigger( 'wp-plugin-delete-error', response );
1534      };
1535  
1536      /**
1537       * Sends an Ajax request to the server to update a theme.
1538       *
1539       * @since 4.6.0
1540       *
1541       * @param {Object}             args         Arguments.
1542       * @param {string}             args.slug    Theme stylesheet.
1543       * @param {updateThemeSuccess} args.success Optional. Success callback. Default: wp.updates.updateThemeSuccess
1544       * @param {updateThemeError}   args.error   Optional. Error callback. Default: wp.updates.updateThemeError
1545       * @return {$.promise} A jQuery promise that represents the request,
1546       *                     decorated with an abort() method.
1547       */
1548      wp.updates.updateTheme = function( args ) {
1549          var $notice;
1550  
1551          args = _.extend( {
1552              success: wp.updates.updateThemeSuccess,
1553              error: wp.updates.updateThemeError
1554          }, args );
1555  
1556          if ( 'themes-network' === pagenow ) {
1557              $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
1558  
1559          } else if ( 'customize' === pagenow ) {
1560  
1561              // Update the theme details UI.
1562              $notice = $( '[data-slug="' + args.slug + '"].notice' ).removeClass( 'notice-large' );
1563  
1564              $notice.find( 'h3' ).remove();
1565  
1566              // Add the top-level UI, and update both.
1567              $notice = $notice.add( $( '#customize-control-installed_theme_' + args.slug ).find( '.update-message' ) );
1568              $notice = $notice.addClass( 'updating-message' ).find( 'p' );
1569  
1570          } else {
1571              $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' );
1572  
1573              $notice.find( 'h3' ).remove();
1574  
1575              $notice = $notice.add( $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ) );
1576              $notice = $notice.addClass( 'updating-message' ).find( 'p' );
1577          }
1578  
1579          if ( $notice.html() !== __( 'Updating...' ) ) {
1580              $notice.data( 'originaltext', $notice.html() );
1581          }
1582  
1583          wp.a11y.speak( __( 'Updating... please wait.' ) );
1584          $notice.text( __( 'Updating...' ) );
1585  
1586          $document.trigger( 'wp-theme-updating', args );
1587  
1588          return wp.updates.ajax( 'update-theme', args );
1589      };
1590  
1591      /**
1592       * Updates the UI appropriately after a successful theme update.
1593       *
1594       * @since 4.6.0
1595       * @since 5.5.0 Auto-update "time to next update" text cleared.
1596       *
1597       * @param {Object} response            The response object from the Ajax request.
1598       * @param {string} response.slug       Slug of the theme to be updated.
1599       * @param {Object} response.theme      Updated theme.
1600       * @param {string} response.oldVersion Old version of the theme.
1601       * @param {string} response.newVersion New version of the theme.
1602       */
1603      wp.updates.updateThemeSuccess = function( response ) {
1604          var isModalOpen    = $( 'body.modal-open' ).length,
1605              $theme         = $( '[data-slug="' + response.slug + '"]' ),
1606              updatedMessage = {
1607                  className: 'updated-message notice-success notice-alt',
1608                  message:   _x( 'Updated!', 'theme' )
1609              },
1610              $notice, newText;
1611  
1612          if ( 'customize' === pagenow ) {
1613              $theme = $( '.updating-message' ).siblings( '.theme-name' );
1614  
1615              if ( $theme.length ) {
1616  
1617                  // Update the version number in the row.
1618                  newText = $theme.html().replace( response.oldVersion, response.newVersion );
1619                  $theme.html( newText );
1620              }
1621  
1622              $notice = $( '.theme-info .notice' ).add( wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' ).find( '.update-message' ) );
1623          } else if ( 'themes-network' === pagenow ) {
1624              $notice = $theme.find( '.update-message' );
1625  
1626              // Update the version number in the row.
1627              newText = $theme.find( '.theme-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
1628              $theme.find( '.theme-version-author-uri' ).html( newText );
1629  
1630              // Clear the "time to next auto-update" text.
1631              $theme.find( '.auto-update-time' ).empty();
1632          } else {
1633              $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) );
1634  
1635              // Focus on Customize button after updating.
1636              if ( isModalOpen ) {
1637                  $( '.load-customize:visible' ).trigger( 'focus' );
1638                  $( '.theme-info .theme-autoupdate' ).find( '.auto-update-time' ).empty();
1639              } else {
1640                  $theme.find( '.load-customize' ).trigger( 'focus' );
1641              }
1642          }
1643  
1644          wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) );
1645          wp.a11y.speak( __( 'Update completed successfully.' ) );
1646  
1647          wp.updates.decrementCount( 'theme' );
1648  
1649          $document.trigger( 'wp-theme-update-success', response );
1650  
1651          // Show updated message after modal re-rendered.
1652          if ( isModalOpen && 'customize' !== pagenow ) {
1653              $( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) );
1654          }
1655      };
1656  
1657      /**
1658       * Updates the UI appropriately after a failed theme update.
1659       *
1660       * @since 4.6.0
1661       *
1662       * @param {Object} response              Response from the server.
1663       * @param {string} response.slug         Slug of the theme to be updated.
1664       * @param {string} response.errorCode    Error code for the error that occurred.
1665       * @param {string} response.errorMessage The error that occurred.
1666       */
1667      wp.updates.updateThemeError = function( response ) {
1668          var $theme       = $( '[data-slug="' + response.slug + '"]' ),
1669              errorMessage = sprintf(
1670                  /* translators: %s: Error string for a failed update. */
1671                   __( 'Update failed: %s' ),
1672                  response.errorMessage
1673              ),
1674              $notice;
1675  
1676          if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
1677              return;
1678          }
1679  
1680          if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) {
1681              return;
1682          }
1683  
1684          if ( 'customize' === pagenow ) {
1685              $theme = wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' );
1686          }
1687  
1688          if ( 'themes-network' === pagenow ) {
1689              $notice = $theme.find( '.update-message ' );
1690          } else {
1691              $notice = $( '.theme-info .notice' ).add( $theme.find( '.notice' ) );
1692  
1693              $( 'body.modal-open' ).length ? $( '.load-customize:visible' ).trigger( 'focus' ) : $theme.find( '.load-customize' ).trigger( 'focus');
1694          }
1695  
1696          wp.updates.addAdminNotice( {
1697              selector:  $notice,
1698              className: 'update-message notice-error notice-alt is-dismissible',
1699              message:   errorMessage
1700          } );
1701  
1702          wp.a11y.speak( errorMessage );
1703  
1704          $document.trigger( 'wp-theme-update-error', response );
1705      };
1706  
1707      /**
1708       * Sends an Ajax request to the server to install a theme.
1709       *
1710       * @since 4.6.0
1711       *
1712       * @param {Object}              args         The arguments for the theme installation request.
1713       * @param {string}              args.slug    Theme stylesheet.
1714       * @param {installThemeSuccess} args.success Optional. Success callback. Default: wp.updates.installThemeSuccess
1715       * @param {installThemeError}   args.error   Optional. Error callback. Default: wp.updates.installThemeError
1716       * @return {$.promise} A jQuery promise that represents the request,
1717       *                     decorated with an abort() method.
1718       */
1719      wp.updates.installTheme = function( args ) {
1720          var $message = $( '.theme-install[data-slug="' + args.slug + '"]' );
1721  
1722          args = _.extend( {
1723              success: wp.updates.installThemeSuccess,
1724              error: wp.updates.installThemeError
1725          }, args );
1726  
1727          $message.addClass( 'updating-message' );
1728          $message.parents( '.theme' ).addClass( 'focus' );
1729          if ( $message.html() !== __( 'Installing...' ) ) {
1730              $message.data( 'originaltext', $message.html() );
1731          }
1732  
1733          $message
1734              .attr(
1735                  'aria-label',
1736                  sprintf(
1737                      /* translators: %s: Theme name and version. */
1738                      _x( 'Installing %s...', 'theme' ),
1739                      $message.data( 'name' )
1740                  )
1741              )
1742              .text( __( 'Installing...' ) );
1743  
1744          wp.a11y.speak( __( 'Installing... please wait.' ) );
1745  
1746          // Remove previous error messages, if any.
1747          $( '.install-theme-info, [data-slug="' + args.slug + '"]' ).removeClass( 'theme-install-failed' ).find( '.notice.notice-error' ).remove();
1748  
1749          $document.trigger( 'wp-theme-installing', args );
1750  
1751          return wp.updates.ajax( 'install-theme', args );
1752      };
1753  
1754      /**
1755       * Updates the UI appropriately after a successful theme install.
1756       *
1757       * @since 4.6.0
1758       *
1759       * @param {Object} response              Response from the server.
1760       * @param {string} response.slug         Slug of the theme to be installed.
1761       * @param {string} response.customizeUrl URL to the Customizer for the just installed theme.
1762       * @param {string} response.activateUrl  URL to activate the just installed theme.
1763       */
1764      wp.updates.installThemeSuccess = function( response ) {
1765          var $card = $( '.wp-full-overlay-header, [data-slug=' + response.slug + ']' ),
1766              $message;
1767  
1768          $document.trigger( 'wp-theme-install-success', response );
1769  
1770          $message = $card.find( '.button-primary' )
1771              .removeClass( 'updating-message' )
1772              .addClass( 'updated-message disabled' )
1773              .attr(
1774                  'aria-label',
1775                  sprintf(
1776                      /* translators: %s: Theme name and version. */
1777                      _x( '%s installed!', 'theme' ),
1778                      response.themeName
1779                  )
1780              )
1781              .text( _x( 'Installed!', 'theme' ) );
1782  
1783          wp.a11y.speak( __( 'Installation completed successfully.' ) );
1784  
1785          setTimeout( function() {
1786  
1787              if ( response.activateUrl ) {
1788  
1789                  // Transform the 'Install' button into an 'Activate' button.
1790                  $message
1791                      .attr( 'href', response.activateUrl )
1792                      .removeClass( 'theme-install updated-message disabled' )
1793                      .addClass( 'activate' );
1794  
1795                  if ( 'themes-network' === pagenow ) {
1796                      $message
1797                          .attr(
1798                              'aria-label',
1799                              sprintf(
1800                                  /* translators: %s: Theme name. */
1801                                  _x( 'Network Activate %s', 'theme' ),
1802                                  response.themeName
1803                              )
1804                          )
1805                          .text( __( 'Network Enable' ) );
1806                  } else {
1807                      $message
1808                          .attr(
1809                              'aria-label',
1810                              sprintf(
1811                                  /* translators: %s: Theme name. */
1812                                  _x( 'Activate %s', 'theme' ),
1813                                  response.themeName
1814                              )
1815                          )
1816                          .text( _x( 'Activate', 'theme' ) );
1817                  }
1818              }
1819  
1820              if ( response.customizeUrl ) {
1821  
1822                  // Transform the 'Preview' button into a 'Live Preview' button.
1823                  $message.siblings( '.preview' ).replaceWith( function () {
1824                      return $( '<a>' )
1825                          .attr( 'href', response.customizeUrl )
1826                          .addClass( 'button load-customize' )
1827                          .text( __( 'Live Preview' ) );
1828                  } );
1829              }
1830          }, 1000 );
1831      };
1832  
1833      /**
1834       * Updates the UI appropriately after a failed theme install.
1835       *
1836       * @since 4.6.0
1837       *
1838       * @param {Object} response              Response from the server.
1839       * @param {string} response.slug         Slug of the theme to be installed.
1840       * @param {string} response.errorCode    Error code for the error that occurred.
1841       * @param {string} response.errorMessage The error that occurred.
1842       */
1843      wp.updates.installThemeError = function( response ) {
1844          var $card, $button,
1845              errorMessage = sprintf(
1846                  /* translators: %s: Error string for a failed installation. */
1847                  __( 'Installation failed: %s' ),
1848                  response.errorMessage
1849              ),
1850              $message     = wp.updates.adminNotice( {
1851                  className: 'update-message notice-error notice-alt',
1852                  message:   errorMessage
1853              } );
1854  
1855          if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
1856              return;
1857          }
1858  
1859          if ( wp.updates.maybeHandleCredentialError( response, 'install-theme' ) ) {
1860              return;
1861          }
1862  
1863          if ( 'customize' === pagenow ) {
1864              if ( $document.find( 'body' ).hasClass( 'modal-open' ) ) {
1865                  $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
1866                  $card   = $( '.theme-overlay .theme-info' ).prepend( $message );
1867              } else {
1868                  $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
1869                  $card   = $button.closest( '.theme' ).addClass( 'theme-install-failed' ).append( $message );
1870              }
1871              wp.customize.notifications.remove( 'theme_installing' );
1872          } else {
1873              if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) {
1874                  $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
1875                  $card   = $( '.install-theme-info' ).prepend( $message );
1876              } else {
1877                  $card   = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message );
1878                  $button = $card.find( '.theme-install' );
1879              }
1880          }
1881  
1882          $button
1883              .removeClass( 'updating-message' )
1884              .attr(
1885                  'aria-label',
1886                  sprintf(
1887                      /* translators: %s: Theme name and version. */
1888                      _x( '%s installation failed', 'theme' ),
1889                      $button.data( 'name' )
1890                  )
1891              )
1892              .text( __( 'Installation failed.' ) );
1893  
1894          wp.a11y.speak( errorMessage, 'assertive' );
1895  
1896          $document.trigger( 'wp-theme-install-error', response );
1897      };
1898  
1899      /**
1900       * Sends an Ajax request to the server to delete a theme.
1901       *
1902       * @since 4.6.0
1903       *
1904       * @param {Object}             args         The arguments for the theme deletion request.
1905       * @param {string}             args.slug    Theme stylesheet.
1906       * @param {deleteThemeSuccess} args.success Optional. Success callback. Default: wp.updates.deleteThemeSuccess
1907       * @param {deleteThemeError}   args.error   Optional. Error callback. Default: wp.updates.deleteThemeError
1908       * @return {$.promise} A jQuery promise that represents the request,
1909       *                     decorated with an abort() method.
1910       */
1911      wp.updates.deleteTheme = function( args ) {
1912          var $button;
1913  
1914          if ( 'themes' === pagenow ) {
1915              $button = $( '.theme-actions .delete-theme' );
1916          } else if ( 'themes-network' === pagenow ) {
1917              $button = $( '[data-slug="' + args.slug + '"]' ).find( '.row-actions a.delete' );
1918          }
1919  
1920          args = _.extend( {
1921              success: wp.updates.deleteThemeSuccess,
1922              error: wp.updates.deleteThemeError
1923          }, args );
1924  
1925          if ( $button && $button.html() !== __( 'Deleting...' ) ) {
1926              $button
1927                  .data( 'originaltext', $button.html() )
1928                  .text( __( 'Deleting...' ) );
1929          }
1930  
1931          wp.a11y.speak( __( 'Deleting...' ) );
1932  
1933          // Remove previous error messages, if any.
1934          $( '.theme-info .update-message' ).remove();
1935  
1936          $document.trigger( 'wp-theme-deleting', args );
1937  
1938          return wp.updates.ajax( 'delete-theme', args );
1939      };
1940  
1941      /**
1942       * Updates the UI appropriately after a successful theme deletion.
1943       *
1944       * @since 4.6.0
1945       *
1946       * @param {Object} response      Response from the server.
1947       * @param {string} response.slug Slug of the theme that was deleted.
1948       */
1949      wp.updates.deleteThemeSuccess = function( response ) {
1950          var $themeRows = $( '[data-slug="' + response.slug + '"]' );
1951  
1952          if ( 'themes-network' === pagenow ) {
1953  
1954              // Removes the theme and updates rows.
1955              $themeRows.css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
1956                  var $views     = $( '.subsubsub' ),
1957                      $themeRow  = $( this ),
1958                      themes     = settings.themes,
1959                      deletedRow = wp.template( 'item-deleted-row' );
1960  
1961                  if ( ! $themeRow.hasClass( 'plugin-update-tr' ) ) {
1962                      $themeRow.after(
1963                          deletedRow( {
1964                              slug:    response.slug,
1965                              colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
1966                              name:    $themeRow.find( '.theme-title strong' ).text()
1967                          } )
1968                      );
1969                  }
1970  
1971                  $themeRow.remove();
1972  
1973                  // Remove theme from update count.
1974                  if ( -1 !== _.indexOf( themes.upgrade, response.slug ) ) {
1975                      themes.upgrade = _.without( themes.upgrade, response.slug );
1976                      wp.updates.decrementCount( 'theme' );
1977                  }
1978  
1979                  // Remove from views.
1980                  if ( -1 !== _.indexOf( themes.disabled, response.slug ) ) {
1981                      themes.disabled = _.without( themes.disabled, response.slug );
1982                      if ( themes.disabled.length ) {
1983                          $views.find( '.disabled .count' ).text( '(' + themes.disabled.length + ')' );
1984                      } else {
1985                          $views.find( '.disabled' ).remove();
1986                      }
1987                  }
1988  
1989                  if ( -1 !== _.indexOf( themes['auto-update-enabled'], response.slug ) ) {
1990                      themes['auto-update-enabled'] = _.without( themes['auto-update-enabled'], response.slug );
1991                      if ( themes['auto-update-enabled'].length ) {
1992                          $views.find( '.auto-update-enabled .count' ).text( '(' + themes['auto-update-enabled'].length + ')' );
1993                      } else {
1994                          $views.find( '.auto-update-enabled' ).remove();
1995                      }
1996                  }
1997  
1998                  if ( -1 !== _.indexOf( themes['auto-update-disabled'], response.slug ) ) {
1999                      themes['auto-update-disabled'] = _.without( themes['auto-update-disabled'], response.slug );
2000                      if ( themes['auto-update-disabled'].length ) {
2001                          $views.find( '.auto-update-disabled .count' ).text( '(' + themes['auto-update-disabled'].length + ')' );
2002                      } else {
2003                          $views.find( '.auto-update-disabled' ).remove();
2004                      }
2005                  }
2006  
2007                  themes.all = _.without( themes.all, response.slug );
2008  
2009                  // There is always at least one theme available.
2010                  $views.find( '.all .count' ).text( '(' + themes.all.length + ')' );
2011              } );
2012          }
2013  
2014          // DecrementCount from update count.
2015          if ( 'themes' === pagenow ) {
2016              var theme = _.find( _wpThemeSettings.themes, { id: response.slug } );
2017              if ( theme.hasUpdate ) {
2018                  wp.updates.decrementCount( 'theme' );
2019              }
2020          }
2021  
2022          wp.a11y.speak( _x( 'Deleted!', 'theme' ) );
2023  
2024          $document.trigger( 'wp-theme-delete-success', response );
2025      };
2026  
2027      /**
2028       * Updates the UI appropriately after a failed theme deletion.
2029       *
2030       * @since 4.6.0
2031       *
2032       * @param {Object} response              Response from the server.
2033       * @param {string} response.slug         Slug of the theme to be deleted.
2034       * @param {string} response.errorCode    Error code for the error that occurred.
2035       * @param {string} response.errorMessage The error that occurred.
2036       */
2037      wp.updates.deleteThemeError = function( response ) {
2038          var $themeRow    = $( 'tr.inactive[data-slug="' + response.slug + '"]' ),
2039              $button      = $( '.theme-actions .delete-theme' ),
2040              updateRow    = wp.template( 'item-update-row' ),
2041              $updateRow   = $themeRow.siblings( '#' + response.slug + '-update' ),
2042              errorMessage = sprintf(
2043                  /* translators: %s: Error string for a failed deletion. */
2044                  __( 'Deletion failed: %s' ),
2045                  response.errorMessage
2046              ),
2047              $message     = wp.updates.adminNotice( {
2048                  className: 'update-message notice-error notice-alt',
2049                  message:   errorMessage
2050              } );
2051  
2052          if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) {
2053              return;
2054          }
2055  
2056          if ( 'themes-network' === pagenow ) {
2057              if ( ! $updateRow.length ) {
2058                  $themeRow.addClass( 'update' ).after(
2059                      updateRow( {
2060                          slug: response.slug,
2061                          colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
2062                          content: $message
2063                      } )
2064                  );
2065              } else {
2066                  // Remove previous error messages, if any.
2067                  $updateRow.find( '.notice-error' ).remove();
2068                  $updateRow.find( '.plugin-update' ).append( $message );
2069              }
2070          } else {
2071              $( '.theme-info .theme-description' ).before( $message );
2072          }
2073  
2074          $button.html( $button.data( 'originaltext' ) );
2075  
2076          wp.a11y.speak( errorMessage, 'assertive' );
2077  
2078          $document.trigger( 'wp-theme-delete-error', response );
2079      };
2080  
2081      /**
2082       * Adds the appropriate callback based on the type of action and the current page.
2083       *
2084       * @since 4.6.0
2085       * @private
2086       *
2087       * @param {Object} data   Ajax payload.
2088       * @param {string} action The type of request to perform.
2089       * @return {Object} The Ajax payload with the appropriate callbacks.
2090       */
2091      wp.updates._addCallbacks = function( data, action ) {
2092          if ( 'import' === pagenow && 'install-plugin' === action ) {
2093              data.success = wp.updates.installImporterSuccess;
2094              data.error   = wp.updates.installImporterError;
2095          }
2096  
2097          return data;
2098      };
2099  
2100      /**
2101       * Pulls available jobs from the queue and runs them.
2102       *
2103       * @since 4.2.0
2104       * @since 4.6.0 Can handle multiple job types.
2105       */
2106      wp.updates.queueChecker = function() {
2107          var job;
2108  
2109          if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) {
2110              return;
2111          }
2112  
2113          job = wp.updates.queue.shift();
2114  
2115          // Handle a queue job.
2116          switch ( job.action ) {
2117              case 'install-plugin':
2118                  wp.updates.installPlugin( job.data );
2119                  break;
2120  
2121              case 'update-plugin':
2122                  wp.updates.updatePlugin( job.data );
2123                  break;
2124  
2125              case 'delete-plugin':
2126                  wp.updates.deletePlugin( job.data );
2127                  break;
2128  
2129              case 'install-theme':
2130                  wp.updates.installTheme( job.data );
2131                  break;
2132  
2133              case 'update-theme':
2134                  wp.updates.updateTheme( job.data );
2135                  break;
2136  
2137              case 'delete-theme':
2138                  wp.updates.deleteTheme( job.data );
2139                  break;
2140  
2141              default:
2142                  break;
2143          }
2144      };
2145  
2146      /**
2147       * Requests the users filesystem credentials if they aren't already known.
2148       *
2149       * @since 4.2.0
2150       *
2151       * @param {Event} event Optional. Event interface.
2152       */
2153      wp.updates.requestFilesystemCredentials = function( event ) {
2154          if ( false === wp.updates.filesystemCredentials.available ) {
2155              /*
2156               * After exiting the credentials request modal,
2157               * return the focus to the element triggering the request.
2158               */
2159              if ( event && ! wp.updates.$elToReturnFocusToFromCredentialsModal ) {
2160                  wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target );
2161              }
2162  
2163              wp.updates.ajaxLocked = true;
2164              wp.updates.requestForCredentialsModalOpen();
2165          }
2166      };
2167  
2168      /**
2169       * Requests the users filesystem credentials if needed and there is no lock.
2170       *
2171       * @since 4.6.0
2172       *
2173       * @param {Event} event Optional. Event interface.
2174       */
2175      wp.updates.maybeRequestFilesystemCredentials = function( event ) {
2176          if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
2177              wp.updates.requestFilesystemCredentials( event );
2178          }
2179      };
2180  
2181      /**
2182       * Keydown handler for the request for credentials modal.
2183       *
2184       * Closes the modal when the escape key is pressed and
2185       * constrains keyboard navigation to inside the modal.
2186       *
2187       * @since 4.2.0
2188       *
2189       * @param {Event} event Event interface.
2190       */
2191      wp.updates.keydown = function( event ) {
2192          if ( 27 === event.keyCode ) {
2193              wp.updates.requestForCredentialsModalCancel();
2194          } else if ( 9 === event.keyCode ) {
2195  
2196              // #upgrade button must always be the last focus-able element in the dialog.
2197              if ( 'upgrade' === event.target.id && ! event.shiftKey ) {
2198                  $( '#hostname' ).trigger( 'focus' );
2199  
2200                  event.preventDefault();
2201              } else if ( 'hostname' === event.target.id && event.shiftKey ) {
2202                  $( '#upgrade' ).trigger( 'focus' );
2203  
2204                  event.preventDefault();
2205              }
2206          }
2207      };
2208  
2209      /**
2210       * Opens the request for credentials modal.
2211       *
2212       * @since 4.2.0
2213       */
2214      wp.updates.requestForCredentialsModalOpen = function() {
2215          var $modal = $( '#request-filesystem-credentials-dialog' );
2216  
2217          $( 'body' ).addClass( 'modal-open' );
2218          $modal.show();
2219          $modal.find( 'input:enabled:first' ).trigger( 'focus' );
2220          $modal.on( 'keydown', wp.updates.keydown );
2221      };
2222  
2223      /**
2224       * Closes the request for credentials modal.
2225       *
2226       * @since 4.2.0
2227       */
2228      wp.updates.requestForCredentialsModalClose = function() {
2229          $( '#request-filesystem-credentials-dialog' ).hide();
2230          $( 'body' ).removeClass( 'modal-open' );
2231  
2232          if ( wp.updates.$elToReturnFocusToFromCredentialsModal ) {
2233              wp.updates.$elToReturnFocusToFromCredentialsModal.trigger( 'focus' );
2234          }
2235      };
2236  
2237      /**
2238       * Takes care of the steps that need to happen when the modal is canceled out.
2239       *
2240       * @since 4.2.0
2241       * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions.
2242       */
2243      wp.updates.requestForCredentialsModalCancel = function() {
2244  
2245          // Not ajaxLocked and no queue means we already have cleared things up.
2246          if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) {
2247              return;
2248          }
2249  
2250          _.each( wp.updates.queue, function( job ) {
2251              $document.trigger( 'credential-modal-cancel', job );
2252          } );
2253  
2254          // Remove the lock, and clear the queue.
2255          wp.updates.ajaxLocked = false;
2256          wp.updates.queue = [];
2257  
2258          wp.updates.requestForCredentialsModalClose();
2259      };
2260  
2261      /**
2262       * Displays an error message in the request for credentials form.
2263       *
2264       * @since 4.2.0
2265       *
2266       * @param {string} message Error message.
2267       */
2268      wp.updates.showErrorInCredentialsForm = function( message ) {
2269          var $filesystemForm = $( '#request-filesystem-credentials-form' );
2270  
2271          // Remove any existing error.
2272          $filesystemForm.find( '.notice' ).remove();
2273          $filesystemForm.find( '#request-filesystem-credentials-title' ).after( '<div class="notice notice-alt notice-error" role="alert"><p>' + message + '</p></div>' );
2274      };
2275  
2276      /**
2277       * Handles credential errors and runs events that need to happen in that case.
2278       *
2279       * @since 4.2.0
2280       *
2281       * @param {Object} response Ajax response.
2282       * @param {string} action   The type of request to perform.
2283       */
2284      wp.updates.credentialError = function( response, action ) {
2285  
2286          // Restore callbacks.
2287          response = wp.updates._addCallbacks( response, action );
2288  
2289          wp.updates.queue.unshift( {
2290              action: action,
2291  
2292              /*
2293               * Not cool that we're depending on response for this data.
2294               * This would feel more whole in a view all tied together.
2295               */
2296              data: response
2297          } );
2298  
2299          wp.updates.filesystemCredentials.available = false;
2300          wp.updates.showErrorInCredentialsForm( response.errorMessage );
2301          wp.updates.requestFilesystemCredentials();
2302      };
2303  
2304      /**
2305       * Handles credentials errors if it could not connect to the filesystem.
2306       *
2307       * @since 4.6.0
2308       *
2309       * @param {Object} response              Response from the server.
2310       * @param {string} response.errorCode    Error code for the error that occurred.
2311       * @param {string} response.errorMessage The error that occurred.
2312       * @param {string} action                The type of request to perform.
2313       * @return {boolean} Whether there is an error that needs to be handled or not.
2314       */
2315      wp.updates.maybeHandleCredentialError = function( response, action ) {
2316          if ( wp.updates.shouldRequestFilesystemCredentials && response.errorCode && 'unable_to_connect_to_filesystem' === response.errorCode ) {
2317              wp.updates.credentialError( response, action );
2318              return true;
2319          }
2320  
2321          return false;
2322      };
2323  
2324      /**
2325       * Validates an Ajax response to ensure it's a proper object.
2326       *
2327       * If the response deems to be invalid, an admin notice is being displayed.
2328       *
2329       * @param {(Object|string)} response                Response from the server.
2330       * @param {Function}        [response.always]       Optional. Callback for when the Deferred is resolved or rejected.
2331       * @param {string}          [response.statusText]   Optional. Status message corresponding to the status code.
2332       * @param {string}          [response.responseText] Optional. Request response as text.
2333       * @param {string}          action                  Type of action the response is referring to. Can be 'delete',
2334       *                                                  'update' or 'install'.
2335       * @return {boolean} Whether the response is valid or not.
2336       */
2337      wp.updates.isValidResponse = function( response, action ) {
2338          var error = __( 'An error occurred during the update process. Please try again.' ),
2339              errorMessage;
2340  
2341          // Make sure the response is a valid data object and not a Promise object.
2342          if ( _.isObject( response ) && ! _.isFunction( response.always ) ) {
2343              return true;
2344          }
2345  
2346          if ( _.isString( response ) && '-1' === response ) {
2347              error = __( 'An error has occurred. Please reload the page and try again.' );
2348          } else if ( _.isString( response ) ) {
2349              error = response;
2350          } else if ( 'undefined' !== typeof response.readyState && 0 === response.readyState ) {
2351              error = __( 'Connection lost or the server is busy. Please try again later.' );
2352          } else if ( _.isString( response.responseText ) && '' !== response.responseText ) {
2353              error = response.responseText;
2354          } else if ( _.isString( response.statusText ) ) {
2355              error = response.statusText;
2356          }
2357  
2358          switch ( action ) {
2359              case 'update':
2360                  /* translators: %s: Error string for a failed update. */
2361                  errorMessage = __( 'Update failed: %s' );
2362                  break;
2363  
2364              case 'install':
2365                  /* translators: %s: Error string for a failed installation. */
2366                  errorMessage = __( 'Installation failed: %s' );
2367                  break;
2368  
2369              case 'check-dependencies':
2370                  /* translators: %s: Error string for a failed dependencies check. */
2371                  errorMessage = __( 'Dependencies check failed: %s' );
2372                  break;
2373  
2374              case 'activate':
2375                  /* translators: %s: Error string for a failed activation. */
2376                  errorMessage = __( 'Activation failed: %s' );
2377                  break;
2378  
2379              case 'delete':
2380                  /* translators: %s: Error string for a failed deletion. */
2381                  errorMessage = __( 'Deletion failed: %s' );
2382                  break;
2383          }
2384  
2385          // Messages are escaped, remove HTML tags to make them more readable.
2386          error = error.replace( /<[\/a-z][^<>]*>/gi, '' );
2387          errorMessage = errorMessage.replace( '%s', error );
2388  
2389          // Add admin notice.
2390          wp.updates.addAdminNotice( {
2391              id:        'unknown_error',
2392              className: 'notice-error is-dismissible',
2393              message:   _.escape( errorMessage )
2394          } );
2395  
2396          // Remove the lock, and clear the queue.
2397          wp.updates.ajaxLocked = false;
2398          wp.updates.queue      = [];
2399  
2400          // Change buttons of all running updates.
2401          $( '.button.updating-message' )
2402              .removeClass( 'updating-message' )
2403              .removeAttr( 'aria-label' )
2404              .prop( 'disabled', true )
2405              .text( __( 'Update failed.' ) );
2406  
2407          $( '.updating-message:not(.button):not(.thickbox)' )
2408              .removeClass( 'updating-message notice-warning' )
2409              .addClass( 'notice-error' )
2410              .find( 'p' )
2411                  .removeAttr( 'aria-label' )
2412                  .text( errorMessage );
2413  
2414          wp.a11y.speak( errorMessage, 'assertive' );
2415  
2416          return false;
2417      };
2418  
2419      /**
2420       * Potentially adds an AYS to a user attempting to leave the page.
2421       *
2422       * If an update is on-going and a user attempts to leave the page,
2423       * opens an "Are you sure?" alert.
2424       *
2425       * @since 4.2.0
2426       * @return {string|void} Message to display in the "Are you sure?" alert, or nothing.
2427       */
2428      wp.updates.beforeunload = function() {
2429          if ( wp.updates.ajaxLocked ) {
2430              return __( 'Updates may not complete if you navigate away from this page.' );
2431          }
2432      };
2433  
2434      $( function() {
2435          var $pluginFilter        = $( '#plugin-filter, #plugin-information-footer' ),
2436              $bulkActionForm      = $( '#bulk-action-form' ),
2437              $filesystemForm      = $( '#request-filesystem-credentials-form' ),
2438              $filesystemModal     = $( '#request-filesystem-credentials-dialog' ),
2439              $pluginSearch        = $( '.plugins-php .wp-filter-search' ),
2440              $pluginInstallSearch = $( '.plugin-install-php .wp-filter-search' );
2441  
2442          settings = _.extend( settings, window._wpUpdatesItemCounts || {} );
2443  
2444          if ( settings.totals ) {
2445              wp.updates.refreshCount();
2446          }
2447  
2448          /*
2449           * Whether a user needs to submit filesystem credentials.
2450           *
2451           * This is based on whether the form was output on the page server-side.
2452           *
2453           * @see {wp_print_request_filesystem_credentials_modal() in PHP}
2454           */
2455          wp.updates.shouldRequestFilesystemCredentials = $filesystemModal.length > 0;
2456  
2457          /**
2458           * File system credentials form submit noop-er / handler.
2459           *
2460           * @since 4.2.0
2461           */
2462          $filesystemModal.on( 'submit', 'form', function( event ) {
2463              event.preventDefault();
2464  
2465              // Persist the credentials input by the user for the duration of the page load.
2466              wp.updates.filesystemCredentials.ftp.hostname       = $( '#hostname' ).val();
2467              wp.updates.filesystemCredentials.ftp.username       = $( '#username' ).val();
2468              wp.updates.filesystemCredentials.ftp.password       = $( '#password' ).val();
2469              wp.updates.filesystemCredentials.ftp.connectionType = $( 'input[name="connection_type"]:checked' ).val();
2470              wp.updates.filesystemCredentials.ssh.publicKey      = $( '#public_key' ).val();
2471              wp.updates.filesystemCredentials.ssh.privateKey     = $( '#private_key' ).val();
2472              wp.updates.filesystemCredentials.fsNonce            = $( '#_fs_nonce' ).val();
2473              wp.updates.filesystemCredentials.available          = true;
2474  
2475              // Unlock and invoke the queue.
2476              wp.updates.ajaxLocked = false;
2477              wp.updates.queueChecker();
2478  
2479              wp.updates.requestForCredentialsModalClose();
2480          } );
2481  
2482          /**
2483           * Closes the request credentials modal when clicking the 'Cancel' button or outside of the modal.
2484           *
2485           * @since 4.2.0
2486           */
2487          $filesystemModal.on( 'click', '[data-js-action="close"], .notification-dialog-background', wp.updates.requestForCredentialsModalCancel );
2488  
2489          /**
2490           * Hide SSH fields when not selected.
2491           *
2492           * @since 4.2.0
2493           */
2494          $filesystemForm.on( 'change', 'input[name="connection_type"]', function() {
2495              $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) );
2496          } ).trigger( 'change' );
2497  
2498          /**
2499           * Handles events after the credential modal was closed.
2500           *
2501           * @since 4.6.0
2502           *
2503           * @param {Event}  event Event interface.
2504           * @param {string} job   The install/update.delete request.
2505           */
2506          $document.on( 'credential-modal-cancel', function( event, job ) {
2507              var $updatingMessage = $( '.updating-message' ),
2508                  $message, originalText;
2509  
2510              if ( 'import' === pagenow ) {
2511                  $updatingMessage.removeClass( 'updating-message' );
2512              } else if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
2513                  if ( 'update-plugin' === job.action ) {
2514                      $message = $( 'tr[data-plugin="' + job.data.plugin + '"]' ).find( '.update-message' );
2515                  } else if ( 'delete-plugin' === job.action ) {
2516                      $message = $( '[data-plugin="' + job.data.plugin + '"]' ).find( '.row-actions a.delete' );
2517                  }
2518              } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) {
2519                  if ( 'update-theme' === job.action ) {
2520                      $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.update-message' );
2521                  } else if ( 'delete-theme' === job.action && 'themes-network' === pagenow ) {
2522                      $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.row-actions a.delete' );
2523                  } else if ( 'delete-theme' === job.action && 'themes' === pagenow ) {
2524                      $message = $( '.theme-actions .delete-theme' );
2525                  }
2526              } else {
2527                  $message = $updatingMessage;
2528              }
2529  
2530              if ( $message && $message.hasClass( 'updating-message' ) ) {
2531                  originalText = $message.data( 'originaltext' );
2532  
2533                  if ( 'undefined' === typeof originalText ) {
2534                      originalText = $( '<p>' ).html( $message.find( 'p' ).data( 'originaltext' ) );
2535                  }
2536  
2537                  $message
2538                      .removeClass( 'updating-message' )
2539                      .html( originalText );
2540  
2541                  if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
2542                      if ( 'update-plugin' === job.action ) {
2543                          $message.attr(
2544                              'aria-label',
2545                              sprintf(
2546                                  /* translators: %s: Plugin name and version. */
2547                                  _x( 'Update %s now', 'plugin' ),
2548                                  $message.data( 'name' )
2549                              )
2550                          );
2551                      } else if ( 'install-plugin' === job.action ) {
2552                          $message.attr(
2553                              'aria-label',
2554                              sprintf(
2555                                  /* translators: %s: Plugin name. */
2556                                  _x( 'Install %s now', 'plugin' ),
2557                                  $message.data( 'name' )
2558                              )
2559                          );
2560                      }
2561                  }
2562              }
2563  
2564              wp.a11y.speak( __( 'Update canceled.' ) );
2565          } );
2566  
2567          /**
2568           * Click handler for plugin updates in List Table view.
2569           *
2570           * @since 4.2.0
2571           *
2572           * @param {Event} event Event interface.
2573           */
2574          $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) {
2575              var $message   = $( event.target ),
2576                  $pluginRow = $message.parents( 'tr' );
2577  
2578              event.preventDefault();
2579  
2580              if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
2581                  return;
2582              }
2583  
2584              wp.updates.maybeRequestFilesystemCredentials( event );
2585  
2586              // Return the user to the input box of the plugin's table row after closing the modal.
2587              wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' );
2588              wp.updates.updatePlugin( {
2589                  plugin: $pluginRow.data( 'plugin' ),
2590                  slug:   $pluginRow.data( 'slug' )
2591              } );
2592          } );
2593  
2594          /**
2595           * Click handler for plugin updates in plugin install view.
2596           *
2597           * @since 4.2.0
2598           *
2599           * @param {Event} event Event interface.
2600           */
2601          $pluginFilter.on( 'click', '.update-now', function( event ) {
2602              var $button = $( event.target );
2603              event.preventDefault();
2604  
2605              if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
2606                  return;
2607              }
2608  
2609              wp.updates.maybeRequestFilesystemCredentials( event );
2610  
2611              wp.updates.updatePlugin( {
2612                  plugin: $button.data( 'plugin' ),
2613                  slug:   $button.data( 'slug' )
2614              } );
2615          } );
2616  
2617          /**
2618           * Click handler for plugin installs in plugin install view.
2619           *
2620           * @since 4.6.0
2621           *
2622           * @param {Event} event Event interface.
2623           */
2624          $pluginFilter.on( 'click', '.install-now', function( event ) {
2625              var $button = $( event.target );
2626              event.preventDefault();
2627  
2628              if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
2629                  return;
2630              }
2631  
2632              if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
2633                  wp.updates.requestFilesystemCredentials( event );
2634  
2635                  $document.on( 'credential-modal-cancel', function() {
2636                      var $message = $( '.install-now.updating-message' );
2637  
2638                      $message
2639                          .removeClass( 'updating-message' )
2640                          .text( _x( 'Install Now', 'plugin' ) );
2641  
2642                      wp.a11y.speak( __( 'Update canceled.' ) );
2643                  } );
2644              }
2645  
2646              wp.updates.installPlugin( {
2647                  slug: $button.data( 'slug' )
2648              } );
2649          } );
2650  
2651          /**
2652           * Click handler for plugin activations in plugin activation modal view.
2653           *
2654           * @since 6.5.0
2655           * @since 6.5.4 Redirect the parent window to the activation URL.
2656           *
2657           * @param {Event} event Event interface.
2658           */
2659          $document.on( 'click', '#plugin-information-footer .activate-now', function( event ) {
2660              event.preventDefault();
2661              window.parent.location.href = $( event.target ).attr( 'href' );
2662          });
2663  
2664          /**
2665           * Click handler for importer plugins installs in the Import screen.
2666           *
2667           * @since 4.6.0
2668           *
2669           * @param {Event} event Event interface.
2670           */
2671          $document.on( 'click', '.importer-item .install-now', function( event ) {
2672              var $button = $( event.target ),
2673                  pluginName = $( this ).data( 'name' );
2674  
2675              event.preventDefault();
2676  
2677              if ( $button.hasClass( 'updating-message' ) ) {
2678                  return;
2679              }
2680  
2681              if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
2682                  wp.updates.requestFilesystemCredentials( event );
2683  
2684                  $document.on( 'credential-modal-cancel', function() {
2685  
2686                      $button
2687                          .removeClass( 'updating-message' )
2688                          .attr(
2689                              'aria-label',
2690                              sprintf(
2691                                  /* translators: %s: Plugin name. */
2692                                  _x( 'Install %s now', 'plugin' ),
2693                                  pluginName
2694                              )
2695                          )
2696                          .text( _x( 'Install Now', 'plugin' ) );
2697  
2698                      wp.a11y.speak( __( 'Update canceled.' ) );
2699                  } );
2700              }
2701  
2702              wp.updates.installPlugin( {
2703                  slug:    $button.data( 'slug' ),
2704                  pagenow: pagenow,
2705                  success: wp.updates.installImporterSuccess,
2706                  error:   wp.updates.installImporterError
2707              } );
2708          } );
2709  
2710          /**
2711           * Click handler for plugin deletions.
2712           *
2713           * @since 4.6.0
2714           *
2715           * @param {Event} event Event interface.
2716           */
2717          $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) {
2718              var $pluginRow = $( event.target ).parents( 'tr' ),
2719                  confirmMessage;
2720  
2721              if ( $pluginRow.hasClass( 'is-uninstallable' ) ) {
2722                  confirmMessage = sprintf(
2723                      /* translators: %s: Plugin name. */
2724                      __( 'Are you sure you want to delete %s and its data?' ),
2725                      $pluginRow.find( '.plugin-title strong' ).text()
2726                  );
2727              } else {
2728                  confirmMessage = sprintf(
2729                      /* translators: %s: Plugin name. */
2730                      __( 'Are you sure you want to delete %s?' ),
2731                      $pluginRow.find( '.plugin-title strong' ).text()
2732                  );
2733              }
2734  
2735              event.preventDefault();
2736  
2737              if ( ! window.confirm( confirmMessage ) ) {
2738                  return;
2739              }
2740  
2741              wp.updates.maybeRequestFilesystemCredentials( event );
2742  
2743              wp.updates.deletePlugin( {
2744                  plugin: $pluginRow.data( 'plugin' ),
2745                  slug:   $pluginRow.data( 'slug' )
2746              } );
2747  
2748          } );
2749  
2750          /**
2751           * Click handler for theme updates.
2752           *
2753           * @since 4.6.0
2754           *
2755           * @param {Event} event Event interface.
2756           */
2757          $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) {
2758              var $message  = $( event.target ),
2759                  $themeRow = $message.parents( 'tr' );
2760  
2761              event.preventDefault();
2762  
2763              if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
2764                  return;
2765              }
2766  
2767              wp.updates.maybeRequestFilesystemCredentials( event );
2768  
2769              // Return the user to the input box of the theme's table row after closing the modal.
2770              wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' );
2771              wp.updates.updateTheme( {
2772                  slug: $themeRow.data( 'slug' )
2773              } );
2774          } );
2775  
2776          /**
2777           * Click handler for theme deletions.
2778           *
2779           * @since 4.6.0
2780           *
2781           * @param {Event} event Event interface.
2782           */
2783          $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) {
2784              var $themeRow = $( event.target ).parents( 'tr' ),
2785                  confirmMessage = sprintf(
2786                      /* translators: %s: Theme name. */
2787                      __( 'Are you sure you want to delete %s?' ),
2788                      $themeRow.find( '.theme-title strong' ).text()
2789                  );
2790  
2791              event.preventDefault();
2792  
2793              if ( ! window.confirm( confirmMessage ) ) {
2794                  return;
2795              }
2796  
2797              wp.updates.maybeRequestFilesystemCredentials( event );
2798  
2799              wp.updates.deleteTheme( {
2800                  slug: $themeRow.data( 'slug' )
2801              } );
2802          } );
2803  
2804          /**
2805           * Bulk action handler for plugins and themes.
2806           *
2807           * Handles both deletions and updates.
2808           *
2809           * @since 4.6.0
2810           *
2811           * @param {Event} event Event interface.
2812           */
2813          $bulkActionForm.on( 'click', '[type="submit"]:not([name="clear-recent-list"])', function( event ) {
2814              var bulkAction    = $( event.target ).siblings( 'select' ).val(),
2815                  itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ),
2816                  success       = 0,
2817                  error         = 0,
2818                  errorMessages = [],
2819                  type, action;
2820  
2821              // Determine which type of item we're dealing with.
2822              switch ( pagenow ) {
2823                  case 'plugins':
2824                  case 'plugins-network':
2825                      type = 'plugin';
2826                      break;
2827  
2828                  case 'themes-network':
2829                      type = 'theme';
2830                      break;
2831  
2832                  default:
2833                      return;
2834              }
2835  
2836              // Bail if there were no items selected.
2837              if ( ! itemsSelected.length ) {
2838                  bulkAction = false;
2839              }
2840  
2841              // Determine the type of request we're dealing with.
2842              switch ( bulkAction ) {
2843                  case 'update-selected':
2844                      action = bulkAction.replace( 'selected', type );
2845                      break;
2846  
2847                  case 'delete-selected':
2848                      var confirmMessage = 'plugin' === type ?
2849                          __( 'Are you sure you want to delete the selected plugins and their data?' ) :
2850                          __( 'Caution: These themes may be active on other sites in the network. Are you sure you want to proceed?' );
2851  
2852                      if ( ! window.confirm( confirmMessage ) ) {
2853                          event.preventDefault();
2854                          return;
2855                      }
2856  
2857                      action = bulkAction.replace( 'selected', type );
2858                      break;
2859  
2860                  default:
2861                      return;
2862              }
2863  
2864              wp.updates.maybeRequestFilesystemCredentials( event );
2865  
2866              event.preventDefault();
2867  
2868              // Un-check the bulk checkboxes.
2869              $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false );
2870  
2871              $document.trigger( 'wp-' + type + '-bulk-' + bulkAction, itemsSelected );
2872  
2873              // Find all the checkboxes which have been checked.
2874              itemsSelected.each( function( index, element ) {
2875                  var $checkbox = $( element ),
2876                      $itemRow = $checkbox.parents( 'tr' );
2877  
2878                  // Only add update-able items to the update queue.
2879                  if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) {
2880  
2881                      // Un-check the box.
2882                      $checkbox.prop( 'checked', false );
2883                      return;
2884                  }
2885  
2886                  // Don't add items to the update queue again, even if the user clicks the update button several times.
2887                  if ( 'update-selected' === bulkAction && $itemRow.hasClass( 'is-enqueued' ) ) {
2888                      return;
2889                  }
2890  
2891                  $itemRow.addClass( 'is-enqueued' );
2892  
2893                  // Add it to the queue.
2894                  wp.updates.queue.push( {
2895                      action: action,
2896                      data:   {
2897                          plugin: $itemRow.data( 'plugin' ),
2898                          slug:   $itemRow.data( 'slug' )
2899                      }
2900                  } );
2901              } );
2902  
2903              // Display bulk notification for updates of any kind.
2904              $document.on( 'wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error', function( event, response ) {
2905                  var $itemRow = $( '[data-slug="' + response.slug + '"]' ),
2906                      $bulkActionNotice, itemName;
2907  
2908                  if ( 'wp-' + response.update + '-update-success' === event.type ) {
2909                      success++;
2910                  } else {
2911                      itemName = response.pluginName ? response.pluginName : $itemRow.find( '.column-primary strong' ).text();
2912  
2913                      error++;
2914                      errorMessages.push( itemName + ': ' + response.errorMessage );
2915                  }
2916  
2917                  $itemRow.find( 'input[name="checked[]"]:checked' ).prop( 'checked', false );
2918  
2919                  wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' );
2920  
2921                  var successMessage = null;
2922  
2923                  if ( success ) {
2924                      if ( 'plugin' === response.update ) {
2925                          successMessage = sprintf(
2926                              /* translators: %s: Number of plugins. */
2927                              _n( '%s plugin successfully updated.', '%s plugins successfully updated.', success ),
2928                              success
2929                          );
2930                      } else {
2931                          successMessage = sprintf(
2932                              /* translators: %s: Number of themes. */
2933                              _n( '%s theme successfully updated.', '%s themes successfully updated.', success ),
2934                              success
2935                          );
2936                      }
2937                  }
2938  
2939                  var errorMessage = null;
2940  
2941                  if ( error ) {
2942                      errorMessage = sprintf(
2943                          /* translators: %s: Number of failed updates. */
2944                          _n( '%s update failed.', '%s updates failed.', error ),
2945                          error
2946                      );
2947                  }
2948  
2949                  wp.updates.addAdminNotice( {
2950                      id:            'bulk-action-notice',
2951                      className:     'bulk-action-notice',
2952                      successMessage: successMessage,
2953                      errorMessage:   errorMessage,
2954                      errorMessages:  errorMessages,
2955                      type:           response.update
2956                  } );
2957  
2958                  $bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() {
2959                      // $( this ) is the clicked button, no need to get it again.
2960                      $( this )
2961                          .toggleClass( 'bulk-action-errors-collapsed' )
2962                          .attr( 'aria-expanded', ! $( this ).hasClass( 'bulk-action-errors-collapsed' ) );
2963                      // Show the errors list.
2964                      $bulkActionNotice.find( '.bulk-action-errors' ).toggleClass( 'hidden' );
2965                  } );
2966  
2967                  if ( error > 0 && ! wp.updates.queue.length ) {
2968                      $( 'html, body' ).animate( { scrollTop: 0 } );
2969                  }
2970              } );
2971  
2972              // Reset admin notice template after #bulk-action-notice was added.
2973              $document.on( 'wp-updates-notice-added', function() {
2974                  wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
2975              } );
2976  
2977              // Check the queue, now that the event handlers have been added.
2978              wp.updates.queueChecker();
2979          } );
2980  
2981          if ( $pluginInstallSearch.length ) {
2982              $pluginInstallSearch.attr( 'aria-describedby', 'live-search-desc' );
2983          }
2984  
2985          // Track the previous search string length.
2986          var previousSearchStringLength = 0;
2987          wp.updates.shouldSearch = function( searchStringLength ) {
2988              var shouldSearch = searchStringLength >= wp.updates.searchMinCharacters ||
2989                  previousSearchStringLength > wp.updates.searchMinCharacters;
2990              previousSearchStringLength = searchStringLength;
2991              return shouldSearch;
2992          };
2993  
2994          /**
2995           * Handles changes to the plugin search box on the new-plugin page,
2996           * searching the repository dynamically.
2997           *
2998           * @since 4.6.0
2999           */
3000          $pluginInstallSearch.on( 'keyup input', _.debounce( function( event, eventtype ) {
3001              var $searchTab = $( '.plugin-install-search' ), data, searchLocation,
3002                  searchStringLength = $pluginInstallSearch.val().length;
3003  
3004              data = {
3005                  _ajax_nonce: wp.updates.ajaxNonce,
3006                  s:           encodeURIComponent( event.target.value ),
3007                  tab:         'search',
3008                  type:        $( '#typeselector' ).val(),
3009                  pagenow:     pagenow
3010              };
3011              searchLocation = location.href.split( '?' )[ 0 ] + '?' + $.param( _.omit( data, [ '_ajax_nonce', 'pagenow' ] ) );
3012  
3013              // Set the autocomplete attribute, turning off autocomplete 1 character before ajax search kicks in.
3014              if ( wp.updates.shouldSearch( searchStringLength ) ) {
3015                  $pluginInstallSearch.attr( 'autocomplete', 'off' );
3016              } else {
3017                  $pluginInstallSearch.attr( 'autocomplete', 'on' );
3018                  return;
3019              }
3020  
3021              // Clear on escape.
3022              if ( 'keyup' === event.type && 27 === event.which ) {
3023                  event.target.value = '';
3024              }
3025  
3026              if ( wp.updates.searchTerm === data.s && 'typechange' !== eventtype ) {
3027                  return;
3028              } else {
3029                  $pluginFilter.empty();
3030                  wp.updates.searchTerm = data.s;
3031              }
3032  
3033              if ( window.history && window.history.replaceState ) {
3034                  window.history.replaceState( null, '', searchLocation );
3035              }
3036  
3037              if ( ! $searchTab.length ) {
3038                  $searchTab = $( '<li class="plugin-install-search" />' )
3039                      .append( $( '<a />', {
3040                          'class': 'current',
3041                          'href': searchLocation,
3042                          'text': __( 'Search Results' )
3043                      } ) );
3044  
3045                  $( '.wp-filter .filter-links .current' )
3046                      .removeClass( 'current' )
3047                      .parents( '.filter-links' )
3048                      .prepend( $searchTab );
3049  
3050                  $pluginFilter.prev( 'p' ).remove();
3051                  $( '.plugins-popular-tags-wrapper' ).remove();
3052              }
3053  
3054              if ( 'undefined' !== typeof wp.updates.searchRequest ) {
3055                  wp.updates.searchRequest.abort();
3056              }
3057              $( 'body' ).addClass( 'loading-content' );
3058  
3059              wp.updates.searchRequest = wp.ajax.post( 'search-install-plugins', data ).done( function( response ) {
3060                  $( 'body' ).removeClass( 'loading-content' );
3061                  $pluginFilter.append( response.items );
3062                  delete wp.updates.searchRequest;
3063  
3064                  if ( 0 === response.count ) {
3065                      wp.a11y.speak( __( 'You do not appear to have any plugins available at this time.' ) );
3066                  } else {
3067                      wp.a11y.speak(
3068                          sprintf(
3069                              /* translators: %s: Number of plugins. */
3070                              __( 'Number of plugins found: %d' ),
3071                              response.count
3072                          )
3073                      );
3074                  }
3075              } );
3076          }, 1000 ) );
3077  
3078          if ( $pluginSearch.length ) {
3079              $pluginSearch.attr( 'aria-describedby', 'live-search-desc' );
3080  
3081          }
3082  
3083          /**
3084           * Handles changes to the plugin search box on the Installed Plugins screen,
3085           * searching the plugin list dynamically.
3086           *
3087           * @since 4.6.0
3088           */
3089          $pluginSearch.on( 'keyup input', _.debounce( function( event ) {
3090              var data = {
3091                  _ajax_nonce:   wp.updates.ajaxNonce,
3092                  s:             encodeURIComponent( event.target.value ),
3093                  pagenow:       pagenow,
3094                  plugin_status: 'all'
3095              },
3096              queryArgs,
3097              searchStringLength = $pluginSearch.val().length;
3098  
3099              // Set the autocomplete attribute, turning off autocomplete 1 character before ajax search kicks in.
3100              if ( wp.updates.shouldSearch( searchStringLength ) ) {
3101                  $pluginSearch.attr( 'autocomplete', 'off' );
3102              } else {
3103                  $pluginSearch.attr( 'autocomplete', 'on' );
3104                  return;
3105              }
3106  
3107              // Clear on escape.
3108              if ( 'keyup' === event.type && 27 === event.which ) {
3109                  event.target.value = '';
3110              }
3111  
3112              if ( wp.updates.searchTerm === data.s ) {
3113                  return;
3114              } else {
3115                  wp.updates.searchTerm = data.s;
3116              }
3117  
3118              queryArgs = _.object( _.compact( _.map( location.search.slice( 1 ).split( '&' ), function( item ) {
3119                  if ( item ) return item.split( '=' );
3120              } ) ) );
3121  
3122              data.plugin_status = queryArgs.plugin_status || 'all';
3123  
3124              if ( window.history && window.history.replaceState ) {
3125                  window.history.replaceState( null, '', location.href.split( '?' )[ 0 ] + '?s=' + data.s + '&plugin_status=' + data.plugin_status );
3126              }
3127  
3128              if ( 'undefined' !== typeof wp.updates.searchRequest ) {
3129                  wp.updates.searchRequest.abort();
3130              }
3131  
3132              $bulkActionForm.empty();
3133              $( 'body' ).addClass( 'loading-content' );
3134              $( '.subsubsub .current' ).removeClass( 'current' );
3135  
3136              wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) {
3137  
3138                  // Can we just ditch this whole subtitle business?
3139                  var $subTitle    = $( '<span />' ).addClass( 'subtitle' ).html(
3140                      sprintf(
3141                          /* translators: %s: Search query. */
3142                          __( 'Search results for: %s' ),
3143                          '<strong>' + _.escape( decodeURIComponent( data.s ) ) + '</strong>'
3144                      ) ),
3145                      $oldSubTitle = $( '.wrap .subtitle' );
3146  
3147                  if ( ! data.s.length ) {
3148                      $oldSubTitle.remove();
3149                      $( '.subsubsub .' + data.plugin_status + ' a' ).addClass( 'current' );
3150                  } else if ( $oldSubTitle.length ) {
3151                      $oldSubTitle.replaceWith( $subTitle );
3152                  } else {
3153                      $( '.wp-header-end' ).before( $subTitle );
3154                  }
3155  
3156                  $( 'body' ).removeClass( 'loading-content' );
3157                  $bulkActionForm.append( response.items );
3158                  delete wp.updates.searchRequest;
3159  
3160                  if ( 0 === response.count ) {
3161                      wp.a11y.speak( __( 'No plugins found. Try a different search.'  ) );
3162                  } else {
3163                      wp.a11y.speak(
3164                          sprintf(
3165                              /* translators: %s: Number of plugins. */
3166                              __( 'Number of plugins found: %d' ),
3167                              response.count
3168                          )
3169                      );
3170                  }
3171              } );
3172          }, 500 ) );
3173  
3174          /**
3175           * Trigger a search event when the search form gets submitted.
3176           *
3177           * @since 4.6.0
3178           */
3179          $document.on( 'submit', '.search-plugins', function( event ) {
3180              event.preventDefault();
3181  
3182              $( 'input.wp-filter-search' ).trigger( 'input' );
3183          } );
3184  
3185          /**
3186           * Trigger a search event when the "Try Again" button is clicked.
3187           *
3188           * @since 4.9.0
3189           */
3190          $document.on( 'click', '.try-again', function( event ) {
3191              event.preventDefault();
3192              $pluginInstallSearch.trigger( 'input' );
3193          } );
3194  
3195          /**
3196           * Trigger a search event when the search type gets changed.
3197           *
3198           * @since 4.6.0
3199           */
3200          $( '#typeselector' ).on( 'change', function() {
3201              var $search = $( 'input[name="s"]' );
3202  
3203              if ( $search.val().length ) {
3204                  $search.trigger( 'input', 'typechange' );
3205              }
3206          } );
3207  
3208          /**
3209           * Click handler for updating a plugin from the details modal on `plugin-install.php`.
3210           *
3211           * @since 4.2.0
3212           *
3213           * @param {Event} event Event interface.
3214           */
3215          $( '#plugin_update_from_iframe' ).on( 'click', function( event ) {
3216              var target = window.parent === window ? null : window.parent,
3217                  update;
3218  
3219              $.support.postMessage = !! window.postMessage;
3220  
3221              if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'update-core.php' ) ) {
3222                  return;
3223              }
3224  
3225              event.preventDefault();
3226  
3227              update = {
3228                  action: 'update-plugin',
3229                  data:   {
3230                      plugin: $( this ).data( 'plugin' ),
3231                      slug:   $( this ).data( 'slug' )
3232                  }
3233              };
3234  
3235              target.postMessage( JSON.stringify( update ), window.location.origin );
3236          } );
3237  
3238          /**
3239           * Handles postMessage events.
3240           *
3241           * @since 4.2.0
3242           * @since 4.6.0 Switched `update-plugin` action to use the queue.
3243           *
3244           * @param {Event} event Event interface.
3245           */
3246          $( window ).on( 'message', function( event ) {
3247              var originalEvent  = event.originalEvent,
3248                  expectedOrigin = document.location.protocol + '//' + document.location.host,
3249                  message;
3250  
3251              if ( originalEvent.origin !== expectedOrigin ) {
3252                  return;
3253              }
3254  
3255              try {
3256                  message = JSON.parse( originalEvent.data );
3257              } catch ( e ) {
3258                  return;
3259              }
3260  
3261              if ( ! message ) {
3262                  return;
3263              }
3264  
3265              if (
3266                  'undefined' !== typeof message.status &&
3267                  'undefined' !== typeof message.slug &&
3268                  'undefined' !== typeof message.text &&
3269                  'undefined' !== typeof message.ariaLabel
3270              ) {
3271                  var $card = $( '.plugin-card-' + message.slug ),
3272                      $message = $card.find( '[data-slug="' + message.slug + '"]' );
3273  
3274                  if ( 'undefined' !== typeof message.removeClasses ) {
3275                      $message.removeClass( message.removeClasses );
3276                  }
3277  
3278                  if ( 'undefined' !== typeof message.addClasses ) {
3279                      $message.addClass( message.addClasses );
3280                  }
3281  
3282                  if ( '' === message.ariaLabel ) {
3283                      $message.removeAttr( 'aria-label' );
3284                  } else {
3285                      $message.attr( 'aria-label', message.ariaLabel );
3286                  }
3287  
3288                  if ( 'dependencies-check-success' === message.status ) {
3289                      $message
3290                          .attr( 'data-name', message.pluginName )
3291                          .attr( 'data-slug', message.slug )
3292                          .attr( 'data-plugin', message.plugin )
3293                          .attr( 'href', message.href );
3294                  }
3295  
3296                  $message.text( message.text );
3297              }
3298  
3299              if ( 'undefined' === typeof message.action ) {
3300                  return;
3301              }
3302  
3303              switch ( message.action ) {
3304  
3305                  // Called from `wp-admin/includes/class-wp-upgrader-skins.php`.
3306                  case 'decrementUpdateCount':
3307                      /** @property {string} message.upgradeType */
3308                      wp.updates.decrementCount( message.upgradeType );
3309                      break;
3310  
3311                  case 'install-plugin':
3312                  case 'update-plugin':
3313                      if ( 'undefined' === typeof message.data || 'undefined' === typeof message.data.slug ) {
3314                          return;
3315                      }
3316  
3317                      message.data = wp.updates._addCallbacks( message.data, message.action );
3318  
3319                      wp.updates.queue.push( message );
3320                      wp.updates.queueChecker();
3321                      break;
3322              }
3323          } );
3324  
3325          /**
3326           * Adds a callback to display a warning before leaving the page.
3327           *
3328           * @since 4.2.0
3329           */
3330          $( window ).on( 'beforeunload', wp.updates.beforeunload );
3331  
3332          /**
3333           * Prevents the page form scrolling when activating auto-updates with the Spacebar key.
3334           *
3335           * @since 5.5.0
3336           */
3337          $document.on( 'keydown', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) {
3338              if ( 32 === event.which ) {
3339                  event.preventDefault();
3340              }
3341          } );
3342  
3343          /**
3344           * Click and keyup handler for enabling and disabling plugin and theme auto-updates.
3345           *
3346           * These controls can be either links or buttons. When JavaScript is enabled,
3347           * we want them to behave like buttons. An ARIA role `button` is added via
3348           * the JavaScript that targets elements with the CSS class `aria-button-if-js`.
3349           *
3350           * @since 5.5.0
3351           */
3352          $document.on( 'click keyup', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) {
3353              var data, asset, type, $parent,
3354                  $toggler = $( this ),
3355                  action = $toggler.attr( 'data-wp-action' ),
3356                  $label = $toggler.find( '.label' );
3357  
3358              if ( 'keyup' === event.type && 32 !== event.which ) {
3359                  return;
3360              }
3361  
3362              if ( 'themes' !== pagenow ) {
3363                  $parent = $toggler.closest( '.column-auto-updates' );
3364              } else {
3365                  $parent = $toggler.closest( '.theme-autoupdate' );
3366              }
3367  
3368              event.preventDefault();
3369  
3370              // Prevent multiple simultaneous requests.
3371              if ( $toggler.attr( 'data-doing-ajax' ) === 'yes' ) {
3372                  return;
3373              }
3374  
3375              $toggler.attr( 'data-doing-ajax', 'yes' );
3376  
3377              switch ( pagenow ) {
3378                  case 'plugins':
3379                  case 'plugins-network':
3380                      type = 'plugin';
3381                      asset = $toggler.closest( 'tr' ).attr( 'data-plugin' );
3382                      break;
3383                  case 'themes-network':
3384                      type = 'theme';
3385                      asset = $toggler.closest( 'tr' ).attr( 'data-slug' );
3386                      break;
3387                  case 'themes':
3388                      type = 'theme';
3389                      asset = $toggler.attr( 'data-slug' );
3390                      break;
3391              }
3392  
3393              // Clear any previous errors.
3394              $parent.find( '.notice.notice-error' ).addClass( 'hidden' );
3395  
3396              // Show loading status.
3397              if ( 'enable' === action ) {
3398                  $label.text( __( 'Enabling...' ) );
3399              } else {
3400                  $label.text( __( 'Disabling...' ) );
3401              }
3402  
3403              $toggler.find( '.dashicons-update' ).removeClass( 'hidden' );
3404  
3405              data = {
3406                  action: 'toggle-auto-updates',
3407                  _ajax_nonce: settings.ajax_nonce,
3408                  state: action,
3409                  type: type,
3410                  asset: asset
3411              };
3412  
3413              $.post( window.ajaxurl, data )
3414                  .done( function( response ) {
3415                      var $enabled, $disabled, enabledNumber, disabledNumber, errorMessage,
3416                          href = $toggler.attr( 'href' );
3417  
3418                      if ( ! response.success ) {
3419                          // if WP returns 0 for response (which can happen in a few cases),
3420                          // output the general error message since we won't have response.data.error.
3421                          if ( response.data && response.data.error ) {
3422                              errorMessage = response.data.error;
3423                          } else {
3424                              errorMessage = __( 'The request could not be completed.' );
3425                          }
3426  
3427                          $parent.find( '.notice.notice-error' ).removeClass( 'hidden' ).find( 'p' ).text( errorMessage );
3428                          wp.a11y.speak( errorMessage, 'assertive' );
3429                          return;
3430                      }
3431  
3432                      // Update the counts in the enabled/disabled views if on a screen
3433                      // with a list table.
3434                      if ( 'themes' !== pagenow ) {
3435                          $enabled       = $( '.auto-update-enabled span' );
3436                          $disabled      = $( '.auto-update-disabled span' );
3437                          enabledNumber  = parseInt( $enabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0;
3438                          disabledNumber = parseInt( $disabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0;
3439  
3440                          switch ( action ) {
3441                              case 'enable':
3442                                  ++enabledNumber;
3443                                  --disabledNumber;
3444                                  break;
3445                              case 'disable':
3446                                  --enabledNumber;
3447                                  ++disabledNumber;
3448                                  break;
3449                          }
3450  
3451                          enabledNumber = Math.max( 0, enabledNumber );
3452                          disabledNumber = Math.max( 0, disabledNumber );
3453  
3454                          $enabled.text( '(' + enabledNumber + ')' );
3455                          $disabled.text( '(' + disabledNumber + ')' );
3456                      }
3457  
3458                      if ( 'enable' === action ) {
3459                          // The toggler control can be either a link or a button.
3460                          if ( $toggler[ 0 ].hasAttribute( 'href' ) ) {
3461                              href = href.replace( 'action=enable-auto-update', 'action=disable-auto-update' );
3462                              $toggler.attr( 'href', href );
3463                          }
3464                          $toggler.attr( 'data-wp-action', 'disable' );
3465  
3466                          $label.text( __( 'Disable auto-updates' ) );
3467                          $parent.find( '.auto-update-time' ).removeClass( 'hidden' );
3468                          wp.a11y.speak( __( 'Auto-updates enabled' ) );
3469                      } else {
3470                          // The toggler control can be either a link or a button.
3471                          if ( $toggler[ 0 ].hasAttribute( 'href' ) ) {
3472                              href = href.replace( 'action=disable-auto-update', 'action=enable-auto-update' );
3473                              $toggler.attr( 'href', href );
3474                          }
3475                          $toggler.attr( 'data-wp-action', 'enable' );
3476  
3477                          $label.text( __( 'Enable auto-updates' ) );
3478                          $parent.find( '.auto-update-time' ).addClass( 'hidden' );
3479                          wp.a11y.speak( __( 'Auto-updates disabled' ) );
3480                      }
3481  
3482                      $document.trigger( 'wp-auto-update-setting-changed', { state: action, type: type, asset: asset } );
3483                  } )
3484                  .fail( function() {
3485                      $parent.find( '.notice.notice-error' )
3486                          .removeClass( 'hidden' )
3487                          .find( 'p' )
3488                          .text( __( 'The request could not be completed.' ) );
3489  
3490                      wp.a11y.speak( __( 'The request could not be completed.' ), 'assertive' );
3491                  } )
3492                  .always( function() {
3493                      $toggler.removeAttr( 'data-doing-ajax' ).find( '.dashicons-update' ).addClass( 'hidden' );
3494                  } );
3495              }
3496          );
3497      } );
3498  })( jQuery, window.wp, window._wpUpdatesSettings );


Generated : Mon Sep 14 08:20:31 2026 Cross-referenced by PHPXref