From eb79d7f1ee0f59d1035260895b134f134a0ac703 Mon Sep 17 00:00:00 2001 From: Jake Spurlock Date: Wed, 20 May 2020 18:47:24 +0000 Subject: [PATCH] Security: Add user interface to auto-update themes and plugins. Building on core update mechanisms, this adds the ability to enable automatic updates for themes and plugins to the WordPress admin. Fixes: #50052. Props: afercia, afragen, audrasjb, azaozz, bookdude13, davidperonne, desrosj, gmays, gmays, javiercasares, karmatosed, knutsp, mapk, mukesh27, netweb, nicolaskulka, nielsdeblaauw, paaljoachim, passoniate, pbiron, pedromendonca, whodunitagency, whyisjake, wpamitkumar, and xkon. git-svn-id: https://develop.svn.wordpress.org/trunk@47835 602fd350-edb4-49c9-b593-d223f7449a82 --- src/js/_enqueues/wp/updates.js | 148 ++++++++++++++ src/wp-admin/admin-ajax.php | 1 + src/wp-admin/css/common.css | 4 +- src/wp-admin/css/list-tables.css | 4 + src/wp-admin/css/themes.css | 3 +- src/wp-admin/includes/ajax-actions.php | 73 +++++++ .../includes/class-wp-automatic-updater.php | 190 ++++++++++++++++++ src/wp-admin/includes/class-wp-debug-data.php | 60 ++++++ .../class-wp-ms-themes-list-table.php | 135 ++++++++++++- .../includes/class-wp-plugins-list-table.php | 122 ++++++++++- src/wp-admin/includes/theme.php | 15 +- src/wp-admin/includes/update.php | 80 +++++++- src/wp-admin/network/themes.php | 111 ++++++++-- src/wp-admin/plugins.php | 143 +++++++++++-- src/wp-admin/themes.php | 71 +++++++ src/wp-admin/update-core.php | 20 ++ src/wp-includes/script-loader.php | 7 + 17 files changed, 1142 insertions(+), 45 deletions(-) diff --git a/src/js/_enqueues/wp/updates.js b/src/js/_enqueues/wp/updates.js index d7e0de1c39..8b52bfcdd6 100644 --- a/src/js/_enqueues/wp/updates.js +++ b/src/js/_enqueues/wp/updates.js @@ -409,6 +409,7 @@ * * @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. @@ -431,6 +432,9 @@ // Update the version number in the row. newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion ); $pluginRow.find( '.plugin-version-author-uri' ).html( newText ); + + // Clear the "time to next auto-update" text. + $pluginRow.find( '.auto-update-time' ).empty(); } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { $updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' ) .removeClass( 'updating-message' ) @@ -969,6 +973,7 @@ * 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. @@ -1002,12 +1007,16 @@ // 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' ).focus(); + $( '.theme-info .theme-autoupdate' ).find( '.auto-update-time' ).empty(); } else { $theme.find( '.load-customize' ).focus(); } @@ -2461,5 +2470,144 @@ * @since 4.2.0 */ $( window ).on( 'beforeunload', wp.updates.beforeunload ); + + /** + * Click handler for enabling and disabling plugin and theme auto-updates. + * + * @since 5.5.0 + */ + $document.on( 'click', '.column-auto-updates a.toggle-auto-update, .theme-overlay a.toggle-auto-update', function( event ) { + var data, asset, type, $parent; + var $anchor = $( this ), + action = $anchor.attr( 'data-wp-action' ), + $label = $anchor.find( '.label' ); + + if ( 'themes' !== pagenow ) { + $parent = $anchor.closest( '.column-auto-updates' ); + } else { + $parent = $anchor.closest( '.theme-autoupdate' ); + } + + event.preventDefault(); + + // Prevent multiple simultaneous requests. + if ( $anchor.attr( 'data-doing-ajax' ) === 'yes' ) { + return; + } + + $anchor.attr( 'data-doing-ajax', 'yes' ); + + switch ( pagenow ) { + case 'plugins': + case 'plugins-network': + type = 'plugin'; + asset = $anchor.closest( 'tr' ).attr( 'data-plugin' ); + break; + case 'themes-network': + type = 'theme'; + asset = $anchor.closest( 'tr' ).attr( 'data-slug' ); + break; + case 'themes': + type = 'theme'; + asset = $anchor.attr( 'data-slug' ); + break; + } + + // Clear any previous errors. + $parent.find( '.notice.error' ).addClass( 'hidden' ); + + // Show loading status. + if ( 'enable' === action ) { + $label.text( wp.updates.l10n.autoUpdatesEnabling ); + } else { + $label.text( wp.updates.l10n.autoUpdatesDisabling ); + } + + $anchor.find( '.dashicons-update' ).removeClass( 'hidden' ); + + data = { + action: 'toggle-auto-updates', + _ajax_nonce: settings.ajax_nonce, + state: action, + type: type, + asset: asset + }; + + $.post( window.ajaxurl, data ) + .done( function( response ) { + var $enabled, $disabled, enabledNumber, disabledNumber, errorMessage; + var href = $anchor.attr( 'href' ); + + if ( ! response.success ) { + // if WP returns 0 for response (which can happen in a few cases), + // output the general error message since we won't have response.data.error. + if ( response.data && response.data.error ) { + errorMessage = response.data.error; + } else { + errorMessage = wp.updates.l10n.autoUpdatesError; + } + + $parent.find( '.notice.error' ).removeClass( 'hidden' ).find( 'p' ).text( errorMessage ); + wp.a11y.speak( errorMessage, 'polite' ); + return; + } + + // Update the counts in the enabled/disabled views if on a screen + // with a list table. + if ( 'themes' !== pagenow ) { + $enabled = $( '.auto-update-enabled span' ); + $disabled = $( '.auto-update-disabled span' ); + enabledNumber = parseInt( $enabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0; + disabledNumber = parseInt( $disabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0; + + switch ( action ) { + case 'enable': + ++enabledNumber; + --disabledNumber; + break; + case 'disable': + --enabledNumber; + ++disabledNumber; + break; + } + + enabledNumber = Math.max( 0, enabledNumber ); + disabledNumber = Math.max( 0, disabledNumber ); + + $enabled.text( '(' + enabledNumber + ')' ); + $disabled.text( '(' + disabledNumber + ')' ); + } + + if ( 'enable' === action ) { + href = href.replace( 'action=enable-auto-update', 'action=disable-auto-update' ); + $anchor.attr( { + 'data-wp-action': 'disable', + href: href + } ); + + $label.text( wp.updates.l10n.autoUpdatesDisable ); + $parent.find( '.auto-update-time' ).removeClass( 'hidden' ); + wp.a11y.speak( wp.updates.l10n.autoUpdatesEnabled, 'polite' ); + } else { + href = href.replace( 'action=disable-auto-update', 'action=enable-auto-update' ); + $anchor.attr( { + 'data-wp-action': 'enable', + href: href + } ); + + $label.text( wp.updates.l10n.autoUpdatesEnable ); + $parent.find( '.auto-update-time' ).addClass( 'hidden' ); + wp.a11y.speak( wp.updates.l10n.autoUpdatesDisabled, 'polite' ); + } + } ) + .fail( function() { + $parent.find( '.notice.error' ).removeClass( 'hidden' ).find( 'p' ).text( wp.updates.l10n.autoUpdatesError ); + wp.a11y.speak( wp.updates.l10n.autoUpdatesError, 'polite' ); + } ) + .always( function() { + $anchor.removeAttr( 'data-doing-ajax' ).find( '.dashicons-update' ).addClass( 'hidden' ); + } ); + } + ); } ); })( jQuery, window.wp, window._wpUpdatesSettings ); diff --git a/src/wp-admin/admin-ajax.php b/src/wp-admin/admin-ajax.php index 4f41a31bd6..28caf3d227 100644 --- a/src/wp-admin/admin-ajax.php +++ b/src/wp-admin/admin-ajax.php @@ -139,6 +139,7 @@ $core_actions_post = array( 'health-check-background-updates', 'health-check-loopback-requests', 'health-check-get-sizes', + 'toggle-auto-updates', ); // Deprecated. diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index 3d0dacf277..fbf69fe767 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -1524,7 +1524,9 @@ div.error { .updating-message p:before, .import-php .updating-message:before, .button.updating-message:before, -.button.installing:before { +.button.installing:before, +.plugins .column-auto-updates .dashicons-update.spin, +.theme-overlay .theme-autoupdate .dashicons-update.spin { animation: rotation 2s infinite linear; } diff --git a/src/wp-admin/css/list-tables.css b/src/wp-admin/css/list-tables.css index f621aef2ab..9aaaefebe0 100644 --- a/src/wp-admin/css/list-tables.css +++ b/src/wp-admin/css/list-tables.css @@ -1236,6 +1236,10 @@ ul.cat-checklist { width: 85px; } +.plugins .column-auto-updates { + width: 14.2em; +} + .plugins .inactive .plugin-title strong { font-weight: 400; } diff --git a/src/wp-admin/css/themes.css b/src/wp-admin/css/themes.css index a5ef219f77..7d87a06aeb 100644 --- a/src/wp-admin/css/themes.css +++ b/src/wp-admin/css/themes.css @@ -679,7 +679,8 @@ body.folded .theme-browser ~ .theme-overlay .theme-wrap { line-height: inherit; } -.theme-overlay .theme-author a { +.theme-overlay .theme-author a, +.theme-overlay .theme-autoupdate a { text-decoration: none; } diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 5b740af44c..9b74b9f4ba 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -4567,6 +4567,9 @@ function wp_ajax_delete_plugin() { function wp_ajax_search_plugins() { check_ajax_referer( 'updates' ); + // Ensure after_plugin_row_{$plugin_file} gets hooked. + wp_plugin_update_rows(); + $pagenow = isset( $_POST['pagenow'] ) ? sanitize_key( $_POST['pagenow'] ) : ''; if ( 'plugins-network' === $pagenow || 'plugins' === $pagenow ) { set_current_screen( $pagenow ); @@ -5267,3 +5270,73 @@ function wp_ajax_health_check_get_sizes() { function wp_ajax_rest_nonce() { exit( wp_create_nonce( 'wp_rest' ) ); } + +/** + * Ajax handler to enable or disable plugin and theme auto-updates. + * + * @since 5.5.0 + */ +function wp_ajax_toggle_auto_updates() { + check_ajax_referer( 'updates' ); + + if ( empty( $_POST['type'] ) || empty( $_POST['asset'] ) || empty( $_POST['state'] ) ) { + wp_send_json_error( array( 'error' => __( 'Invalid data. No selected item.' ) ) ); + } + + $asset = sanitize_text_field( urldecode( $_POST['asset'] ) ); + + if ( 'enable' !== $_POST['state'] && 'disable' !== $_POST['state'] ) { + wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown state.' ) ) ); + } + $state = $_POST['state']; + + if ( 'plugin' !== $_POST['type'] && 'theme' !== $_POST['type'] ) { + wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown type.' ) ) ); + } + $type = $_POST['type']; + + switch ( $type ) { + case 'plugin': + if ( ! current_user_can( 'update_plugins' ) ) { + $error_message = __( 'You do not have permission to modify plugins.' ); + wp_send_json_error( array( 'error' => $error_message ) ); + } + + $option = 'auto_update_plugins'; + /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */ + $all_items = apply_filters( 'all_plugins', get_plugins() ); + break; + case 'theme': + if ( ! current_user_can( 'update_themes' ) ) { + $error_message = __( 'You do not have permission to modify themes.' ); + wp_send_json_error( array( 'error' => $error_message ) ); + } + + $option = 'auto_update_themes'; + $all_items = wp_get_themes(); + break; + default: + wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown type.' ) ) ); + } + + if ( ! array_key_exists( $asset, $all_items ) ) { + $error_message = __( 'Invalid data. The item does not exist.' ); + wp_send_json_error( array( 'error' => $error_message ) ); + } + + $auto_updates = (array) get_site_option( $option, array() ); + + if ( 'disable' === $state ) { + $auto_updates = array_diff( $auto_updates, array( $asset ) ); + } else { + $auto_updates[] = $asset; + $auto_updates = array_unique( $auto_updates ); + } + + // Remove items that have been deleted since the site option was last updated. + $auto_updates = array_intersect( $auto_updates, array_keys( $all_items ) ); + + update_site_option( $option, $auto_updates ); + + wp_send_json_success(); +} diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index 3780512efa..20f1847de7 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -158,6 +158,14 @@ class WP_Automatic_Updater { // Next up, is this an item we can update? if ( 'core' === $type ) { $update = Core_Upgrader::should_update_to_version( $item->current ); + } elseif ( 'plugin' === $type || 'theme' === $type ) { + $update = ! empty( $item->autoupdate ); + + if ( ! $update && wp_is_auto_update_enabled_for_type( $type ) ) { + // Check if the site admin has enabled auto-updates by default for the specific item. + $auto_updates = (array) get_site_option( "auto_update_{$type}s", array() ); + $update = in_array( $item->{$type}, $auto_updates, true ); + } } else { $update = ! empty( $item->autoupdate ); } @@ -501,6 +509,8 @@ class WP_Automatic_Updater { if ( ! empty( $this->update_results['core'] ) ) { $this->after_core_update( $this->update_results['core'][0] ); + } elseif ( ! empty( $this->update_results['plugin'] ) || ! empty( $this->update_results['theme'] ) ) { + $this->after_plugin_theme_update( $this->update_results ); } /** @@ -854,6 +864,186 @@ class WP_Automatic_Updater { wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] ); } + + /** + * If we tried to perform plugin or theme updates, check if we should send an email. + * + * @since 5.5.0 + * + * @param object $results The result of updates tasks. + */ + protected function after_plugin_theme_update( $update_results ) { + $successful_updates = array(); + $failed_updates = array(); + + /** + * Filters whether to send an email following an automatic background plugin update. + * + * @since 5.5.0 + * + * @param bool $enabled True if plugins notifications are enabled, false otherwise. + */ + $notifications_enabled = apply_filters( 'auto_plugin_update_send_email', true ); + + if ( ! empty( $update_results['plugin'] ) && $notifications_enabled ) { + foreach ( $update_results['plugin'] as $update_result ) { + if ( true === $update_result->result ) { + $successful_updates['plugin'][] = $update_result; + } else { + $failed_updates['plugin'][] = $update_result; + } + } + } + + /** + * Filters whether to send an email following an automatic background theme update. + * + * @since 5.5.0 + * + * @param bool $enabled True if notifications are enabled, false otherwise. + */ + $notifications_enabled = apply_filters( 'send_theme_auto_update_email', true ); + + if ( ! empty( $update_results['theme'] ) && $notifications_enabled ) { + foreach ( $update_results['theme'] as $update_result ) { + if ( true === $update_result->result ) { + $successful_updates['theme'][] = $update_result; + } else { + $failed_updates['theme'][] = $update_result; + } + } + } + + if ( empty( $successful_updates ) && empty( $failed_updates ) ) { + return; + } + + if ( empty( $failed_updates ) ) { + $this->send_plugin_theme_email( 'success', $successful_updates, $failed_updates ); + } elseif ( empty( $successful_updates ) ) { + $this->send_plugin_theme_email( 'fail', $successful_updates, $failed_updates ); + } else { + $this->send_plugin_theme_email( 'mixed', $successful_updates, $failed_updates ); + } + } + + /** + * Sends an email upon the completion or failure of a plugin or theme background update. + * + * @since 5.5.0 + * + * @param string $type The type of email to send. Can be one of 'success', 'failure', 'mixed'. + * @param array $successful_updates A list of updates that succeeded. + * @param array $failed_updates A list of updates that failed. + */ + protected function send_plugin_theme_email( $type, $successful_updates, $failed_updates ) { + // No updates were attempted. + if ( empty( $successful_updates ) && empty( $failed_updates ) ) { + return; + } + $body = array(); + + switch ( $type ) { + case 'success': + /* translators: %s: Site title. */ + $subject = __( '[%s] Some plugins or themes were automatically updated' ); + break; + case 'fail': + /* translators: %s: Site title. */ + $subject = __( '[%s] Some plugins or themes have failed to update' ); + $body[] = sprintf( + /* translators: %s: Home URL. */ + __( 'Howdy! Failures occurred when attempting to update plugins/themes on your site at %s.' ), + home_url() + ); + $body[] = "\n"; + $body[] = __( 'Please check out your site now. It’s possible that everything is working. If it says you need to update, you should do so.' ); + break; + case 'mixed': + /* translators: %s: Site title. */ + $subject = __( '[%s] Some plugins or themes were automatically updated' ); + $body[] = sprintf( + /* translators: %s: Home URL. */ + __( 'Howdy! Failures occurred when attempting to update plugins/themes on your site at %s.' ), + home_url() + ); + $body[] = "\n"; + $body[] = __( 'Please check out your site now. It’s possible that everything is working. If it says you need to update, you should do so.' ); + $body[] = "\n"; + break; + } + + // Get failed plugin updates. + if ( in_array( $type, array( 'fail', 'mixed' ), true ) && ! empty( $failed_updates['plugin'] ) ) { + $body[] = __( 'The following plugins failed to update:' ); + // List failed updates. + foreach ( $failed_updates['plugin'] as $item ) { + $body[] = "- {$item->name}"; + } + $body[] = "\n"; + } + // Get failed theme updates. + if ( in_array( $type, array( 'fail', 'mixed' ), true ) && ! empty( $failed_updates['theme'] ) ) { + $body[] = __( 'The following themes failed to update:' ); + // List failed updates. + foreach ( $failed_updates['theme'] as $item ) { + $body[] = "- {$item->name}"; + } + $body[] = "\n"; + } + // Get successful plugin updates. + if ( in_array( $type, array( 'success', 'mixed' ), true ) && ! empty( $successful_updates['plugin'] ) ) { + $body[] = __( 'The following plugins were successfully updated:' ); + // List successful updates. + foreach ( $successful_updates['plugin'] as $item ) { + $body[] = "- {$item->name}"; + } + $body[] = "\n"; + } + // Get successful theme updates. + if ( in_array( $type, array( 'success', 'mixed' ), true ) && ! empty( $successful_updates['theme'] ) ) { + $body[] = __( 'The following themes were successfully updated:' ); + // List successful updates. + foreach ( $successful_updates['theme'] as $item ) { + $body[] = "- {$item->name}"; + } + $body[] = "\n"; + } + $body[] = "\n"; + + // Add a note about the support forums. + $body[] = __( 'If you experience any issues or need support, the volunteers in the WordPress.org support forums may be able to help.' ); + $body[] = __( 'https://wordpress.org/support/forums/' ); + $body[] = "\n" . __( 'The WordPress Team' ); + + $body = implode( "\n", $body ); + $to = get_site_option( 'admin_email' ); + $subject = sprintf( $subject, wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ); + $headers = ''; + + $email = compact( 'to', 'subject', 'body', 'headers' ); + + /** + * Filters the email sent following an automatic background plugin update. + * + * @param array $email { + * Array of email arguments that will be passed to wp_mail(). + * + * @type string $to The email recipient. An array of emails + * can be returned, as handled by wp_mail(). + * @type string $subject The email's subject. + * @type string $body The email message body. + * @type string $headers Any email headers, defaults to no headers. + * } + * @param string $type The type of email being sent. Can be one of + * 'success', 'fail', 'mixed'. + * @param object $successful_updates The updates that succeeded. + * @param object $failed_updates The updates that failed. + */ + $email = apply_filters( 'auto_plugin_theme_update_email', $email, $type, $successful_updates, $failed_updates ); + wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] ); + } + /** * Prepares and sends an email of a full log of background update results, useful for debugging and geekery. * diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index 476bf55d59..41ce09ef84 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -858,6 +858,15 @@ class WP_Debug_Data { // List all available plugins. $plugins = get_plugins(); $plugin_updates = get_plugin_updates(); + $auto_updates = array(); + + $auto_updates_enabled = wp_is_auto_update_enabled_for_type( 'plugin' ); + $auto_updates_enabled_str = __( 'Auto-updates enabled' ); + $auto_updates_disabled_str = __( 'Auto-updates disabled' ); + + if ( $auto_updates_enabled ) { + $auto_updates = (array) get_site_option( 'auto_update_plugins', array() ); + } foreach ( $plugins as $plugin_path => $plugin ) { $plugin_part = ( is_plugin_active( $plugin_path ) ) ? 'wp-plugins-active' : 'wp-plugins-inactive'; @@ -892,6 +901,16 @@ class WP_Debug_Data { $plugin_version_string_debug .= sprintf( ' (latest version: %s)', $plugin_updates[ $plugin_path ]->update->new_version ); } + if ( $auto_updates_enabled ) { + if ( in_array( $plugin_path, $auto_updates, true ) ) { + $plugin_version_string .= ' | ' . $auto_updates_enabled_str; + $plugin_version_string_debug .= ', ' . $auto_updates_enabled_str; + } else { + $plugin_version_string .= ' | ' . $auto_updates_disabled_str; + $plugin_version_string_debug .= ', ' . $auto_updates_disabled_str; + } + } + $info[ $plugin_part ]['fields'][ sanitize_text_field( $plugin['Name'] ) ] = array( 'label' => $plugin['Name'], 'value' => $plugin_version_string, @@ -915,6 +934,12 @@ class WP_Debug_Data { $active_theme_version = $active_theme->version; $active_theme_version_debug = $active_theme_version; + $auto_updates = array(); + $auto_updates_enabled = wp_is_auto_update_enabled_for_type( 'theme' ); + if ( $auto_updates_enabled ) { + $auto_updates = (array) get_site_option( 'auto_update_themes', array() ); + } + if ( array_key_exists( $active_theme->stylesheet, $theme_updates ) ) { $theme_update_new_version = $theme_updates[ $active_theme->stylesheet ]->update['new_version']; @@ -980,7 +1005,19 @@ class WP_Debug_Data { 'value' => get_stylesheet_directory(), ), ); + if ( $auto_updates_enabled ) { + if ( in_array( $active_theme->stylesheet, $auto_updates ) ) { + $theme_auto_update_string = __( 'Enabled' ); + } else { + $theme_auto_update_string = __( 'Disabled' ); + } + $info['wp-active-theme']['fields']['auto_update'] = array( + 'label' => __( 'Auto-update' ), + 'value' => $theme_auto_update_string, + 'debug' => $theme_auto_update_string, + ); + } $parent_theme = $active_theme->parent(); if ( $parent_theme ) { @@ -1026,6 +1063,19 @@ class WP_Debug_Data { 'value' => get_template_directory(), ), ); + if ( $auto_updates_enabled ) { + if ( in_array( $parent_theme->stylesheet, $auto_updates ) ) { + $parent_theme_auto_update_string = __( 'Enabled' ); + } else { + $parent_theme_auto_update_string = __( 'Disabled' ); + } + + $info['wp-parent-theme']['fields']['auto_update'] = array( + 'label' => __( 'Auto-update' ), + 'value' => $parent_theme_auto_update_string, + 'debug' => $parent_theme_auto_update_string, + ); + } } // Populate a list of all themes available in the install. @@ -1075,6 +1125,16 @@ class WP_Debug_Data { $theme_version_string_debug .= sprintf( ' (latest version: %s)', $theme_updates[ $theme_slug ]->update['new_version'] ); } + if ( $auto_updates_enabled ) { + if ( in_array( $theme_slug, $auto_updates ) ) { + $theme_version_string .= ' | ' . $auto_updates_enabled_str; + $theme_version_string_debug .= ',' . $auto_updates_enabled_str; + } else { + $theme_version_string .= ' | ' . $auto_updates_disabled_str; + $theme_version_string_debug .= ', ' . $auto_updates_disabled_str; + } + } + $info['wp-themes-inactive']['fields'][ sanitize_text_field( $theme->name ) ] = array( 'label' => sprintf( /* translators: 1: Theme name. 2: Theme slug. */ diff --git a/src/wp-admin/includes/class-wp-ms-themes-list-table.php b/src/wp-admin/includes/class-wp-ms-themes-list-table.php index 169f783ec8..11ef7391de 100644 --- a/src/wp-admin/includes/class-wp-ms-themes-list-table.php +++ b/src/wp-admin/includes/class-wp-ms-themes-list-table.php @@ -22,6 +22,15 @@ class WP_MS_Themes_List_Table extends WP_List_Table { private $has_items; + /** + * Whether to show the auto-updates UI. + * + * @since 5.5.0 + * + * @var bool True if auto-updates UI is to be shown, false otherwise. + */ + protected $show_autoupdates = true; + /** * Constructor. * @@ -45,7 +54,7 @@ class WP_MS_Themes_List_Table extends WP_List_Table { ); $status = isset( $_REQUEST['theme_status'] ) ? $_REQUEST['theme_status'] : 'all'; - if ( ! in_array( $status, array( 'all', 'enabled', 'disabled', 'upgrade', 'search', 'broken' ), true ) ) { + if ( ! in_array( $status, array( 'all', 'enabled', 'disabled', 'upgrade', 'search', 'broken', 'auto-update-enabled', 'auto-update-disabled' ), true ) ) { $status = 'all'; } @@ -56,6 +65,9 @@ class WP_MS_Themes_List_Table extends WP_List_Table { if ( $this->is_site_themes ) { $this->site_id = isset( $_REQUEST['id'] ) ? intval( $_REQUEST['id'] ) : 0; } + + $this->show_autoupdates = wp_is_auto_update_enabled_for_type( 'theme' ) && + ! $this->is_site_themes && current_user_can( 'update_themes' ); } /** @@ -107,6 +119,13 @@ class WP_MS_Themes_List_Table extends WP_List_Table { 'broken' => $this->is_site_themes ? array() : wp_get_themes( array( 'errors' => true ) ), ); + if ( $this->show_autoupdates ) { + $auto_updates = (array) get_site_option( 'auto_update_themes', array() ); + + $themes['auto-update-enabled'] = array(); + $themes['auto-update-disabled'] = array(); + } + if ( $this->is_site_themes ) { $themes_per_page = $this->get_items_per_page( 'site_themes_network_per_page' ); $allowed_where = 'site'; @@ -131,6 +150,14 @@ class WP_MS_Themes_List_Table extends WP_List_Table { $filter = $theme->is_allowed( $allowed_where, $this->site_id ) ? 'enabled' : 'disabled'; $themes[ $filter ][ $key ] = $themes['all'][ $key ]; + + if ( $this->show_autoupdates ) { + if ( in_array( $key, $auto_updates, true ) ) { + $themes['auto-update-enabled'][ $key ] = $themes['all'][ $key ]; + } else { + $themes['auto-update-disabled'][ $key ] = $themes['all'][ $key ]; + } + } } if ( $s ) { @@ -257,11 +284,17 @@ class WP_MS_Themes_List_Table extends WP_List_Table { * @return array */ public function get_columns() { - return array( + $columns = array( 'cb' => '', 'name' => __( 'Theme' ), 'description' => __( 'Description' ), ); + + if ( $this->show_autoupdates ) { + $columns['auto-updates'] = __( 'Automatic Updates' ); + } + + return $columns; } /** @@ -344,6 +377,22 @@ class WP_MS_Themes_List_Table extends WP_List_Table { 'themes' ); break; + case 'auto-update-enabled': + /* translators: %s: Number of themes. */ + $text = _n( + 'Auto-updates Enabled (%s)', + 'Auto-updates Enabled (%s)', + $count + ); + break; + case 'auto-update-disabled': + /* translators: %s: Number of themes. */ + $text = _n( + 'Auto-updates Disabled (%s)', + 'Auto-updates Disabled (%s)', + $count + ); + break; } if ( $this->is_site_themes ) { @@ -388,6 +437,17 @@ class WP_MS_Themes_List_Table extends WP_List_Table { $actions['delete-selected'] = __( 'Delete' ); } } + + if ( $this->show_autoupdates ) { + if ( 'auto-update-enabled' !== $status ) { + $actions['enable-auto-update-selected'] = __( 'Enable Auto-updates' ); + } + + if ( 'auto-update-disabled' !== $status ) { + $actions['disable-auto-update-selected'] = __( 'Disable Auto-updates' ); + } + } + return $actions; } @@ -639,6 +699,70 @@ class WP_MS_Themes_List_Table extends WP_List_Table { echo ''; } + /** + * Handles the auto-updates column output. + * + * @since 5.5.0 + * + * @global string $status + * @global int $page + * + * @param WP_Theme $theme The current WP_Theme object. + */ + public function column_autoupdates( $theme ) { + global $status, $page; + + static $auto_updates, $available_updates; + + if ( ! $auto_updates ) { + $auto_updates = (array) get_site_option( 'auto_update_themes', array() ); + } + if ( ! $available_updates ) { + $available_updates = get_site_transient( 'update_themes' ); + } + + $stylesheet = $theme->get_stylesheet(); + + if ( in_array( $stylesheet, $auto_updates, true ) ) { + $text = __( 'Disable auto-updates' ); + $action = 'disable'; + $time_class = ''; + } else { + $text = __( 'Enable auto-updates' ); + $action = 'enable'; + $time_class = ' hidden'; + } + + $query_args = array( + 'action' => "{$action}-auto-update", + 'theme' => $stylesheet, + 'paged' => $page, + 'theme_status' => $status, + ); + + $url = add_query_arg( $query_args, 'themes.php' ); + + printf( + '', + wp_nonce_url( $url, 'updates' ), + $action + ); + + echo ''; + echo '' . $text . ''; + echo ''; + + $available_updates = get_site_transient( 'update_themes' ); + if ( isset( $available_updates->response[ $stylesheet ] ) ) { + printf( + '
%s
', + $time_class, + wp_get_auto_update_message() + ); + } + echo ''; + } + /** * Handles default column output. * @@ -721,6 +845,13 @@ class WP_MS_Themes_List_Table extends WP_List_Table { echo ''; break; + case 'auto-updates': + echo ""; + + $this->column_autoupdates( $item ); + + echo ''; + break; default: echo ""; diff --git a/src/wp-admin/includes/class-wp-plugins-list-table.php b/src/wp-admin/includes/class-wp-plugins-list-table.php index b67d75c4b0..12793e810e 100644 --- a/src/wp-admin/includes/class-wp-plugins-list-table.php +++ b/src/wp-admin/includes/class-wp-plugins-list-table.php @@ -16,6 +16,14 @@ * @see WP_List_Table */ class WP_Plugins_List_Table extends WP_List_Table { + /** + * Whether to show the auto-updates UI. + * + * @since 5.5.0 + * + * @var bool True if auto-updates UI is to be shown, false otherwise. + */ + protected $show_autoupdates = true; /** * Constructor. @@ -39,7 +47,7 @@ class WP_Plugins_List_Table extends WP_List_Table { ) ); - $status_whitelist = array( 'active', 'inactive', 'recently_activated', 'upgrade', 'mustuse', 'dropins', 'search', 'paused' ); + $status_whitelist = array( 'active', 'inactive', 'recently_activated', 'upgrade', 'mustuse', 'dropins', 'search', 'paused', 'auto-update-enabled', 'auto-update-disabled' ); $status = 'all'; if ( isset( $_REQUEST['plugin_status'] ) && in_array( $_REQUEST['plugin_status'], $status_whitelist, true ) ) { @@ -51,6 +59,10 @@ class WP_Plugins_List_Table extends WP_List_Table { } $page = $this->get_pagenum(); + + $this->show_autoupdates = wp_is_auto_update_enabled_for_type( 'plugin' ) && + current_user_can( 'update_plugins' ) && + ( ! is_multisite() || $this->screen->in_admin( 'network' ) ); } /** @@ -103,6 +115,12 @@ class WP_Plugins_List_Table extends WP_List_Table { 'dropins' => array(), 'paused' => array(), ); + if ( $this->show_autoupdates ) { + $auto_updates = (array) get_site_option( 'auto_update_plugins', array() ); + + $plugins['auto-update-enabled'] = array(); + $plugins['auto-update-disabled'] = array(); + } $screen = $this->screen; @@ -233,6 +251,14 @@ class WP_Plugins_List_Table extends WP_List_Table { // Populate the inactive list with plugins that aren't activated. $plugins['inactive'][ $plugin_file ] = $plugin_data; } + + if ( $this->show_autoupdates ) { + if ( in_array( $plugin_file, $auto_updates, true ) ) { + $plugins['auto-update-enabled'][ $plugin_file ] = $plugins['all'][ $plugin_file ]; + } else { + $plugins['auto-update-disabled'][ $plugin_file ] = $plugins['all'][ $plugin_file ]; + } + } } if ( strlen( $s ) ) { @@ -399,11 +425,17 @@ class WP_Plugins_List_Table extends WP_List_Table { public function get_columns() { global $status; - return array( + $columns = array( 'cb' => ! in_array( $status, array( 'mustuse', 'dropins' ), true ) ? '' : '', 'name' => __( 'Plugin' ), 'description' => __( 'Description' ), ); + + if ( $this->show_autoupdates ) { + $columns['auto-updates'] = __( 'Automatic Updates' ); + } + + return $columns; } /** @@ -493,6 +525,22 @@ class WP_Plugins_List_Table extends WP_List_Table { $count ); break; + case 'auto-update-enabled': + /* translators: %s: Number of plugins. */ + $text = _n( + 'Auto-updates Enabled (%s)', + 'Auto-updates Enabled (%s)', + $count + ); + break; + case 'auto-update-disabled': + /* translators: %s: Number of plugins. */ + $text = _n( + 'Auto-updates Disabled (%s)', + 'Auto-updates Disabled (%s)', + $count + ); + break; } if ( 'search' !== $type ) { @@ -533,6 +581,15 @@ class WP_Plugins_List_Table extends WP_List_Table { if ( current_user_can( 'delete_plugins' ) && ( 'active' !== $status ) ) { $actions['delete-selected'] = __( 'Delete' ); } + + if ( $this->show_autoupdates ) { + if ( 'auto-update-enabled' !== $status ) { + $actions['enable-auto-update-selected'] = __( 'Enable Auto-updates' ); + } + if ( 'auto-update-disabled' !== $status ) { + $actions['disable-auto-update-selected'] = __( 'Disable Auto-updates' ); + } + } } return $actions; @@ -882,6 +939,9 @@ class WP_Plugins_List_Table extends WP_List_Table { list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); + $auto_updates = (array) get_site_option( 'auto_update_plugins', array() ); + $available_updates = get_site_transient( 'update_plugins' ); + foreach ( $columns as $column_name => $column_display_name ) { $extra_classes = ''; if ( in_array( $column_name, $hidden, true ) ) { @@ -973,6 +1033,56 @@ class WP_Plugins_List_Table extends WP_List_Table { } echo ''; + break; + case 'auto-updates': + if ( ! $this->show_autoupdates ) { + break; + } + + echo ""; + + if ( in_array( $plugin_file, $auto_updates, true ) ) { + $text = __( 'Disable auto-updates' ); + $action = 'disable'; + $time_class = ''; + } else { + $text = __( 'Enable auto-updates' ); + $action = 'enable'; + $time_class = ' hidden'; + } + + $query_args = array( + 'action' => "{$action}-auto-update", + 'plugin' => $plugin_file, + 'paged' => $page, + 'plugin_status' => $status, + ); + + $url = add_query_arg( $query_args, 'plugins.php' ); + + printf( + '', + wp_nonce_url( $url, 'updates' ), + $action + ); + + echo ''; + echo '' . $text . ''; + echo ''; + + $available_updates = get_site_transient( 'update_plugins' ); + + if ( isset( $available_updates->response[ $plugin_file ] ) ) { + printf( + '
%s
', + $time_class, + wp_get_auto_update_message() + ); + } + + echo ''; + echo ''; + break; default: $classes = "$column_name column-$column_name $class"; @@ -1000,12 +1110,14 @@ class WP_Plugins_List_Table extends WP_List_Table { * Fires after each row in the Plugins list table. * * @since 2.3.0 + * @since 5.5.0 Added 'Auto-updates Enabled' and 'Auto-updates Disabled' `$status`. * * @param string $plugin_file Path to the plugin file relative to the plugins directory. * @param array $plugin_data An array of plugin data. * @param string $status Status of the plugin. Defaults are 'All', 'Active', * 'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use', - * 'Drop-ins', 'Search', 'Paused'. + * 'Drop-ins', 'Search', 'Paused', 'Auto-updates Enabled', + * 'Auto-updates Disabled'. */ do_action( 'after_plugin_row', $plugin_file, $plugin_data, $status ); @@ -1016,12 +1128,14 @@ class WP_Plugins_List_Table extends WP_List_Table { * to the plugin file, relative to the plugins directory. * * @since 2.7.0 + * @since 5.5.0 Added 'Auto-updates Enabled' and 'Auto-updates Disabled' `$status`. * * @param string $plugin_file Path to the plugin file relative to the plugins directory. * @param array $plugin_data An array of plugin data. * @param string $status Status of the plugin. Defaults are 'All', 'Active', * 'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use', - * 'Drop-ins', 'Search', 'Paused'. + * 'Drop-ins', 'Search', 'Paused', 'Auto-updates Enabled', + * 'Auto-updates Disabled'. */ do_action( "after_plugin_row_{$plugin_file}", $plugin_file, $plugin_data, $status ); } diff --git a/src/wp-admin/includes/theme.php b/src/wp-admin/includes/theme.php index 5c5afee52a..0c917c52d9 100644 --- a/src/wp-admin/includes/theme.php +++ b/src/wp-admin/includes/theme.php @@ -660,6 +660,8 @@ function wp_prepare_themes_for_js( $themes = null ) { $parents = array(); + $auto_updates = (array) get_site_option( 'auto_update_themes', array() ); + foreach ( $themes as $theme ) { $slug = $theme->get_stylesheet(); $encoded_slug = urlencode( $slug ); @@ -683,6 +685,9 @@ function wp_prepare_themes_for_js( $themes = null ) { ); } + $auto_update = in_array( $slug, $auto_updates, true ); + $auto_update_action = $auto_update ? 'disable-auto-update' : 'enable-auto-update'; + $prepared_themes[ $slug ] = array( 'id' => $slug, 'name' => $theme->display( 'Name' ), @@ -699,10 +704,14 @@ function wp_prepare_themes_for_js( $themes = null ) { 'hasUpdate' => isset( $updates[ $slug ] ), 'hasPackage' => isset( $updates[ $slug ] ) && ! empty( $updates[ $slug ]['package'] ), 'update' => get_theme_update_available( $theme ), + 'autoupdate' => $auto_update, 'actions' => array( - 'activate' => current_user_can( 'switch_themes' ) ? wp_nonce_url( admin_url( 'themes.php?action=activate&stylesheet=' . $encoded_slug ), 'switch-theme_' . $slug ) : null, - 'customize' => $customize_action, - 'delete' => current_user_can( 'delete_themes' ) ? wp_nonce_url( admin_url( 'themes.php?action=delete&stylesheet=' . $encoded_slug ), 'delete-theme_' . $slug ) : null, + 'activate' => current_user_can( 'switch_themes' ) ? wp_nonce_url( admin_url( 'themes.php?action=activate&stylesheet=' . $encoded_slug ), 'switch-theme_' . $slug ) : null, + 'customize' => $customize_action, + 'delete' => current_user_can( 'delete_themes' ) ? wp_nonce_url( admin_url( 'themes.php?action=delete&stylesheet=' . $encoded_slug ), 'delete-theme_' . $slug ) : null, + 'autoupdate' => wp_is_auto_update_enabled_for_type( 'theme' ) && ! is_multisite() && current_user_can( 'update_themes' ) + ? wp_nonce_url( admin_url( 'themes.php?action=' . $auto_update_action . '&stylesheet=' . $encoded_slug ), 'updates' ) + : null, ), ); } diff --git a/src/wp-admin/includes/update.php b/src/wp-admin/includes/update.php index 085aa00f9e..3bc42e8fc1 100644 --- a/src/wp-admin/includes/update.php +++ b/src/wp-admin/includes/update.php @@ -435,7 +435,12 @@ function wp_plugin_update_row( $file, $plugin_data ) { $details_url = self_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . $response->slug . '§ion=changelog&TB_iframe=true&width=600&height=800' ); /** @var WP_Plugins_List_Table $wp_list_table */ - $wp_list_table = _get_list_table( 'WP_Plugins_List_Table' ); + $wp_list_table = _get_list_table( + 'WP_Plugins_List_Table', + array( + 'screen' => get_current_screen(), + ) + ); if ( is_network_admin() || ! is_multisite() ) { if ( is_network_admin() ) { @@ -933,3 +938,76 @@ function wp_recovery_mode_nag() { 0; + if ( $overdue ) { + return sprintf( + /* translators: Duration that WP-Cron has been overdue. */ + __( 'There may be a problem with WP-Cron. Automatic update overdue by %s.' ), + $time_to_next_update + ); + } else { + return sprintf( + /* translators: Time until the next update. */ + __( 'Auto-update scheduled in %s.' ), + $time_to_next_update + ); + } +} diff --git a/src/wp-admin/network/themes.php b/src/wp-admin/network/themes.php index e8027b939c..c71315aa2a 100644 --- a/src/wp-admin/network/themes.php +++ b/src/wp-admin/network/themes.php @@ -22,7 +22,15 @@ $action = $wp_list_table->current_action(); $s = isset( $_REQUEST['s'] ) ? $_REQUEST['s'] : ''; // Clean up request URI from temporary args for screen options/paging uri's to work as expected. -$temp_args = array( 'enabled', 'disabled', 'deleted', 'error' ); +$temp_args = array( + 'enabled', + 'disabled', + 'deleted', + 'error', + 'enabled-auto-update', + 'disabled-auto-update', +); + $_SERVER['REQUEST_URI'] = remove_query_arg( $temp_args, $_SERVER['REQUEST_URI'] ); $referer = remove_query_arg( $temp_args, wp_get_referer() ); @@ -123,8 +131,8 @@ if ( $action ) { require_once ABSPATH . 'wp-admin/admin-header.php'; $themes_to_delete = count( $themes ); ?> -
- +
+

@@ -145,7 +153,7 @@ if ( $action ) { } ?> - +

@@ -154,27 +162,28 @@ if ( $action ) { '; } - wp_nonce_field( 'bulk-themes' ); + wp_nonce_field( 'bulk-themes' ); - if ( 1 == $themes_to_delete ) { + if ( 1 === $themes_to_delete ) { submit_button( __( 'Yes, delete this theme' ), '', 'submit', false ); } else { submit_button( __( 'Yes, delete these themes' ), '', 'submit', false ); } + ?> - +
-
+

' . sprintf( $message, number_format_i18n( $enabled ) ) . '

'; } elseif ( isset( $_GET['disabled'] ) ) { $disabled = absint( $_GET['disabled'] ); - if ( 1 == $disabled ) { + if ( 1 === $disabled ) { $message = __( 'Theme disabled.' ); } else { /* translators: %s: Number of themes. */ @@ -302,16 +363,34 @@ if ( isset( $_GET['enabled'] ) ) { echo '

' . sprintf( $message, number_format_i18n( $disabled ) ) . '

'; } elseif ( isset( $_GET['deleted'] ) ) { $deleted = absint( $_GET['deleted'] ); - if ( 1 == $deleted ) { + if ( 1 === $deleted ) { $message = __( 'Theme deleted.' ); } else { /* translators: %s: Number of themes. */ $message = _n( '%s theme deleted.', '%s themes deleted.', $deleted ); } echo '

' . sprintf( $message, number_format_i18n( $deleted ) ) . '

'; -} elseif ( isset( $_GET['error'] ) && 'none' == $_GET['error'] ) { +} elseif ( isset( $_GET['enabled-auto-update'] ) ) { + $enabled = absint( $_GET['enabled-auto-update'] ); + if ( 1 === $enabled ) { + $message = __( 'Theme will be auto-updated.' ); + } else { + /* translators: %s: Number of themes. */ + $message = _n( '%s theme will be auto-updated.', '%s themes will be auto-updated.', $enabled ); + } + echo '

' . sprintf( $message, number_format_i18n( $enabled ) ) . '

'; +} elseif ( isset( $_GET['disabled-auto-update'] ) ) { + $disabled = absint( $_GET['disabled-auto-update'] ); + if ( 1 === $disabled ) { + $message = __( 'Theme will no longer be auto-updated.' ); + } else { + /* translators: %s: Number of themes. */ + $message = _n( '%s theme will no longer be auto-updated.', '%s themes will no longer be auto-updated.', $disabled ); + } + echo '

' . sprintf( $message, number_format_i18n( $disabled ) ) . '

'; +} elseif ( isset( $_GET['error'] ) && 'none' === $_GET['error'] ) { echo '

' . __( 'No theme selected.' ) . '

'; -} elseif ( isset( $_GET['error'] ) && 'main' == $_GET['error'] ) { +} elseif ( isset( $_GET['error'] ) && 'main' === $_GET['error'] ) { echo '

' . __( 'You cannot delete a theme while it is active on the main site.' ) . '

'; } @@ -324,7 +403,7 @@ if ( isset( $_GET['enabled'] ) ) { views(); -if ( 'broken' == $status ) { +if ( 'broken' === $status ) { echo '

' . __( 'The following themes are installed but incomplete.' ) . '

'; } ?> diff --git a/src/wp-admin/plugins.php b/src/wp-admin/plugins.php index 8352e207ab..62726ba12f 100644 --- a/src/wp-admin/plugins.php +++ b/src/wp-admin/plugins.php @@ -22,7 +22,21 @@ $plugin = isset( $_REQUEST['plugin'] ) ? wp_unslash( $_REQUEST['plugin'] ) : ''; $s = isset( $_REQUEST['s'] ) ? urlencode( wp_unslash( $_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'] ); +$query_args_to_remove = array( + 'error', + 'deleted', + 'activate', + 'activate-multi', + 'deactivate', + 'deactivate-multi', + 'enabled-auto-update', + 'disabled-auto-update', + 'enabled-auto-update-multi', + 'disabled-auto-update-multi', + '_error_nonce', +); + +$_SERVER['REQUEST_URI'] = remove_query_arg( $query_args_to_remove, $_SERVER['REQUEST_URI'] ); wp_enqueue_script( 'updates' ); @@ -284,11 +298,14 @@ if ( $action ) { if ( ! isset( $_REQUEST['verify-delete'] ) ) { wp_enqueue_script( 'jquery' ); require_once ABSPATH . 'wp-admin/admin-header.php'; + ?> -
+
- +

@@ -332,7 +351,9 @@ if ( $action ) {
    ', sprintf( _x( '%1$s by %2$s', 'plugin' ), '' . $plugin['Name'] . '', '' . $plugin['AuthorName'] ) . '', ''; } } + ?>

'; } + ?>
-
+
'true' ), $redirect ); + } elseif ( 'disable-auto-update' === $action ) { + $auto_updates = array_diff( $auto_updates, array( $plugin ) ); + $redirect = add_query_arg( array( 'disabled-auto-update' => 'true' ), $redirect ); + } else { + $plugins = (array) wp_unslash( $_POST['checked'] ); + + if ( 'enable-auto-update-selected' === $action ) { + $new_auto_updates = array_merge( $auto_updates, $plugins ); + $new_auto_updates = array_unique( $new_auto_updates ); + $query_args = array( 'enabled-auto-update-multi' => 'true' ); + } else { + $new_auto_updates = array_diff( $auto_updates, $plugins ); + $query_args = array( 'disabled-auto-update-multi' => 'true' ); + } + + // Return early if all selected plugins already have auto-updates enabled or disabled. + // Must use non-strict comparison, so that array order is not treated as significant. + if ( $new_auto_updates == $auto_updates ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison + wp_redirect( $redirect ); + exit; + } + + $auto_updates = $new_auto_updates; + $redirect = add_query_arg( $query_args, $redirect ); + } + + /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */ + $all_items = apply_filters( 'all_plugins', get_plugins() ); + + // Remove plugins that don't exist or have been deleted since the option was last updated. + $auto_updates = array_intersect( $auto_updates, array_keys( $all_items ) ); + + update_site_option( 'auto_update_plugins', $auto_updates ); + + wp_redirect( $redirect ); + exit; default: if ( isset( $_POST['checked'] ) ) { check_admin_referer( 'bulk-plugins' ); @@ -498,9 +597,7 @@ if ( ! empty( $invalid ) ) { echo '

'; } } -?> -fatal error.' ); } + ?>

- +
@@ -562,7 +663,7 @@ elseif ( isset( $_GET['deleted'] ) ) :

- +

@@ -583,6 +684,14 @@ elseif ( isset( $_GET['deleted'] ) ) :

+ +

+ +

+ +

+ +

diff --git a/src/wp-admin/themes.php b/src/wp-admin/themes.php index b9df71b70f..6ee6b14462 100644 --- a/src/wp-admin/themes.php +++ b/src/wp-admin/themes.php @@ -80,6 +80,45 @@ if ( current_user_can( 'switch_themes' ) && isset( $_GET['action'] ) ) { delete_theme( $_GET['stylesheet'] ); wp_redirect( admin_url( 'themes.php?deleted=true' ) ); } + exit; + } elseif ( 'enable-auto-update' === $_GET['action'] ) { + if ( ! ( current_user_can( 'update_themes' ) && wp_is_auto_update_enabled_for_type( 'theme' ) ) ) { + wp_die( __( 'Sorry, you are not allowed to enable themes automatic updates.' ) ); + } + + check_admin_referer( 'updates' ); + + $all_items = wp_get_themes(); + $auto_updates = (array) get_site_option( 'auto_update_themes', array() ); + + $auto_updates[] = $_GET['stylesheet']; + $auto_updates = array_unique( $auto_updates ); + // Remove themes that have been deleted since the site option was last updated. + $auto_updates = array_intersect( $auto_updates, array_keys( $all_items ) ); + + update_site_option( 'auto_update_themes', $auto_updates ); + + wp_redirect( admin_url( 'themes.php?enabled-auto-update=true' ) ); + + exit; + } elseif ( 'disable-auto-update' === $_GET['action'] ) { + if ( ! ( current_user_can( 'update_themes' ) && wp_is_auto_update_enabled_for_type( 'theme' ) ) ) { + wp_die( __( 'Sorry, you are not allowed to disable themes automatic updates.' ) ); + } + + check_admin_referer( 'updates' ); + + $all_items = wp_get_themes(); + $auto_updates = (array) get_site_option( 'auto_update_themes', array() ); + + $auto_updates = array_diff( $auto_updates, array( $_GET['stylesheet'] ) ); + // Remove themes that have been deleted since the site option was last updated. + $auto_updates = array_intersect( $auto_updates, array_keys( $all_items ) ); + + update_site_option( 'auto_update_themes', $auto_updates ); + + wp_redirect( admin_url( 'themes.php?disabled-auto-update=true' ) ); + exit; } } @@ -228,6 +267,14 @@ if ( ! validate_current_theme() || isset( $_GET['broken'] ) ) { ?>

fatal error.' ); ?>

+

+ +

+

+ <# if ( data.actions.autoupdate ) { #> +

+ <# if ( data.autoupdate ) { #> + + + + + <# } else { #> + + + + + <# } #> + <# if ( data.hasUpdate ) { #> + <# if ( data.autoupdate) { #> +
+ <# } else { #> + + <# } #> + <# } #> +

+

+ <# } #> + <# if ( data.hasUpdate ) { #>

diff --git a/src/wp-admin/update-core.php b/src/wp-admin/update-core.php index c74e5c5546..aa2f91c37d 100644 --- a/src/wp-admin/update-core.php +++ b/src/wp-admin/update-core.php @@ -328,6 +328,13 @@ function list_plugin_updates() { $plugin_data ) { $plugin_data = (object) _get_plugin_data_markup_translate( $plugin_file, (array) $plugin_data, false, true ); @@ -419,6 +426,9 @@ function list_plugin_updates() { $plugin_data->update->new_version ); echo ' ' . $details . $compat . $upgrade_notice; + if ( in_array( $plugin_file, $auto_updates, true ) ) { + echo $auto_update_notice; + } ?>

@@ -478,8 +488,15 @@ function list_theme_updates() { $theme ) { $checkbox_id = 'checkbox_' . md5( $theme->get( 'Name' ) ); + ?> @@ -501,6 +518,9 @@ function list_theme_updates() { $theme->display( 'Version' ), $theme->update['new_version'] ); + if ( in_array( $stylesheet, $auto_updates, true ) ) { + echo $auto_update_notice; + } ?>

diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 59aed70a22..e765a03163 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1520,6 +1520,13 @@ function wp_default_scripts( $scripts ) { /* translators: %s: Number of plugins. */ 'pluginsFound' => __( 'Number of plugins found: %d' ), 'noPluginsFound' => __( 'No plugins found. Try a different search.' ), + 'autoUpdatesEnable' => __( 'Enable auto-updates' ), + 'autoUpdatesEnabling' => __( 'Enabling...' ), + 'autoUpdatesEnabled' => __( 'Auto-updates enabled' ), + 'autoUpdatesDisable' => __( 'Disable auto-updates' ), + 'autoUpdatesDisabling' => __( 'Disabling...' ), + 'autoUpdatesDisabled' => __( 'Auto-updates disabled' ), + 'autoUpdatesError' => __( 'The request could not be completed.' ), ), ) );