X-Git-Url: https://scripts.mit.edu/gitweb/autoinstalls/wordpress.git/blobdiff_plain/9e77185fafaf4e60e2b73821e0e4b9b1a11fb85f..8d3bb1a5dcfdea9857d3c88c3751f09593e34dc8:/wp-admin/js/updates.js diff --git a/wp-admin/js/updates.js b/wp-admin/js/updates.js index 3139c3ef..239829fe 100644 --- a/wp-admin/js/updates.js +++ b/wp-admin/js/updates.js @@ -1,65 +1,2380 @@ -window.wp = window.wp || {}; +/** + * Functions for ajaxified updates, deletions and installs inside the WordPress admin. + * + * @version 4.2.0 + * + * @package WordPress + * @subpackage Administration + */ -(function( $, wp ) { +/* global pagenow */ +/** + * @param {jQuery} $ jQuery object. + * @param {object} wp WP object. + * @param {object} settings WP Updates settings. + * @param {string} settings.ajax_nonce AJAX nonce. + * @param {object} settings.l10n Translation strings. + * @param {object=} settings.plugins Base names of plugins in their different states. + * @param {Array} settings.plugins.all Base names of all plugins. + * @param {Array} settings.plugins.active Base names of active plugins. + * @param {Array} settings.plugins.inactive Base names of inactive plugins. + * @param {Array} settings.plugins.upgrade Base names of plugins with updates available. + * @param {Array} settings.plugins.recently_activated Base names of recently activated plugins. + * @param {object=} settings.totals Plugin/theme status information or null. + * @param {number} settings.totals.all Amount of all plugins or themes. + * @param {number} settings.totals.upgrade Amount of plugins or themes with updates available. + * @param {number} settings.totals.disabled Amount of disabled themes. + */ +(function( $, wp, settings ) { + var $document = $( document ); + + wp = wp || {}; + + /** + * The WP Updates object. + * + * @since 4.2.0 + * + * @type {object} + */ wp.updates = {}; /** - * Decrement update counts throughout the various menus + * User nonce for ajax calls. + * + * @since 4.2.0 + * + * @type {string} + */ + wp.updates.ajaxNonce = settings.ajax_nonce; + + /** + * Localized strings. + * + * @since 4.2.0 + * + * @type {object} + */ + wp.updates.l10n = settings.l10n; + + /** + * Current search term. + * + * @since 4.6.0 + * + * @type {string} + */ + wp.updates.searchTerm = ''; + + /** + * Whether filesystem credentials need to be requested from the user. + * + * @since 4.2.0 + * + * @type {bool} + */ + wp.updates.shouldRequestFilesystemCredentials = false; + + /** + * Filesystem credentials to be packaged along with the request. + * + * @since 4.2.0 + * @since 4.6.0 Added `available` property to indicate whether credentials have been provided. + * + * @type {object} filesystemCredentials Holds filesystem credentials. + * @type {object} filesystemCredentials.ftp Holds FTP credentials. + * @type {string} filesystemCredentials.ftp.host FTP host. Default empty string. + * @type {string} filesystemCredentials.ftp.username FTP user name. Default empty string. + * @type {string} filesystemCredentials.ftp.password FTP password. Default empty string. + * @type {string} filesystemCredentials.ftp.connectionType Type of FTP connection. 'ssh', 'ftp', or 'ftps'. + * Default empty string. + * @type {object} filesystemCredentials.ssh Holds SSH credentials. + * @type {string} filesystemCredentials.ssh.publicKey The public key. Default empty string. + * @type {string} filesystemCredentials.ssh.privateKey The private key. Default empty string. + * @type {bool} filesystemCredentials.available Whether filesystem credentials have been provided. + * Default 'false'. + */ + wp.updates.filesystemCredentials = { + ftp: { + host: '', + username: '', + password: '', + connectionType: '' + }, + ssh: { + publicKey: '', + privateKey: '' + }, + available: false + }; + + /** + * Whether we're waiting for an Ajax request to complete. + * + * @since 4.2.0 + * @since 4.6.0 More accurately named `ajaxLocked`. + * + * @type {bool} + */ + wp.updates.ajaxLocked = false; + + /** + * Admin notice template. + * + * @since 4.6.0 + * + * @type {function} A function that lazily-compiles the template requested. + */ + wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' ); + + /** + * Update queue. + * + * If the user tries to update a plugin while an update is + * already happening, it can be placed in this queue to perform later. + * + * @since 4.2.0 + * @since 4.6.0 More accurately named `queue`. + * + * @type {Array.object} + */ + wp.updates.queue = []; + + /** + * Holds a jQuery reference to return focus to when exiting the request credentials modal. + * + * @since 4.2.0 + * + * @type {jQuery} + */ + wp.updates.$elToReturnFocusToFromCredentialsModal = undefined; + + /** + * Adds or updates an admin notice. + * + * @since 4.6.0 + * + * @param {object} data + * @param {*=} data.selector Optional. Selector of an element to be replaced with the admin notice. + * @param {string=} data.id Optional. Unique id that will be used as the notice's id attribute. + * @param {string=} data.className Optional. Class names that will be used in the admin notice. + * @param {string=} data.message Optional. The message displayed in the notice. + * @param {number=} data.successes Optional. The amount of successful operations. + * @param {number=} data.errors Optional. The amount of failed operations. + * @param {Array=} data.errorMessages Optional. Error messages of failed operations. + * + */ + wp.updates.addAdminNotice = function( data ) { + var $notice = $( data.selector ), $adminNotice; + + delete data.selector; + $adminNotice = wp.updates.adminNotice( data ); + + // Check if this admin notice already exists. + if ( ! $notice.length ) { + $notice = $( '#' + data.id ); + } + + if ( $notice.length ) { + $notice.replaceWith( $adminNotice ); + } else { + $( '.wrap' ).find( '> h1' ).after( $adminNotice ); + } + + $document.trigger( 'wp-updates-notice-added' ); + }; + + /** + * Handles Ajax requests to WordPress. + * + * @since 4.6.0 + * + * @param {string} action The type of Ajax request ('update-plugin', 'install-theme', etc). + * @param {object} data Data that needs to be passed to the ajax callback. + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. + */ + wp.updates.ajax = function( action, data ) { + var options = {}; + + if ( wp.updates.ajaxLocked ) { + wp.updates.queue.push( { + action: action, + data: data + } ); + + // Return a Deferred object so callbacks can always be registered. + return $.Deferred(); + } + + wp.updates.ajaxLocked = true; + + if ( data.success ) { + options.success = data.success; + delete data.success; + } + + if ( data.error ) { + options.error = data.error; + delete data.error; + } + + options.data = _.extend( data, { + action: action, + _ajax_nonce: wp.updates.ajaxNonce, + username: wp.updates.filesystemCredentials.ftp.username, + password: wp.updates.filesystemCredentials.ftp.password, + hostname: wp.updates.filesystemCredentials.ftp.hostname, + connection_type: wp.updates.filesystemCredentials.ftp.connectionType, + public_key: wp.updates.filesystemCredentials.ssh.publicKey, + private_key: wp.updates.filesystemCredentials.ssh.privateKey + } ); + + return wp.ajax.send( options ).always( wp.updates.ajaxAlways ); + }; + + /** + * Actions performed after every Ajax request. + * + * @since 4.6.0 + * + * @param {object} response + * @param {array=} response.debug Optional. Debug information. + * @param {string=} response.errorCode Optional. Error code for an error that occurred. + */ + wp.updates.ajaxAlways = function( response ) { + if ( ! response.errorCode || 'unable_to_connect_to_filesystem' !== response.errorCode ) { + wp.updates.ajaxLocked = false; + wp.updates.queueChecker(); + } + + if ( 'undefined' !== typeof response.debug && window.console && window.console.log ) { + _.map( response.debug, function( message ) { + window.console.log( $( '
' ).html( message ).text() ); + } ); + } + }; + + /** + * Decrements the update counts throughout the various menus. + * + * This includes the toolbar, the "Updates" menu item and the menu items + * for plugins and themes. + * + * @since 3.9.0 * - * @param {string} updateType + * @param {string} type The type of item that was updated or deleted. + * Can be 'plugin', 'theme'. */ - wp.updates.decrementCount = function( upgradeType ) { - var count, pluginCount, $elem; + wp.updates.decrementCount = function( type ) { + var $adminBarUpdates = $( '#wp-admin-bar-updates' ), + $dashboardNavMenuUpdateCount = $( 'a[href="update-core.php"] .update-plugins' ), + count = $adminBarUpdates.find( '.ab-label' ).text(), + $menuItem, $itemCount, itemCount; - $elem = $( '#wp-admin-bar-updates .ab-label' ); - count = $elem.text(); count = parseInt( count, 10 ) - 1; - if ( count < 0 ) { + + if ( count < 0 || isNaN( count ) ) { return; } - $( '#wp-admin-bar-updates .ab-item' ).removeAttr( 'title' ); - $elem.text( count ); - $elem = $( 'a[href="update-core.php"] .update-plugins' ); - $elem.each( function( index, elem ) { - elem.className = elem.className.replace( /count-\d+/, 'count-' + count ); + $adminBarUpdates.find( '.ab-item' ).removeAttr( 'title' ); + $adminBarUpdates.find( '.ab-label' ).text( count ); + + // Remove the update count from the toolbar if it's zero. + if ( ! count ) { + $adminBarUpdates.find( '.ab-label' ).parents( 'li' ).remove(); + } + + // Update the "Updates" menu item. + $dashboardNavMenuUpdateCount.each( function( index, element ) { + element.className = element.className.replace( /count-\d+/, 'count-' + count ); } ); - $elem.removeAttr( 'title' ); - $elem.find( '.update-count' ).text( count ); - if ( 'plugin' === upgradeType ) { - $elem = $( '#menu-plugins' ); - pluginCount = $elem.find( '.plugin-count' ).eq(0).text(); - pluginCount = parseInt( pluginCount, 10 ) - 1; - if ( pluginCount < 0 ) { - return; + $dashboardNavMenuUpdateCount.removeAttr( 'title' ); + $dashboardNavMenuUpdateCount.find( '.update-count' ).text( count ); + + if ( 'plugin' === type ) { + $menuItem = $( '#menu-plugins' ); + $itemCount = $menuItem.find( '.plugin-count' ); + } else if ( 'theme' === type ) { + $menuItem = $( '#menu-appearance' ); + $itemCount = $menuItem.find( '.theme-count' ); + } + + // Decrement the counter of the other menu items. + if ( $itemCount ) { + itemCount = $itemCount.eq( 0 ).text(); + itemCount = parseInt( itemCount, 10 ) - 1; + } + + if ( itemCount < 0 || isNaN( itemCount ) ) { + return; + } + + if ( itemCount > 0 ) { + $( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' ); + + $itemCount.text( itemCount ); + $menuItem.find( '.update-plugins' ).each( function( index, element ) { + element.className = element.className.replace( /count-\d+/, 'count-' + itemCount ); + } ); + } else { + $( '.subsubsub .upgrade' ).remove(); + $menuItem.find( '.update-plugins' ).remove(); + } + }; + + /** + * Sends an Ajax request to the server to update a plugin. + * + * @since 4.2.0 + * @since 4.6.0 More accurately named `updatePlugin`. + * + * @param {object} args Arguments. + * @param {string} args.plugin Plugin basename. + * @param {string} args.slug Plugin slug. + * @param {updatePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.updatePluginSuccess + * @param {updatePluginError=} args.error Optional. Error callback. Default: wp.updates.updatePluginError + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. + */ + wp.updates.updatePlugin = function( args ) { + var $updateRow, $card, $message, message; + + args = _.extend( { + success: wp.updates.updatePluginSuccess, + error: wp.updates.updatePluginError + }, args ); + + if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { + $updateRow = $( 'tr[data-plugin="' + args.plugin + '"]' ); + $message = $updateRow.find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' ); + message = wp.updates.l10n.updatingLabel.replace( '%s', $updateRow.find( '.plugin-title strong' ).text() ); + } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { + $card = $( '.plugin-card-' + args.slug ); + $message = $card.find( '.update-now' ).addClass( 'updating-message' ); + message = wp.updates.l10n.updatingLabel.replace( '%s', $message.data( 'name' ) ); + + // Remove previous error messages, if any. + $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove(); + } + + if ( $message.html() !== wp.updates.l10n.updating ) { + $message.data( 'originaltext', $message.html() ); + } + + $message + .attr( 'aria-label', message ) + .text( wp.updates.l10n.updating ); + + $document.trigger( 'wp-plugin-updating', args ); + + return wp.updates.ajax( 'update-plugin', args ); + }; + + /** + * Updates the UI appropriately after a successful plugin update. + * + * @since 4.2.0 + * @since 4.6.0 More accurately named `updatePluginSuccess`. + * + * @typedef {object} updatePluginSuccess + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the plugin to be updated. + * @param {string} response.plugin Basename of the plugin to be updated. + * @param {string} response.pluginName Name of the plugin to be updated. + * @param {string} response.oldVersion Old version of the plugin. + * @param {string} response.newVersion New version of the plugin. + */ + wp.updates.updatePluginSuccess = function( response ) { + var $pluginRow, $updateMessage, newText; + + if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { + $pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' ) + .removeClass( 'update' ) + .addClass( 'updated' ); + $updateMessage = $pluginRow.find( '.update-message' ) + .removeClass( 'updating-message notice-warning' ) + .addClass( 'updated-message notice-success' ).find( 'p' ); + + // Update the version number in the row. + newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion ); + $pluginRow.find( '.plugin-version-author-uri' ).html( newText ); + } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { + $updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' ) + .removeClass( 'updating-message' ) + .addClass( 'button-disabled updated-message' ); + } + + $updateMessage + .attr( 'aria-label', wp.updates.l10n.updatedLabel.replace( '%s', response.pluginName ) ) + .text( wp.updates.l10n.updated ); + + wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' ); + + wp.updates.decrementCount( 'plugin' ); + + $document.trigger( 'wp-plugin-update-success', response ); + }; + + /** + * Updates the UI appropriately after a failed plugin update. + * + * @since 4.2.0 + * @since 4.6.0 More accurately named `updatePluginError`. + * + * @typedef {object} updatePluginError + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the plugin to be updated. + * @param {string} response.plugin Basename of the plugin to be updated. + * @param {string=} response.pluginName Optional. Name of the plugin to be updated. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + */ + wp.updates.updatePluginError = function( response ) { + var $card, $message, errorMessage; + + if ( ! wp.updates.isValidResponse( response, 'update' ) ) { + return; + } + + if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) { + return; + } + + errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage ); + + if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { + if ( response.plugin ) { + $message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' ); + } else { + $message = $( 'tr[data-slug="' + response.slug + '"]' ).find( '.update-message' ); + } + $message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage ); + + if ( response.pluginName ) { + $message.find( 'p' ) + .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) ); + } else { + $message.find( 'p' ).removeAttr( 'aria-label' ); } - $elem.find( '.plugin-count' ).text( pluginCount ); - $elem.find( '.update-plugins' ).each( function( index, elem ) { - elem.className = elem.className.replace( /count-\d+/, 'count-' + pluginCount ); + } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { + $card = $( '.plugin-card-' + response.slug ) + .addClass( 'plugin-card-update-failed' ) + .append( wp.updates.adminNotice( { + className: 'update-message notice-error notice-alt is-dismissible', + message: errorMessage + } ) ); + + $card.find( '.update-now' ) + .text( wp.updates.l10n.updateFailedShort ).removeClass( 'updating-message' ); + + if ( response.pluginName ) { + $card.find( '.update-now' ) + .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) ); + } else { + $card.find( '.update-now' ).removeAttr( 'aria-label' ); + } + + $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() { + + // Use same delay as the total duration of the notice fadeTo + slideUp animation. + setTimeout( function() { + $card + .removeClass( 'plugin-card-update-failed' ) + .find( '.column-name a' ).focus(); + + $card.find( '.update-now' ) + .attr( 'aria-label', false ) + .text( wp.updates.l10n.updateNow ); + }, 200 ); } ); } + + wp.a11y.speak( errorMessage, 'assertive' ); + + $document.trigger( 'wp-plugin-update-error', response ); + }; + + /** + * Sends an Ajax request to the server to install a plugin. + * + * @since 4.6.0 + * + * @param {object} args Arguments. + * @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository. + * @param {installPluginSuccess=} args.success Optional. Success callback. Default: wp.updates.installPluginSuccess + * @param {installPluginError=} args.error Optional. Error callback. Default: wp.updates.installPluginError + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. + */ + wp.updates.installPlugin = function( args ) { + var $card = $( '.plugin-card-' + args.slug ), + $message = $card.find( '.install-now' ); + + args = _.extend( { + success: wp.updates.installPluginSuccess, + error: wp.updates.installPluginError + }, args ); + + if ( 'import' === pagenow ) { + $message = $( '[data-slug="' + args.slug + '"]' ); + } + + if ( $message.html() !== wp.updates.l10n.installing ) { + $message.data( 'originaltext', $message.html() ); + } + + $message + .addClass( 'updating-message' ) + .attr( 'aria-label', wp.updates.l10n.pluginInstallingLabel.replace( '%s', $message.data( 'name' ) ) ) + .text( wp.updates.l10n.installing ); + + wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' ); + + // Remove previous error messages, if any. + $card.removeClass( 'plugin-card-install-failed' ).find( '.notice.notice-error' ).remove(); + + $document.trigger( 'wp-plugin-installing', args ); + + return wp.updates.ajax( 'install-plugin', args ); + }; + + /** + * Updates the UI appropriately after a successful plugin install. + * + * @since 4.6.0 + * + * @typedef {object} installPluginSuccess + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the installed plugin. + * @param {string} response.pluginName Name of the installed plugin. + * @param {string} response.activateUrl URL to activate the just installed plugin. + */ + wp.updates.installPluginSuccess = function( response ) { + var $message = $( '.plugin-card-' + response.slug ).find( '.install-now' ); + + $message + .removeClass( 'updating-message' ) + .addClass( 'updated-message installed button-disabled' ) + .attr( 'aria-label', wp.updates.l10n.pluginInstalledLabel.replace( '%s', response.pluginName ) ) + .text( wp.updates.l10n.installed ); + + wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' ); + + $document.trigger( 'wp-plugin-install-success', response ); + + if ( response.activateUrl ) { + setTimeout( function() { + + // Transform the 'Install' button into an 'Activate' button. + $message.removeClass( 'install-now installed button-disabled updated-message' ).addClass( 'activate-now button-primary' ) + .attr( 'href', response.activateUrl ) + .attr( 'aria-label', wp.updates.l10n.activatePluginLabel.replace( '%s', response.pluginName ) ) + .text( wp.updates.l10n.activatePlugin ); + }, 1000 ); + } }; - $( window ).on( 'message', function( e ) { - var event = e.originalEvent, - message, - loc = document.location, - expectedOrigin = loc.protocol + '//' + loc.hostname; + /** + * Updates the UI appropriately after a failed plugin install. + * + * @since 4.6.0 + * + * @typedef {object} installPluginError + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the plugin to be installed. + * @param {string=} response.pluginName Optional. Name of the plugin to be installed. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + */ + wp.updates.installPluginError = function( response ) { + var $card = $( '.plugin-card-' + response.slug ), + $button = $card.find( '.install-now' ), + errorMessage; - if ( event.origin !== expectedOrigin ) { + if ( ! wp.updates.isValidResponse( response, 'install' ) ) { return; } - message = $.parseJSON( event.data ); + if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) { + return; + } + + errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage ); + + $card + .addClass( 'plugin-card-update-failed' ) + .append( '' + errorMessage + '
' + message + '
' ).html( $message.find( 'p' ).data( 'originaltext' ) ); + } + + $message + .removeClass( 'updating-message' ) + .html( originalText ); + + if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { + if ( 'update-plugin' === job.action ) { + $message.attr( 'aria-label', wp.updates.l10n.updateNowLabel.replace( '%s', $message.data( 'name' ) ) ); + } else if ( 'install-plugin' === job.action ) { + $message.attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', $message.data( 'name' ) ) ); + } + } + } + + wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' ); + } ); + + /** + * Click handler for plugin updates in List Table view. + * + * @since 4.2.0 + * + * @param {Event} event Event interface. + */ + $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) { + var $message = $( event.target ), + $pluginRow = $message.parents( 'tr' ); + + event.preventDefault(); + + if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + // Return the user to the input box of the plugin's table row after closing the modal. + wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' ); + wp.updates.updatePlugin( { + plugin: $pluginRow.data( 'plugin' ), + slug: $pluginRow.data( 'slug' ) + } ); + } ); + + /** + * Click handler for plugin updates in plugin install view. + * + * @since 4.2.0 + * + * @param {Event} event Event interface. + */ + $pluginFilter.on( 'click', '.update-now', function( event ) { + var $button = $( event.target ); + event.preventDefault(); + + if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + wp.updates.updatePlugin( { + plugin: $button.data( 'plugin' ), + slug: $button.data( 'slug' ) + } ); + } ); + + /** + * Click handler for plugin installs in plugin install view. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $pluginFilter.on( 'click', '.install-now', function( event ) { + var $button = $( event.target ); + event.preventDefault(); + + if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) { + return; + } + + if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) { + wp.updates.requestFilesystemCredentials( event ); + + $document.on( 'credential-modal-cancel', function() { + var $message = $( '.install-now.updating-message' ); + + $message + .removeClass( 'updating-message' ) + .text( wp.updates.l10n.installNow ); + + wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' ); + } ); + } + + wp.updates.installPlugin( { + slug: $button.data( 'slug' ) + } ); + } ); + + /** + * Click handler for importer plugins installs in the Import screen. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $document.on( 'click', '.importer-item .install-now', function( event ) { + var $button = $( event.target ), + pluginName = $( this ).data( 'name' ); + + event.preventDefault(); + + if ( $button.hasClass( 'updating-message' ) ) { + return; + } + + if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) { + wp.updates.requestFilesystemCredentials( event ); + + $document.on( 'credential-modal-cancel', function() { + + $button + .removeClass( 'updating-message' ) + .text( wp.updates.l10n.installNow ) + .attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', pluginName ) ); + + wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' ); + } ); + } + + wp.updates.installPlugin( { + slug: $button.data( 'slug' ), + success: wp.updates.installImporterSuccess, + error: wp.updates.installImporterError + } ); + } ); + + /** + * Click handler for plugin deletions. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) { + var $pluginRow = $( event.target ).parents( 'tr' ); + + event.preventDefault(); + + if ( ! window.confirm( wp.updates.l10n.aysDeleteUninstall.replace( '%s', $pluginRow.find( '.plugin-title strong' ).text() ) ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + wp.updates.deletePlugin( { + plugin: $pluginRow.data( 'plugin' ), + slug: $pluginRow.data( 'slug' ) + } ); + + } ); + + /** + * Click handler for theme updates. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) { + var $message = $( event.target ), + $themeRow = $message.parents( 'tr' ); + + event.preventDefault(); + + if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + // Return the user to the input box of the theme's table row after closing the modal. + wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' ); + wp.updates.updateTheme( { + slug: $themeRow.data( 'slug' ) + } ); + } ); + + /** + * Click handler for theme deletions. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) { + var $themeRow = $( event.target ).parents( 'tr' ); + + event.preventDefault(); + + if ( ! window.confirm( wp.updates.l10n.aysDelete.replace( '%s', $themeRow.find( '.theme-title strong' ).text() ) ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + wp.updates.deleteTheme( { + slug: $themeRow.data( 'slug' ) + } ); + } ); + + /** + * Bulk action handler for plugins and themes. + * + * Handles both deletions and updates. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $bulkActionForm.on( 'click', '[type="submit"]', function( event ) { + var bulkAction = $( event.target ).siblings( 'select' ).val(), + itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ), + success = 0, + error = 0, + errorMessages = [], + type, action; + + // Determine which type of item we're dealing with. + switch ( pagenow ) { + case 'plugins': + case 'plugins-network': + type = 'plugin'; + break; + + case 'themes-network': + type = 'theme'; + break; + + default: + return; + } + + // Bail if there were no items selected. + if ( ! itemsSelected.length ) { + event.preventDefault(); + $( 'html, body' ).animate( { scrollTop: 0 } ); + + return wp.updates.addAdminNotice( { + id: 'no-items-selected', + className: 'notice-error is-dismissible', + message: wp.updates.l10n.noItemsSelected + } ); + } + + // Determine the type of request we're dealing with. + switch ( bulkAction ) { + case 'update-selected': + action = bulkAction.replace( 'selected', type ); + break; + + case 'delete-selected': + if ( ! window.confirm( 'plugin' === type ? wp.updates.l10n.aysBulkDelete : wp.updates.l10n.aysBulkDeleteThemes ) ) { + event.preventDefault(); + return; + } + + action = bulkAction.replace( 'selected', type ); + break; + + default: + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + event.preventDefault(); + + // Un-check the bulk checkboxes. + $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false ); + + $document.trigger( 'wp-' + type + '-bulk-' + bulkAction, itemsSelected ); + + // Find all the checkboxes which have been checked. + itemsSelected.each( function( index, element ) { + var $checkbox = $( element ), + $itemRow = $checkbox.parents( 'tr' ); + + // Only add update-able items to the update queue. + if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) { + + // Un-check the box. + $checkbox.prop( 'checked', false ); + return; + } + + // Add it to the queue. + wp.updates.queue.push( { + action: action, + data: { + plugin: $itemRow.data( 'plugin' ), + slug: $itemRow.data( 'slug' ) + } + } ); + } ); + + // Display bulk notification for updates of any kind. + $document.on( 'wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error', function( event, response ) { + var $itemRow = $( '[data-slug="' + response.slug + '"]' ), + $bulkActionNotice, itemName; + + if ( 'wp-' + response.update + '-update-success' === event.type ) { + success++; + } else { + itemName = response.pluginName ? response.pluginName : $itemRow.find( '.column-primary strong' ).text(); + + error++; + errorMessages.push( itemName + ': ' + response.errorMessage ); + } + + $itemRow.find( 'input[name="checked[]"]:checked' ).prop( 'checked', false ); + + wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' ); + + wp.updates.addAdminNotice( { + id: 'bulk-action-notice', + className: 'bulk-action-notice', + successes: success, + errors: error, + errorMessages: errorMessages, + type: response.update + } ); + + $bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() { + // $( this ) is the clicked button, no need to get it again. + $( this ) + .toggleClass( 'bulk-action-errors-collapsed' ) + .attr( 'aria-expanded', ! $( this ).hasClass( 'bulk-action-errors-collapsed' ) ); + // Show the errors list. + $bulkActionNotice.find( '.bulk-action-errors' ).toggleClass( 'hidden' ); + } ); + + if ( error > 0 && ! wp.updates.queue.length ) { + $( 'html, body' ).animate( { scrollTop: 0 } ); + } + } ); + + // Reset admin notice template after #bulk-action-notice was added. + $document.on( 'wp-updates-notice-added', function() { + wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' ); + } ); + + // Check the queue, now that the event handlers have been added. + wp.updates.queueChecker(); + } ); + + if ( $pluginInstallSearch.length ) { + $pluginInstallSearch.attr( 'aria-describedby', 'live-search-desc' ); + } + + /** + * Handles changes to the plugin search box on the new-plugin page, + * searching the repository dynamically. + * + * @since 4.6.0 + */ + $pluginInstallSearch.on( 'keyup input', _.debounce( function( event, eventtype ) { + var $searchTab = $( '.plugin-install-search' ), data, searchLocation; + + data = { + _ajax_nonce: wp.updates.ajaxNonce, + s: event.target.value, + tab: 'search', + type: $( '#typeselector' ).val(), + pagenow: pagenow + }; + searchLocation = location.href.split( '?' )[ 0 ] + '?' + $.param( _.omit( data, [ '_ajax_nonce', 'pagenow' ] ) ); + + // Clear on escape. + if ( 'keyup' === event.type && 27 === event.which ) { + event.target.value = ''; + } + + if ( wp.updates.searchTerm === data.s && 'typechange' !== eventtype ) { + return; + } else { + $pluginFilter.empty(); + wp.updates.searchTerm = data.s; + } + + if ( window.history && window.history.replaceState ) { + window.history.replaceState( null, '', searchLocation ); + } + + if ( ! $searchTab.length ) { + $searchTab = $( '
' ) + .append( $( '', { + 'class': 'current', + 'href': searchLocation, + 'text': wp.updates.l10n.searchResultsLabel + } ) ); + + $( '.wp-filter .filter-links .current' ) + .removeClass( 'current' ) + .parents( '.filter-links' ) + .prepend( $searchTab ); + + $pluginFilter.prev( 'p' ).remove(); + $( '.plugins-popular-tags-wrapper' ).remove(); + } + + if ( 'undefined' !== typeof wp.updates.searchRequest ) { + wp.updates.searchRequest.abort(); + } + $( 'body' ).addClass( 'loading-content' ); + + wp.updates.searchRequest = wp.ajax.post( 'search-install-plugins', data ).done( function( response ) { + $( 'body' ).removeClass( 'loading-content' ); + $pluginFilter.append( response.items ); + delete wp.updates.searchRequest; + + if ( 0 === response.count ) { + wp.a11y.speak( wp.updates.l10n.noPluginsFound ); + } else { + wp.a11y.speak( wp.updates.l10n.pluginsFound.replace( '%d', response.count ) ); + } + } ); + }, 500 ) ); + + if ( $pluginSearch.length ) { + $pluginSearch.attr( 'aria-describedby', 'live-search-desc' ); + } + + /** + * Handles changes to the plugin search box on the Installed Plugins screen, + * searching the plugin list dynamically. + * + * @since 4.6.0 + */ + $pluginSearch.on( 'keyup input', _.debounce( function( event ) { + var data = { + _ajax_nonce: wp.updates.ajaxNonce, + s: event.target.value, + pagenow: pagenow + }; + + // Clear on escape. + if ( 'keyup' === event.type && 27 === event.which ) { + event.target.value = ''; + } + + if ( wp.updates.searchTerm === data.s ) { + return; + } else { + wp.updates.searchTerm = data.s; + } + + if ( window.history && window.history.replaceState ) { + window.history.replaceState( null, '', location.href.split( '?' )[ 0 ] + '?s=' + data.s ); + } + + if ( 'undefined' !== typeof wp.updates.searchRequest ) { + wp.updates.searchRequest.abort(); + } + + $bulkActionForm.empty(); + $( 'body' ).addClass( 'loading-content' ); + + wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) { + + // Can we just ditch this whole subtitle business? + var $subTitle = $( '' ).addClass( 'subtitle' ).html( wp.updates.l10n.searchResults.replace( '%s', _.escape( data.s ) ) ), + $oldSubTitle = $( '.wrap .subtitle' ); + + if ( ! data.s.length ) { + $oldSubTitle.remove(); + } else if ( $oldSubTitle.length ) { + $oldSubTitle.replaceWith( $subTitle ); + } else { + $( '.wrap h1' ).append( $subTitle ); + } + + $( 'body' ).removeClass( 'loading-content' ); + $bulkActionForm.append( response.items ); + delete wp.updates.searchRequest; + + if ( 0 === response.count ) { + wp.a11y.speak( wp.updates.l10n.noPluginsFound ); + } else { + wp.a11y.speak( wp.updates.l10n.pluginsFound.replace( '%d', response.count ) ); + } + } ); + }, 500 ) ); + + /** + * Trigger a search event when the search form gets submitted. + * + * @since 4.6.0 + */ + $document.on( 'submit', '.search-plugins', function( event ) { + event.preventDefault(); + + $( 'input.wp-filter-search' ).trigger( 'input' ); + } ); + + /** + * Trigger a search event when the search type gets changed. + * + * @since 4.6.0 + */ + $( '#typeselector' ).on( 'change', function() { + var $search = $( 'input[name="s"]' ); + + if ( $search.val().length ) { + $search.trigger( 'input', 'typechange' ); + } + } ); + + /** + * Click handler for updating a plugin from the details modal on `plugin-install.php`. + * + * @since 4.2.0 + * + * @param {Event} event Event interface. + */ + $( '#plugin_update_from_iframe' ).on( 'click', function( event ) { + var target = window.parent === window ? null : window.parent, + update; + + $.support.postMessage = !! window.postMessage; + + if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'update-core.php' ) ) { + return; + } + + event.preventDefault(); + + update = { + action: 'update-plugin', + data: { + plugin: $( this ).data( 'plugin' ), + slug: $( this ).data( 'slug' ) + } + }; + + target.postMessage( JSON.stringify( update ), window.location.origin ); + } ); + + /** + * Click handler for installing a plugin from the details modal on `plugin-install.php`. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $( '#plugin_install_from_iframe' ).on( 'click', function( event ) { + var target = window.parent === window ? null : window.parent, + install; + + $.support.postMessage = !! window.postMessage; + + if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'index.php' ) ) { + return; + } + + event.preventDefault(); + + install = { + action: 'install-plugin', + data: { + slug: $( this ).data( 'slug' ) + } + }; + + target.postMessage( JSON.stringify( install ), window.location.origin ); + } ); + + /** + * Handles postMessage events. + * + * @since 4.2.0 + * @since 4.6.0 Switched `update-plugin` action to use the queue. + * + * @param {Event} event Event interface. + */ + $( window ).on( 'message', function( event ) { + var originalEvent = event.originalEvent, + expectedOrigin = document.location.protocol + '//' + document.location.hostname, + message; + + if ( originalEvent.origin !== expectedOrigin ) { + return; + } + + try { + message = $.parseJSON( originalEvent.data ); + } catch ( e ) { + return; + } + + if ( 'undefined' === typeof message.action ) { + return; + } + + switch ( message.action ) { + + // Called from `wp-admin/includes/class-wp-upgrader-skins.php`. + case 'decrementUpdateCount': + /** @property {string} message.upgradeType */ + wp.updates.decrementCount( message.upgradeType ); + break; + + case 'install-plugin': + case 'update-plugin': + /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */ + window.tb_remove(); + /* jscs:enable */ + + message.data = wp.updates._addCallbacks( message.data, message.action ); + + wp.updates.queue.push( message ); + wp.updates.queueChecker(); + break; + } + } ); + + /** + * Adds a callback to display a warning before leaving the page. + * + * @since 4.2.0 + */ + $( window ).on( 'beforeunload', wp.updates.beforeunload ); + } ); +})( jQuery, window.wp, _.extend( window._wpUpdatesSettings, window._wpUpdatesItemCounts || {} ) );