[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Tue Jan 26 08:20:01 2021 Cross-referenced by PHPXref