[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

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

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


Generated: Tue Sep 21 01:00:05 2021 Cross-referenced by PHPXref 0.7.1