+ $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 {
+ $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() !== wp.updates.l10n.updating ) {
+ $notice.data( 'originaltext', $notice.html() );
+ }
+
+ wp.a11y.speak( wp.updates.l10n.updatingMsg, 'polite' );
+ $notice.text( wp.updates.l10n.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
+ *
+ * @typedef {object} updateThemeSuccess
+ * @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: wp.updates.l10n.updated
+ },
+ $notice, newText;
+
+ 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 );
+ } else {
+ $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) );
+
+ // Focus on Customize button after updating.
+ if ( isModalOpen ) {
+ $( '.load-customize:visible' ).focus();
+ } else {
+ $theme.find( '.load-customize' ).focus();
+ }
+ }
+
+ wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) );
+ wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' );
+
+ wp.updates.decrementCount( 'theme' );
+
+ $document.trigger( 'wp-theme-update-success', response );
+
+ // Show updated message after modal re-rendered.
+ if ( isModalOpen ) {
+ $( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) );
+ }
+ };
+
+ /**
+ * Updates the UI appropriately after a failed theme update.
+ *
+ * @since 4.6.0
+ *
+ * @typedef {object} updateThemeError
+ * @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 = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage ),
+ $notice;
+
+ if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
+ return;
+ }
+
+ if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) {
+ return;
+ }
+
+ 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' ).focus() : $theme.find( '.load-customize' ).focus();
+ }
+
+ wp.updates.addAdminNotice( {
+ selector: $notice,
+ className: 'update-message notice-error notice-alt is-dismissible',
+ message: errorMessage
+ } );
+
+ wp.a11y.speak( errorMessage, 'polite' );
+
+ $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() !== wp.updates.l10n.installing ) {
+ $message.data( 'originaltext', $message.html() );
+ }
+
+ $message
+ .text( wp.updates.l10n.installing )
+ .attr( 'aria-label', wp.updates.l10n.themeInstallingLabel.replace( '%s', $message.data( 'name' ) ) );
+ wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' );
+
+ // 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
+ *
+ * @typedef {object} installThemeSuccess
+ * @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', wp.updates.l10n.themeInstalledLabel.replace( '%s', response.themeName ) )
+ .text( wp.updates.l10n.installed );
+
+ wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
+
+ 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' )
+ .attr( 'aria-label', wp.updates.l10n.activateThemeLabel.replace( '%s', response.themeName ) )
+ .text( wp.updates.l10n.activateTheme );
+ }
+
+ 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 button-secondary load-customize' )
+ .text( wp.updates.l10n.livePreview );
+ } );
+ }
+ }, 1000 );
+ };
+
+ /**
+ * Updates the UI appropriately after a failed theme install.
+ *
+ * @since 4.6.0
+ *
+ * @typedef {object} installThemeError
+ * @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 = wp.updates.l10n.installFailed.replace( '%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 ( $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', wp.updates.l10n.themeInstallFailedLabel.replace( '%s', $button.data( 'name' ) ) )
+ .text( wp.updates.l10n.installFailedShort );
+
+ wp.a11y.speak( errorMessage, 'assertive' );
+
+ $document.trigger( 'wp-theme-install-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 {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() !== wp.updates.l10n.deleting ) {
+ $button
+ .data( 'originaltext', $button.html() )
+ .text( wp.updates.l10n.deleting );
+ }
+
+ wp.a11y.speak( wp.updates.l10n.deleting, 'polite' );
+
+ // 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
+ *
+ * @typedef {object} deleteThemeSuccess
+ * @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 ),
+ totals = settings.totals,
+ 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 ( $themeRow.hasClass( 'update' ) ) {
+ totals.upgrade--;
+ wp.updates.decrementCount( 'theme' );
+ }
+
+ // Remove from views.
+ if ( $themeRow.hasClass( 'inactive' ) ) {
+ totals.disabled--;
+ if ( totals.disabled ) {
+ $views.find( '.disabled .count' ).text( '(' + totals.disabled + ')' );
+ } else {
+ $views.find( '.disabled' ).remove();
+ }
+ }
+
+ // There is always at least one theme available.
+ $views.find( '.all .count' ).text( '(' + --totals.all + ')' );
+ } );
+ }
+
+ wp.a11y.speak( wp.updates.l10n.deleted, 'polite' );
+
+ $document.trigger( 'wp-theme-delete-success', response );
+ };
+
+ /**
+ * Updates the UI appropriately after a failed theme deletion.
+ *
+ * @since 4.6.0
+ *
+ * @typedef {object} deleteThemeError
+ * @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 = wp.updates.l10n.deleteFailed.replace( '%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
+ * @private
+ *
+ * @param {object} data AJAX payload.
+ * @param {string} action The type of request to perform.
+ * @return {object} The AJAX payload with the appropriate callbacks.
+ */
+ wp.updates._addCallbacks = function( data, action ) {
+ if ( 'import' === pagenow && 'install-plugin' === action ) {
+ data.success = wp.updates.installImporterSuccess;
+ data.error = wp.updates.installImporterError;
+ }
+
+ return data;
+ };
+
+ /**
+ * Pulls available jobs from the queue and runs them.
+ *
+ * @since 4.2.0
+ * @since 4.6.0 Can handle multiple job types.
+ */
+ wp.updates.queueChecker = function() {
+ var job;
+
+ if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) {
+ return;
+ }
+
+ job = wp.updates.queue.shift();
+
+ // Handle a queue job.
+ switch ( job.action ) {
+ case 'install-plugin':
+ wp.updates.installPlugin( job.data );
+ break;
+
+ case 'update-plugin':
+ wp.updates.updatePlugin( job.data );
+ break;
+
+ case 'delete-plugin':
+ wp.updates.deletePlugin( job.data );
+ break;
+
+ case 'install-theme':
+ wp.updates.installTheme( job.data );
+ break;
+
+ case 'update-theme':
+ wp.updates.updateTheme( job.data );
+ break;
+
+ case 'delete-theme':
+ wp.updates.deleteTheme( job.data );
+ break;
+
+ default:
+ break;
+ }
+ };
+
+ /**
+ * Requests the users filesystem credentials if they aren't already known.
+ *
+ * @since 4.2.0
+ *
+ * @param {Event=} event Optional. Event interface.
+ */
+ wp.updates.requestFilesystemCredentials = function( event ) {
+ if ( false === wp.updates.filesystemCredentials.available ) {
+ /*
+ * After exiting the credentials request modal,
+ * return the focus to the element triggering the request.
+ */
+ if ( event && ! wp.updates.$elToReturnFocusToFromCredentialsModal ) {
+ wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target );
+ }
+
+ wp.updates.ajaxLocked = true;
+ wp.updates.requestForCredentialsModalOpen();
+ }
+ };
+
+ /**
+ * Requests the users filesystem credentials if needed and there is no lock.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event=} event Optional. Event interface.
+ */
+ wp.updates.maybeRequestFilesystemCredentials = function( event ) {
+ if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
+ wp.updates.requestFilesystemCredentials( event );
+ }
+ };
+
+ /**
+ * Keydown handler for the request for credentials modal.
+ *
+ * Closes the modal when the escape key is pressed and
+ * constrains keyboard navigation to inside the modal.
+ *
+ * @since 4.2.0
+ *
+ * @param {Event} event Event interface.
+ */
+ wp.updates.keydown = function( event ) {
+ if ( 27 === event.keyCode ) {
+ wp.updates.requestForCredentialsModalCancel();
+ } else if ( 9 === event.keyCode ) {
+
+ // #upgrade button must always be the last focus-able element in the dialog.
+ if ( 'upgrade' === event.target.id && ! event.shiftKey ) {
+ $( '#hostname' ).focus();
+
+ event.preventDefault();
+ } else if ( 'hostname' === event.target.id && event.shiftKey ) {
+ $( '#upgrade' ).focus();
+
+ event.preventDefault();
+ }
+ }
+ };
+
+ /**
+ * Opens the request for credentials modal.
+ *
+ * @since 4.2.0
+ */
+ wp.updates.requestForCredentialsModalOpen = function() {
+ var $modal = $( '#request-filesystem-credentials-dialog' );
+
+ $( 'body' ).addClass( 'modal-open' );
+ $modal.show();
+ $modal.find( 'input:enabled:first' ).focus();
+ $modal.on( 'keydown', wp.updates.keydown );
+ };
+
+ /**
+ * Closes the request for credentials modal.
+ *
+ * @since 4.2.0
+ */
+ wp.updates.requestForCredentialsModalClose = function() {
+ $( '#request-filesystem-credentials-dialog' ).hide();
+ $( 'body' ).removeClass( 'modal-open' );
+
+ if ( wp.updates.$elToReturnFocusToFromCredentialsModal ) {
+ wp.updates.$elToReturnFocusToFromCredentialsModal.focus();
+ }
+ };
+
+ /**
+ * Takes care of the steps that need to happen when the modal is canceled out.
+ *
+ * @since 4.2.0
+ * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions.
+ */
+ wp.updates.requestForCredentialsModalCancel = function() {
+
+ // Not ajaxLocked and no queue means we already have cleared things up.
+ if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) {
+ return;
+ }
+
+ _.each( wp.updates.queue, function( job ) {
+ $document.trigger( 'credential-modal-cancel', job );
+ } );
+
+ // Remove the lock, and clear the queue.
+ wp.updates.ajaxLocked = false;
+ wp.updates.queue = [];
+
+ wp.updates.requestForCredentialsModalClose();
+ };
+
+ /**
+ * Displays an error message in the request for credentials form.
+ *
+ * @since 4.2.0
+ *
+ * @param {string} message Error message.
+ */
+ wp.updates.showErrorInCredentialsForm = function( message ) {
+ var $modal = $( '#request-filesystem-credentials-form' );
+
+ // Remove any existing error.
+ $modal.find( '.notice' ).remove();
+ $modal.find( '#request-filesystem-credentials-title' ).after( '<div class="notice notice-alt notice-error"><p>' + message + '</p></div>' );
+ };
+
+ /**
+ * Handles credential errors and runs events that need to happen in that case.
+ *
+ * @since 4.2.0
+ *
+ * @param {object} response Ajax response.
+ * @param {string} action The type of request to perform.
+ */
+ wp.updates.credentialError = function( response, action ) {
+
+ // Restore callbacks.
+ response = wp.updates._addCallbacks( response, action );
+
+ wp.updates.queue.unshift( {
+ action: action,
+
+ /*
+ * Not cool that we're depending on response for this data.
+ * This would feel more whole in a view all tied together.
+ */
+ data: response
+ } );
+
+ wp.updates.filesystemCredentials.available = false;
+ wp.updates.showErrorInCredentialsForm( response.errorMessage );
+ wp.updates.requestFilesystemCredentials();
+ };
+
+ /**
+ * Handles credentials errors if it could not connect to the filesystem.
+ *
+ * @since 4.6.0
+ *
+ * @typedef {object} maybeHandleCredentialError
+ * @param {object} response Response from the server.
+ * @param {string} response.errorCode Error code for the error that occurred.
+ * @param {string} response.errorMessage The error that occurred.
+ * @param {string} action The type of request to perform.
+ * @returns {boolean} Whether there is an error that needs to be handled or not.
+ */
+ wp.updates.maybeHandleCredentialError = function( response, action ) {
+ if ( wp.updates.shouldRequestFilesystemCredentials && response.errorCode && 'unable_to_connect_to_filesystem' === response.errorCode ) {
+ wp.updates.credentialError( response, action );
+ return true;
+ }
+
+ return false;
+ };
+
+ /**
+ * Validates an AJAX response to ensure it's a proper object.
+ *
+ * If the response deems to be invalid, an admin notice is being displayed.
+ *
+ * @param {(object|string)} response Response from the server.
+ * @param {function=} response.always Optional. Callback for when the Deferred is resolved or rejected.
+ * @param {string=} response.statusText Optional. Status message corresponding to the status code.
+ * @param {string=} response.responseText Optional. Request response as text.
+ * @param {string} action Type of action the response is referring to. Can be 'delete',
+ * 'update' or 'install'.
+ */
+ wp.updates.isValidResponse = function( response, action ) {
+ var error = wp.updates.l10n.unknownError,
+ errorMessage;
+
+ // Make sure the response is a valid data object and not a Promise object.
+ if ( _.isObject( response ) && ! _.isFunction( response.always ) ) {
+ return true;
+ }
+
+ if ( _.isString( response ) && '-1' === response ) {
+ error = wp.updates.l10n.nonceError;
+ } else if ( _.isString( response ) ) {
+ error = response;
+ } else if ( 'undefined' !== typeof response.readyState && 0 === response.readyState ) {
+ error = wp.updates.l10n.connectionError;
+ } else if ( _.isString( response.responseText ) && '' !== response.responseText ) {
+ error = response.responseText;
+ } else if ( _.isString( response.statusText ) ) {
+ error = response.statusText;
+ }
+
+ switch ( action ) {
+ case 'update':
+ errorMessage = wp.updates.l10n.updateFailed;
+ break;
+
+ case 'install':
+ errorMessage = wp.updates.l10n.installFailed;
+ break;
+
+ case 'delete':
+ errorMessage = wp.updates.l10n.deleteFailed;
+ break;
+ }
+
+ // Messages are escaped, remove HTML tags to make them more readable.
+ error = error.replace( /<[\/a-z][^<>]*>/gi, '' );
+ errorMessage = errorMessage.replace( '%s', error );
+
+ // Add admin notice.
+ wp.updates.addAdminNotice( {
+ id: 'unknown_error',
+ className: 'notice-error is-dismissible',
+ message: _.escape( errorMessage )
+ } );
+
+ // Remove the lock, and clear the queue.
+ wp.updates.ajaxLocked = false;
+ wp.updates.queue = [];
+
+ // Change buttons of all running updates.
+ $( '.button.updating-message' )
+ .removeClass( 'updating-message' )
+ .removeAttr( 'aria-label' )
+ .prop( 'disabled', true )
+ .text( wp.updates.l10n.updateFailedShort );
+
+ $( '.updating-message:not(.button):not(.thickbox)' )
+ .removeClass( 'updating-message notice-warning' )
+ .addClass( 'notice-error' )
+ .find( 'p' )
+ .removeAttr( 'aria-label' )
+ .text( errorMessage );
+
+ wp.a11y.speak( errorMessage, 'assertive' );
+
+ return false;
+ };
+
+ /**
+ * Potentially adds an AYS to a user attempting to leave the page.
+ *
+ * If an update is on-going and a user attempts to leave the page,
+ * opens an "Are you sure?" alert.
+ *
+ * @since 4.2.0
+ */
+ wp.updates.beforeunload = function() {
+ if ( wp.updates.ajaxLocked ) {
+ return wp.updates.l10n.beforeunload;
+ }
+ };
+
+ $( function() {
+ var $pluginFilter = $( '#plugin-filter' ),
+ $bulkActionForm = $( '#bulk-action-form' ),
+ $filesystemModal = $( '#request-filesystem-credentials-dialog' ),
+ $pluginSearch = $( '.plugins-php .wp-filter-search' ),
+ $pluginInstallSearch = $( '.plugin-install-php .wp-filter-search' );
+
+ /*
+ * Whether a user needs to submit filesystem credentials.
+ *
+ * This is based on whether the form was output on the page server-side.
+ *
+ * @see {wp_print_request_filesystem_credentials_modal() in PHP}
+ */
+ wp.updates.shouldRequestFilesystemCredentials = $filesystemModal.length > 0;
+
+ /**
+ * File system credentials form submit noop-er / handler.
+ *
+ * @since 4.2.0
+ */
+ $filesystemModal.on( 'submit', 'form', function( event ) {
+ event.preventDefault();
+
+ // Persist the credentials input by the user for the duration of the page load.
+ wp.updates.filesystemCredentials.ftp.hostname = $( '#hostname' ).val();
+ wp.updates.filesystemCredentials.ftp.username = $( '#username' ).val();
+ wp.updates.filesystemCredentials.ftp.password = $( '#password' ).val();
+ wp.updates.filesystemCredentials.ftp.connectionType = $( 'input[name="connection_type"]:checked' ).val();
+ wp.updates.filesystemCredentials.ssh.publicKey = $( '#public_key' ).val();
+ wp.updates.filesystemCredentials.ssh.privateKey = $( '#private_key' ).val();
+ wp.updates.filesystemCredentials.available = true;
+
+ // Unlock and invoke the queue.
+ wp.updates.ajaxLocked = false;
+ wp.updates.queueChecker();
+
+ wp.updates.requestForCredentialsModalClose();
+ } );
+
+ /**
+ * Closes the request credentials modal when clicking the 'Cancel' button or outside of the modal.
+ *
+ * @since 4.2.0
+ */
+ $filesystemModal.on( 'click', '[data-js-action="close"], .notification-dialog-background', wp.updates.requestForCredentialsModalCancel );
+
+ /**
+ * Hide SSH fields when not selected.
+ *
+ * @since 4.2.0
+ */
+ $filesystemModal.on( 'change', 'input[name="connection_type"]', function() {
+ $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) );
+ } ).change();
+
+ /**
+ * Handles events after the credential modal was closed.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event} event Event interface.
+ * @param {string} job The install/update.delete request.
+ */
+ $document.on( 'credential-modal-cancel', function( event, job ) {
+ var $updatingMessage = $( '.updating-message' ),
+ $message, originalText;
+
+ if ( 'import' === pagenow ) {
+ $updatingMessage.removeClass( 'updating-message' );
+ } else if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
+ if ( 'update-plugin' === job.action ) {
+ $message = $( 'tr[data-plugin="' + job.data.plugin + '"]' ).find( '.update-message' );
+ } else if ( 'delete-plugin' === job.action ) {
+ $message = $( '[data-plugin="' + job.data.plugin + '"]' ).find( '.row-actions a.delete' );
+ }
+ } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) {
+ if ( 'update-theme' === job.action ) {
+ $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.update-message' );
+ } else if ( 'delete-theme' === job.action && 'themes-network' === pagenow ) {
+ $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.row-actions a.delete' );
+ } else if ( 'delete-theme' === job.action && 'themes' === pagenow ) {
+ $message = $( '.theme-actions .delete-theme' );
+ }
+ } else {
+ $message = $updatingMessage;
+ }
+
+ if ( $message && $message.hasClass( 'updating-message' ) ) {
+ originalText = $message.data( 'originaltext' );
+
+ if ( 'undefined' === typeof originalText ) {
+ originalText = $( '<p>' ).html( $message.find( 'p' ).data( 'originaltext' ) );
+ }
+
+ $message
+ .removeClass( 'updating-message' )
+ .html( originalText );
+
+ if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
+ if ( 'update-plugin' === job.action ) {
+ $message.attr( 'aria-label', wp.updates.l10n.updateNowLabel.replace( '%s', $message.data( 'name' ) ) );
+ } else if ( 'install-plugin' === job.action ) {
+ $message.attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', $message.data( 'name' ) ) );
+ }
+ }
+ }
+
+ wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
+ } );
+
+ /**
+ * Click handler for plugin updates in List Table view.
+ *
+ * @since 4.2.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) {
+ var $message = $( event.target ),
+ $pluginRow = $message.parents( 'tr' );
+
+ event.preventDefault();
+
+ if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
+ return;
+ }
+
+ wp.updates.maybeRequestFilesystemCredentials( event );
+
+ // Return the user to the input box of the plugin's table row after closing the modal.
+ wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' );
+ wp.updates.updatePlugin( {
+ plugin: $pluginRow.data( 'plugin' ),
+ slug: $pluginRow.data( 'slug' )
+ } );
+ } );
+
+ /**
+ * Click handler for plugin updates in plugin install view.
+ *
+ * @since 4.2.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $pluginFilter.on( 'click', '.update-now', function( event ) {
+ var $button = $( event.target );
+ event.preventDefault();
+
+ if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
+ return;
+ }
+
+ wp.updates.maybeRequestFilesystemCredentials( event );
+
+ wp.updates.updatePlugin( {
+ plugin: $button.data( 'plugin' ),
+ slug: $button.data( 'slug' )
+ } );
+ } );
+
+ /**
+ * Click handler for plugin installs in plugin install view.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $pluginFilter.on( 'click', '.install-now', function( event ) {
+ var $button = $( event.target );
+ event.preventDefault();
+
+ if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
+ return;
+ }
+
+ if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
+ wp.updates.requestFilesystemCredentials( event );
+
+ $document.on( 'credential-modal-cancel', function() {
+ var $message = $( '.install-now.updating-message' );
+
+ $message
+ .removeClass( 'updating-message' )
+ .text( wp.updates.l10n.installNow );
+
+ wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
+ } );
+ }
+
+ wp.updates.installPlugin( {
+ slug: $button.data( 'slug' )
+ } );
+ } );
+
+ /**
+ * Click handler for importer plugins installs in the Import screen.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $document.on( 'click', '.importer-item .install-now', function( event ) {
+ var $button = $( event.target ),
+ pluginName = $( this ).data( 'name' );
+
+ event.preventDefault();
+
+ if ( $button.hasClass( 'updating-message' ) ) {
+ return;
+ }
+
+ if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
+ wp.updates.requestFilesystemCredentials( event );
+
+ $document.on( 'credential-modal-cancel', function() {
+
+ $button
+ .removeClass( 'updating-message' )
+ .text( wp.updates.l10n.installNow )
+ .attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', pluginName ) );
+
+ wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
+ } );
+ }
+
+ wp.updates.installPlugin( {
+ slug: $button.data( 'slug' ),
+ success: wp.updates.installImporterSuccess,
+ error: wp.updates.installImporterError
+ } );
+ } );
+
+ /**
+ * Click handler for plugin deletions.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) {
+ var $pluginRow = $( event.target ).parents( 'tr' );
+
+ event.preventDefault();
+
+ if ( ! window.confirm( wp.updates.l10n.aysDeleteUninstall.replace( '%s', $pluginRow.find( '.plugin-title strong' ).text() ) ) ) {
+ return;
+ }
+
+ wp.updates.maybeRequestFilesystemCredentials( event );
+
+ wp.updates.deletePlugin( {
+ plugin: $pluginRow.data( 'plugin' ),
+ slug: $pluginRow.data( 'slug' )
+ } );
+
+ } );
+
+ /**
+ * Click handler for theme updates.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) {
+ var $message = $( event.target ),
+ $themeRow = $message.parents( 'tr' );
+
+ event.preventDefault();
+
+ if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
+ return;
+ }
+
+ wp.updates.maybeRequestFilesystemCredentials( event );
+
+ // Return the user to the input box of the theme's table row after closing the modal.
+ wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' );
+ wp.updates.updateTheme( {
+ slug: $themeRow.data( 'slug' )
+ } );
+ } );
+
+ /**
+ * Click handler for theme deletions.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) {
+ var $themeRow = $( event.target ).parents( 'tr' );
+
+ event.preventDefault();
+
+ if ( ! window.confirm( wp.updates.l10n.aysDelete.replace( '%s', $themeRow.find( '.theme-title strong' ).text() ) ) ) {
+ return;
+ }
+
+ wp.updates.maybeRequestFilesystemCredentials( event );
+
+ wp.updates.deleteTheme( {
+ slug: $themeRow.data( 'slug' )
+ } );
+ } );
+
+ /**
+ * Bulk action handler for plugins and themes.
+ *
+ * Handles both deletions and updates.
+ *
+ * @since 4.6.0
+ *
+ * @param {Event} event Event interface.
+ */
+ $bulkActionForm.on( 'click', '[type="submit"]', function( event ) {
+ var bulkAction = $( event.target ).siblings( 'select' ).val(),
+ itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ),
+ success = 0,
+ error = 0,
+ errorMessages = [],
+ type, action;
+
+ // Determine which type of item we're dealing with.
+ switch ( pagenow ) {
+ case 'plugins':
+ case 'plugins-network':
+ type = 'plugin';
+ break;
+
+ case 'themes-network':
+ type = 'theme';
+ break;
+
+ default:
+ return;
+ }
+
+ // Bail if there were no items selected.
+ if ( ! itemsSelected.length ) {
+ event.preventDefault();
+ $( 'html, body' ).animate( { scrollTop: 0 } );
+
+ return wp.updates.addAdminNotice( {
+ id: 'no-items-selected',
+ className: 'notice-error is-dismissible',
+ message: wp.updates.l10n.noItemsSelected
+ } );
+ }
+
+ // Determine the type of request we're dealing with.
+ switch ( bulkAction ) {
+ case 'update-selected':
+ action = bulkAction.replace( 'selected', type );
+ break;
+
+ case 'delete-selected':
+ if ( ! window.confirm( 'plugin' === type ? wp.updates.l10n.aysBulkDelete : wp.updates.l10n.aysBulkDeleteThemes ) ) {
+ event.preventDefault();
+ return;
+ }
+
+ action = bulkAction.replace( 'selected', type );
+ break;
+
+ default:
+ return;
+ }
+
+ wp.updates.maybeRequestFilesystemCredentials( event );
+
+ event.preventDefault();
+
+ // Un-check the bulk checkboxes.
+ $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false );
+
+ $document.trigger( 'wp-' + type + '-bulk-' + bulkAction, itemsSelected );
+
+ // Find all the checkboxes which have been checked.
+ itemsSelected.each( function( index, element ) {
+ var $checkbox = $( element ),
+ $itemRow = $checkbox.parents( 'tr' );
+
+ // Only add update-able items to the update queue.
+ if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) {