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


Generated : Thu Nov 12 08:20:02 2026 Cross-referenced by PHPXref