[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

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

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


Generated: Wed Aug 12 01:00:03 2020 Cross-referenced by PHPXref 0.7.1