/** * Functions for ajaxified updates, deletions and installs inside the WordPress admin. * * @version 4.2.0 * @output wp-admin/js/updates.js */ /* 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.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.themes Plugin/theme status information or null. * @param {number} settings.themes.all Amount of all themes. * @param {number} settings.themes.upgrade Amount of themes with updates available. * @param {number} settings.themes.disabled Amount of disabled themes. * @param {object=} settings.totals Combined information for available update counts. * @param {number} settings.totals.count Holds the amount of available updates. */ (function( $, wp, settings ) { var $document = $( document ), __ = wp.i18n.__, _x = wp.i18n._x, sprintf = wp.i18n.sprintf; wp = wp || {}; /** * The WP Updates object. * * @since 4.2.0 * * @namespace wp.updates */ wp.updates = {}; /** * User nonce for ajax calls. * * @since 4.2.0 * * @type {string} */ wp.updates.ajaxNonce = settings.ajax_nonce; /** * 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} * @property {Object} filesystemCredentials.ftp Holds FTP credentials. * @property {string} filesystemCredentials.ftp.host FTP host. Default empty string. * @property {string} filesystemCredentials.ftp.username FTP user name. Default empty string. * @property {string} filesystemCredentials.ftp.password FTP password. Default empty string. * @property {string} filesystemCredentials.ftp.connectionType Type of FTP connection. 'ssh', 'ftp', or 'ftps'. * Default empty string. * @property {Object} filesystemCredentials.ssh Holds SSH credentials. * @property {string} filesystemCredentials.ssh.publicKey The public key. Default empty string. * @property {string} filesystemCredentials.ssh.privateKey The private key. Default empty string. * @property {string} filesystemCredentials.fsNonce Filesystem credentials form nonce. * @property {bool} filesystemCredentials.available Whether filesystem credentials have been provided. * Default 'false'. */ wp.updates.filesystemCredentials = { ftp: { host: '', username: '', password: '', connectionType: '' }, ssh: { publicKey: '', privateKey: '' }, fsNonce: '', 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} */ 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 ), $headerEnd = $( '.wp-header-end' ), $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 if ( $headerEnd.length ) { $headerEnd.after( $adminNotice ); } else { if ( 'customize' === pagenow ) { $( '.customize-themes-notifications' ).append( $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, _fs_nonce: wp.updates.filesystemCredentials.fsNonce, 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 ) { // Remove all HTML tags and write a message to the console. window.console.log( wp.sanitize.stripTagsAndEncodeText( message ) ); } ); } }; /** * Refreshes update counts everywhere on the screen. * * @since 4.7.0 */ wp.updates.refreshCount = function() { var $adminBarUpdates = $( '#wp-admin-bar-updates' ), $dashboardNavMenuUpdateCount = $( 'a[href="update-core.php"] .update-plugins' ), $pluginsNavMenuUpdateCount = $( 'a[href="plugins.php"] .update-plugins' ), $appearanceNavMenuUpdateCount = $( 'a[href="themes.php"] .update-plugins' ), itemCount; $adminBarUpdates.find( '.ab-item' ).removeAttr( 'title' ); $adminBarUpdates.find( '.ab-label' ).text( settings.totals.counts.total ); // Remove the update count from the toolbar if it's zero. if ( 0 === settings.totals.counts.total ) { $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-' + settings.totals.counts.total ); } ); if ( settings.totals.counts.total > 0 ) { $dashboardNavMenuUpdateCount.find( '.update-count' ).text( settings.totals.counts.total ); } else { $dashboardNavMenuUpdateCount.remove(); } // Update the "Plugins" menu item. $pluginsNavMenuUpdateCount.each( function( index, element ) { element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.plugins ); } ); if ( settings.totals.counts.total > 0 ) { $pluginsNavMenuUpdateCount.find( '.plugin-count' ).text( settings.totals.counts.plugins ); } else { $pluginsNavMenuUpdateCount.remove(); } // Update the "Appearance" menu item. $appearanceNavMenuUpdateCount.each( function( index, element ) { element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.themes ); } ); if ( settings.totals.counts.total > 0 ) { $appearanceNavMenuUpdateCount.find( '.theme-count' ).text( settings.totals.counts.themes ); } else { $appearanceNavMenuUpdateCount.remove(); } // Update list table filter navigation. if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { itemCount = settings.totals.counts.plugins; } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) { itemCount = settings.totals.counts.themes; } if ( itemCount > 0 ) { $( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' ); } else { $( '.subsubsub .upgrade' ).remove(); $( '.subsubsub li:last' ).html( function() { return $( this ).children(); } ); } }; /** * 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} type The type of item that was updated or deleted. * Can be 'plugin', 'theme'. */ wp.updates.decrementCount = function( type ) { settings.totals.counts.total = Math.max( --settings.totals.counts.total, 0 ); if ( 'plugin' === type ) { settings.totals.counts.plugins = Math.max( --settings.totals.counts.plugins, 0 ); } else if ( 'theme' === type ) { settings.totals.counts.themes = Math.max( --settings.totals.counts.themes, 0 ); } wp.updates.refreshCount( type ); }; /** * 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 = sprintf( /* translators: %s: Plugin name and version. */ _x( 'Updating %s...', 'plugin' ), $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 = sprintf( /* translators: %s: Plugin name and version. */ _x( 'Updating %s...', 'plugin' ), $message.data( 'name' ) ); // Remove previous error messages, if any. $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove(); } if ( $message.html() !== __( 'Updating...' ) ) { $message.data( 'originaltext', $message.html() ); } $message .attr( 'aria-label', message ) .text( __( '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`. * @since 5.5.0 Auto-update "time to next update" text cleared. * * @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 ); // Clear the "time to next auto-update" text. $pluginRow.find( '.auto-update-time' ).empty(); } 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', sprintf( /* translators: %s: Plugin name and version. */ _x( '%s updated!', 'plugin' ), response.pluginName ) ) .text( _x( 'Updated!', 'plugin' ) ); wp.a11y.speak( __( 'Update completed successfully.' ) ); 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`. * * @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 = sprintf( /* translators: %s: Error string for a failed update. */ __( 'Update Failed: %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', sprintf( /* translators: %s: Plugin name and version. */ _x( '%s update failed.', 'plugin' ), response.pluginName ) ); } else { $message.find( 'p' ).removeAttr( 'aria-label' ); } } 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( __( 'Update Failed!' ) ) .removeClass( 'updating-message' ); if ( response.pluginName ) { $card.find( '.update-now' ) .attr( 'aria-label', sprintf( /* translators: %s: Plugin name and version. */ _x( '%s update failed.', 'plugin' ), 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( __( 'Update Now' ) ); }, 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() !== __( 'Installing...' ) ) { $message.data( 'originaltext', $message.html() ); } $message .addClass( 'updating-message' ) .attr( 'aria-label', sprintf( /* translators: %s: Plugin name and version. */ _x( 'Installing %s...', 'plugin' ), $message.data( 'name' ) ) ) .text( __( 'Installing...' ) ); wp.a11y.speak( __( 'Installing... please wait.' ) ); // 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 * * @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', sprintf( /* translators: %s: Plugin name and version. */ _x( '%s installed!', 'plugin' ), response.pluginName ) ) .text( _x( 'Installed!', 'plugin' ) ); wp.a11y.speak( __( 'Installation completed successfully.' ) ); $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 ); if ( 'plugins-network' === pagenow ) { $message .attr( 'aria-label', sprintf( /* translators: %s: Plugin name. */ _x( 'Network Activate %s', 'plugin' ), response.pluginName ) ) .text( __( 'Network Activate' ) ); } else { $message .attr( 'aria-label', sprintf( /* translators: %s: Plugin name. */ _x( 'Activate %s', 'plugin' ), response.pluginName ) ) .text( __( 'Activate' ) ); } }, 1000 ); } }; /** * Updates the UI appropriately after a failed plugin install. * * @since 4.6.0 * * @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 ( ! wp.updates.isValidResponse( response, 'install' ) ) { return; } if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) { return; } errorMessage = sprintf( /* translators: %s: Error string for a failed installation. */ __( 'Installation failed: %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', sprintf( /* translators: %s: Plugin name and version. */ _x( 'Update %s now', 'plugin' ), $message.data( 'name' ) ) ); } else if ( 'install-plugin' === job.action ) { $message.attr( 'aria-label', sprintf( /* translators: %s: Plugin name. */ _x( 'Install %s now', 'plugin' ), $message.data( 'name' ) ) ); } } } wp.a11y.speak( __( 'Update canceled.' ) ); } ); /** * 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( __( 'Install Now' ) ); wp.a11y.speak( __( 'Update canceled.' ) ); } ); } 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' ) .attr( 'aria-label', sprintf( /* translators: %s: Plugin name. */ _x( 'Install %s now', 'plugin' ), pluginName ) ) .text( __( 'Install Now' ) ); wp.a11y.speak( __( 'Update canceled.' ) ); } ); } wp.updates.installPlugin( { slug: $button.data( 'slug' ), pagenow: pagenow, 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' ), confirmMessage; if ( $pluginRow.hasClass( 'is-uninstallable' ) ) { confirmMessage = sprintf( /* translators: %s: Plugin name. */ __( 'Are you sure you want to delete %s and its data?' ), $pluginRow.find( '.plugin-title strong' ).text() ); } else { confirmMessage = sprintf( /* translators: %s: Plugin name. */ __( 'Are you sure you want to delete %s?' ), $pluginRow.find( '.plugin-title strong' ).text() ); } event.preventDefault(); if ( ! window.confirm( confirmMessage ) ) { 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' ), confirmMessage = sprintf( /* translators: %s: Theme name. */ __( 'Are you sure you want to delete %s?' ), $themeRow.find( '.theme-title strong' ).text() ); event.preventDefault(); if ( ! window.confirm( confirmMessage ) ) { 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"]:not([name="clear-recent-list"])', 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: __( 'Please select at least one item to perform this action on.' ) } ); } // Determine the type of request we're dealing with. switch ( bulkAction ) { case 'update-selected': action = bulkAction.replace( 'selected', type ); break; case 'delete-selected': var confirmMessage = 'plugin' === type ? __( 'Are you sure you want to delete the selected plugins and their data?' ) : __( 'Caution: These themes may be active on other sites in the network. Are you sure you want to proceed?' ); if ( ! window.confirm( confirmMessage ) ) { 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': __( 'Search Results' ) } ) ); $( '.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( __( 'You do not appear to have any plugins available at this time.' ) ); } else { wp.a11y.speak( sprintf( /* translators: %s: Number of plugins. */ __( 'Number of plugins found: %d' ), response.count ) ); } } ); }, 1000 ) ); 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, plugin_status: 'all' }, queryArgs; // 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; } queryArgs = _.object( _.compact( _.map( location.search.slice( 1 ).split( '&' ), function( item ) { if ( item ) return item.split( '=' ); } ) ) ); data.plugin_status = queryArgs.plugin_status || 'all'; if ( window.history && window.history.replaceState ) { window.history.replaceState( null, '', location.href.split( '?' )[ 0 ] + '?s=' + data.s + '&plugin_status=' + data.plugin_status ); } if ( 'undefined' !== typeof wp.updates.searchRequest ) { wp.updates.searchRequest.abort(); } $bulkActionForm.empty(); $( 'body' ).addClass( 'loading-content' ); $( '.subsubsub .current' ).removeClass( 'current' ); 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( sprintf( /* translators: %s: Search query. */ __( 'Search results for “%s”' ), _.escape( data.s ) ) ), $oldSubTitle = $( '.wrap .subtitle' ); if ( ! data.s.length ) { $oldSubTitle.remove(); $( '.subsubsub .' + data.plugin_status + ' a' ).addClass( 'current' ); } else if ( $oldSubTitle.length ) { $oldSubTitle.replaceWith( $subTitle ); } else { $( '.wp-header-end' ).before( $subTitle ); } $( 'body' ).removeClass( 'loading-content' ); $bulkActionForm.append( response.items ); delete wp.updates.searchRequest; if ( 0 === response.count ) { wp.a11y.speak( __( 'No plugins found. Try a different search.' ) ); } else { wp.a11y.speak( sprintf( /* translators: %s: Number of plugins. */ __( 'Number of plugins found: %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 "Try Again" button is clicked. * * @since 4.9.0 */ $document.on( 'click', '.try-again', function( event ) { event.preventDefault(); $pluginInstallSearch.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.host, message; if ( originalEvent.origin !== expectedOrigin ) { return; } try { message = $.parseJSON( originalEvent.data ); } catch ( e ) { return; } if ( ! message || '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 ); /** * Prevents the page form scrolling when activating auto-updates with the Spacebar key. * * @since 5.5.0 */ $document.on( 'keydown', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) { if ( 32 === event.which ) { event.preventDefault(); } } ); /** * Click and keyup handler for enabling and disabling plugin and theme auto-updates. * * These controls can be either links or buttons. When JavaScript is enabled, * we want them to behave like buttons. An ARIA role `button` is added via * the JavaScript that targets elements with the CSS class `aria-button-if-js`. * * @since 5.5.0 */ $document.on( 'click keyup', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) { var data, asset, type, $parent, $toggler = $( this ), action = $toggler.attr( 'data-wp-action' ), $label = $toggler.find( '.label' ); if ( 'keyup' === event.type && 32 !== event.which ) { return; } if ( 'themes' !== pagenow ) { $parent = $toggler.closest( '.column-auto-updates' ); } else { $parent = $toggler.closest( '.theme-autoupdate' ); } event.preventDefault(); // Prevent multiple simultaneous requests. if ( $toggler.attr( 'data-doing-ajax' ) === 'yes' ) { return; } $toggler.attr( 'data-doing-ajax', 'yes' ); switch ( pagenow ) { case 'plugins': case 'plugins-network': type = 'plugin'; asset = $toggler.closest( 'tr' ).attr( 'data-plugin' ); break; case 'themes-network': type = 'theme'; asset = $toggler.closest( 'tr' ).attr( 'data-slug' ); break; case 'themes': type = 'theme'; asset = $toggler.attr( 'data-slug' ); break; } // Clear any previous errors. $parent.find( '.notice.notice-error' ).addClass( 'hidden' ); // Show loading status. if ( 'enable' === action ) { $label.text( __( 'Enabling...' ) ); } else { $label.text( __( 'Disabling...' ) ); } $toggler.find( '.dashicons-update' ).removeClass( 'hidden' ); data = { action: 'toggle-auto-updates', _ajax_nonce: settings.ajax_nonce, state: action, type: type, asset: asset }; $.post( window.ajaxurl, data ) .done( function( response ) { var $enabled, $disabled, enabledNumber, disabledNumber, errorMessage, href = $toggler.attr( 'href' ); if ( ! response.success ) { // if WP returns 0 for response (which can happen in a few cases), // output the general error message since we won't have response.data.error. if ( response.data && response.data.error ) { errorMessage = response.data.error; } else { errorMessage = __( 'The request could not be completed.' ); } $parent.find( '.notice.notice-error' ).removeClass( 'hidden' ).find( 'p' ).text( errorMessage ); wp.a11y.speak( errorMessage, 'assertive' ); return; } // Update the counts in the enabled/disabled views if on a screen // with a list table. if ( 'themes' !== pagenow ) { $enabled = $( '.auto-update-enabled span' ); $disabled = $( '.auto-update-disabled span' ); enabledNumber = parseInt( $enabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0; disabledNumber = parseInt( $disabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0; switch ( action ) { case 'enable': ++enabledNumber; --disabledNumber; break; case 'disable': --enabledNumber; ++disabledNumber; break; } enabledNumber = Math.max( 0, enabledNumber ); disabledNumber = Math.max( 0, disabledNumber ); $enabled.text( '(' + enabledNumber + ')' ); $disabled.text( '(' + disabledNumber + ')' ); } if ( 'enable' === action ) { // The toggler control can be either a link or a button. if ( $toggler[ 0 ].hasAttribute( 'href' ) ) { href = href.replace( 'action=enable-auto-update', 'action=disable-auto-update' ); $toggler.attr( 'href', href ); } $toggler.attr( 'data-wp-action', 'disable' ); $label.text( __( 'Disable auto-updates' ) ); $parent.find( '.auto-update-time' ).removeClass( 'hidden' ); wp.a11y.speak( __( 'Auto-updates enabled' ) ); } else { // The toggler control can be either a link or a button. if ( $toggler[ 0 ].hasAttribute( 'href' ) ) { href = href.replace( 'action=disable-auto-update', 'action=enable-auto-update' ); $toggler.attr( 'href', href ); } $toggler.attr( 'data-wp-action', 'enable' ); $label.text( __( 'Enable auto-updates' ) ); $parent.find( '.auto-update-time' ).addClass( 'hidden' ); wp.a11y.speak( __( 'Auto-updates disabled' ) ); } $document.trigger( 'wp-auto-update-setting-changed', { state: action, type: type, asset: asset } ); } ) .fail( function() { $parent.find( '.notice.notice-error' ) .removeClass( 'hidden' ) .find( 'p' ) .text( __( 'The request could not be completed.' ) ); wp.a11y.speak( __( 'The request could not be completed.' ), 'assertive' ); } ) .always( function() { $toggler.removeAttr( 'data-doing-ajax' ).find( '.dashicons-update' ).addClass( 'hidden' ); } ); } ); } ); })( jQuery, window.wp, window._wpUpdatesSettings );