Skip to content

Commit eb79d7f

Browse files
committed
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
1 parent cdd8b92 commit eb79d7f

17 files changed

Lines changed: 1142 additions & 45 deletions

src/js/_enqueues/wp/updates.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@
409409
*
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.
414415
* @param {string} response.slug Slug of the plugin to be updated.
@@ -431,6 +432,9 @@
431432
// Update the version number in the row.
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' )
436440
.removeClass( 'updating-message' )
@@ -969,6 +973,7 @@
969973
* Updates the UI appropriately after a successful theme update.
970974
*
971975
* @since 4.6.0
976+
* @since 5.5.0 Auto-update "time to next update" text cleared.
972977
*
973978
* @param {object} response
974979
* @param {string} response.slug Slug of the theme to be updated.
@@ -1002,12 +1007,16 @@
10021007
// Update the version number in the row.
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' ) );
10071015

10081016
// Focus on Customize button after updating.
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();
10131022
}
@@ -2461,5 +2470,144 @@
24612470
* @since 4.2.0
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 );

src/wp-admin/admin-ajax.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@
139139
'health-check-background-updates',
140140
'health-check-loopback-requests',
141141
'health-check-get-sizes',
142+
'toggle-auto-updates',
142143
);
143144

144145
// Deprecated.

src/wp-admin/css/common.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1524,7 +1524,9 @@ div.error {
15241524
.updating-message p:before,
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
}
15301532

src/wp-admin/css/list-tables.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,10 @@ ul.cat-checklist {
12361236
width: 85px;
12371237
}
12381238

1239+
.plugins .column-auto-updates {
1240+
width: 14.2em;
1241+
}
1242+
12391243
.plugins .inactive .plugin-title strong {
12401244
font-weight: 400;
12411245
}

src/wp-admin/css/themes.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,8 @@ body.folded .theme-browser ~ .theme-overlay .theme-wrap {
679679
line-height: inherit;
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
}
685686

src/wp-admin/includes/ajax-actions.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4567,6 +4567,9 @@ function wp_ajax_delete_plugin() {
45674567
function wp_ajax_search_plugins() {
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 ) {
45724575
set_current_screen( $pagenow );
@@ -5267,3 +5270,73 @@ function wp_ajax_health_check_get_sizes() {
52675270
function wp_ajax_rest_nonce() {
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+
*/
5279+
function 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+
}

0 commit comments

Comments
 (0)