diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 90691e6a40..5e86d792dd 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -3653,28 +3653,29 @@ function wp_ajax_update_plugin() { ) ); } - $plugin = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) ); - $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); + $plugin = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) ); $status = array( 'update' => 'plugin', - 'plugin' => $plugin, 'slug' => sanitize_key( wp_unslash( $_POST['slug'] ) ), - 'pluginName' => $plugin_data['Name'], 'oldVersion' => '', 'newVersion' => '', ); + if ( ! current_user_can( 'update_plugins' ) || 0 !== validate_file( $plugin ) ) { + $status['errorMessage'] = __( 'Sorry, you are not allowed to update plugins for this site.' ); + wp_send_json_error( $status ); + } + + $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); + $status['plugin'] = $plugin; + $status['pluginName'] = $plugin_data['Name']; + if ( $plugin_data['Version'] ) { /* translators: %s: Plugin version */ $status['oldVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] ); } - if ( ! current_user_can( 'update_plugins' ) ) { - $status['errorMessage'] = __( 'Sorry, you are not allowed to update plugins for this site.' ); - wp_send_json_error( $status ); - } - include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; wp_update_plugins(); @@ -3748,24 +3749,29 @@ function wp_ajax_delete_plugin() { check_ajax_referer( 'updates' ); if ( empty( $_POST['slug'] ) || empty( $_POST['plugin'] ) ) { - wp_send_json_error( array( 'errorCode' => 'no_plugin_specified' ) ); + wp_send_json_error( array( + 'slug' => '', + 'errorCode' => 'no_plugin_specified', + 'errorMessage' => __( 'No plugin specified.' ), + ) ); } - $plugin = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) ); - $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); + $plugin = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) ); $status = array( - 'delete' => 'plugin', - 'slug' => sanitize_key( wp_unslash( $_POST['slug'] ) ), - 'plugin' => $plugin, - 'pluginName' => $plugin_data['Name'], + 'delete' => 'plugin', + 'slug' => sanitize_key( wp_unslash( $_POST['slug'] ) ), ); - if ( ! current_user_can( 'delete_plugins' ) ) { + if ( ! current_user_can( 'delete_plugins' ) || 0 !== validate_file( $plugin ) ) { $status['errorMessage'] = __( 'Sorry, you are not allowed to delete plugins for this site.' ); wp_send_json_error( $status ); } + $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); + $status['plugin'] = $plugin; + $status['pluginName'] = $plugin_data['Name']; + if ( is_plugin_active( $plugin ) ) { $status['errorMessage'] = __( 'You cannot delete a plugin while it is active on the main site.' ); wp_send_json_error( $status ); diff --git a/src/wp-admin/js/updates.js b/src/wp-admin/js/updates.js index accf0382f7..b58a2267e0 100644 --- a/src/wp-admin/js/updates.js +++ b/src/wp-admin/js/updates.js @@ -447,7 +447,11 @@ errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage ); if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { - $message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' ); + if ( response.plugin ) { + $message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' ); + } else { + $message = $( 'tr[data-slug="' + response.slug + '"]' ).find( '.update-message' ); + } $message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage ); } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { $card = $( '.plugin-card-' + response.slug ) @@ -458,9 +462,13 @@ } ) ); $card.find( '.update-now' ) - .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) ) .text( wp.updates.l10n.updateFailedShort ).removeClass( 'updating-message' ); + if ( response.pluginName ) { + $card.find( '.update-now' ) + .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) ); + } + $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() { // Use same delay as the total duration of the notice fadeTo + slideUp animation. @@ -814,14 +822,21 @@ * @param {string} response.errorMessage The error that occurred. */ wp.updates.deletePluginError = function( response ) { - var $plugin = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' ), + var $plugin, $pluginUpdateRow, pluginUpdateRow = wp.template( 'item-update-row' ), - $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' ), noticeContent = wp.updates.adminNotice( { className: 'update-message notice-error notice-alt', message: response.errorMessage } ); + if ( response.plugin ) { + $plugin = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' ); + $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' ); + } else { + $plugin = $( 'tr.inactive[data-slug="' + response.slug + '"]' ); + $pluginUpdateRow = $plugin.siblings( '[data-slug="' + response.slug + '"]' ); + } + if ( ! wp.updates.isValidResponse( response, 'delete' ) ) { return; } @@ -835,7 +850,7 @@ $plugin.addClass( 'update' ).after( pluginUpdateRow( { slug: response.slug, - plugin: response.plugin, + plugin: response.plugin || response.slug, colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, content: noticeContent } ) diff --git a/tests/phpunit/includes/testcase-ajax.php b/tests/phpunit/includes/testcase-ajax.php index c4466aa3a2..787e6bf5fd 100644 --- a/tests/phpunit/includes/testcase-ajax.php +++ b/tests/phpunit/includes/testcase-ajax.php @@ -18,13 +18,13 @@ abstract class WP_Ajax_UnitTestCase extends WP_UnitTestCase { /** * Last AJAX response. This is set via echo -or- wp_die. - * @var type + * @var string */ protected $_last_response = ''; /** * List of ajax actions called via POST - * @var type + * @var array */ protected static $_core_actions_get = array( 'fetch-list', 'ajax-tag-search', 'wp-compression-test', 'imgedit-preview', 'oembed-cache', @@ -39,7 +39,7 @@ abstract class WP_Ajax_UnitTestCase extends WP_UnitTestCase { /** * List of ajax actions called via GET - * @var type + * @var array */ protected static $_core_actions_post = array( 'oembed_cache', 'image-editor', 'delete-comment', 'delete-tag', 'delete-link', @@ -53,7 +53,9 @@ abstract class WP_Ajax_UnitTestCase extends WP_UnitTestCase { 'wp-remove-post-lock', 'dismiss-wp-pointer', 'send-attachment-to-editor', 'heartbeat', 'nopriv_heartbeat', 'get-revision-diffs', 'save-user-color-scheme', 'update-widget', 'query-themes', 'parse-embed', 'set-attachment-thumbnail', 'parse-media-shortcode', 'destroy-sessions', 'install-plugin', 'update-plugin', 'press-this-save-post', - 'press-this-add-category', 'crop-image', 'generate-password', + 'press-this-add-category', 'crop-image', 'generate-password', 'save-wporg-username', 'delete-plugin', + 'search-plugins', 'search-install-plugins', 'activate-plugin', 'update-theme', 'delete-theme', + 'install-theme', 'get-post-thumbnail-html', ); public static function setUpBeforeClass() { diff --git a/tests/phpunit/tests/ajax/DeletePlugin.php b/tests/phpunit/tests/ajax/DeletePlugin.php new file mode 100644 index 0000000000..4ab04f7c28 --- /dev/null +++ b/tests/phpunit/tests/ajax/DeletePlugin.php @@ -0,0 +1,158 @@ +_handleAjax( 'delete-plugin' ); + } + + public function test_missing_plugin() { + $_POST['_ajax_nonce'] = wp_create_nonce( 'updates' ); + $_POST['slug'] = 'foo'; + + // Make the request + try { + $this->_handleAjax( 'delete-plugin' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Get the response. + $response = json_decode( $this->_last_response, true ); + + $expected = array( + 'success' => false, + 'data' => array( + 'slug' => '', + 'errorCode' => 'no_plugin_specified', + 'errorMessage' => 'No plugin specified.', + ), + ); + + $this->assertEqualSets( $expected, $response ); + } + + public function test_missing_slug() { + $_POST['_ajax_nonce'] = wp_create_nonce( 'updates' ); + $_POST['plugin'] = 'foo/bar.php'; + + // Make the request + try { + $this->_handleAjax( 'delete-plugin' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Get the response. + $response = json_decode( $this->_last_response, true ); + + $expected = array( + 'success' => false, + 'data' => array( + 'slug' => '', + 'errorCode' => 'no_plugin_specified', + 'errorMessage' => 'No plugin specified.', + ), + ); + + $this->assertEqualSets( $expected, $response ); + } + + public function test_missing_capability() { + $_POST['_ajax_nonce'] = wp_create_nonce( 'updates' ); + $_POST['plugin'] = 'foo/bar.php'; + $_POST['slug'] = 'foo'; + + // Make the request + try { + $this->_handleAjax( 'delete-plugin' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Get the response. + $response = json_decode( $this->_last_response, true ); + + $expected = array( + 'success' => false, + 'data' => array( + 'delete' => 'plugin', + 'slug' => 'foo', + 'errorMessage' => 'Sorry, you are not allowed to delete plugins for this site.', + ), + ); + + $this->assertEqualSets( $expected, $response ); + } + + public function test_invalid_file() { + $this->_setRole( 'administrator' ); + + $_POST['_ajax_nonce'] = wp_create_nonce( 'updates' ); + $_POST['plugin'] = '../foo/bar.php'; + $_POST['slug'] = 'foo'; + + // Make the request + try { + $this->_handleAjax( 'delete-plugin' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Get the response. + $response = json_decode( $this->_last_response, true ); + + $expected = array( + 'success' => false, + 'data' => array( + 'delete' => 'plugin', + 'slug' => 'foo', + 'errorMessage' => 'Sorry, you are not allowed to delete plugins for this site.', + ), + ); + + $this->assertEqualSets( $expected, $response ); + } + + public function test_delete_plugin() { + $this->_setRole( 'administrator' ); + + $_POST['_ajax_nonce'] = wp_create_nonce( 'updates' ); + $_POST['plugin'] = 'foo.php'; + $_POST['slug'] = 'foo'; + + // Make the request + try { + $this->_handleAjax( 'delete-plugin' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Get the response. + $response = json_decode( $this->_last_response, true ); + + $expected = array( + 'success' => true, + 'data' => array( + 'delete' => 'plugin', + 'slug' => 'foo', + 'plugin' => 'foo.php', + 'pluginName' => '', + ), + ); + + $this->assertEqualSets( $expected, $response ); + } +} diff --git a/tests/phpunit/tests/ajax/UpdatePlugin.php b/tests/phpunit/tests/ajax/UpdatePlugin.php new file mode 100644 index 0000000000..b75603fd36 --- /dev/null +++ b/tests/phpunit/tests/ajax/UpdatePlugin.php @@ -0,0 +1,169 @@ +_handleAjax( 'update-plugin' ); + } + + public function test_missing_plugin() { + $_POST['_ajax_nonce'] = wp_create_nonce( 'updates' ); + $_POST['slug'] = 'foo'; + + // Make the request + try { + $this->_handleAjax( 'update-plugin' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Get the response. + $response = json_decode( $this->_last_response, true ); + + $expected = array( + 'success' => false, + 'data' => array( + 'slug' => '', + 'errorCode' => 'no_plugin_specified', + 'errorMessage' => 'No plugin specified.', + ), + ); + + $this->assertEqualSets( $expected, $response ); + } + + public function test_missing_slug() { + $_POST['_ajax_nonce'] = wp_create_nonce( 'updates' ); + $_POST['plugin'] = 'foo/bar.php'; + + // Make the request + try { + $this->_handleAjax( 'update-plugin' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Get the response. + $response = json_decode( $this->_last_response, true ); + + $expected = array( + 'success' => false, + 'data' => array( + 'slug' => '', + 'errorCode' => 'no_plugin_specified', + 'errorMessage' => 'No plugin specified.', + ), + ); + + $this->assertEqualSets( $expected, $response ); + } + + public function test_missing_capability() { + $_POST['_ajax_nonce'] = wp_create_nonce( 'updates' ); + $_POST['plugin'] = 'foo/bar.php'; + $_POST['slug'] = 'foo'; + + // Make the request + try { + $this->_handleAjax( 'update-plugin' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Get the response. + $response = json_decode( $this->_last_response, true ); + + $expected = array( + 'success' => false, + 'data' => array( + 'update' => 'plugin', + 'slug' => 'foo', + 'errorMessage' => 'Sorry, you are not allowed to update plugins for this site.', + 'oldVersion' => '', + 'newVersion' => '', + ), + ); + + $this->assertEqualSets( $expected, $response ); + } + + public function test_invalid_file() { + $this->_setRole( 'administrator' ); + + $_POST['_ajax_nonce'] = wp_create_nonce( 'updates' ); + $_POST['plugin'] = '../foo/bar.php'; + $_POST['slug'] = 'foo'; + + // Make the request + try { + $this->_handleAjax( 'update-plugin' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Get the response. + $response = json_decode( $this->_last_response, true ); + + $expected = array( + 'success' => false, + 'data' => array( + 'update' => 'plugin', + 'slug' => 'foo', + 'errorMessage' => 'Sorry, you are not allowed to update plugins for this site.', + 'oldVersion' => '', + 'newVersion' => '', + ), + ); + + $this->assertEqualSets( $expected, $response ); + } + + public function test_update_plugin() { + $this->_setRole( 'administrator' ); + + $_POST['_ajax_nonce'] = wp_create_nonce( 'updates' ); + $_POST['plugin'] = 'hello.php'; + $_POST['slug'] = 'hello-dolly'; + + // Make the request + try { + // Prevent wp_update_plugins() from running + wp_installing( true ); + $this->_handleAjax( 'update-plugin' ); + wp_installing( false ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Get the response. + $response = json_decode( $this->_last_response, true ); + + $expected = array( + 'success' => false, + 'data' => array( + 'update' => 'plugin', + 'slug' => 'hello-dolly', + 'plugin' => 'hello.php', + 'pluginName' => 'Hello Dolly', + 'errorMessage' => 'Plugin update failed.', + 'oldVersion' => 'Version 1.6', + 'newVersion' => '', + 'debug' => array( 'The plugin is at the latest version.' ), + ), + ); + + $this->assertEqualSets( $expected, $response ); + } +}