/** * Functions for ajaxified updates, deletions and installs inside the WordPress admin. * * @version 4.2.0 * @output wp-admin/js/updates.js */ /* global pagenow, _wpThemeSettings */ /** * @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 {Array} settings.plugins['auto-update-enabled'] Base names of plugins set to auto-update. * @param {Array} settings.plugins['auto-update-disabled'] Base names of plugins set to not auto-update. * @param {object=} settings.themes Slugs of themes in their different states. * @param {Array} settings.themes.all Slugs of all themes. * @param {Array} settings.themes.upgrade Slugs of themes with updates available. * @param {Arrat} settings.themes.disabled Slugs of disabled themes. * @param {Array} settings.themes['auto-update-enabled'] Slugs of themes set to auto-update. * @param {Array} settings.themes['auto-update-disabled'] Slugs of themes set to not auto-update. * @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, _n = wp.i18n._n, _nx = wp.i18n._nx, sprintf = wp.i18n.sprintf; wp = wp || {}; /** * The WP Updates object. * * @since 4.2.0 * * @namespace wp.updates */ wp.updates = {}; /** * Removed in 5.5.0, needed for back-compatibility. * * @since 4.2.0 * @deprecated 5.5.0 * * @type {object} */ wp.updates.l10n = { searchResults: '', searchResultsLabel: '', noPlugins: '', noItemsSelected: '', updating: '', pluginUpdated: '', themeUpdated: '', update: '', updateNow: '', pluginUpdateNowLabel: '', updateFailedShort: '', updateFailed: '', pluginUpdatingLabel: '', pluginUpdatedLabel: '', pluginUpdateFailedLabel: '', updatingMsg: '', updatedMsg: '', updateCancel: '', beforeunload: '', installNow: '', pluginInstallNowLabel: '', installing: '', pluginInstalled: '', themeInstalled: '', installFailedShort: '', installFailed: '', pluginInstallingLabel: '', themeInstallingLabel: '', pluginInstalledLabel: '', themeInstalledLabel: '', pluginInstallFailedLabel: '', themeInstallFailedLabel: '', installingMsg: '', installedMsg: '', importerInstalledMsg: '', aysDelete: '', aysDeleteUninstall: '', aysBulkDelete: '', aysBulkDeleteThemes: '', deleting: '', deleteFailed: '', pluginDeleted: '', themeDeleted: '', livePreview: '', activatePlugin: '', activateTheme: '', activatePluginLabel: '', activateThemeLabel: '', activateImporter: '', activateImporterLabel: '', unknownError: '', connectionError: '', nonceError: '', pluginsFound: '', noPluginsFound: '', autoUpdatesEnable: '', autoUpdatesEnabling: '', autoUpdatesEnabled: '', autoUpdatesDisable: '', autoUpdatesDisabling: '', autoUpdatesDisabled: '', autoUpdatesError: '' }; wp.updates.l10n = window.wp.deprecateL10nObject( 'wp.updates.l10n', wp.updates.l10n, '5.5.0' ); /** * 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-label' ).text( settings.totals.counts.total ); $adminBarUpdates.find( '.updates-available-text' ).text( sprintf( /* translators: %s: Total number of updates available. */ _n( '%s update available', '%s updates available', settings.totals.counts.total ), 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(); } ); } }; /** * Sends a message from a modal to the main screen to update buttons in plugin cards. * * @since 6.5.0 * * @param {Object} data An object of data to use for the button. * @param {string} data.slug The plugin's slug. * @param {string} data.text The text to use for the button. * @param {string} data.ariaLabel The value for the button's aria-label attribute. An empty string removes the attribute. * @param {string=} data.status Optional. An identifier for the status. * @param {string=} data.removeClasses Optional. A space-separated list of classes to remove from the button. * @param {string=} data.addClasses Optional. A space-separated list of classes to add to the button. * @param {string=} data.href Optional. The button's URL. * @param {string=} data.pluginName Optional. The plugin's name. * @param {string=} data.plugin Optional. The plugin file, relative to the plugins directory. */ wp.updates.setCardButtonStatus = function( data ) { var target = window.parent === window ? null : window.parent; $.support.postMessage = !! window.postMessage; if ( false !== $.support.postMessage && null !== target && -1 === window.parent.location.pathname.indexOf( 'index.php' ) ) { target.postMessage( JSON.stringify( data ), window.location.origin ); } }; /** * 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, $adminBarUpdates = $( '#wp-admin-bar-updates' ), buttonText = __( 'Updating...' ), isPluginInstall = 'plugin-install' === pagenow || 'plugin-install-network' === pagenow; 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 ( isPluginInstall ) { $card = $( '.plugin-card-' + args.slug + ', #plugin-information-footer' ); $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(); } $adminBarUpdates.addClass( 'spin' ); if ( $message.html() !== __( 'Updating...' ) ) { $message.data( 'originaltext', $message.html() ); } $message .attr( 'aria-label', message ) .text( buttonText ); $document.trigger( 'wp-plugin-updating', args ); if ( isPluginInstall && 'plugin-information-footer' === $card.attr( 'id' ) ) { wp.updates.setCardButtonStatus( { status: 'updating-plugin', slug: args.slug, addClasses: 'updating-message', text: buttonText, ariaLabel: message } ); } 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, $adminBarUpdates = $( '#wp-admin-bar-updates' ), buttonText = _x( 'Updated!', 'plugin' ), ariaLabel = sprintf( /* translators: %s: Plugin name and version. */ _x( '%s updated!', 'plugin' ), response.pluginName ); if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { $pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' ) .removeClass( 'update is-enqueued' ) .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 + ', #plugin-information-footer' ).find( '.update-now' ) .removeClass( 'updating-message' ) .addClass( 'button-disabled updated-message' ); } $adminBarUpdates.removeClass( 'spin' ); $updateMessage .attr( 'aria-label', ariaLabel ) .text( buttonText ); wp.a11y.speak( __( 'Update completed successfully.' ) ); if ( 'plugin_install_from_iframe' !== $updateMessage.attr( 'id' ) ) { wp.updates.decrementCount( 'plugin' ); } else { wp.updates.setCardButtonStatus( { status: 'updated-plugin', slug: response.slug, removeClasses: 'updating-message', addClasses: 'button-disabled updated-message', text: buttonText, ariaLabel: ariaLabel } ); } $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 $pluginRow, $card, $message, errorMessage, buttonText, ariaLabel, $adminBarUpdates = $( '#wp-admin-bar-updates' ); 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 ) { $pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' ).removeClass( 'is-enqueued' ); 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 ) { buttonText = __( 'Update failed.' ); $card = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ) .append( wp.updates.adminNotice( { className: 'update-message notice-error notice-alt is-dismissible', message: errorMessage } ) ); if ( $card.hasClass( 'plugin-card-' + response.slug ) ) { $card.addClass( 'plugin-card-update-failed' ); } $card.find( '.update-now' ) .text( buttonText ) .removeClass( 'updating-message' ); if ( response.pluginName ) { ariaLabel = sprintf( /* translators: %s: Plugin name and version. */ _x( '%s update failed.', 'plugin' ), response.pluginName ); $card.find( '.update-now' ).attr( 'aria-label', ariaLabel ); } else { ariaLabel = ''; $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' ).trigger( 'focus' ); $card.find( '.update-now' ) .attr( 'aria-label', false ) .text( __( 'Update Now' ) ); }, 200 ); } ); } $adminBarUpdates.removeClass( 'spin' ); wp.a11y.speak( errorMessage, 'assertive' ); if ( 'plugin-information-footer' === $card.attr('id' ) ) { wp.updates.setCardButtonStatus( { status: 'plugin-update-failed', slug: response.slug, removeClasses: 'updating-message', text: buttonText, ariaLabel: ariaLabel } ); } $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 + ', #plugin-information-footer' ), $message = $card.find( '.install-now' ), buttonText = __( 'Installing...' ), ariaLabel; 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() ); } ariaLabel = sprintf( /* translators: %s: Plugin name and version. */ _x( 'Installing %s...', 'plugin' ), $message.data( 'name' ) ); $message .addClass( 'updating-message' ) .attr( 'aria-label', ariaLabel ) .text( buttonText ); 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 ); if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { wp.updates.setCardButtonStatus( { status: 'installing-plugin', slug: args.slug, addClasses: 'updating-message', text: buttonText, ariaLabel: ariaLabel } ); } 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 + ', #plugin-information-footer' ).find( '.install-now' ), buttonText = _x( 'Installed!', 'plugin' ), ariaLabel = sprintf( /* translators: %s: Plugin name and version. */ _x( '%s installed!', 'plugin' ), response.pluginName ); $message .removeClass( 'updating-message' ) .addClass( 'updated-message installed button-disabled' ) .attr( 'aria-label', ariaLabel ) .text( buttonText ); wp.a11y.speak( __( 'Installation completed successfully.' ) ); $document.trigger( 'wp-plugin-install-success', response ); if ( response.activateUrl ) { setTimeout( function() { wp.updates.checkPluginDependencies( { slug: response.slug } ); }, 1000 ); } if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { wp.updates.setCardButtonStatus( { status: 'installed-plugin', slug: response.slug, removeClasses: 'updating-message', addClasses: 'updated-message installed button-disabled', text: buttonText, ariaLabel: ariaLabel } ); } }; /** * 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 + ', #plugin-information-footer' ), $button = $card.find( '.install-now' ), buttonText = __( 'Installation failed.' ), ariaLabel = sprintf( /* translators: %s: Plugin name and version. */ _x( '%s installation failed', 'plugin' ), $button.data( 'name' ) ), 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( '<div class="notice notice-error notice-alt is-dismissible"><p>' + errorMessage + '</p></div>' ); $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' ).trigger( 'focus' ); }, 200 ); } ); $button .removeClass( 'updating-message' ).addClass( 'button-disabled' ) .attr( 'aria-label', ariaLabel ) .text( buttonText ); wp.a11y.speak( errorMessage, 'assertive' ); wp.updates.setCardButtonStatus( { status: 'plugin-install-failed', slug: response.slug, removeClasses: 'updating-message', addClasses: 'button-disabled', text: buttonText, ariaLabel: ariaLabel } ); $document.trigger( 'wp-plugin-install-error', response ); }; /** * Sends an Ajax request to the server to check a plugin's dependencies. * * @since 6.5.0 * * @param {Object} args Arguments. * @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository. * @param {checkPluginDependenciesSuccess=} args.success Optional. Success callback. Default: wp.updates.checkPluginDependenciesSuccess * @param {checkPluginDependenciesError=} args.error Optional. Error callback. Default: wp.updates.checkPluginDependenciesError * @return {$.promise} A jQuery promise that represents the request, * decorated with an abort() method. */ wp.updates.checkPluginDependencies = function( args ) { args = _.extend( { success: wp.updates.checkPluginDependenciesSuccess, error: wp.updates.checkPluginDependenciesError }, args ); wp.a11y.speak( __( 'Checking plugin dependencies... please wait.' ) ); $document.trigger( 'wp-checking-plugin-dependencies', args ); return wp.updates.ajax( 'check_plugin_dependencies', args ); }; /** * Updates the UI appropriately after a successful plugin dependencies check. * * @since 6.5.0 * * @param {Object} response Response from the server. * @param {string} response.slug Slug of the checked plugin. * @param {string} response.pluginName Name of the checked plugin. * @param {string} response.plugin The plugin file, relative to the plugins directory. * @param {string} response.activateUrl URL to activate the just checked plugin. */ wp.updates.checkPluginDependenciesSuccess = function( response ) { var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.install-now' ), buttonText, ariaLabel; // 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 ); wp.a11y.speak( __( 'Plugin dependencies check completed successfully.' ) ); $document.trigger( 'wp-check-plugin-dependencies-success', response ); if ( 'plugins-network' === pagenow ) { buttonText = _x( 'Network Activate', 'plugin' ); ariaLabel = sprintf( /* translators: %s: Plugin name. */ _x( 'Network Activate %s', 'plugin' ), response.pluginName ); $message .attr( 'aria-label', ariaLabel ) .text( buttonText ); } else { buttonText = _x( 'Activate', 'plugin' ); ariaLabel = sprintf( /* translators: %s: Plugin name. */ _x( 'Activate %s', 'plugin' ), response.pluginName ); $message .attr( 'aria-label', ariaLabel ) .attr( 'data-name', response.pluginName ) .attr( 'data-slug', response.slug ) .attr( 'data-plugin', response.plugin ) .text( buttonText ); } if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { wp.updates.setCardButtonStatus( { status: 'dependencies-check-success', slug: response.slug, removeClasses: 'install-now installed button-disabled updated-message', addClasses: 'activate-now button-primary', text: buttonText, ariaLabel: ariaLabel, pluginName: response.pluginName, plugin: response.plugin, href: response.activateUrl } ); } }; /** * Updates the UI appropriately after a failed plugin dependencies check. * * @since 6.5.0 * * @param {Object} response Response from the server. * @param {string} response.slug Slug of the plugin to be checked. * @param {string=} response.pluginName Optional. Name of the plugin to be checked. * @param {string} response.errorCode Error code for the error that occurred. * @param {string} response.errorMessage The error that occurred. */ wp.updates.checkPluginDependenciesError = function( response ) { var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.install-now' ), buttonText = _x( 'Activate', 'plugin' ), ariaLabel = sprintf( /* translators: 1: Plugin name, 2. The reason the plugin cannot be activated. */ _x( 'Cannot activate %1$s. %2$s', 'plugin' ), response.pluginName, response.errorMessage ), errorMessage; if ( ! wp.updates.isValidResponse( response, 'check-dependencies' ) ) { return; } errorMessage = sprintf( /* translators: %s: Error string for a failed activation. */ __( 'Activation failed: %s' ), response.errorMessage ); wp.a11y.speak( errorMessage, 'assertive' ); $document.trigger( 'wp-check-plugin-dependencies-error', response ); $message .removeClass( 'install-now installed updated-message' ) .addClass( 'activate-now button-primary' ) .attr( 'aria-label', ariaLabel ) .text( buttonText ); if ( 'plugin-information-footer' === $message.parent().attr('id' ) ) { wp.updates.setCardButtonStatus( { status: 'dependencies-check-failed', slug: response.slug, removeClasses: 'install-now installed updated-message', addClasses: 'activate-now button-primary', text: buttonText, ariaLabel: ariaLabel } ); } }; /** * Sends an Ajax request to the server to activate a plugin. * * @since 6.5.0 * * @param {Object} args Arguments. * @param {string} args.name The name of the plugin. * @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository. * @param {string} args.plugin The plugin file, relative to the plugins directory. * @param {activatePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.activatePluginSuccess * @param {activatePluginError=} args.error Optional. Error callback. Default: wp.updates.activatePluginError * @return {$.promise} A jQuery promise that represents the request, * decorated with an abort() method. */ wp.updates.activatePlugin = function( args ) { var $message = $( '.plugin-card-' + args.slug + ', #plugin-information-footer' ).find( '.activate-now, .activating-message' ); args = _.extend( { success: wp.updates.activatePluginSuccess, error: wp.updates.activatePluginError }, args ); wp.a11y.speak( __( 'Activating... please wait.' ) ); $document.trigger( 'wp-activating-plugin', args ); if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { wp.updates.setCardButtonStatus( { status: 'activating-plugin', slug: args.slug, removeClasses: 'installed updated-message button-primary', addClasses: 'activating-message', text: __( 'Activating...' ), ariaLabel: sprintf( /* translators: %s: Plugin name. */ _x( 'Activating %s', 'plugin' ), args.name ) } ); } return wp.updates.ajax( 'activate-plugin', args ); }; /** * Updates the UI appropriately after a successful plugin activation. * * @since 6.5.0 * * @param {Object} response Response from the server. * @param {string} response.slug Slug of the activated plugin. * @param {string} response.pluginName Name of the activated plugin. * @param {string} response.plugin The plugin file, relative to the plugins directory. */ wp.updates.activatePluginSuccess = function( response ) { var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.activating-message' ), buttonText = _x( 'Activated!', 'plugin' ), ariaLabel = sprintf( /* translators: %s: The plugin name. */ '%s activated successfully.', response.pluginName ); wp.a11y.speak( __( 'Activation completed successfully.' ) ); $document.trigger( 'wp-plugin-activate-success', response ); $message .removeClass( 'activating-message' ) .addClass( 'activated-message button-disabled' ) .attr( 'aria-label', ariaLabel ) .text( buttonText ); if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { wp.updates.setCardButtonStatus( { status: 'activated-plugin', slug: response.slug, removeClasses: 'activating-message', addClasses: 'activated-message button-disabled', text: buttonText, ariaLabel: ariaLabel } ); } setTimeout( function() { $message.removeClass( 'activated-message' ) .text( _x( 'Active', 'plugin' ) ); if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { wp.updates.setCardButtonStatus( { status: 'plugin-active', slug: response.slug, removeClasses: 'activated-message', text: _x( 'Active', 'plugin' ), ariaLabel: sprintf( /* translators: %s: The plugin name. */ '%s is active.', response.pluginName ) } ); } }, 1000 ); }; /** * Updates the UI appropriately after a failed plugin activation. * * @since 6.5.0 * * @param {Object} response Response from the server. * @param {string} response.slug Slug of the plugin to be activated. * @param {string=} response.pluginName Optional. Name of the plugin to be activated. * @param {string} response.errorCode Error code for the error that occurred. * @param {string} response.errorMessage The error that occurred. */ wp.updates.activatePluginError = function( response ) { var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.activating-message' ), buttonText = __( 'Activation failed.' ), ariaLabel = sprintf( /* translators: %s: Plugin name. */ _x( '%s activation failed', 'plugin' ), response.pluginName ), errorMessage; if ( ! wp.updates.isValidResponse( response, 'activate' ) ) { return; } errorMessage = sprintf( /* translators: %s: Error string for a failed activation. */ __( 'Activation failed: %s' ), response.errorMessage ); wp.a11y.speak( errorMessage, 'assertive' ); $document.trigger( 'wp-plugin-activate-error', response ); $message .removeClass( 'install-now installed activating-message' ) .addClass( 'button-disabled' ) .attr( 'aria-label', ariaLabel ) .text( buttonText ); if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { wp.updates.setCardButtonStatus( { status: 'plugin-activation-failed', slug: response.slug, removeClasses: 'install-now installed activating-message', addClasses: 'button-disabled', text: buttonText, ariaLabel: ariaLabel } ); } }; /** * Updates the UI appropriately after a successful importer 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.installImporterSuccess = function( response ) { wp.updates.addAdminNotice( { id: 'install-success', className: 'notice-success is-dismissible', message: sprintf( /* translators: %s: Activation URL. */ __( 'Importer installed successfully. <a href="%s">Run importer</a>' ), response.activateUrl + '&from=import' ) } ); $( '[data-slug="' + response.slug + '"]' ) .removeClass( 'install-now updating-message' ) .addClass( 'activate-now' ) .attr({ 'href': response.activateUrl + '&from=import', 'aria-label':sprintf( /* translators: %s: Importer name. */ __( 'Run %s' ), response.pluginName ) }) .text( __( 'Run Importer' ) ); wp.a11y.speak( __( 'Installation completed successfully.' ) ); $document.trigger( 'wp-importer-install-success', response ); }; /** * Updates the UI appropriately after a failed importer 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.installImporterError = function( response ) { var errorMessage = sprintf( /* translators: %s: Error string for a failed installation. */ __( 'Installation failed: %s' ), response.errorMessage ), $installLink = $( '[data-slug="' + response.slug + '"]' ), pluginName = $installLink.data( 'name' ); if ( ! wp.updates.isValidResponse( response, 'install' ) ) { return; } if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) { return; } wp.updates.addAdminNotice( { id: response.errorCode, className: 'notice-error is-dismissible', message: errorMessage } ); $installLink .removeClass( 'updating-message' ) .attr( 'aria-label', sprintf( /* translators: %s: Plugin name. */ _x( 'Install %s now', 'plugin' ), pluginName ) ) .text( _x( 'Install Now', 'plugin' ) ); wp.a11y.speak( errorMessage, 'assertive' ); $document.trigger( 'wp-importer-install-error', response ); }; /** * Sends an Ajax request to the server to delete a plugin. * * @since 4.6.0 * * @param {Object} args Arguments. * @param {string} args.plugin Basename of the plugin to be deleted. * @param {string} args.slug Slug of the plugin to be deleted. * @param {deletePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.deletePluginSuccess * @param {deletePluginError=} args.error Optional. Error callback. Default: wp.updates.deletePluginError * @return {$.promise} A jQuery promise that represents the request, * decorated with an abort() method. */ wp.updates.deletePlugin = function( args ) { var $link = $( '[data-plugin="' + args.plugin + '"]' ).find( '.row-actions a.delete' ); args = _.extend( { success: wp.updates.deletePluginSuccess, error: wp.updates.deletePluginError }, args ); if ( $link.html() !== __( 'Deleting...' ) ) { $link .data( 'originaltext', $link.html() ) .text( __( 'Deleting...' ) ); } wp.a11y.speak( __( 'Deleting...' ) ); $document.trigger( 'wp-plugin-deleting', args ); return wp.updates.ajax( 'delete-plugin', args ); }; /** * Updates the UI appropriately after a successful plugin deletion. * * @since 4.6.0 * * @param {Object} response Response from the server. * @param {string} response.slug Slug of the plugin that was deleted. * @param {string} response.plugin Base name of the plugin that was deleted. * @param {string} response.pluginName Name of the plugin that was deleted. */ wp.updates.deletePluginSuccess = function( response ) { // Removes the plugin and updates rows. $( '[data-plugin="' + response.plugin + '"]' ).css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() { var $form = $( '#bulk-action-form' ), $views = $( '.subsubsub' ), $pluginRow = $( this ), $currentView = $views.find( '[aria-current="page"]' ), $itemsCount = $( '.displaying-num' ), columnCount = $form.find( 'thead th:not(.hidden), thead td' ).length, pluginDeletedRow = wp.template( 'item-deleted-row' ), /** * Plugins Base names of plugins in their different states. * * @type {Object} */ plugins = settings.plugins, remainingCount; // Add a success message after deleting a plugin. if ( ! $pluginRow.hasClass( 'plugin-update-tr' ) ) { $pluginRow.after( pluginDeletedRow( { slug: response.slug, plugin: response.plugin, colspan: columnCount, name: response.pluginName } ) ); } $pluginRow.remove(); // Remove plugin from update count. if ( -1 !== _.indexOf( plugins.upgrade, response.plugin ) ) { plugins.upgrade = _.without( plugins.upgrade, response.plugin ); wp.updates.decrementCount( 'plugin' ); } // Remove from views. if ( -1 !== _.indexOf( plugins.inactive, response.plugin ) ) { plugins.inactive = _.without( plugins.inactive, response.plugin ); if ( plugins.inactive.length ) { $views.find( '.inactive .count' ).text( '(' + plugins.inactive.length + ')' ); } else { $views.find( '.inactive' ).remove(); } } if ( -1 !== _.indexOf( plugins.active, response.plugin ) ) { plugins.active = _.without( plugins.active, response.plugin ); if ( plugins.active.length ) { $views.find( '.active .count' ).text( '(' + plugins.active.length + ')' ); } else { $views.find( '.active' ).remove(); } } if ( -1 !== _.indexOf( plugins.recently_activated, response.plugin ) ) { plugins.recently_activated = _.without( plugins.recently_activated, response.plugin ); if ( plugins.recently_activated.length ) { $views.find( '.recently_activated .count' ).text( '(' + plugins.recently_activated.length + ')' ); } else { $views.find( '.recently_activated' ).remove(); } } if ( -1 !== _.indexOf( plugins['auto-update-enabled'], response.plugin ) ) { plugins['auto-update-enabled'] = _.without( plugins['auto-update-enabled'], response.plugin ); if ( plugins['auto-update-enabled'].length ) { $views.find( '.auto-update-enabled .count' ).text( '(' + plugins['auto-update-enabled'].length + ')' ); } else { $views.find( '.auto-update-enabled' ).remove(); } } if ( -1 !== _.indexOf( plugins['auto-update-disabled'], response.plugin ) ) { plugins['auto-update-disabled'] = _.without( plugins['auto-update-disabled'], response.plugin ); if ( plugins['auto-update-disabled'].length ) { $views.find( '.auto-update-disabled .count' ).text( '(' + plugins['auto-update-disabled'].length + ')' ); } else { $views.find( '.auto-update-disabled' ).remove(); } } plugins.all = _.without( plugins.all, response.plugin ); if ( plugins.all.length ) { $views.find( '.all .count' ).text( '(' + plugins.all.length + ')' ); } else { $form.find( '.tablenav' ).css( { visibility: 'hidden' } ); $views.find( '.all' ).remove(); if ( ! $form.find( 'tr.no-items' ).length ) { $form.find( '#the-list' ).append( '<tr class="no-items"><td class="colspanchange" colspan="' + columnCount + '">' + __( 'No plugins are currently available.' ) + '</td></tr>' ); } } if ( $itemsCount.length && $currentView.length ) { remainingCount = plugins[ $currentView.parent( 'li' ).attr('class') ].length; $itemsCount.text( sprintf( /* translators: %s: The remaining number of plugins. */ _nx( '%s item', '%s items', remainingCount, 'plugin/plugins' ), remainingCount ) ); } } ); wp.a11y.speak( _x( 'Deleted!', 'plugin' ) ); $document.trigger( 'wp-plugin-delete-success', response ); }; /** * Updates the UI appropriately after a failed plugin deletion. * * @since 4.6.0 * * @param {Object} response Response from the server. * @param {string} response.slug Slug of the plugin to be deleted. * @param {string} response.plugin Base name of the plugin to be deleted * @param {string=} response.pluginName Optional. Name of the plugin to be deleted. * @param {string} response.errorCode Error code for the error that occurred. * @param {string} response.errorMessage The error that occurred. */ wp.updates.deletePluginError = function( response ) { var $plugin, $pluginUpdateRow, pluginUpdateRow = wp.template( 'item-update-row' ), noticeContent = wp.updates.adminNotice( { className: 'update-message notice-error notice-alt', message: response.errorMessage } ); if ( response.plugin ) { $plugin = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' ); $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' ); } else { $plugin = $( 'tr.inactive[data-slug="' + response.slug + '"]' ); $pluginUpdateRow = $plugin.siblings( '[data-slug="' + response.slug + '"]' ); } if ( ! wp.updates.isValidResponse( response, 'delete' ) ) { return; } if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) { return; } // Add a plugin update row if it doesn't exist yet. if ( ! $pluginUpdateRow.length ) { $plugin.addClass( 'update' ).after( pluginUpdateRow( { slug: response.slug, plugin: response.plugin || response.slug, colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, content: noticeContent } ) ); } else { // Remove previous error messages, if any. $pluginUpdateRow.find( '.notice-error' ).remove(); $pluginUpdateRow.find( '.plugin-update' ).append( noticeContent ); } $document.trigger( 'wp-plugin-delete-error', response ); }; /** * Sends an Ajax request to the server to update a theme. * * @since 4.6.0 * * @param {Object} args Arguments. * @param {string} args.slug Theme stylesheet. * @param {updateThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.updateThemeSuccess * @param {updateThemeError=} args.error Optional. Error callback. Default: wp.updates.updateThemeError * @return {$.promise} A jQuery promise that represents the request, * decorated with an abort() method. */ wp.updates.updateTheme = function( args ) { var $notice; args = _.extend( { success: wp.updates.updateThemeSuccess, error: wp.updates.updateThemeError }, args ); if ( 'themes-network' === pagenow ) { $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' ); } else if ( 'customize' === pagenow ) { // Update the theme details UI. $notice = $( '[data-slug="' + args.slug + '"].notice' ).removeClass( 'notice-large' ); $notice.find( 'h3' ).remove(); // Add the top-level UI, and update both. $notice = $notice.add( $( '#customize-control-installed_theme_' + args.slug ).find( '.update-message' ) ); $notice = $notice.addClass( 'updating-message' ).find( 'p' ); } else { $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' ); $notice.find( 'h3' ).remove(); $notice = $notice.add( $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ) ); $notice = $notice.addClass( 'updating-message' ).find( 'p' ); } if ( $notice.html() !== __( 'Updating...' ) ) { $notice.data( 'originaltext', $notice.html() ); } wp.a11y.speak( __( 'Updating... please wait.' ) ); $notice.text( __( 'Updating...' ) ); $document.trigger( 'wp-theme-updating', args ); return wp.updates.ajax( 'update-theme', args ); }; /** * Updates the UI appropriately after a successful theme update. * * @since 4.6.0 * @since 5.5.0 Auto-update "time to next update" text cleared. * * @param {Object} response * @param {string} response.slug Slug of the theme to be updated. * @param {Object} response.theme Updated theme. * @param {string} response.oldVersion Old version of the theme. * @param {string} response.newVersion New version of the theme. */ wp.updates.updateThemeSuccess = function( response ) { var isModalOpen = $( 'body.modal-open' ).length, $theme = $( '[data-slug="' + response.slug + '"]' ), updatedMessage = { className: 'updated-message notice-success notice-alt', message: _x( 'Updated!', 'theme' ) }, $notice, newText; if ( 'customize' === pagenow ) { $theme = $( '.updating-message' ).siblings( '.theme-name' ); if ( $theme.length ) { // Update the version number in the row. newText = $theme.html().replace( response.oldVersion, response.newVersion ); $theme.html( newText ); } $notice = $( '.theme-info .notice' ).add( wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' ).find( '.update-message' ) ); } else if ( 'themes-network' === pagenow ) { $notice = $theme.find( '.update-message' ); // Update the version number in the row. newText = $theme.find( '.theme-version-author-uri' ).html().replace( response.oldVersion, response.newVersion ); $theme.find( '.theme-version-author-uri' ).html( newText ); // Clear the "time to next auto-update" text. $theme.find( '.auto-update-time' ).empty(); } else { $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) ); // Focus on Customize button after updating. if ( isModalOpen ) { $( '.load-customize:visible' ).trigger( 'focus' ); $( '.theme-info .theme-autoupdate' ).find( '.auto-update-time' ).empty(); } else { $theme.find( '.load-customize' ).trigger( 'focus' ); } } wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) ); wp.a11y.speak( __( 'Update completed successfully.' ) ); wp.updates.decrementCount( 'theme' ); $document.trigger( 'wp-theme-update-success', response ); // Show updated message after modal re-rendered. if ( isModalOpen && 'customize' !== pagenow ) { $( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) ); } }; /** * Updates the UI appropriately after a failed theme update. * * @since 4.6.0 * * @param {Object} response Response from the server. * @param {string} response.slug Slug of the theme to be updated. * @param {string} response.errorCode Error code for the error that occurred. * @param {string} response.errorMessage The error that occurred. */ wp.updates.updateThemeError = function( response ) { var $theme = $( '[data-slug="' + response.slug + '"]' ), errorMessage = sprintf( /* translators: %s: Error string for a failed update. */ __( 'Update failed: %s' ), response.errorMessage ), $notice; if ( ! wp.updates.isValidResponse( response, 'update' ) ) { return; } if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) { return; } if ( 'customize' === pagenow ) { $theme = wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' ); } if ( 'themes-network' === pagenow ) { $notice = $theme.find( '.update-message ' ); } else { $notice = $( '.theme-info .notice' ).add( $theme.find( '.notice' ) ); $( 'body.modal-open' ).length ? $( '.load-customize:visible' ).trigger( 'focus' ) : $theme.find( '.load-customize' ).trigger( 'focus'); } wp.updates.addAdminNotice( { selector: $notice, className: 'update-message notice-error notice-alt is-dismissible', message: errorMessage } ); wp.a11y.speak( errorMessage ); $document.trigger( 'wp-theme-update-error', response ); }; /** * Sends an Ajax request to the server to install a theme. * * @since 4.6.0 * * @param {Object} args * @param {string} args.slug Theme stylesheet. * @param {installThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.installThemeSuccess * @param {installThemeError=} args.error Optional. Error callback. Default: wp.updates.installThemeError * @return {$.promise} A jQuery promise that represents the request, * decorated with an abort() method. */ wp.updates.installTheme = function( args ) { var $message = $( '.theme-install[data-slug="' + args.slug + '"]' ); args = _.extend( { success: wp.updates.installThemeSuccess, error: wp.updates.installThemeError }, args ); $message.addClass( 'updating-message' ); $message.parents( '.theme' ).addClass( 'focus' ); if ( $message.html() !== __( 'Installing...' ) ) { $message.data( 'originaltext', $message.html() ); } $message .attr( 'aria-label', sprintf( /* translators: %s: Theme name and version. */ _x( 'Installing %s...', 'theme' ), $message.data( 'name' ) ) ) .text( __( 'Installing...' ) ); wp.a11y.speak( __( 'Installing... please wait.' ) ); // Remove previous error messages, if any. $( '.install-theme-info, [data-slug="' + args.slug + '"]' ).removeClass( 'theme-install-failed' ).find( '.notice.notice-error' ).remove(); $document.trigger( 'wp-theme-installing', args ); return wp.updates.ajax( 'install-theme', args ); }; /** * Updates the UI appropriately after a successful theme install. * * @since 4.6.0 * * @param {Object} response Response from the server. * @param {string} response.slug Slug of the theme to be installed. * @param {string} response.customizeUrl URL to the Customizer for the just installed theme. * @param {string} response.activateUrl URL to activate the just installed theme. */ wp.updates.installThemeSuccess = function( response ) { var $card = $( '.wp-full-overlay-header, [data-slug=' + response.slug + ']' ), $message; $document.trigger( 'wp-theme-install-success', response ); $message = $card.find( '.button-primary' ) .removeClass( 'updating-message' ) .addClass( 'updated-message disabled' ) .attr( 'aria-label', sprintf( /* translators: %s: Theme name and version. */ _x( '%s installed!', 'theme' ), response.themeName ) ) .text( _x( 'Installed!', 'theme' ) ); wp.a11y.speak( __( 'Installation completed successfully.' ) ); setTimeout( function() { if ( response.activateUrl ) { // Transform the 'Install' button into an 'Activate' button. $message .attr( 'href', response.activateUrl ) .removeClass( 'theme-install updated-message disabled' ) .addClass( 'activate' ); if ( 'themes-network' === pagenow ) { $message .attr( 'aria-label', sprintf( /* translators: %s: Theme name. */ _x( 'Network Activate %s', 'theme' ), response.themeName ) ) .text( __( 'Network Enable' ) ); } else { $message .attr( 'aria-label', sprintf( /* translators: %s: Theme name. */ _x( 'Activate %s', 'theme' ), response.themeName ) ) .text( _x( 'Activate', 'theme' ) ); } } if ( response.customizeUrl ) { // Transform the 'Preview' button into a 'Live Preview' button. $message.siblings( '.preview' ).replaceWith( function () { return $( '<a>' ) .attr( 'href', response.customizeUrl ) .addClass( 'button load-customize' ) .text( __( 'Live Preview' ) ); } ); } }, 1000 ); }; /** * Updates the UI appropriately after a failed theme install. * * @since 4.6.0 * * @param {Object} response Response from the server. * @param {string} response.slug Slug of the theme to be installed. * @param {string} response.errorCode Error code for the error that occurred. * @param {string} response.errorMessage The error that occurred. */ wp.updates.installThemeError = function( response ) { var $card, $button, errorMessage = sprintf( /* translators: %s: Error string for a failed installation. */ __( 'Installation failed: %s' ), response.errorMessage ), $message = wp.updates.adminNotice( { className: 'update-message notice-error notice-alt', message: errorMessage } ); if ( ! wp.updates.isValidResponse( response, 'install' ) ) { return; } if ( wp.updates.maybeHandleCredentialError( response, 'install-theme' ) ) { return; } if ( 'customize' === pagenow ) { if ( $document.find( 'body' ).hasClass( 'modal-open' ) ) { $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); $card = $( '.theme-overlay .theme-info' ).prepend( $message ); } else { $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); $card = $button.closest( '.theme' ).addClass( 'theme-install-failed' ).append( $message ); } wp.customize.notifications.remove( 'theme_installing' ); } else { if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) { $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); $card = $( '.install-theme-info' ).prepend( $message ); } else { $card = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message ); $button = $card.find( '.theme-install' ); } } $button .removeClass( 'updating-message' ) .attr( 'aria-label', sprintf( /* translators: %s: Theme name and version. */ _x( '%s installation failed', 'theme' ), $button.data( 'name' ) ) ) .text( __( 'Installation failed.' ) ); wp.a11y.speak( errorMessage, 'assertive' ); $document.trigger( 'wp-theme-install-error', response ); }; /** * Sends an Ajax request to the server to delete a theme. * * @since 4.6.0 * * @param {Object} args * @param {string} args.slug Theme stylesheet. * @param {deleteThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.deleteThemeSuccess * @param {deleteThemeError=} args.error Optional. Error callback. Default: wp.updates.deleteThemeError * @return {$.promise} A jQuery promise that represents the request, * decorated with an abort() method. */ wp.updates.deleteTheme = function( args ) { var $button; if ( 'themes' === pagenow ) { $button = $( '.theme-actions .delete-theme' ); } else if ( 'themes-network' === pagenow ) { $button = $( '[data-slug="' + args.slug + '"]' ).find( '.row-actions a.delete' ); } args = _.extend( { success: wp.updates.deleteThemeSuccess, error: wp.updates.deleteThemeError }, args ); if ( $button && $button.html() !== __( 'Deleting...' ) ) { $button .data( 'originaltext', $button.html() ) .text( __( 'Deleting...' ) ); } wp.a11y.speak( __( 'Deleting...' ) ); // Remove previous error messages, if any. $( '.theme-info .update-message' ).remove(); $document.trigger( 'wp-theme-deleting', args ); return wp.updates.ajax( 'delete-theme', args ); }; /** * Updates the UI appropriately after a successful theme deletion. * * @since 4.6.0 * * @param {Object} response Response from the server. * @param {string} response.slug Slug of the theme that was deleted. */ wp.updates.deleteThemeSuccess = function( response ) { var $themeRows = $( '[data-slug="' + response.slug + '"]' ); if ( 'themes-network' === pagenow ) { // Removes the theme and updates rows. $themeRows.css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() { var $views = $( '.subsubsub' ), $themeRow = $( this ), themes = settings.themes, deletedRow = wp.template( 'item-deleted-row' ); if ( ! $themeRow.hasClass( 'plugin-update-tr' ) ) { $themeRow.after( deletedRow( { slug: response.slug, colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, name: $themeRow.find( '.theme-title strong' ).text() } ) ); } $themeRow.remove(); // Remove theme from update count. if ( -1 !== _.indexOf( themes.upgrade, response.slug ) ) { themes.upgrade = _.without( themes.upgrade, response.slug ); wp.updates.decrementCount( 'theme' ); } // Remove from views. if ( -1 !== _.indexOf( themes.disabled, response.slug ) ) { themes.disabled = _.without( themes.disabled, response.slug ); if ( themes.disabled.length ) { $views.find( '.disabled .count' ).text( '(' + themes.disabled.length + ')' ); } else { $views.find( '.disabled' ).remove(); } } if ( -1 !== _.indexOf( themes['auto-update-enabled'], response.slug ) ) { themes['auto-update-enabled'] = _.without( themes['auto-update-enabled'], response.slug ); if ( themes['auto-update-enabled'].length ) { $views.find( '.auto-update-enabled .count' ).text( '(' + themes['auto-update-enabled'].length + ')' ); } else { $views.find( '.auto-update-enabled' ).remove(); } } if ( -1 !== _.indexOf( themes['auto-update-disabled'], response.slug ) ) { themes['auto-update-disabled'] = _.without( themes['auto-update-disabled'], response.slug ); if ( themes['auto-update-disabled'].length ) { $views.find( '.auto-update-disabled .count' ).text( '(' + themes['auto-update-disabled'].length + ')' ); } else { $views.find( '.auto-update-disabled' ).remove(); } } themes.all = _.without( themes.all, response.slug ); // There is always at least one theme available. $views.find( '.all .count' ).text( '(' + themes.all.length + ')' ); } ); } // DecrementCount from update count. if ( 'themes' === pagenow ) { var theme = _.find( _wpThemeSettings.themes, { id: response.slug } ); if ( theme.hasUpdate ) { wp.updates.decrementCount( 'theme' ); } } wp.a11y.speak( _x( 'Deleted!', 'theme' ) ); $document.trigger( 'wp-theme-delete-success', response ); }; /** * Updates the UI appropriately after a failed theme deletion. * * @since 4.6.0 * * @param {Object} response Response from the server. * @param {string} response.slug Slug of the theme to be deleted. * @param {string} response.errorCode Error code for the error that occurred. * @param {string} response.errorMessage The error that occurred. */ wp.updates.deleteThemeError = function( response ) { var $themeRow = $( 'tr.inactive[data-slug="' + response.slug + '"]' ), $button = $( '.theme-actions .delete-theme' ), updateRow = wp.template( 'item-update-row' ), $updateRow = $themeRow.siblings( '#' + response.slug + '-update' ), errorMessage = sprintf( /* translators: %s: Error string for a failed deletion. */ __( 'Deletion failed: %s' ), response.errorMessage ), $message = wp.updates.adminNotice( { className: 'update-message notice-error notice-alt', message: errorMessage } ); if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) { return; } if ( 'themes-network' === pagenow ) { if ( ! $updateRow.length ) { $themeRow.addClass( 'update' ).after( updateRow( { slug: response.slug, colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, content: $message } ) ); } else { // Remove previous error messages, if any. $updateRow.find( '.notice-error' ).remove(); $updateRow.find( '.plugin-update' ).append( $message ); } } else { $( '.theme-info .theme-description' ).before( $message ); } $button.html( $button.data( 'originaltext' ) ); wp.a11y.speak( errorMessage, 'assertive' ); $document.trigger( 'wp-theme-delete-error', response ); }; /** * Adds the appropriate callback based on the type of action and the current page. * * @since 4.6.0 * @priv