diff --git a/src/wp-admin/admin-ajax.php b/src/wp-admin/admin-ajax.php index a3571afb58..d50aa42003 100644 --- a/src/wp-admin/admin-ajax.php +++ b/src/wp-admin/admin-ajax.php @@ -61,7 +61,7 @@ $core_actions_post = array( 'query-attachments', 'save-attachment', 'save-attachment-compat', 'send-link-to-editor', 'send-attachment-to-editor', 'save-attachment-order', 'heartbeat', 'get-revision-diffs', 'save-user-color-scheme', 'update-widget', 'query-themes', 'parse-embed', 'set-attachment-thumbnail', - 'parse-media-shortcode', 'destroy-sessions' + 'parse-media-shortcode', 'destroy-sessions', 'install-plugin', 'update-plugin' ); // Register core Ajax calls. diff --git a/src/wp-admin/css/list-tables.css b/src/wp-admin/css/list-tables.css index a7f067fb07..f461635fd7 100644 --- a/src/wp-admin/css/list-tables.css +++ b/src/wp-admin/css/list-tables.css @@ -1143,7 +1143,11 @@ ul.cat-checklist { .upgrade .plugins tr:last-of-type td, .upgrade .plugins tr:last-of-type th, .plugins tr.active + tr.inactive.update th, -.plugins tr.active + tr.inactive.update td { +.plugins tr.active + tr.inactive.update td, +.plugins .updated td, +.plugins .updated th, +.plugins tr.active + tr.inactive.updated th, +.plugins tr.active + tr.inactive.updated td { -webkit-box-shadow: none; box-shadow: none; } @@ -1154,7 +1158,8 @@ tr.active.update + tr.plugin-update-tr .plugin-update { background-color: #fef7f1; } -.plugins .active th.check-column { +.plugins .active th.check-column, +.plugin-update-tr.active td { border-left: 4px solid #2ea2cc; } @@ -1199,18 +1204,49 @@ tr.active.update + tr.plugin-update-tr .plugin-update { background-color: rgba(0,0,0,0.03); } -.plugin-update-tr .update-message:before { +.plugin-update-tr .update-message:before, +.plugin-card .update-now:before, +.plugin-card .install-now:before { color: #d54e21; - content: '\f463'; display: inline-block; font: normal 20px/1 'dashicons'; speak: none; - margin: 0 8px 0 -2px; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; vertical-align: top; } +.plugin-update-tr .update-message:before, +.plugin-card .update-now:before { + content: '\f463'; +} + +.plugin-update-tr .update-message:before { + margin: 0 8px 0 -2px; +} + +.plugin-card .update-now:before, +.plugin-card .install-now:before { + margin: 3px 5px 0 -2px; +} + +.plugin-update-tr .updating-message:before, +.plugin-card .updating-message:before { + content: '\f463'; + -webkit-animation: rotation 2s infinite linear; +} + +@-webkit-keyframes rotation { + from {-webkit-transform: rotate(0deg);} + to {-webkit-transform: rotate(359deg);} +} + +.plugin-update-tr .updated-message:before, +.plugin-card .updated-message:before { + color: #79ba49; + content: '\f147'; +} + .plugins .plugin-update { padding: 0; border: none; @@ -1223,7 +1259,7 @@ tr.active + tr.plugin-update-tr .plugin-update { background-color: #f7fcfe; } -tr.active + tr.plugin-update-tr .plugin-update .update-message { +tr.active + tr.plugin-update-tr:not(.updated) .plugin-update .update-message { background-color: #fcf3ef; } diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 23cb62fae3..b75033c5d6 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -2828,3 +2828,81 @@ function wp_ajax_destroy_sessions() { wp_send_json_success( array( 'message' => $message ) ); } + +/** + * AJAX handler for installing a plugin. + * + * @since 4.2.0 + */ +function wp_ajax_install_plugin() { + check_ajax_referer( 'updates' ); + + include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); + include_once( ABSPATH . 'wp-admin/includes/plugin-install.php' ); + + $status = array( + 'install' => 'plugin', + 'slug' => sanitize_key( $_POST['slug'] ), + ); + + $api = plugins_api( 'plugin_information', array( + 'slug' => sanitize_key( $_POST['slug'] ), + 'fields' => array( 'sections' => false ) + ) ); + + if ( is_wp_error( $api ) ) { + $status['error'] = $api->get_error_message(); + wp_send_json_error( $status ); + } + + $upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() ); + $result = $upgrader->install( $api->download_link ); + + if ( is_wp_error( $result ) ) { + $status['error'] = $result->get_error_message(); + wp_send_json_error( $status ); + } + + $plugin_status = install_plugin_install_status( array( 'slug' => sanitize_key( $_POST['slug'] ) ) ); + activate_plugin( $plugin_status['file'] ); + + wp_send_json_success( $status ); +} + +/** + * AJAX handler for updating a plugin. + * + * @since 4.2.0 + */ +function wp_ajax_update_plugin() { + check_ajax_referer( 'updates' ); + + include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); + + $plugin = urldecode( $_POST['plugin'] ); + + $status = array( + 'update' => 'plugin', + 'plugin' => $plugin, + 'slug' => sanitize_key( $_POST['slug'] ), + ); + + $current = get_site_transient( 'update_plugins' ); + if ( empty( $current ) ) { + wp_update_plugins(); + } + + $upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() ); + $result = $upgrader->bulk_upgrade( array( $plugin ) ); + + if ( is_array( $result ) ) { + $result = $result[ $plugin ]; + } + + if ( is_wp_error( $result ) ) { + $status['error'] = $result->get_error_message(); + wp_send_json_error( $status ); + } + + wp_send_json_success( $status ); +} diff --git a/src/wp-admin/includes/class-wp-plugin-install-list-table.php b/src/wp-admin/includes/class-wp-plugin-install-list-table.php index b523d48c43..c2040bc504 100644 --- a/src/wp-admin/includes/class-wp-plugin-install-list-table.php +++ b/src/wp-admin/includes/class-wp-plugin-install-list-table.php @@ -402,14 +402,14 @@ class WP_Plugin_Install_List_Table extends WP_List_Table { case 'install': if ( $status['url'] ) { /* translators: 1: Plugin name and version. */ - $action_links[] = '' . __( 'Install Now' ) . ''; + $action_links[] = '' . __( 'Install Now' ) . ''; } break; case 'update_available': if ( $status['url'] ) { /* translators: 1: Plugin name and version */ - $action_links[] = '' . __( 'Update Now' ) . ''; + $action_links[] = '' . __( 'Update Now' ) . ''; } break; @@ -449,7 +449,7 @@ class WP_Plugin_Install_List_Table extends WP_List_Table { $date_format = __( 'M j, Y @ G:i' ); $last_updated_timestamp = strtotime( $plugin['last_updated'] ); ?> -
+
diff --git a/src/wp-admin/includes/plugin-install.php b/src/wp-admin/includes/plugin-install.php index 5cb74413ae..f9b487add1 100644 --- a/src/wp-admin/includes/plugin-install.php +++ b/src/wp-admin/includes/plugin-install.php @@ -276,6 +276,7 @@ function install_plugin_install_status($api, $loop = false) { // Default to a "new" plugin $status = 'install'; $url = false; + $update_file = false; /* * Check to see if this plugin is known to be installed, @@ -304,6 +305,7 @@ function install_plugin_install_status($api, $loop = false) { } else { $key = array_keys( $installed_plugin ); $key = array_shift( $key ); //Use the first plugin regardless of the name, Could have issues for multiple-plugins in one directory if they share different version numbers + $update_file = $api->slug . '/' . $key; if ( version_compare($api->version, $installed_plugin[ $key ]['Version'], '=') ){ $status = 'latest_installed'; } elseif ( version_compare($api->version, $installed_plugin[ $key ]['Version'], '<') ) { @@ -327,7 +329,8 @@ function install_plugin_install_status($api, $loop = false) { if ( isset($_GET['from']) ) $url .= '&from=' . urlencode( wp_unslash( $_GET['from'] ) ); - return compact('status', 'url', 'version'); + $file = $update_file; + return compact( 'status', 'url', 'version', 'file' ); } /** diff --git a/src/wp-admin/includes/update.php b/src/wp-admin/includes/update.php index 5639459547..b78fbfeaba 100644 --- a/src/wp-admin/includes/update.php +++ b/src/wp-admin/includes/update.php @@ -276,14 +276,15 @@ function wp_plugin_update_row( $file, $plugin_data ) { $wp_list_table = _get_list_table('WP_Plugins_List_Table'); if ( is_network_admin() || !is_multisite() ) { - echo '
'; + $active_class = ( is_plugin_active( $plugin_data['plugin'] ) ) ? ' active' : ''; + echo '
'; if ( ! current_user_can( 'update_plugins' ) ) { printf( __('There is a new version of %1$s available. View version %4$s details.'), $plugin_name, esc_url($details_url), esc_attr($plugin_name), $r->new_version ); } elseif ( empty($r->package) ) { printf( __('There is a new version of %1$s available. View version %4$s details. Automatic update is unavailable for this plugin.'), $plugin_name, esc_url($details_url), esc_attr($plugin_name), $r->new_version ); } else { - printf( __('There is a new version of %1$s available. View version %4$s details or update now.'), $plugin_name, esc_url($details_url), esc_attr($plugin_name), $r->new_version, wp_nonce_url( self_admin_url('update.php?action=upgrade-plugin&plugin=') . $file, 'upgrade-plugin_' . $file) ); + printf( __( 'There is a new version of %1$s available. View version %4$s details or update now.' ), $plugin_name, esc_url( $details_url ), esc_attr( $plugin_name ), $r->new_version, wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $file, 'upgrade-plugin_' . $file ) ); } /** * Fires at the end of the update message container in each diff --git a/src/wp-admin/js/plugin-install.js b/src/wp-admin/js/plugin-install.js index 7403769e8b..36a6bdc57d 100644 --- a/src/wp-admin/js/plugin-install.js +++ b/src/wp-admin/js/plugin-install.js @@ -69,8 +69,4 @@ jQuery( document ).ready( function( $ ) { $( '#section-holder div.section' ).hide(); // Hide 'em all. $( '#section-' + tab ).show(); }); - - $( 'a.install-now' ).click( function() { - return confirm( plugininstallL10n.ays ); - }); }); diff --git a/src/wp-admin/js/updates.js b/src/wp-admin/js/updates.js index b03e4ba8f0..025d977ece 100644 --- a/src/wp-admin/js/updates.js +++ b/src/wp-admin/js/updates.js @@ -5,43 +5,370 @@ window.wp = window.wp || {}; wp.updates = {}; /** - * Decrement update counts throughout the various menus + * User nonce for ajax calls. + * + * @since 4.2.0 + * + * @var string + */ + wp.updates.ajaxNonce = window._wpUpdatesSettings.ajax_nonce; + + /** + * Localized strings. + * + * @since 4.2.0 + * + * @var object + */ + wp.updates.l10n = window._wpUpdatesSettings.l10n; + + /** + * Flag if we're waiting for an install/update to complete. + * + * @since 4.2.0 + * + * @var bool + */ + wp.updates.updateLock = false; + + /** + * If the user tries to install/update a plugin while an install/update is + * already happening, it can be placed in this queue to perform later. + * + * @since 4.2.0 + * + * @var array + */ + wp.updates.updateQueue = []; + + /** + * Decrement update counts throughout the various menus. + * + * @since 3.9.0 * * @param {string} updateType */ wp.updates.decrementCount = function( upgradeType ) { - var count, pluginCount, $elem; + var count, + pluginCount, + $adminBarUpdateCount = $( '#wp-admin-bar-updates .ab-label' ), + $dashboardNavMenuUpdateCount = $( 'a[href="update-core.php"] .update-plugins' ) + $pluginsMenuItem = $( '#menu-plugins' ); - $elem = $( '#wp-admin-bar-updates .ab-label' ); - count = $elem.text(); + + count = $adminBarUpdateCount.text(); count = parseInt( count, 10 ) - 1; if ( count < 0 || isNaN( count ) ) { return; } $( '#wp-admin-bar-updates .ab-item' ).removeAttr( 'title' ); - $elem.text( count ); + $adminBarUpdateCount.text( count ); - $elem = $( 'a[href="update-core.php"] .update-plugins' ); - $elem.each( function( index, elem ) { + + $dashboardNavMenuUpdateCount.each( function( index, elem ) { elem.className = elem.className.replace( /count-\d+/, 'count-' + count ); } ); - $elem.removeAttr( 'title' ); - $elem.find( '.update-count' ).text( count ); + $dashboardNavMenuUpdateCount.removeAttr( 'title' ); + $dashboardNavMenuUpdateCount.find( '.update-count' ).text( count ); if ( 'plugin' === upgradeType ) { - $elem = $( '#menu-plugins' ); - pluginCount = $elem.find( '.plugin-count' ).eq(0).text(); + pluginCount = $pluginsMenuItem.find( '.plugin-count' ).eq(0).text(); pluginCount = parseInt( pluginCount, 10 ) - 1; if ( pluginCount < 0 || isNaN( pluginCount ) ) { return; } - $elem.find( '.plugin-count' ).text( pluginCount ); - $elem.find( '.update-plugins' ).each( function( index, elem ) { + $pluginsMenuItem.find( '.plugin-count' ).text( pluginCount ); + $pluginsMenuItem.find( '.update-plugins' ).each( function( index, elem ) { elem.className = elem.className.replace( /count-\d+/, 'count-' + pluginCount ); } ); + + if (pluginCount > 0 ) { + $( '.subsubsub .upgrade .count' ).text( '(' + pluginCount + ')' ); + } else { + $( '.subsubsub .upgrade' ).remove(); + } } }; + /** + * Send an Ajax request to the server to update a plugin. + * + * @since 4.2.0 + * + * @param {string} plugin + * @param {string} slug + */ + wp.updates.updatePlugin = function( plugin, slug ) { + var $message; + if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { + $message = $( '#' + slug ).next().find( '.update-message' ); + } else if ( 'plugin-install' === pagenow ) { + $message = $( '.plugin-card-' + slug ).find( '.update-now' ); + } + + $message.addClass( 'updating-message' ); + $message.text( wp.updates.l10n.updating ); + + if ( wp.updates.updateLock ) { + wp.updates.updateQueue.push( { + type: 'update-plugin', + data: { + plugin: plugin, + slug: slug + } + } ); + return; + } + + wp.updates.updateLock = true; + + var data = { + 'action': 'update-plugin', + '_ajax_nonce': wp.updates.ajaxNonce, + 'plugin': plugin, + 'slug': slug + }; + + $.ajax( { + type: 'post', + url: ajaxurl, + data: data, + complete: wp.updates.updateRequestComplete + } ); + }; + + /** + * After an update attempt has completed, deal with the response. + * + * @since 4.2.0 + * + * @param {jqXHR} jqxhr The jQuery XMLHttpRequest for the request. + */ + wp.updates.updateRequestComplete = function( jqxhr ) { + wp.updates.updateLock = false; + if ( jqxhr.responseJSON && jqxhr.responseJSON.success ) { + wp.updates.updateSuccess( jqxhr.responseJSON ); + } else { + var alertText = wp.updates.l10n.updateFailed; + if ( jqxhr.responseJSON && jqxhr.responseJSON.data && jqxhr.responseJSON.data.error ) { + alertText += ': ' + jqxhr.responseJSON.data.error; + } + alert( alertText ); + if ( jqxhr.responseJSON && jqxhr.responseJSON.data && jqxhr.responseJSON.data.slug ) { + wp.updates.updateError( jqxhr.responseJSON ); + } + } + /** + * Check the queue. + */ + wp.updates.queueChecker(); + } + + /** + * On a successful plugin update, update the UI with the result. + * + * @since 4.2.0 + * + * @param {object} response + */ + wp.updates.updateSuccess = function( response ) { + var $message; + if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { + $message = $( '#' + response.data.slug ).next().find( '.update-message' ); + $( '#' + response.data.slug ).addClass( 'updated' ).removeClass( 'update' ); + $( '#' + response.data.slug + '-update' ).addClass( 'updated' ).removeClass( 'update' ); + } else if ( 'plugin-install' === pagenow ) { + $message = $( '.plugin-card-' + response.data.slug ).find( '.update-now' ); + $message.addClass( 'button-disabled' ); + } + + $message.removeClass( 'updating-message' ).addClass( 'updated-message' ); + $message.text( wp.updates.l10n.updated ); + + wp.updates.decrementCount( 'plugin' ); + }; + + /** + * On a plugin update error, update the UI appropriately. + * + * @since 4.2.0 + * + * @param {object} response + */ + wp.updates.updateError = function( response ) { + var $message; + if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { + $message = $( '#' + response.data.slug ).next().find( '.update-message' ); + } else if ( 'plugin-install' === pagenow ) { + $message = $( '.plugin-card-' + response.data.slug ).find( '.update-now' ); + } + $message.removeClass( 'updating-message' ); + $message.text( wp.updates.l10n.updateFailed ); + }; + + /** + * Send an Ajax request to the server to install a plugin. + * + * @since 4.2.0 + * + * @param {string} slug + */ + wp.updates.installPlugin = function( slug ) { + var $message = $( '.plugin-card-' + slug ).find( '.install-now' ); + + $message.addClass( 'updating-message' ); + $message.text( wp.updates.l10n.installing ); + + if ( wp.updates.updateLock ) { + wp.updates.updateQueue.push( { + type: 'install-plugin', + data: { + slug: slug + } + } ); + return; + } + + wp.updates.updateLock = true; + + var data = { + 'action': 'install-plugin', + '_ajax_nonce': wp.updates.ajaxNonce, + 'slug': slug + }; + + $.ajax( { + type: 'post', + url: ajaxurl, + data: data, + complete: wp.updates.installRequestComplete + } ); + }; + + + /** + * After an installation attempt has completed, deal with the response. + * + * @since 4.2.0 + * + * @param {jqXHR} jqxhr The jQuery XMLHttpRequest for the request. + */ + wp.updates.installRequestComplete = function( jqxhr ) { + wp.updates.updateLock = false; + if ( jqxhr.responseJSON && jqxhr.responseJSON.success ) { + wp.updates.installSuccess( jqxhr.responseJSON ); + } else { + var alertText = wp.updates.l10n.installFailed; + if ( jqxhr.responseJSON && jqxhr.responseJSON.data && jqxhr.responseJSON.data.error ) { + alertText += ': ' + jqxhr.responseJSON.data.error; + } + alert( alertText ); + if ( jqxhr.responseJSON && jqxhr.responseJSON.data && jqxhr.responseJSON.data.slug ) { + wp.updates.installError( jqxhr.responseJSON ); + } + } + /** + * Check the queue. + */ + wp.updates.queueChecker(); + }; + + /** + * On plugin install success, update the UI with the result. + * + * @since 4.2.0 + * + * @param {object} response + */ + wp.updates.installSuccess = function( response ) { + var $message = $( '.plugin-card-' + response.data.slug ).find( '.install-now' ); + + $message.removeClass( 'updating-message' ).addClass( 'updated-message button-disabled' ); + $message.text( wp.updates.l10n.installed ); + }; + + /** + * On plugin install failure, update the UI appropriately. + * + * @since 4.2.0 + * + * @param {object} response + */ + wp.updates.installError = function( response ) { + var $message = $( '.plugin-card-' + response.data.slug ).find( '.install-now' ); + + $message.removeClass( 'updating-message' ); + $message.text( wp.updates.l10n.installNow ); + }; + + + /** + * If an install/update job has been placed in the queue, queueChecker pulls it out and runs it. + * + * @since 4.2.0 + */ + wp.updates.queueChecker = function() { + if ( wp.updates.updateLock || wp.updates.updateQueue.length <= 0 ) { + return; + } + + var job = wp.updates.updateQueue.shift(); + + switch ( job.type ) { + case 'update-plugin': + wp.updates.updatePlugin( job.data.plugin, job.data.slug ); + break; + case 'install-plugin': + wp.updates.installPlugin( job.data.slug ); + break; + default: + console.log( 'Failed to exect queued update job.' ); + console.log( job ); + break; + } + }; + + $( document ).ready( function() { + $( '.plugin-update-tr .update-link' ).on( 'click', function( e ) { + e.preventDefault(); + var $row = $( e.target ).parents( '.plugin-update-tr' ); + wp.updates.updatePlugin( $row.data( 'plugin' ), $row.data( 'slug' ) ); + } ); + + $( '#bulk-action-form' ).on( 'submit', function( e ) { + var checkbox, plugin, slug; + + if ( $( '#bulk-action-selector-top' ).val() == 'update-selected' ) { + e.preventDefault(); + + $( 'input[name="checked[]"]:checked' ).each( function( index, elem ) { + $checkbox = $( elem ); + plugin = $checkbox.val(); + slug = $checkbox.parents( 'tr' ).prop( 'id' ); + + wp.updates.updatePlugin( plugin, slug ); + + $checkbox.attr( 'checked', false ); + } ); + } + } ); + + $( '.plugin-card .update-now' ).on( 'click', function( e ) { + e.preventDefault(); + $button = $( e.target ); + wp.updates.updatePlugin( $button.data( 'plugin' ), $button.data( 'slug' ) ); + } ); + + $( '.plugin-card .install-now' ).on( 'click', function( e ) { + e.preventDefault(); + $button = $( e.target ); + if ( $button.hasClass( 'button-disabled' ) ) { + return; + } + wp.updates.installPlugin( $button.data( 'slug' ) ); + } ); + } ); + $( window ).on( 'message', function( e ) { var event = e.originalEvent, message, diff --git a/src/wp-admin/plugin-install.php b/src/wp-admin/plugin-install.php index e2431eb9b5..03085c7813 100644 --- a/src/wp-admin/plugin-install.php +++ b/src/wp-admin/plugin-install.php @@ -54,6 +54,8 @@ if ( 'plugin-information' != $tab ) $body_id = $tab; +wp_enqueue_script( 'updates' ); + /** * Fires before each tab on the Install Plugins screen is loaded. * diff --git a/src/wp-admin/plugins.php b/src/wp-admin/plugins.php index 29bca4ab7e..629fdbff53 100644 --- a/src/wp-admin/plugins.php +++ b/src/wp-admin/plugins.php @@ -23,6 +23,8 @@ $s = isset($_REQUEST['s']) ? urlencode($_REQUEST['s']) : ''; // Clean up request URI from temporary args for screen options/paging uri's to work as expected. $_SERVER['REQUEST_URI'] = remove_query_arg(array('error', 'deleted', 'activate', 'activate-multi', 'deactivate', 'deactivate-multi', '_error_nonce'), $_SERVER['REQUEST_URI']); +wp_enqueue_script( 'updates' ); + if ( $action ) { switch ( $action ) { @@ -460,7 +462,7 @@ do_action( 'pre_current_active_plugins', $plugins['all'] ); search_box( __( 'Search Installed Plugins' ), 'plugin' ); ?> -
+ diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 23c89051bb..8f32fbce7d 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -501,6 +501,18 @@ function wp_default_scripts( &$scripts ) { ) ); $scripts->add( 'updates', "/wp-admin/js/updates$suffix.js", array( 'jquery' ) ); + did_action( 'init' ) && $scripts->localize( 'updates', '_wpUpdatesSettings', array( + 'ajax_nonce' => wp_create_nonce( 'updates' ), + 'l10n' => array( + 'updating' => __( 'Updating...' ), + 'updated' => __( 'Updated!' ), + 'updateFailed' => __( 'Update failed' ), + 'installNow' => __( 'Install Now' ), + 'installing' => __( 'Installing...' ), + 'installed' => __( 'Installed!' ), + 'installFailed' => __( 'Installation failed' ), + ) + ) ); $scripts->add( 'farbtastic', '/wp-admin/js/farbtastic.js', array('jquery'), '1.2' );