/** * Functions for ajaxified updates, deletions and installs inside the WordPress admin. * * @version 4.2.0 * * @package WordPress * @subpackage Administration */ /* 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.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 = wp || {}; /** * The WP Updates object. * * @since 4.2.0 * * @type {object} */ wp.updates = {}; /** * 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 {string} filesystemCredentials.fsNonce Filesystem credentials form nonce. * @type {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} 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 { 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 ) { window.console.log( $( '
' ).html( message ).text() ); } ); } }; /** * 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 = wp.updates.l10n.pluginUpdatingLabel.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.pluginUpdatingLabel.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.pluginUpdatedLabel.replace( '%s', response.pluginName ) ) .text( wp.updates.l10n.pluginUpdated ); 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.pluginUpdateFailedLabel.replace( '%s', 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( wp.updates.l10n.updateFailedShort ).removeClass( 'updating-message' ); if ( response.pluginName ) { $card.find( '.update-now' ) .attr( 'aria-label', wp.updates.l10n.pluginUpdateFailedLabel.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.pluginInstalled ); 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 ); } }; /** * 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 ( ! wp.updates.isValidResponse( response, 'install' ) ) { return; } 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.pluginUpdateNowLabel.replace( '%s', $message.data( 'name' ) ) ); } else if ( 'install-plugin' === job.action ) { $message.attr( 'aria-label', wp.updates.l10n.pluginInstallNowLabel.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' ), 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' ); 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"]: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: 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, 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( wp.updates.l10n.searchResults.replace( '%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( 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 "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.hostname, 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 ); } ); })( jQuery, window.wp, window._wpUpdatesSettings );