WordPress.org

Make WordPress Core

Changeset 47835


Ignore:
Timestamp:
05/20/2020 06:47:24 PM (8 days ago)
Author:
whyisjake
Message:

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.

Location:
trunk/src
Files:
17 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/js/_enqueues/wp/updates.js

    r46800 r47835  
    410410     * @since 4.2.0
    411411     * @since 4.6.0 More accurately named `updatePluginSuccess`.
     412     * @since 5.5.0 Auto-update "time to next update" text cleared.
    412413     *
    413414     * @param {object} response            Response from the server.
     
    432433            newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
    433434            $pluginRow.find( '.plugin-version-author-uri' ).html( newText );
     435
     436            // Clear the "time to next auto-update" text.
     437            $pluginRow.find( '.auto-update-time' ).empty();
    434438        } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
    435439            $updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' )
     
    970974     *
    971975     * @since 4.6.0
     976     * @since 5.5.0 Auto-update "time to next update" text cleared.
    972977     *
    973978     * @param {object} response
     
    10031008            newText = $theme.find( '.theme-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
    10041009            $theme.find( '.theme-version-author-uri' ).html( newText );
     1010
     1011            // Clear the "time to next auto-update" text.
     1012            $theme.find( '.auto-update-time' ).empty();
    10051013        } else {
    10061014            $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) );
     
    10091017            if ( isModalOpen ) {
    10101018                $( '.load-customize:visible' ).focus();
     1019                $( '.theme-info .theme-autoupdate' ).find( '.auto-update-time' ).empty();
    10111020            } else {
    10121021                $theme.find( '.load-customize' ).focus();
     
    24622471         */
    24632472        $( window ).on( 'beforeunload', wp.updates.beforeunload );
     2473
     2474        /**
     2475         * Click handler for enabling and disabling plugin and theme auto-updates.
     2476         *
     2477         * @since 5.5.0
     2478         */
     2479        $document.on( 'click', '.column-auto-updates a.toggle-auto-update, .theme-overlay a.toggle-auto-update', function( event ) {
     2480            var data, asset, type, $parent;
     2481            var $anchor = $( this ),
     2482                action = $anchor.attr( 'data-wp-action' ),
     2483                $label = $anchor.find( '.label' );
     2484
     2485            if ( 'themes' !== pagenow ) {
     2486                $parent = $anchor.closest( '.column-auto-updates' );
     2487            } else {
     2488                $parent = $anchor.closest( '.theme-autoupdate' );
     2489            }
     2490
     2491            event.preventDefault();
     2492
     2493            // Prevent multiple simultaneous requests.
     2494            if ( $anchor.attr( 'data-doing-ajax' ) === 'yes' ) {
     2495                return;
     2496            }
     2497
     2498            $anchor.attr( 'data-doing-ajax', 'yes' );
     2499
     2500            switch ( pagenow ) {
     2501                case 'plugins':
     2502                case 'plugins-network':
     2503                    type = 'plugin';
     2504                    asset = $anchor.closest( 'tr' ).attr( 'data-plugin' );
     2505                    break;
     2506                case 'themes-network':
     2507                    type = 'theme';
     2508                    asset = $anchor.closest( 'tr' ).attr( 'data-slug' );
     2509                    break;
     2510                case 'themes':
     2511                    type = 'theme';
     2512                    asset = $anchor.attr( 'data-slug' );
     2513                    break;
     2514            }
     2515
     2516            // Clear any previous errors.
     2517            $parent.find( '.notice.error' ).addClass( 'hidden' );
     2518
     2519            // Show loading status.
     2520            if ( 'enable' === action ) {
     2521                $label.text( wp.updates.l10n.autoUpdatesEnabling );
     2522            } else {
     2523                $label.text( wp.updates.l10n.autoUpdatesDisabling );
     2524            }
     2525
     2526            $anchor.find( '.dashicons-update' ).removeClass( 'hidden' );
     2527
     2528            data = {
     2529                action: 'toggle-auto-updates',
     2530                _ajax_nonce: settings.ajax_nonce,
     2531                state: action,
     2532                type: type,
     2533                asset: asset
     2534            };
     2535
     2536            $.post( window.ajaxurl, data )
     2537                .done( function( response ) {
     2538                    var $enabled, $disabled, enabledNumber, disabledNumber, errorMessage;
     2539                    var href = $anchor.attr( 'href' );
     2540
     2541                    if ( ! response.success ) {
     2542                        // if WP returns 0 for response (which can happen in a few cases),
     2543                        // output the general error message since we won't have response.data.error.
     2544                        if ( response.data && response.data.error ) {
     2545                            errorMessage = response.data.error;
     2546                        } else {
     2547                            errorMessage = wp.updates.l10n.autoUpdatesError;
     2548                        }
     2549
     2550                        $parent.find( '.notice.error' ).removeClass( 'hidden' ).find( 'p' ).text( errorMessage );
     2551                        wp.a11y.speak( errorMessage, 'polite' );
     2552                        return;
     2553                    }
     2554
     2555                    // Update the counts in the enabled/disabled views if on a screen
     2556                    // with a list table.
     2557                    if ( 'themes' !== pagenow ) {
     2558                        $enabled       = $( '.auto-update-enabled span' );
     2559                        $disabled      = $( '.auto-update-disabled span' );
     2560                        enabledNumber  = parseInt( $enabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0;
     2561                        disabledNumber = parseInt( $disabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0;
     2562
     2563                        switch ( action ) {
     2564                            case 'enable':
     2565                                ++enabledNumber;
     2566                                --disabledNumber;
     2567                                break;
     2568                            case 'disable':
     2569                                --enabledNumber;
     2570                                ++disabledNumber;
     2571                                break;
     2572                        }
     2573
     2574                        enabledNumber = Math.max( 0, enabledNumber );
     2575                        disabledNumber = Math.max( 0, disabledNumber );
     2576
     2577                        $enabled.text( '(' + enabledNumber + ')' );
     2578                        $disabled.text( '(' + disabledNumber + ')' );
     2579                    }
     2580
     2581                    if ( 'enable' === action ) {
     2582                        href = href.replace( 'action=enable-auto-update', 'action=disable-auto-update' );
     2583                        $anchor.attr( {
     2584                            'data-wp-action': 'disable',
     2585                            href: href
     2586                        } );
     2587
     2588                        $label.text( wp.updates.l10n.autoUpdatesDisable );
     2589                        $parent.find( '.auto-update-time' ).removeClass( 'hidden' );
     2590                        wp.a11y.speak( wp.updates.l10n.autoUpdatesEnabled, 'polite' );
     2591                    } else {
     2592                        href = href.replace( 'action=disable-auto-update', 'action=enable-auto-update' );
     2593                        $anchor.attr( {
     2594                            'data-wp-action': 'enable',
     2595                            href: href
     2596                        } );
     2597
     2598                        $label.text( wp.updates.l10n.autoUpdatesEnable );
     2599                        $parent.find( '.auto-update-time' ).addClass( 'hidden' );
     2600                        wp.a11y.speak( wp.updates.l10n.autoUpdatesDisabled, 'polite' );
     2601                    }
     2602                } )
     2603                .fail( function() {
     2604                    $parent.find( '.notice.error' ).removeClass( 'hidden' ).find( 'p' ).text( wp.updates.l10n.autoUpdatesError );
     2605                    wp.a11y.speak( wp.updates.l10n.autoUpdatesError, 'polite' );
     2606                } )
     2607                .always( function() {
     2608                    $anchor.removeAttr( 'data-doing-ajax' ).find( '.dashicons-update' ).addClass( 'hidden' );
     2609                } );
     2610            }
     2611        );
    24642612    } );
    24652613})( jQuery, window.wp, window._wpUpdatesSettings );
  • trunk/src/wp-admin/admin-ajax.php

    r47550 r47835  
    140140    'health-check-loopback-requests',
    141141    'health-check-get-sizes',
     142    'toggle-auto-updates',
    142143);
    143144
  • trunk/src/wp-admin/css/common.css

    r47771 r47835  
    15251525.import-php .updating-message:before,
    15261526.button.updating-message:before,
    1527 .button.installing:before {
     1527.button.installing:before,
     1528.plugins .column-auto-updates .dashicons-update.spin,
     1529.theme-overlay .theme-autoupdate .dashicons-update.spin {
    15281530    animation: rotation 2s infinite linear;
    15291531}
  • trunk/src/wp-admin/css/list-tables.css

    r47771 r47835  
    12371237}
    12381238
     1239.plugins .column-auto-updates {
     1240    width: 14.2em;
     1241}
     1242
    12391243.plugins .inactive .plugin-title strong {
    12401244    font-weight: 400;
  • trunk/src/wp-admin/css/themes.css

    r47771 r47835  
    680680}
    681681
    682 .theme-overlay .theme-author a {
     682.theme-overlay .theme-author a,
     683.theme-overlay .theme-autoupdate a {
    683684    text-decoration: none;
    684685}
  • trunk/src/wp-admin/includes/ajax-actions.php

    r47818 r47835  
    45684568    check_ajax_referer( 'updates' );
    45694569
     4570    // Ensure after_plugin_row_{$plugin_file} gets hooked.
     4571    wp_plugin_update_rows();
     4572
    45704573    $pagenow = isset( $_POST['pagenow'] ) ? sanitize_key( $_POST['pagenow'] ) : '';
    45714574    if ( 'plugins-network' === $pagenow || 'plugins' === $pagenow ) {
     
    52685271    exit( wp_create_nonce( 'wp_rest' ) );
    52695272}
     5273
     5274/**
     5275 * Ajax handler to enable or disable plugin and theme auto-updates.
     5276 *
     5277 * @since 5.5.0
     5278 */
     5279function wp_ajax_toggle_auto_updates() {
     5280    check_ajax_referer( 'updates' );
     5281
     5282    if ( empty( $_POST['type'] ) || empty( $_POST['asset'] ) || empty( $_POST['state'] ) ) {
     5283        wp_send_json_error( array( 'error' => __( 'Invalid data. No selected item.' ) ) );
     5284    }
     5285
     5286    $asset = sanitize_text_field( urldecode( $_POST['asset'] ) );
     5287
     5288    if ( 'enable' !== $_POST['state'] && 'disable' !== $_POST['state'] ) {
     5289        wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown state.' ) ) );
     5290    }
     5291    $state = $_POST['state'];
     5292
     5293    if ( 'plugin' !== $_POST['type'] && 'theme' !== $_POST['type'] ) {
     5294        wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown type.' ) ) );
     5295    }
     5296    $type = $_POST['type'];
     5297
     5298    switch ( $type ) {
     5299        case 'plugin':
     5300            if ( ! current_user_can( 'update_plugins' ) ) {
     5301                $error_message = __( 'You do not have permission to modify plugins.' );
     5302                wp_send_json_error( array( 'error' => $error_message ) );
     5303            }
     5304
     5305            $option = 'auto_update_plugins';
     5306            /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
     5307            $all_items = apply_filters( 'all_plugins', get_plugins() );
     5308            break;
     5309        case 'theme':
     5310            if ( ! current_user_can( 'update_themes' ) ) {
     5311                $error_message = __( 'You do not have permission to modify themes.' );
     5312                wp_send_json_error( array( 'error' => $error_message ) );
     5313            }
     5314
     5315            $option    = 'auto_update_themes';
     5316            $all_items = wp_get_themes();
     5317            break;
     5318        default:
     5319            wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown type.' ) ) );
     5320    }
     5321
     5322    if ( ! array_key_exists( $asset, $all_items ) ) {
     5323        $error_message = __( 'Invalid data. The item does not exist.' );
     5324        wp_send_json_error( array( 'error' => $error_message ) );
     5325    }
     5326
     5327    $auto_updates = (array) get_site_option( $option, array() );
     5328
     5329    if ( 'disable' === $state ) {
     5330        $auto_updates = array_diff( $auto_updates, array( $asset ) );
     5331    } else {
     5332        $auto_updates[] = $asset;
     5333        $auto_updates   = array_unique( $auto_updates );
     5334    }
     5335
     5336    // Remove items that have been deleted since the site option was last updated.
     5337    $auto_updates = array_intersect( $auto_updates, array_keys( $all_items ) );
     5338
     5339    update_site_option( $option, $auto_updates );
     5340
     5341    wp_send_json_success();
     5342}
  • trunk/src/wp-admin/includes/class-wp-automatic-updater.php

    r47808 r47835  
    159159        if ( 'core' === $type ) {
    160160            $update = Core_Upgrader::should_update_to_version( $item->current );
     161        } elseif ( 'plugin' === $type || 'theme' === $type ) {
     162            $update = ! empty( $item->autoupdate );
     163
     164            if ( ! $update && wp_is_auto_update_enabled_for_type( $type ) ) {
     165                // Check if the site admin has enabled auto-updates by default for the specific item.
     166                $auto_updates = (array) get_site_option( "auto_update_{$type}s", array() );
     167                $update       = in_array( $item->{$type}, $auto_updates, true );
     168            }
    161169        } else {
    162170            $update = ! empty( $item->autoupdate );
     
    502510            if ( ! empty( $this->update_results['core'] ) ) {
    503511                $this->after_core_update( $this->update_results['core'][0] );
     512            } elseif ( ! empty( $this->update_results['plugin'] ) || ! empty( $this->update_results['theme'] ) ) {
     513                $this->after_plugin_theme_update( $this->update_results );
    504514            }
    505515
     
    855865    }
    856866
     867
     868    /**
     869     * If we tried to perform plugin or theme updates, check if we should send an email.
     870     *
     871     * @since 5.5.0
     872     *
     873     * @param object $results The result of updates tasks.
     874     */
     875    protected function after_plugin_theme_update( $update_results ) {
     876        $successful_updates = array();
     877        $failed_updates     = array();
     878
     879        /**
     880         * Filters whether to send an email following an automatic background plugin update.
     881         *
     882         * @since 5.5.0
     883         *
     884         * @param bool $enabled True if plugins notifications are enabled, false otherwise.
     885         */
     886        $notifications_enabled = apply_filters( 'auto_plugin_update_send_email', true );
     887
     888        if ( ! empty( $update_results['plugin'] ) && $notifications_enabled ) {
     889            foreach ( $update_results['plugin'] as $update_result ) {
     890                if ( true === $update_result->result ) {
     891                    $successful_updates['plugin'][] = $update_result;
     892                } else {
     893                    $failed_updates['plugin'][] = $update_result;
     894                }
     895            }
     896        }
     897
     898        /**
     899         * Filters whether to send an email following an automatic background theme update.
     900         *
     901         * @since 5.5.0
     902         *
     903         * @param bool $enabled True if notifications are enabled, false otherwise.
     904         */
     905        $notifications_enabled = apply_filters( 'send_theme_auto_update_email', true );
     906
     907        if ( ! empty( $update_results['theme'] ) && $notifications_enabled ) {
     908            foreach ( $update_results['theme'] as $update_result ) {
     909                if ( true === $update_result->result ) {
     910                    $successful_updates['theme'][] = $update_result;
     911                } else {
     912                    $failed_updates['theme'][] = $update_result;
     913                }
     914            }
     915        }
     916
     917        if ( empty( $successful_updates ) && empty( $failed_updates ) ) {
     918            return;
     919        }
     920
     921        if ( empty( $failed_updates ) ) {
     922            $this->send_plugin_theme_email( 'success', $successful_updates, $failed_updates );
     923        } elseif ( empty( $successful_updates ) ) {
     924            $this->send_plugin_theme_email( 'fail', $successful_updates, $failed_updates );
     925        } else {
     926            $this->send_plugin_theme_email( 'mixed', $successful_updates, $failed_updates );
     927        }
     928    }
     929
     930    /**
     931     * Sends an email upon the completion or failure of a plugin or theme background update.
     932     *
     933     * @since 5.5.0
     934     *
     935     * @param string $type               The type of email to send. Can be one of 'success', 'failure', 'mixed'.
     936     * @param array  $successful_updates A list of updates that succeeded.
     937     * @param array  $failed_updates     A list of updates that failed.
     938     */
     939    protected function send_plugin_theme_email( $type, $successful_updates, $failed_updates ) {
     940        // No updates were attempted.
     941        if ( empty( $successful_updates ) && empty( $failed_updates ) ) {
     942            return;
     943        }
     944        $body = array();
     945
     946        switch ( $type ) {
     947            case 'success':
     948                /* translators: %s: Site title. */
     949                $subject = __( '[%s] Some plugins or themes were automatically updated' );
     950                break;
     951            case 'fail':
     952                /* translators: %s: Site title. */
     953                $subject = __( '[%s] Some plugins or themes have failed to update' );
     954                $body[]  = sprintf(
     955                    /* translators: %s: Home URL. */
     956                    __( 'Howdy! Failures occurred when attempting to update plugins/themes on your site at %s.' ),
     957                    home_url()
     958                );
     959                $body[] = "\n";
     960                $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.' );
     961                break;
     962            case 'mixed':
     963                /* translators: %s: Site title. */
     964                $subject = __( '[%s] Some plugins or themes were automatically updated' );
     965                $body[]  = sprintf(
     966                    /* translators: %s: Home URL. */
     967                    __( 'Howdy! Failures occurred when attempting to update plugins/themes on your site at %s.' ),
     968                    home_url()
     969                );
     970                $body[] = "\n";
     971                $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.' );
     972                $body[] = "\n";
     973                break;
     974        }
     975
     976        // Get failed plugin updates.
     977        if ( in_array( $type, array( 'fail', 'mixed' ), true ) && ! empty( $failed_updates['plugin'] ) ) {
     978            $body[] = __( 'The following plugins failed to update:' );
     979            // List failed updates.
     980            foreach ( $failed_updates['plugin'] as $item ) {
     981                $body[] = "- {$item->name}";
     982            }
     983            $body[] = "\n";
     984        }
     985        // Get failed theme updates.
     986        if ( in_array( $type, array( 'fail', 'mixed' ), true ) && ! empty( $failed_updates['theme'] ) ) {
     987            $body[] = __( 'The following themes failed to update:' );
     988            // List failed updates.
     989            foreach ( $failed_updates['theme'] as $item ) {
     990                $body[] = "- {$item->name}";
     991            }
     992            $body[] = "\n";
     993        }
     994        // Get successful plugin updates.
     995        if ( in_array( $type, array( 'success', 'mixed' ), true ) && ! empty( $successful_updates['plugin'] ) ) {
     996            $body[] = __( 'The following plugins were successfully updated:' );
     997            // List successful updates.
     998            foreach ( $successful_updates['plugin'] as $item ) {
     999                $body[] = "- {$item->name}";
     1000            }
     1001            $body[] = "\n";
     1002        }
     1003        // Get successful theme updates.
     1004        if ( in_array( $type, array( 'success', 'mixed' ), true ) && ! empty( $successful_updates['theme'] ) ) {
     1005            $body[] = __( 'The following themes were successfully updated:' );
     1006            // List successful updates.
     1007            foreach ( $successful_updates['theme'] as $item ) {
     1008                $body[] = "- {$item->name}";
     1009            }
     1010            $body[] = "\n";
     1011        }
     1012        $body[] = "\n";
     1013
     1014        // Add a note about the support forums.
     1015        $body[] = __( 'If you experience any issues or need support, the volunteers in the WordPress.org support forums may be able to help.' );
     1016        $body[] = __( 'https://wordpress.org/support/forums/' );
     1017        $body[] = "\n" . __( 'The WordPress Team' );
     1018
     1019        $body    = implode( "\n", $body );
     1020        $to      = get_site_option( 'admin_email' );
     1021        $subject = sprintf( $subject, wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) );
     1022        $headers = '';
     1023
     1024        $email = compact( 'to', 'subject', 'body', 'headers' );
     1025
     1026        /**
     1027         * Filters the email sent following an automatic background plugin update.
     1028         *
     1029         * @param array $email {
     1030         *     Array of email arguments that will be passed to wp_mail().
     1031         *
     1032         *     @type string $to      The email recipient. An array of emails
     1033         *                           can be returned, as handled by wp_mail().
     1034         *     @type string $subject The email's subject.
     1035         *     @type string $body    The email message body.
     1036         *     @type string $headers Any email headers, defaults to no headers.
     1037         * }
     1038         * @param string $type               The type of email being sent. Can be one of
     1039         *                                   'success', 'fail', 'mixed'.
     1040         * @param object $successful_updates The updates that succeeded.
     1041         * @param object $failed_updates     The updates that failed.
     1042         */
     1043        $email = apply_filters( 'auto_plugin_theme_update_email', $email, $type, $successful_updates, $failed_updates );
     1044        wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] );
     1045    }
     1046
    8571047    /**
    8581048     * Prepares and sends an email of a full log of background update results, useful for debugging and geekery.
  • trunk/src/wp-admin/includes/class-wp-debug-data.php

    r47762 r47835  
    859859        $plugins        = get_plugins();
    860860        $plugin_updates = get_plugin_updates();
     861        $auto_updates   = array();
     862
     863        $auto_updates_enabled      = wp_is_auto_update_enabled_for_type( 'plugin' );
     864        $auto_updates_enabled_str  = __( 'Auto-updates enabled' );
     865        $auto_updates_disabled_str = __( 'Auto-updates disabled' );
     866
     867        if ( $auto_updates_enabled ) {
     868            $auto_updates = (array) get_site_option( 'auto_update_plugins', array() );
     869        }
    861870
    862871        foreach ( $plugins as $plugin_path => $plugin ) {
     
    893902            }
    894903
     904            if ( $auto_updates_enabled ) {
     905                if ( in_array( $plugin_path, $auto_updates, true ) ) {
     906                    $plugin_version_string       .= ' | ' . $auto_updates_enabled_str;
     907                    $plugin_version_string_debug .= ', ' . $auto_updates_enabled_str;
     908                } else {
     909                    $plugin_version_string       .= ' | ' . $auto_updates_disabled_str;
     910                    $plugin_version_string_debug .= ', ' . $auto_updates_disabled_str;
     911                }
     912            }
     913
    895914            $info[ $plugin_part ]['fields'][ sanitize_text_field( $plugin['Name'] ) ] = array(
    896915                'label' => $plugin['Name'],
     
    915934        $active_theme_version       = $active_theme->version;
    916935        $active_theme_version_debug = $active_theme_version;
     936
     937        $auto_updates         = array();
     938        $auto_updates_enabled = wp_is_auto_update_enabled_for_type( 'theme' );
     939        if ( $auto_updates_enabled ) {
     940            $auto_updates = (array) get_site_option( 'auto_update_themes', array() );
     941        }
    917942
    918943        if ( array_key_exists( $active_theme->stylesheet, $theme_updates ) ) {
     
    9811006            ),
    9821007        );
    983 
     1008        if ( $auto_updates_enabled ) {
     1009            if ( in_array( $active_theme->stylesheet, $auto_updates ) ) {
     1010                $theme_auto_update_string = __( 'Enabled' );
     1011            } else {
     1012                $theme_auto_update_string = __( 'Disabled' );
     1013            }
     1014
     1015            $info['wp-active-theme']['fields']['auto_update'] = array(
     1016                'label' => __( 'Auto-update' ),
     1017                'value' => $theme_auto_update_string,
     1018                'debug' => $theme_auto_update_string,
     1019            );
     1020        }
    9841021        $parent_theme = $active_theme->parent();
    9851022
     
    10271064                ),
    10281065            );
     1066            if ( $auto_updates_enabled ) {
     1067                if ( in_array( $parent_theme->stylesheet, $auto_updates ) ) {
     1068                    $parent_theme_auto_update_string = __( 'Enabled' );
     1069                } else {
     1070                    $parent_theme_auto_update_string = __( 'Disabled' );
     1071                }
     1072
     1073                $info['wp-parent-theme']['fields']['auto_update'] = array(
     1074                    'label' => __( 'Auto-update' ),
     1075                    'value' => $parent_theme_auto_update_string,
     1076                    'debug' => $parent_theme_auto_update_string,
     1077                );
     1078            }
    10291079        }
    10301080
     
    10741124                $theme_version_string       .= ' ' . sprintf( __( '(Latest version: %s)' ), $theme_updates[ $theme_slug ]->update['new_version'] );
    10751125                $theme_version_string_debug .= sprintf( ' (latest version: %s)', $theme_updates[ $theme_slug ]->update['new_version'] );
     1126            }
     1127
     1128            if ( $auto_updates_enabled ) {
     1129                if ( in_array( $theme_slug, $auto_updates ) ) {
     1130                    $theme_version_string       .= ' | ' . $auto_updates_enabled_str;
     1131                    $theme_version_string_debug .= ',' . $auto_updates_enabled_str;
     1132                } else {
     1133                    $theme_version_string       .= ' | ' . $auto_updates_disabled_str;
     1134                    $theme_version_string_debug .= ', ' . $auto_updates_disabled_str;
     1135                }
    10761136            }
    10771137
  • trunk/src/wp-admin/includes/class-wp-ms-themes-list-table.php

    r47808 r47835  
    2424
    2525    /**
     26     * Whether to show the auto-updates UI.
     27     *
     28     * @since 5.5.0
     29     *
     30     * @var bool True if auto-updates UI is to be shown, false otherwise.
     31     */
     32    protected $show_autoupdates = true;
     33
     34    /**
    2635     * Constructor.
    2736     *
     
    4655
    4756        $status = isset( $_REQUEST['theme_status'] ) ? $_REQUEST['theme_status'] : 'all';
    48         if ( ! in_array( $status, array( 'all', 'enabled', 'disabled', 'upgrade', 'search', 'broken' ), true ) ) {
     57        if ( ! in_array( $status, array( 'all', 'enabled', 'disabled', 'upgrade', 'search', 'broken', 'auto-update-enabled', 'auto-update-disabled' ), true ) ) {
    4958            $status = 'all';
    5059        }
     
    5766            $this->site_id = isset( $_REQUEST['id'] ) ? intval( $_REQUEST['id'] ) : 0;
    5867        }
     68
     69        $this->show_autoupdates = wp_is_auto_update_enabled_for_type( 'theme' ) &&
     70            ! $this->is_site_themes && current_user_can( 'update_themes' );
    5971    }
    6072
     
    108120        );
    109121
     122        if ( $this->show_autoupdates ) {
     123            $auto_updates = (array) get_site_option( 'auto_update_themes', array() );
     124
     125            $themes['auto-update-enabled']  = array();
     126            $themes['auto-update-disabled'] = array();
     127        }
     128
    110129        if ( $this->is_site_themes ) {
    111130            $themes_per_page = $this->get_items_per_page( 'site_themes_network_per_page' );
     
    132151            $filter                    = $theme->is_allowed( $allowed_where, $this->site_id ) ? 'enabled' : 'disabled';
    133152            $themes[ $filter ][ $key ] = $themes['all'][ $key ];
     153
     154            if ( $this->show_autoupdates ) {
     155                if ( in_array( $key, $auto_updates, true ) ) {
     156                    $themes['auto-update-enabled'][ $key ] = $themes['all'][ $key ];
     157                } else {
     158                    $themes['auto-update-disabled'][ $key ] = $themes['all'][ $key ];
     159                }
     160            }
    134161        }
    135162
     
    258285     */
    259286    public function get_columns() {
    260         return array(
     287        $columns = array(
    261288            'cb'          => '<input type="checkbox" />',
    262289            'name'        => __( 'Theme' ),
    263290            'description' => __( 'Description' ),
    264291        );
     292
     293        if ( $this->show_autoupdates ) {
     294            $columns['auto-updates'] = __( 'Automatic Updates' );
     295        }
     296
     297        return $columns;
    265298    }
    266299
     
    345378                    );
    346379                    break;
     380                case 'auto-update-enabled':
     381                    /* translators: %s: Number of themes. */
     382                    $text = _n(
     383                        'Auto-updates Enabled <span class="count">(%s)</span>',
     384                        'Auto-updates Enabled <span class="count">(%s)</span>',
     385                        $count
     386                    );
     387                    break;
     388                case 'auto-update-disabled':
     389                    /* translators: %s: Number of themes. */
     390                    $text = _n(
     391                        'Auto-updates Disabled <span class="count">(%s)</span>',
     392                        'Auto-updates Disabled <span class="count">(%s)</span>',
     393                        $count
     394                    );
     395                    break;
    347396            }
    348397
     
    389438            }
    390439        }
     440
     441        if ( $this->show_autoupdates ) {
     442            if ( 'auto-update-enabled' !== $status ) {
     443                $actions['enable-auto-update-selected'] = __( 'Enable Auto-updates' );
     444            }
     445
     446            if ( 'auto-update-disabled' !== $status ) {
     447                $actions['disable-auto-update-selected'] = __( 'Disable Auto-updates' );
     448            }
     449        }
     450
    391451        return $actions;
    392452    }
     
    641701
    642702    /**
     703     * Handles the auto-updates column output.
     704     *
     705     * @since 5.5.0
     706     *
     707     * @global string $status
     708     * @global int  $page
     709     *
     710     * @param WP_Theme $theme The current WP_Theme object.
     711     */
     712    public function column_autoupdates( $theme ) {
     713        global $status, $page;
     714
     715        static $auto_updates, $available_updates;
     716
     717        if ( ! $auto_updates ) {
     718            $auto_updates = (array) get_site_option( 'auto_update_themes', array() );
     719        }
     720        if ( ! $available_updates ) {
     721            $available_updates = get_site_transient( 'update_themes' );
     722        }
     723
     724        $stylesheet = $theme->get_stylesheet();
     725
     726        if ( in_array( $stylesheet, $auto_updates, true ) ) {
     727            $text       = __( 'Disable auto-updates' );
     728            $action     = 'disable';
     729            $time_class = '';
     730        } else {
     731            $text       = __( 'Enable auto-updates' );
     732            $action     = 'enable';
     733            $time_class = ' hidden';
     734        }
     735
     736        $query_args = array(
     737            'action'       => "{$action}-auto-update",
     738            'theme'        => $stylesheet,
     739            'paged'        => $page,
     740            'theme_status' => $status,
     741        );
     742
     743        $url = add_query_arg( $query_args, 'themes.php' );
     744
     745        printf(
     746            '<a href="%s" class="toggle-auto-update" data-wp-action="%s">',
     747            wp_nonce_url( $url, 'updates' ),
     748            $action
     749        );
     750
     751        echo '<span class="dashicons dashicons-update spin hidden"></span>';
     752        echo '<span class="label">' . $text . '</span>';
     753        echo '</a>';
     754
     755        $available_updates = get_site_transient( 'update_themes' );
     756        if ( isset( $available_updates->response[ $stylesheet ] ) ) {
     757            printf(
     758                '<div class="auto-update-time%s">%s</div>',
     759                $time_class,
     760                wp_get_auto_update_message()
     761            );
     762        }
     763        echo '<div class="auto-updates-error inline notice error hidden"><p></p></div>';
     764    }
     765
     766    /**
    643767     * Handles default column output.
    644768     *
     
    722846                    break;
    723847
     848                case 'auto-updates':
     849                    echo "<td class='column-auto-updates{$extra_classes}'>";
     850
     851                    $this->column_autoupdates( $item );
     852
     853                    echo '</td>';
     854                    break;
    724855                default:
    725856                    echo "<td class='$column_name column-$column_name{$extra_classes}'>";
  • trunk/src/wp-admin/includes/class-wp-plugins-list-table.php

    r47808 r47835  
    1717 */
    1818class WP_Plugins_List_Table extends WP_List_Table {
     19    /**
     20     * Whether to show the auto-updates UI.
     21     *
     22     * @since 5.5.0
     23     *
     24     * @var bool True if auto-updates UI is to be shown, false otherwise.
     25     */
     26    protected $show_autoupdates = true;
    1927
    2028    /**
     
    4048        );
    4149
    42         $status_whitelist = array( 'active', 'inactive', 'recently_activated', 'upgrade', 'mustuse', 'dropins', 'search', 'paused' );
     50        $status_whitelist = array( 'active', 'inactive', 'recently_activated', 'upgrade', 'mustuse', 'dropins', 'search', 'paused', 'auto-update-enabled', 'auto-update-disabled' );
    4351
    4452        $status = 'all';
     
    5260
    5361        $page = $this->get_pagenum();
     62
     63        $this->show_autoupdates = wp_is_auto_update_enabled_for_type( 'plugin' ) &&
     64            current_user_can( 'update_plugins' ) &&
     65            ( ! is_multisite() || $this->screen->in_admin( 'network' ) );
    5466    }
    5567
     
    104116            'paused'             => array(),
    105117        );
     118        if ( $this->show_autoupdates ) {
     119            $auto_updates = (array) get_site_option( 'auto_update_plugins', array() );
     120
     121            $plugins['auto-update-enabled']  = array();
     122            $plugins['auto-update-disabled'] = array();
     123        }
    106124
    107125        $screen = $this->screen;
     
    234252                $plugins['inactive'][ $plugin_file ] = $plugin_data;
    235253            }
     254
     255            if ( $this->show_autoupdates ) {
     256                if ( in_array( $plugin_file, $auto_updates, true ) ) {
     257                    $plugins['auto-update-enabled'][ $plugin_file ] = $plugins['all'][ $plugin_file ];
     258                } else {
     259                    $plugins['auto-update-disabled'][ $plugin_file ] = $plugins['all'][ $plugin_file ];
     260                }
     261            }
    236262        }
    237263
     
    400426        global $status;
    401427
    402         return array(
     428        $columns = array(
    403429            'cb'          => ! in_array( $status, array( 'mustuse', 'dropins' ), true ) ? '<input type="checkbox" />' : '',
    404430            'name'        => __( 'Plugin' ),
    405431            'description' => __( 'Description' ),
    406432        );
     433
     434        if ( $this->show_autoupdates ) {
     435            $columns['auto-updates'] = __( 'Automatic Updates' );
     436        }
     437
     438        return $columns;
    407439    }
    408440
     
    494526                    );
    495527                    break;
     528                case 'auto-update-enabled':
     529                    /* translators: %s: Number of plugins. */
     530                    $text = _n(
     531                        'Auto-updates Enabled <span class="count">(%s)</span>',
     532                        'Auto-updates Enabled <span class="count">(%s)</span>',
     533                        $count
     534                    );
     535                    break;
     536                case 'auto-update-disabled':
     537                    /* translators: %s: Number of plugins. */
     538                    $text = _n(
     539                        'Auto-updates Disabled <span class="count">(%s)</span>',
     540                        'Auto-updates Disabled <span class="count">(%s)</span>',
     541                        $count
     542                    );
     543                    break;
    496544            }
    497545
     
    533581            if ( current_user_can( 'delete_plugins' ) && ( 'active' !== $status ) ) {
    534582                $actions['delete-selected'] = __( 'Delete' );
     583            }
     584
     585            if ( $this->show_autoupdates ) {
     586                if ( 'auto-update-enabled' !== $status ) {
     587                    $actions['enable-auto-update-selected'] = __( 'Enable Auto-updates' );
     588                }
     589                if ( 'auto-update-disabled' !== $status ) {
     590                    $actions['disable-auto-update-selected'] = __( 'Disable Auto-updates' );
     591                }
    535592            }
    536593        }
     
    882939
    883940        list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
     941
     942        $auto_updates      = (array) get_site_option( 'auto_update_plugins', array() );
     943        $available_updates = get_site_transient( 'update_plugins' );
    884944
    885945        foreach ( $columns as $column_name => $column_display_name ) {
     
    9751035                    echo '</td>';
    9761036                    break;
     1037                case 'auto-updates':
     1038                    if ( ! $this->show_autoupdates ) {
     1039                        break;
     1040                    }
     1041
     1042                    echo "<td class='column-auto-updates{$extra_classes}'>";
     1043
     1044                    if ( in_array( $plugin_file, $auto_updates, true ) ) {
     1045                        $text       = __( 'Disable auto-updates' );
     1046                        $action     = 'disable';
     1047                        $time_class = '';
     1048                    } else {
     1049                        $text       = __( 'Enable auto-updates' );
     1050                        $action     = 'enable';
     1051                        $time_class = ' hidden';
     1052                    }
     1053
     1054                    $query_args = array(
     1055                        'action'        => "{$action}-auto-update",
     1056                        'plugin'        => $plugin_file,
     1057                        'paged'         => $page,
     1058                        'plugin_status' => $status,
     1059                    );
     1060
     1061                    $url = add_query_arg( $query_args, 'plugins.php' );
     1062
     1063                    printf(
     1064                        '<a href="%s" class="toggle-auto-update" data-wp-action="%s">',
     1065                        wp_nonce_url( $url, 'updates' ),
     1066                        $action
     1067                    );
     1068
     1069                    echo '<span class="dashicons dashicons-update spin hidden"></span>';
     1070                    echo '<span class="label">' . $text . '</span>';
     1071                    echo '</a>';
     1072
     1073                    $available_updates = get_site_transient( 'update_plugins' );
     1074
     1075                    if ( isset( $available_updates->response[ $plugin_file ] ) ) {
     1076                        printf(
     1077                            '<div class="auto-update-time%s">%s</div>',
     1078                            $time_class,
     1079                            wp_get_auto_update_message()
     1080                        );
     1081                    }
     1082
     1083                    echo '<div class="inline notice error hidden"><p></p></div>';
     1084                    echo '</td>';
     1085
     1086                    break;
    9771087                default:
    9781088                    $classes = "$column_name column-$column_name $class";
     
    10011111         *
    10021112         * @since 2.3.0
     1113         * @since 5.5.0 Added 'Auto-updates Enabled' and 'Auto-updates Disabled' `$status`.
    10031114         *
    10041115         * @param string $plugin_file Path to the plugin file relative to the plugins directory.
     
    10061117         * @param string $status      Status of the plugin. Defaults are 'All', 'Active',
    10071118         *                            'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
    1008          *                            'Drop-ins', 'Search', 'Paused'.
     1119         *                            'Drop-ins', 'Search', 'Paused', 'Auto-updates Enabled',
     1120         *                            'Auto-updates Disabled'.
    10091121         */
    10101122        do_action( 'after_plugin_row', $plugin_file, $plugin_data, $status );
     
    10171129         *
    10181130         * @since 2.7.0
     1131         * @since 5.5.0 Added 'Auto-updates Enabled' and 'Auto-updates Disabled' `$status`.
    10191132         *
    10201133         * @param string $plugin_file Path to the plugin file relative to the plugins directory.
     
    10221135         * @param string $status      Status of the plugin. Defaults are 'All', 'Active',
    10231136         *                            'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
    1024          *                            'Drop-ins', 'Search', 'Paused'.
     1137         *                            'Drop-ins', 'Search', 'Paused', 'Auto-updates Enabled',
     1138         *                            'Auto-updates Disabled'.
    10251139         */
    10261140        do_action( "after_plugin_row_{$plugin_file}", $plugin_file, $plugin_data, $status );
  • trunk/src/wp-admin/includes/theme.php

    r47819 r47835  
    661661    $parents = array();
    662662
     663    $auto_updates = (array) get_site_option( 'auto_update_themes', array() );
     664
    663665    foreach ( $themes as $theme ) {
    664666        $slug         = $theme->get_stylesheet();
     
    683685            );
    684686        }
     687
     688        $auto_update        = in_array( $slug, $auto_updates, true );
     689        $auto_update_action = $auto_update ? 'disable-auto-update' : 'enable-auto-update';
    685690
    686691        $prepared_themes[ $slug ] = array(
     
    700705            'hasPackage'    => isset( $updates[ $slug ] ) && ! empty( $updates[ $slug ]['package'] ),
    701706            'update'        => get_theme_update_available( $theme ),
     707            'autoupdate'    => $auto_update,
    702708            'actions'       => array(
    703                 'activate'  => current_user_can( 'switch_themes' ) ? wp_nonce_url( admin_url( 'themes.php?action=activate&amp;stylesheet=' . $encoded_slug ), 'switch-theme_' . $slug ) : null,
    704                 'customize' => $customize_action,
    705                 'delete'    => current_user_can( 'delete_themes' ) ? wp_nonce_url( admin_url( 'themes.php?action=delete&amp;stylesheet=' . $encoded_slug ), 'delete-theme_' . $slug ) : null,
     709                'activate'   => current_user_can( 'switch_themes' ) ? wp_nonce_url( admin_url( 'themes.php?action=activate&amp;stylesheet=' . $encoded_slug ), 'switch-theme_' . $slug ) : null,
     710                'customize'  => $customize_action,
     711                'delete'     => current_user_can( 'delete_themes' ) ? wp_nonce_url( admin_url( 'themes.php?action=delete&amp;stylesheet=' . $encoded_slug ), 'delete-theme_' . $slug ) : null,
     712                'autoupdate' => wp_is_auto_update_enabled_for_type( 'theme' ) && ! is_multisite() && current_user_can( 'update_themes' )
     713                    ? wp_nonce_url( admin_url( 'themes.php?action=' . $auto_update_action . '&amp;stylesheet=' . $encoded_slug ), 'updates' )
     714                    : null,
    706715            ),
    707716        );
  • trunk/src/wp-admin/includes/update.php

    r47808 r47835  
    436436
    437437    /** @var WP_Plugins_List_Table $wp_list_table */
    438     $wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
     438    $wp_list_table = _get_list_table(
     439        'WP_Plugins_List_Table',
     440        array(
     441            'screen' => get_current_screen(),
     442        )
     443    );
    439444
    440445    if ( is_network_admin() || ! is_multisite() ) {
     
    934939    <?php
    935940}
     941
     942/**
     943 * Checks whether auto-updates are enabled.
     944 *
     945 * @since 5.5.0
     946 *
     947 * @param string $type    The type of update being checked: 'theme' or 'plugin'.
     948 * @return bool True if auto-updates are enabled for `$type`, false otherwise.
     949 */
     950function wp_is_auto_update_enabled_for_type( $type ) {
     951    switch ( $type ) {
     952        case 'plugin':
     953            /**
     954             * Filters whether plugins manual auto-update is enabled.
     955             *
     956             * @since 5.5.0
     957             *
     958             * @param bool $enabled True if plugins auto-update is enabled, false otherwise.
     959             */
     960            return apply_filters( 'wp_plugins_auto_update_enabled', true );
     961        case 'theme':
     962            /**
     963             * Filters whether plugins manual auto-update is enabled.
     964             *
     965             * @since 5.5.0
     966             *
     967             * @param bool True if themes auto-update is enabled, false otherwise.
     968             */
     969            return apply_filters( 'wp_themes_auto_update_enabled', true );
     970    }
     971
     972    return false;
     973}
     974
     975/**
     976 * Determines the appropriate update message to be displayed.
     977 *
     978 * @since 5.5.0
     979 *
     980 * @return string The update message to be shown.
     981 */
     982function wp_get_auto_update_message() {
     983    $next_update_time = wp_next_scheduled( 'wp_version_check' );
     984
     985    // Check if event exists.
     986    if ( false === $next_update_time ) {
     987        return __( 'There may be a problem with WP-Cron. Automatic update not scheduled.' );
     988    }
     989
     990    // See if cron is disabled
     991    $cron_disabled = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
     992    if ( $cron_disabled ) {
     993        return __( 'WP-Cron is disabled. Automatic updates not available.' );
     994    }
     995
     996    $time_to_next_update = human_time_diff( intval( $next_update_time ) );
     997
     998    // See if cron is overdue.
     999    $overdue = ( time() - $next_update_time ) > 0;
     1000    if ( $overdue ) {
     1001        return sprintf(
     1002            /* translators: Duration that WP-Cron has been overdue. */
     1003            __( 'There may be a problem with WP-Cron. Automatic update overdue by %s.' ),
     1004            $time_to_next_update
     1005        );
     1006    } else {
     1007        return sprintf(
     1008            /* translators: Time until the next update. */
     1009            __( 'Auto-update scheduled in %s.' ),
     1010            $time_to_next_update
     1011        );
     1012    }
     1013}
  • trunk/src/wp-admin/network/themes.php

    r47198 r47835  
    2323
    2424// Clean up request URI from temporary args for screen options/paging uri's to work as expected.
    25 $temp_args              = array( 'enabled', 'disabled', 'deleted', 'error' );
     25$temp_args = array(
     26    'enabled',
     27    'disabled',
     28    'deleted',
     29    'error',
     30    'enabled-auto-update',
     31    'disabled-auto-update',
     32);
     33
    2634$_SERVER['REQUEST_URI'] = remove_query_arg( $temp_args, $_SERVER['REQUEST_URI'] );
    2735$referer                = remove_query_arg( $temp_args, wp_get_referer() );
     
    124132                $themes_to_delete = count( $themes );
    125133                ?>
    126             <div class="wrap">
    127                 <?php if ( 1 == $themes_to_delete ) : ?>
     134                <div class="wrap">
     135                <?php if ( 1 === $themes_to_delete ) : ?>
    128136                    <h1><?php _e( 'Delete Theme' ); ?></h1>
    129137                    <div class="error"><p><strong><?php _e( 'Caution:' ); ?></strong> <?php _e( 'This theme may be active on other sites in the network.' ); ?></p></div>
     
    146154                    ?>
    147155                    </ul>
    148                 <?php if ( 1 == $themes_to_delete ) : ?>
     156                <?php if ( 1 === $themes_to_delete ) : ?>
    149157                    <p><?php _e( 'Are you sure you want to delete this theme?' ); ?></p>
    150158                <?php else : ?>
     
    155163                    <input type="hidden" name="action" value="delete-selected" />
    156164                    <?php
     165
    157166                    foreach ( (array) $themes as $theme ) {
    158167                        echo '<input type="hidden" name="checked[]" value="' . esc_attr( $theme ) . '" />';
    159168                    }
    160169
    161                         wp_nonce_field( 'bulk-themes' );
    162 
    163                     if ( 1 == $themes_to_delete ) {
     170                    wp_nonce_field( 'bulk-themes' );
     171
     172                    if ( 1 === $themes_to_delete ) {
    164173                        submit_button( __( 'Yes, delete this theme' ), '', 'submit', false );
    165174                    } else {
    166175                        submit_button( __( 'Yes, delete these themes' ), '', 'submit', false );
    167176                    }
     177
    168178                    ?>
    169179                </form>
    170                 <?php
    171                 $referer = wp_get_referer();
    172                 ?>
     180                <?php $referer = wp_get_referer(); ?>
    173181                <form method="post" action="<?php echo $referer ? esc_url( $referer ) : ''; ?>" style="display:inline;">
    174182                    <?php submit_button( __( 'No, return me to the theme list' ), '', 'submit', false ); ?>
    175183                </form>
    176             </div>
     184                </div>
    177185                <?php
     186
    178187                require_once ABSPATH . 'wp-admin/admin-footer.php';
    179188                exit;
     
    209218            );
    210219            exit;
     220        case 'enable-auto-update':
     221        case 'disable-auto-update':
     222        case 'enable-auto-update-selected':
     223        case 'disable-auto-update-selected':
     224            if ( ! ( current_user_can( 'update_themes' ) && wp_is_auto_update_enabled_for_type( 'theme' ) ) ) {
     225                wp_die( __( 'Sorry, you are not allowed to change themes automatic update settings.' ) );
     226            }
     227
     228            if ( 'enable-auto-update' === $action || 'disable-auto-update' === $action ) {
     229                check_admin_referer( 'updates' );
     230            } else {
     231                if ( empty( $_POST['checked'] ) ) {
     232                    // Nothing to do.
     233                    wp_safe_redirect( add_query_arg( 'error', 'none', $referer ) );
     234                    exit;
     235                }
     236
     237                check_admin_referer( 'bulk-themes' );
     238            }
     239
     240            $auto_updates = (array) get_site_option( 'auto_update_themes', array() );
     241
     242            if ( 'enable-auto-update' === $action ) {
     243                $auto_updates[] = $_GET['theme'];
     244                $auto_updates   = array_unique( $auto_updates );
     245                $referer        = add_query_arg( 'enabled-auto-update', 1, $referer );
     246            } elseif ( 'disable-auto-update' === $action ) {
     247                $auto_updates = array_diff( $auto_updates, array( $_GET['theme'] ) );
     248                $referer      = add_query_arg( 'disabled-auto-update', 1, $referer );
     249            } else {
     250                // Bulk enable/disable.
     251                $themes = (array) wp_unslash( $_POST['checked'] );
     252
     253                if ( 'enable-auto-update-selected' === $action ) {
     254                    $auto_updates = array_merge( $auto_updates, $themes );
     255                    $auto_updates = array_unique( $auto_updates );
     256                    $referer      = add_query_arg( 'enabled-auto-update', count( $themes ), $referer );
     257                } else {
     258                    $auto_updates = array_diff( $auto_updates, $themes );
     259                    $referer      = add_query_arg( 'disabled-auto-update', count( $themes ), $referer );
     260                }
     261            }
     262
     263            $all_items = wp_get_themes();
     264
     265            // Remove themes that don't exist or have been deleted since the option was last updated.
     266            $auto_updates = array_intersect( $auto_updates, array_keys( $all_items ) );
     267
     268            update_site_option( 'auto_update_themes', $auto_updates );
     269
     270            wp_safe_redirect( $referer );
     271            exit;
    211272        default:
    212273            $themes = isset( $_POST['checked'] ) ? (array) $_POST['checked'] : array();
     
    285346if ( isset( $_GET['enabled'] ) ) {
    286347    $enabled = absint( $_GET['enabled'] );
    287     if ( 1 == $enabled ) {
     348    if ( 1 === $enabled ) {
    288349        $message = __( 'Theme enabled.' );
    289350    } else {
     
    294355} elseif ( isset( $_GET['disabled'] ) ) {
    295356    $disabled = absint( $_GET['disabled'] );
    296     if ( 1 == $disabled ) {
     357    if ( 1 === $disabled ) {
    297358        $message = __( 'Theme disabled.' );
    298359    } else {
     
    303364} elseif ( isset( $_GET['deleted'] ) ) {
    304365    $deleted = absint( $_GET['deleted'] );
    305     if ( 1 == $deleted ) {
     366    if ( 1 === $deleted ) {
    306367        $message = __( 'Theme deleted.' );
    307368    } else {
     
    310371    }
    311372    echo '<div id="message" class="updated notice is-dismissible"><p>' . sprintf( $message, number_format_i18n( $deleted ) ) . '</p></div>';
    312 } elseif ( isset( $_GET['error'] ) && 'none' == $_GET['error'] ) {
     373} elseif ( isset( $_GET['enabled-auto-update'] ) ) {
     374    $enabled = absint( $_GET['enabled-auto-update'] );
     375    if ( 1 === $enabled ) {
     376        $message = __( 'Theme will be auto-updated.' );
     377    } else {
     378        /* translators: %s: Number of themes. */
     379        $message = _n( '%s theme will be auto-updated.', '%s themes will be auto-updated.', $enabled );
     380    }
     381    echo '<div id="message" class="updated notice is-dismissible"><p>' . sprintf( $message, number_format_i18n( $enabled ) ) . '</p></div>';
     382} elseif ( isset( $_GET['disabled-auto-update'] ) ) {
     383    $disabled = absint( $_GET['disabled-auto-update'] );
     384    if ( 1 === $disabled ) {
     385        $message = __( 'Theme will no longer be auto-updated.' );
     386    } else {
     387        /* translators: %s: Number of themes. */
     388        $message = _n( '%s theme will no longer be auto-updated.', '%s themes will no longer be auto-updated.', $disabled );
     389    }
     390    echo '<div id="message" class="updated notice is-dismissible"><p>' . sprintf( $message, number_format_i18n( $disabled ) ) . '</p></div>';
     391} elseif ( isset( $_GET['error'] ) && 'none' === $_GET['error'] ) {
    313392    echo '<div id="message" class="error notice is-dismissible"><p>' . __( 'No theme selected.' ) . '</p></div>';
    314 } elseif ( isset( $_GET['error'] ) && 'main' == $_GET['error'] ) {
     393} elseif ( isset( $_GET['error'] ) && 'main' === $_GET['error'] ) {
    315394    echo '<div class="error notice is-dismissible"><p>' . __( 'You cannot delete a theme while it is active on the main site.' ) . '</p></div>';
    316395}
     
    325404$wp_list_table->views();
    326405
    327 if ( 'broken' == $status ) {
     406if ( 'broken' === $status ) {
    328407    echo '<p class="clear">' . __( 'The following themes are installed but incomplete.' ) . '</p>';
    329408}
  • trunk/src/wp-admin/plugins.php

    r47808 r47835  
    2323
    2424// Clean up request URI from temporary args for screen options/paging uri's to work as expected.
    25 $_SERVER['REQUEST_URI'] = remove_query_arg( array( 'error', 'deleted', 'activate', 'activate-multi', 'deactivate', 'deactivate-multi', '_error_nonce' ), $_SERVER['REQUEST_URI'] );
     25$query_args_to_remove = array(
     26    'error',
     27    'deleted',
     28    'activate',
     29    'activate-multi',
     30    'deactivate',
     31    'deactivate-multi',
     32    'enabled-auto-update',
     33    'disabled-auto-update',
     34    'enabled-auto-update-multi',
     35    'disabled-auto-update-multi',
     36    '_error_nonce',
     37);
     38
     39$_SERVER['REQUEST_URI'] = remove_query_arg( $query_args_to_remove, $_SERVER['REQUEST_URI'] );
    2640
    2741wp_enqueue_script( 'updates' );
     
    285299                wp_enqueue_script( 'jquery' );
    286300                require_once ABSPATH . 'wp-admin/admin-header.php';
     301
    287302                ?>
    288             <div class="wrap">
     303                <div class="wrap">
    289304                <?php
    290                     $plugin_info              = array();
    291                     $have_non_network_plugins = false;
     305
     306                $plugin_info              = array();
     307                $have_non_network_plugins = false;
     308
    292309                foreach ( (array) $plugins as $plugin ) {
    293310                    $plugin_slug = dirname( $plugin );
     
    316333                    }
    317334                }
    318                     $plugins_to_delete = count( $plugin_info );
     335
     336                $plugins_to_delete = count( $plugin_info );
     337
    319338                ?>
    320                 <?php if ( 1 == $plugins_to_delete ) : ?>
     339                <?php if ( 1 === $plugins_to_delete ) : ?>
    321340                    <h1><?php _e( 'Delete Plugin' ); ?></h1>
    322341                    <?php if ( $have_non_network_plugins && is_network_admin() ) : ?>
     
    333352                    <ul class="ul-disc">
    334353                        <?php
     354
    335355                        $data_to_delete = false;
     356
    336357                        foreach ( $plugin_info as $plugin ) {
    337358                            if ( $plugin['is_uninstallable'] ) {
     
    344365                            }
    345366                        }
     367
    346368                        ?>
    347369                    </ul>
    348370                <p>
    349371                <?php
     372
    350373                if ( $data_to_delete ) {
    351374                    _e( 'Are you sure you want to delete these files and data?' );
     
    353376                    _e( 'Are you sure you want to delete these files?' );
    354377                }
     378
    355379                ?>
    356380                </p>
     
    359383                    <input type="hidden" name="action" value="delete-selected" />
    360384                    <?php
     385
    361386                    foreach ( (array) $plugins as $plugin ) {
    362387                        echo '<input type="hidden" name="checked[]" value="' . esc_attr( $plugin ) . '" />';
    363388                    }
     389
    364390                    ?>
    365391                    <?php wp_nonce_field( 'bulk-plugins' ); ?>
     
    367393                </form>
    368394                <?php
     395
    369396                $referer = wp_get_referer();
     397
    370398                ?>
    371399                <form method="post" action="<?php echo $referer ? esc_url( $referer ) : ''; ?>" style="display:inline;">
    372400                    <?php submit_button( __( 'No, return me to the plugin list' ), '', 'submit', false ); ?>
    373401                </form>
    374             </div>
     402                </div>
    375403                <?php
     404
    376405                require_once ABSPATH . 'wp-admin/admin-footer.php';
    377406                exit;
     
    386415            wp_redirect( self_admin_url( "plugins.php?deleted=$plugins_to_delete&plugin_status=$status&paged=$page&s=$s" ) );
    387416            exit;
    388 
    389417        case 'clear-recent-list':
    390418            if ( ! is_network_admin() ) {
     
    393421                update_site_option( 'recently_activated', array() );
    394422            }
     423
    395424            break;
    396 
    397425        case 'resume':
    398426            if ( is_multisite() ) {
     
    414442            wp_redirect( self_admin_url( "plugins.php?resume=true&plugin_status=$status&paged=$page&s=$s" ) );
    415443            exit;
    416 
     444        case 'enable-auto-update':
     445        case 'disable-auto-update':
     446        case 'enable-auto-update-selected':
     447        case 'disable-auto-update-selected':
     448            if ( ! current_user_can( 'update_plugins' ) || ! wp_is_auto_update_enabled_for_type( 'plugin' ) ) {
     449                wp_die( __( 'Sorry, you are not allowed to manage plugins automatic updates.' ) );
     450            }
     451
     452            if ( is_multisite() && ! is_network_admin() ) {
     453                wp_die( __( 'Please connect to your network admin to manage plugins automatic updates.' ) );
     454            }
     455
     456            $redirect = self_admin_url( "plugins.php?plugin_status={$status}&paged={$page}&s={$s}" );
     457
     458            if ( 'enable-auto-update' === $action || 'disable-auto-update' === $action ) {
     459                if ( empty( $plugin ) ) {
     460                    wp_redirect( $redirect );
     461                    exit;
     462                }
     463
     464                check_admin_referer( 'updates' );
     465            } else {
     466                if ( empty( $_POST['checked'] ) ) {
     467                    wp_redirect( $redirect );
     468                    exit;
     469                }
     470
     471                check_admin_referer( 'bulk-plugins' );
     472            }
     473
     474            $auto_updates = (array) get_site_option( 'auto_update_plugins', array() );
     475
     476            if ( 'enable-auto-update' === $action ) {
     477                $auto_updates[] = $plugin;
     478                $auto_updates   = array_unique( $auto_updates );
     479                $redirect       = add_query_arg( array( 'enabled-auto-update' => 'true' ), $redirect );
     480            } elseif ( 'disable-auto-update' === $action ) {
     481                $auto_updates = array_diff( $auto_updates, array( $plugin ) );
     482                $redirect     = add_query_arg( array( 'disabled-auto-update' => 'true' ), $redirect );
     483            } else {
     484                $plugins = (array) wp_unslash( $_POST['checked'] );
     485
     486                if ( 'enable-auto-update-selected' === $action ) {
     487                    $new_auto_updates = array_merge( $auto_updates, $plugins );
     488                    $new_auto_updates = array_unique( $new_auto_updates );
     489                    $query_args       = array( 'enabled-auto-update-multi' => 'true' );
     490                } else {
     491                    $new_auto_updates = array_diff( $auto_updates, $plugins );
     492                    $query_args       = array( 'disabled-auto-update-multi' => 'true' );
     493                }
     494
     495                // Return early if all selected plugins already have auto-updates enabled or disabled.
     496                // Must use non-strict comparison, so that array order is not treated as significant.
     497                if ( $new_auto_updates == $auto_updates ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
     498                    wp_redirect( $redirect );
     499                    exit;
     500                }
     501
     502                $auto_updates = $new_auto_updates;
     503                $redirect     = add_query_arg( $query_args, $redirect );
     504            }
     505
     506            /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
     507            $all_items = apply_filters( 'all_plugins', get_plugins() );
     508
     509            // Remove plugins that don't exist or have been deleted since the option was last updated.
     510            $auto_updates = array_intersect( $auto_updates, array_keys( $all_items ) );
     511
     512            update_site_option( 'auto_update_plugins', $auto_updates );
     513
     514            wp_redirect( $redirect );
     515            exit;
    417516        default:
    418517            if ( isset( $_POST['checked'] ) ) {
     
    499598    }
    500599}
    501 ?>
    502 
    503 <?php
     600
    504601if ( isset( $_GET['error'] ) ) :
    505602
     
    522619        $errmsg = __( 'Plugin could not be activated because it triggered a <strong>fatal error</strong>.' );
    523620    }
     621
    524622    ?>
    525623    <div id="message" class="error"><p><?php echo $errmsg; ?></p>
    526624    <?php
     625
    527626    if ( ! isset( $_GET['main'] ) && ! isset( $_GET['charsout'] ) && wp_verify_nonce( $_GET['_error_nonce'], 'plugin-activation-error_' . $plugin ) ) {
    528627        $iframe_url = add_query_arg(
     
    534633            admin_url( 'plugins.php' )
    535634        );
     635
    536636        ?>
    537     <iframe style="border:0" width="100%" height="70px" src="<?php echo esc_url( $iframe_url ); ?>"></iframe>
     637        <iframe style="border:0" width="100%" height="70px" src="<?php echo esc_url( $iframe_url ); ?>"></iframe>
    538638        <?php
    539639    }
     640
    540641    ?>
    541642    </div>
    542643    <?php
    543644elseif ( isset( $_GET['deleted'] ) ) :
    544         $delete_result = get_transient( 'plugins_delete_result_' . $user_ID );
    545         // Delete it once we're done.
    546         delete_transient( 'plugins_delete_result_' . $user_ID );
     645    $delete_result = get_transient( 'plugins_delete_result_' . $user_ID );
     646    // Delete it once we're done.
     647    delete_transient( 'plugins_delete_result_' . $user_ID );
    547648
    548649    if ( is_wp_error( $delete_result ) ) :
     
    563664            <p>
    564665                <?php
    565                 if ( 1 == (int) $_GET['deleted'] ) {
     666                if ( 1 === (int) $_GET['deleted'] ) {
    566667                    _e( 'The selected plugin has been deleted.' );
    567668                } else {
     
    571672            </p>
    572673        </div>
    573         <?php endif; ?>
     674    <?php endif; ?>
    574675<?php elseif ( isset( $_GET['activate'] ) ) : ?>
    575676    <div id="message" class="updated notice is-dismissible"><p><?php _e( 'Plugin activated.' ); ?></p></div>
     
    584685<?php elseif ( isset( $_GET['resume'] ) ) : ?>
    585686    <div id="message" class="updated notice is-dismissible"><p><?php _e( 'Plugin resumed.' ); ?></p></div>
     687<?php elseif ( isset( $_GET['enabled-auto-update'] ) ) : ?>
     688    <div id="message" class="updated notice is-dismissible"><p><?php _e( 'Plugin will be auto-updated.' ); ?></p></div>
     689<?php elseif ( isset( $_GET['disabled-auto-update'] ) ) : ?>
     690    <div id="message" class="updated notice is-dismissible"><p><?php _e( 'Plugin will no longer be auto-updated.' ); ?></p></div>
     691<?php elseif ( isset( $_GET['enabled-auto-update-multi'] ) ) : ?>
     692    <div id="message" class="updated notice is-dismissible"><p><?php _e( 'Selected plugins will be auto-updated.' ); ?></p></div>
     693<?php elseif ( isset( $_GET['disabled-auto-update-multi'] ) ) : ?>
     694    <div id="message" class="updated notice is-dismissible"><p><?php _e( 'Selected plugins will no longer be auto-updated.' ); ?></p></div>
    586695<?php endif; ?>
    587696
  • trunk/src/wp-admin/themes.php

    r47816 r47835  
    8181            wp_redirect( admin_url( 'themes.php?deleted=true' ) );
    8282        }
     83        exit;
     84    } elseif ( 'enable-auto-update' === $_GET['action'] ) {
     85        if ( ! ( current_user_can( 'update_themes' ) && wp_is_auto_update_enabled_for_type( 'theme' ) ) ) {
     86            wp_die( __( 'Sorry, you are not allowed to enable themes automatic updates.' ) );
     87        }
     88
     89        check_admin_referer( 'updates' );
     90
     91        $all_items    = wp_get_themes();
     92        $auto_updates = (array) get_site_option( 'auto_update_themes', array() );
     93
     94        $auto_updates[] = $_GET['stylesheet'];
     95        $auto_updates   = array_unique( $auto_updates );
     96        // Remove themes that have been deleted since the site option was last updated.
     97        $auto_updates = array_intersect( $auto_updates, array_keys( $all_items ) );
     98
     99        update_site_option( 'auto_update_themes', $auto_updates );
     100
     101        wp_redirect( admin_url( 'themes.php?enabled-auto-update=true' ) );
     102
     103        exit;
     104    } elseif ( 'disable-auto-update' === $_GET['action'] ) {
     105        if ( ! ( current_user_can( 'update_themes' ) && wp_is_auto_update_enabled_for_type( 'theme' ) ) ) {
     106            wp_die( __( 'Sorry, you are not allowed to disable themes automatic updates.' ) );
     107        }
     108
     109        check_admin_referer( 'updates' );
     110
     111        $all_items    = wp_get_themes();
     112        $auto_updates = (array) get_site_option( 'auto_update_themes', array() );
     113
     114        $auto_updates = array_diff( $auto_updates, array( $_GET['stylesheet'] ) );
     115        // Remove themes that have been deleted since the site option was last updated.
     116        $auto_updates = array_intersect( $auto_updates, array_keys( $all_items ) );
     117
     118        update_site_option( 'auto_update_themes', $auto_updates );
     119
     120        wp_redirect( admin_url( 'themes.php?disabled-auto-update=true' ) );
     121
    83122        exit;
    84123    }
     
    228267    ?>
    229268    <div id="message6" class="error"><p><?php _e( 'Theme could not be resumed because it triggered a <strong>fatal error</strong>.' ); ?></p></div>
     269    <?php
     270} elseif ( isset( $_GET['enabled-auto-update'] ) ) {
     271    ?>
     272    <div id="message7" class="updated notice is-dismissible"><p><?php _e( 'Theme will be auto-updated.' ); ?></p></div>
     273    <?php
     274} elseif ( isset( $_GET['disabled-auto-update'] ) ) {
     275    ?>
     276    <div id="message8" class="updated notice is-dismissible"><p><?php _e( 'Theme will no longer be auto-updated.' ); ?></p></div>
    230277    <?php
    231278}
     
    582629                </p>
    583630
     631                <# if ( data.actions.autoupdate ) { #>
     632                <p class="theme-autoupdate">
     633                <# if ( data.autoupdate ) { #>
     634                    <a href="{{{ data.actions.autoupdate }}}" class="toggle-auto-update" data-slug="{{ data.id }}" data-wp-action="disable">
     635                        <span class="dashicons dashicons-update spin hidden"></span>
     636                        <span class="label"><?php _e( 'Disable auto-updates' ); ?></span>
     637                    </a>
     638                <# } else { #>
     639                    <a href="{{{ data.actions.autoupdate }}}" class="toggle-auto-update" data-slug="{{ data.id }}" data-wp-action="enable">
     640                        <span class="dashicons dashicons-update spin hidden"></span>
     641                        <span class="label"><?php _e( 'Enable auto-updates' ); ?></span>
     642                    </a>
     643                <# } #>
     644                <# if ( data.hasUpdate ) { #>
     645                    <# if ( data.autoupdate) { #>
     646                    <span class="auto-update-time"><br /><?php echo wp_get_auto_update_message(); ?></span>
     647                    <# } else { #>
     648                    <span class="auto-update-time hidden"><br /><?php echo wp_get_auto_update_message(); ?></span>
     649                    <# } #>
     650                <# } #>
     651                    <span class="auto-updates-error hidden"><p></p></span>
     652                </p>
     653                <# } #>
     654
    584655                <# if ( data.hasUpdate ) { #>
    585656                <div class="notice notice-warning notice-alt notice-large">
  • trunk/src/wp-admin/update-core.php

    r47808 r47835  
    329329    <tbody class="plugins">
    330330    <?php
     331
     332    $auto_updates = array();
     333    if ( wp_is_auto_update_enabled_for_type( 'plugin' ) ) {
     334        $auto_updates       = (array) get_site_option( 'auto_update_plugins', array() );
     335        $auto_update_notice = ' | ' . wp_get_auto_update_message();
     336    }
     337
    331338    foreach ( (array) $plugins as $plugin_file => $plugin_data ) {
    332339        $plugin_data = (object) _get_plugin_data_markup_translate( $plugin_file, (array) $plugin_data, false, true );
     
    420427            );
    421428            echo ' ' . $details . $compat . $upgrade_notice;
     429            if ( in_array( $plugin_file, $auto_updates, true ) ) {
     430                echo $auto_update_notice;
     431            }
    422432            ?>
    423433        </p></td>
     
    479489    <tbody class="plugins">
    480490    <?php
     491    $auto_updates = array();
     492    if ( wp_is_auto_update_enabled_for_type( 'theme' ) ) {
     493        $auto_updates       = (array) get_site_option( 'auto_update_themes', array() );
     494        $auto_update_notice = ' | ' . wp_get_auto_update_message();
     495    }
     496
    481497    foreach ( $themes as $stylesheet => $theme ) {
    482498        $checkbox_id = 'checkbox_' . md5( $theme->get( 'Name' ) );
     499
    483500        ?>
    484501    <tr>
     
    502519                $theme->update['new_version']
    503520            );
     521            if ( in_array( $stylesheet, $auto_updates, true ) ) {
     522                echo $auto_update_notice;
     523            }
    504524            ?>
    505525        </p></td>
  • trunk/src/wp-includes/script-loader.php

    r47771 r47835  
    15211521                    'pluginsFound'             => __( 'Number of plugins found: %d' ),
    15221522                    'noPluginsFound'           => __( 'No plugins found. Try a different search.' ),
     1523                    'autoUpdatesEnable'        => __( 'Enable auto-updates' ),
     1524                    'autoUpdatesEnabling'      => __( 'Enabling...' ),
     1525                    'autoUpdatesEnabled'       => __( 'Auto-updates enabled' ),
     1526                    'autoUpdatesDisable'       => __( 'Disable auto-updates' ),
     1527                    'autoUpdatesDisabling'     => __( 'Disabling...' ),
     1528                    'autoUpdatesDisabled'      => __( 'Auto-updates disabled' ),
     1529                    'autoUpdatesError'         => __( 'The request could not be completed.' ),
    15231530                ),
    15241531            )
Note: See TracChangeset for help on using the changeset viewer.