[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Tue Mar 19 08:20:01 2024 Cross-referenced by PHPXref