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


Generated: Fri Dec 6 01:00:02 2024 Cross-referenced by PHPXref 0.7.1