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


Generated: Tue Jul 16 01:00:03 2019 Cross-referenced by PHPXref 0.7.1