diff --git a/Gruntfile.js b/Gruntfile.js
index a13c6e49526c5..6f40c55d42164 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -360,6 +360,7 @@ module.exports = function(grunt) {
[ WORKING_DIR + 'wp-admin/js/password-strength-meter.js' ]: [ './src/js/_enqueues/wp/password-strength-meter.js' ],
[ WORKING_DIR + 'wp-admin/js/password-toggle.js' ]: [ './src/js/_enqueues/admin/password-toggle.js' ],
[ WORKING_DIR + 'wp-admin/js/plugin-install.js' ]: [ './src/js/_enqueues/admin/plugin-install.js' ],
+ [ WORKING_DIR + 'wp-admin/js/plugin-upload.js' ]: [ './src/js/_enqueues/admin/plugin-upload.js' ],
[ WORKING_DIR + 'wp-admin/js/post.js' ]: [ './src/js/_enqueues/admin/post.js' ],
[ WORKING_DIR + 'wp-admin/js/postbox.js' ]: [ './src/js/_enqueues/admin/postbox.js' ],
[ WORKING_DIR + 'wp-admin/js/revisions.js' ]: [ './src/js/_enqueues/wp/revisions.js' ],
@@ -957,6 +958,7 @@ module.exports = function(grunt) {
'src/wp-admin/js/nav-menu.js': 'src/js/_enqueues/lib/nav-menu.js',
'src/wp-admin/js/password-strength-meter.js': 'src/js/_enqueues/wp/password-strength-meter.js',
'src/wp-admin/js/plugin-install.js': 'src/js/_enqueues/admin/plugin-install.js',
+ 'src/wp-admin/js/plugin-upload.js': 'src/js/_enqueues/admin/plugin-upload.js',
'src/wp-admin/js/post.js': 'src/js/_enqueues/admin/post.js',
'src/wp-admin/js/postbox.js': 'src/js/_enqueues/admin/postbox.js',
'src/wp-admin/js/revisions.js': 'src/js/_enqueues/wp/revisions.js',
diff --git a/src/js/_enqueues/admin/plugin-upload.js b/src/js/_enqueues/admin/plugin-upload.js
new file mode 100644
index 0000000000000..6481ebaee1737
--- /dev/null
+++ b/src/js/_enqueues/admin/plugin-upload.js
@@ -0,0 +1,321 @@
+/**
+ * @file Functionality for the plugin upload screen.
+ *
+ * @output wp-admin/js/plugin-upload.js
+ */
+
+/* global plugin_upload_intl, plupload, pluginUploaderInit, ajaxurl, upload_plugin_nonce, activate_plugin_nonce, cancel_overwrite_nonce */
+
+function pluginFileQueued( fileObj ) {
+ jQuery( '#plugin-upload-list' ).append( `
+
+ ` );
+}
+
+function buildComparisonTable( fileObj, data ) {
+ return `
+
+
+
+
+ |
+ ${ plugin_upload_intl.current } |
+ ${ plugin_upload_intl.uploaded } |
+
+
+ | ${ plugin_upload_intl.plugin_name } |
+ ${ data.Name[ 0 ] } |
+ ${ data.Name[ 1 ] } |
+
+
+ | ${ plugin_upload_intl.version } |
+ ${ data.Version[ 0 ] } |
+ ${ data.Version[ 1 ] } |
+
+
+ | ${ plugin_upload_intl.author } |
+ ${ data.Author[ 0 ] } |
+ ${ data.Author[ 1 ] } |
+
+
+ | ${ plugin_upload_intl.required_wordpress_version } |
+ ${ data.RequiresWP[ 0 ] } |
+ ${ data.RequiresWP[ 1 ] } |
+
+
+ | ${ plugin_upload_intl.required_php_version } |
+ ${ data.RequiresPHP[ 0 ] } |
+ ${ data.RequiresPHP[ 1 ] } |
+
+
+
+
+
+ `;
+}
+function pluginUploadProgress( up, file ) {
+ const item = jQuery( '#plugin-item-' + file.id );
+
+ jQuery( '.bar', item ).width( ( 200 * file.loaded ) / file.size );
+ if ( 100 == file.percent ) {
+ jQuery( '.percent', item ).html(
+ plugin_upload_intl.processing + '...'
+ );
+ } else {
+ jQuery( '.percent', item ).html( file.percent + '%' );
+ }
+}
+
+function pluginUploadSuccess( fileObj, serverData ) {
+ const item = jQuery( '#plugin-item-' + fileObj.id );
+ const action_selector = jQuery( '.plugin-action-buttons', item );
+ const description_selector = jQuery(
+ '.column-description .description',
+ item
+ );
+ const response_json = JSON.parse( serverData );
+ const data = response_json.data;
+ description_selector.text( data.plugin.Description );
+ jQuery( '.column-description .authors', item ).text(
+ 'By ' + data.plugin.Author
+ );
+ // https://s.w.org/plugins/geopattern-icon/cf7-kofi.svg
+ const temp_slug = data.plugin.Title.toLowerCase().replace( ' ', '-' );
+ jQuery( '.name h3', item )
+ .html(
+ `${ data.plugin.Name }
`
+ )
+ .css( { textAlign: 'left' } );
+ jQuery( '.media-item', item ).hide();
+ if ( 'can_override' === data.successCode ) {
+ if ( data.comparisonMessage.Downgrade ) {
+ action_selector.append(
+ ``
+ );
+ } else {
+ action_selector.append(
+ ``
+ );
+ }
+ action_selector.append(
+ ``
+ );
+ action_selector.append(
+ `${ plugin_upload_intl.more_details }`
+ );
+ description_selector.append(
+ buildComparisonTable( fileObj, data.comparisonMessage )
+ );
+ jQuery( '.button.overwrite-plugin' )
+ .unbind()
+ .click( function () {
+ const button = jQuery( this );
+ const attachment_id = button.data( 'attachment' );
+ overwritePlugin( attachment_id, button );
+ } );
+ jQuery( '.button.cancel-overwrite' )
+ .unbind()
+ .click( function () {
+ const button = jQuery( this );
+ const attachment_id = button.data( 'attachment' );
+ const file_id = button.data( 'file' );
+ cancelOverwritePlugin( file_id, attachment_id, button );
+ } );
+ } else {
+ action_selector.append(
+ ``
+ );
+
+ jQuery( '.button.activate-plugin' )
+ .unbind()
+ .click( function () {
+ const button = jQuery( this );
+ const path = button.data( 'path' );
+ const name = button.data( 'name' );
+ activatePlugin( path, name, button );
+ } );
+ }
+}
+
+function pluginUploadError( fileObj, errorCode, message ) {
+ if ( message ) {
+ const item = jQuery( '#plugin-item-' + fileObj.id );
+ const description_selector = jQuery(
+ '.column-description .description',
+ item
+ );
+
+ jQuery( '.media-item', item ).hide();
+ const responseJSON = JSON.parse( message );
+ if (
+ responseJSON &&
+ responseJSON.data &&
+ responseJSON.data.errorMessage
+ ) {
+ description_selector.html(
+ `${ responseJSON.data.errorMessage }
`
+ );
+ } else {
+ description_selector.html(
+ `${ plugin_upload_intl.generic_error }
`
+ );
+ }
+ }
+}
+
+function overwritePlugin( attachment_id, button ) {
+ const formData = new FormData();
+ formData.append( '_wpnonce', upload_plugin_nonce );
+ formData.append( 'action', 'upload-plugin' );
+ button.prop( 'disabled', true );
+ button.text( plugin_upload_intl.processing + '...' );
+ const cancel_overwrite_button = button.parent().parent().find( '.cancel-overwrite' );
+ cancel_overwrite_button.prop( 'disabled', true );
+ jQuery.ajax( {
+ type: 'POST',
+ url: ajaxurl + '?package=' + attachment_id + '&overwrite=update-plugin',
+ data: formData,
+ processData: false, // Important: tell jQuery not to process the data.
+ contentType: false, // Important: tell jQuery not to set contentType.
+
+ success: function () {
+ button.text( plugin_upload_intl.updated );
+ },
+ error: function () {
+ button.prop( 'disabled', false );
+ cancel_overwrite_button.prop( 'disabled', false );
+ button.text( plugin_upload_intl.activation_failed );
+ },
+ } );
+}
+
+function cancelOverwritePlugin( file_id, attachment_id, button ) {
+ const formData = new FormData();
+ formData.append( '_wpnonce', cancel_overwrite_nonce );
+ formData.append( 'action', 'cancel-plugin-overwrite' );
+ button.prop( 'disabled', true );
+ button.text( plugin_upload_intl.processing + '...' );
+ const overwrite_button = button.parent().parent().find( '.overwrite-plugin' );
+ overwrite_button.prop( 'disabled', true );
+ jQuery.ajax( {
+ type: 'POST',
+ url: ajaxurl + '?package=' + attachment_id,
+ data: formData,
+ processData: false, // Important: tell jQuery not to process the data.
+ contentType: false, // Important: tell jQuery not to set contentType.
+
+ success: function () {
+ // Remove element from the dom.
+ jQuery( '#plugin-item-' + file_id ).remove();
+ },
+ error: function () {
+ button.prop( 'disabled', false );
+ overwrite_button.prop( 'disabled', false );
+ button.text( plugin_upload_intl.cancel_failed );
+ },
+ } );
+}
+function activatePlugin( path, name, button ) {
+ const formData = new FormData();
+ formData.append( 'plugin', path );
+ formData.append( 'name', name );
+ formData.append( 'slug', name.toLowerCase().replace( / /g, '-' ) );
+ formData.append( '_wpnonce', activate_plugin_nonce );
+ formData.append( 'action', 'activate-plugin' );
+ button.prop( 'disabled', true );
+ button.text( plugin_upload_intl.processing + '...' );
+ jQuery.ajax( {
+ type: 'POST',
+ url: ajaxurl,
+ data: formData,
+ processData: false, // Important: tell jQuery not to process the data
+ contentType: false, // Important: tell jQuery not to set contentType
+
+ success: function () {
+ button.text( plugin_upload_intl.activated );
+ },
+ error: function () {
+ button.prop( 'disabled', false );
+ button.text( plugin_upload_intl.failed );
+ },
+ } );
+}
+jQuery( function ( $ ) {
+ const uploader_init = function () {
+ const uploader = new plupload.Uploader( pluginUploaderInit );
+
+ uploader.bind( 'Init', function ( up ) {
+ var uploaddiv = $( '#plupload-upload-ui' );
+
+ if (
+ up.features.dragdrop &&
+ ! $( document.body ).hasClass( 'mobile' )
+ ) {
+ uploaddiv.addClass( 'drag-drop' );
+
+ $( '#drag-drop-area' )
+ .on( 'dragover.wp-uploader', function () {
+ // dragenter doesn't fire right :(
+ uploaddiv.addClass( 'drag-over' );
+ } )
+ .on(
+ 'dragleave.wp-uploader, drop.wp-uploader',
+ function () {
+ uploaddiv.removeClass( 'drag-over' );
+ }
+ );
+ } else {
+ uploaddiv.removeClass( 'drag-drop' );
+ $( '#drag-drop-area' ).off( '.wp-uploader' );
+ }
+
+ if ( up.runtime === 'html4' ) {
+ $( '.upload-flash-bypass' ).hide();
+ }
+ } );
+
+ uploader.bind( 'postinit', function ( up ) {
+ up.refresh();
+ } );
+
+ uploader.init();
+
+ uploader.bind( 'FilesAdded', function ( up, files ) {
+ plupload.each( files, function ( file ) {
+ if ( file.type !== 'application/zip' ) {
+ // Ignore zip files
+ return;
+ }
+
+ pluginFileQueued( file );
+ } );
+
+ up.refresh();
+ up.start();
+ } );
+
+ uploader.bind( 'UploadProgress', function ( up, file ) {
+ pluginUploadProgress( up, file );
+ } );
+
+ uploader.bind( 'Error', function ( up, error ) {
+ pluginUploadError( error.file, error.code, error.response );
+ up.refresh();
+ } );
+
+ uploader.bind( 'FileUploaded', function ( up, file, response ) {
+ pluginUploadSuccess( file, response.response );
+ } );
+ };
+
+ uploader_init();
+} );
diff --git a/src/wp-admin/admin-ajax.php b/src/wp-admin/admin-ajax.php
index 3ad60f95766e3..919fb524a932a 100644
--- a/src/wp-admin/admin-ajax.php
+++ b/src/wp-admin/admin-ajax.php
@@ -117,6 +117,8 @@
'parse-media-shortcode',
'destroy-sessions',
'install-plugin',
+ 'upload-plugin',
+ 'cancel-plugin-overwrite',
'activate-plugin',
'update-plugin',
'crop-image',
diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php
index 1df84f204ecb8..79c5f587f870b 100644
--- a/src/wp-admin/includes/ajax-actions.php
+++ b/src/wp-admin/includes/ajax-actions.php
@@ -4549,6 +4549,105 @@ function wp_ajax_install_plugin() {
wp_send_json_success( $status );
}
+/**
+ * Handles uploading a plugin via AJAX.
+ *
+ * @since 6.9.0
+ *
+ * @see Plugin_Upgrader
+ *
+ * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
+ */
+function wp_ajax_upload_plugin() {
+ check_ajax_referer( 'upload-plugin' );
+
+ if ( ! current_user_can( 'upload_plugins' ) ) {
+ wp_send_json_error( __( 'Sorry, you are not allowed to install plugins on this site.' ) );
+ }
+
+ if ( isset( $_FILES['pluginzip']['name'] ) && ! str_ends_with( strtolower( $_FILES['pluginzip']['name'] ), '.zip' ) ) {
+ wp_send_json_error( __( 'Only .zip archives may be uploaded.' ) );
+ }
+
+ require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
+ $file_upload = new File_Upload_Upgrader( 'pluginzip', 'package' );
+
+ $nonce = 'upload-plugin';
+ $type = 'upload'; // Install plugin type, From Web or an Upload.
+
+ $overwrite = isset( $_GET['overwrite'] ) ? sanitize_text_field( $_GET['overwrite'] ) : '';
+ $overwrite = in_array( $overwrite, array( 'update-plugin', 'downgrade-plugin' ), true ) ? $overwrite : '';
+
+ $skin = new WP_Ajax_Upgrader_Skin( compact( 'type', 'nonce', 'overwrite' ) );
+
+ $upgrader = new Plugin_Upgrader( $skin );
+ $result = $upgrader->install( $file_upload->package, array( 'overwrite_package' => $overwrite ) );
+
+ $status = array();
+ $plugin = $skin->upgrader->new_plugin_data;
+
+ $can_override = $skin->can_overwrite_plugin();
+
+ if ( $can_override ) {
+ $status['successCode'] = 'can_override';
+ $status['comparisonMessage'] = $can_override;
+ $status['plugin'] = $plugin;
+ $status['attachment_id'] = $file_upload->id;
+ wp_send_json_success( $status );
+ }
+ if ( is_wp_error( $result ) ) {
+ $file_upload->cleanup();
+ wp_send_json_error( $result->get_error_message(), 400 );
+ } elseif ( is_wp_error( $skin->result ) ) {
+ $status['errorCode'] = $skin->result->get_error_code();
+ $status['errorMessage'] = $skin->result->get_error_message();
+ $file_upload->cleanup();
+ wp_send_json_error( $status, 400 );
+ } elseif ( $skin->get_errors()->has_errors() ) {
+ $status['errorMessage'] = $skin->get_error_messages();
+ $file_upload->cleanup();
+ wp_send_json_error( $status, 400 );
+ }
+ $status = array(
+ 'successCode' => 'can_activate',
+ 'plugin' => $plugin,
+ 'path' => $skin->upgrader->plugin_info(),
+ );
+
+ wp_send_json_success( $status );
+}
+
+/**
+ * Cancel plugin overwrite via AJAX.
+ *
+ * @since 6.9.0
+ *
+ * @see Plugin_Upgrader
+ *
+ * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
+ */
+function wp_ajax_cancel_plugin_overwrite() {
+ check_admin_referer( 'plugin-upload-cancel-overwrite' );
+
+ if ( ! current_user_can( 'upload_plugins' ) ) {
+ wp_die( __( 'Sorry, you are not allowed to install plugins on this site.' ) );
+ }
+ require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
+
+ // Make sure the attachment still exists, or File_Upload_Upgrader will call wp_die()
+ // that shows a generic "Please select a file" error.
+ if ( ! empty( $_GET['package'] ) ) {
+ $attachment_id = (int) $_GET['package'];
+
+ if ( get_post( $attachment_id ) ) {
+ $file_upload = new File_Upload_Upgrader( 'pluginzip', 'package' );
+ $file_upload->cleanup();
+ }
+ }
+
+ wp_send_json_success();
+}
+
/**
* Handles activating a plugin via AJAX.
*
diff --git a/src/wp-admin/includes/class-wp-ajax-upgrader-skin.php b/src/wp-admin/includes/class-wp-ajax-upgrader-skin.php
index 1ac8e31b43e00..4306454f3637c 100644
--- a/src/wp-admin/includes/class-wp-ajax-upgrader-skin.php
+++ b/src/wp-admin/includes/class-wp-ajax-upgrader-skin.php
@@ -101,6 +101,59 @@ public function get_error_messages() {
return implode( ', ', $messages );
}
+ /**
+ * Checks if the plugin can be overwritten and outputs an array of changes for overwriting a plugin on upload.
+ *
+ * @since 6.9.0
+ *
+ * @return bool|array Whether the plugin can be overwritten and an array of changes returned.
+ */
+ public function can_overwrite_plugin() {
+ if ( ! is_wp_error( $this->result ) || 'folder_exists' !== $this->result->get_error_code() ) {
+ return false;
+ }
+
+ $folder = $this->result->get_error_data( 'folder_exists' );
+ $folder = ltrim( substr( $folder, strlen( WP_PLUGIN_DIR ) ), '/' );
+
+ $current_plugin_data = false;
+ $all_plugins = get_plugins();
+
+ foreach ( $all_plugins as $plugin => $plugin_data ) {
+ if ( strrpos( $plugin, $folder ) !== 0 ) {
+ continue;
+ }
+
+ $current_plugin_data = $plugin_data;
+ }
+
+ $new_plugin_data = $this->upgrader->new_plugin_data;
+
+ if ( ! $current_plugin_data || ! $new_plugin_data ) {
+ return false;
+ }
+
+ $rows = array(
+ 'Downgrade' => version_compare( $current_plugin_data['Version'], $new_plugin_data['Version'], '>' ),
+ );
+
+ $fields = array( 'Name', 'Version', 'Author', 'RequiresWP', 'RequiresPHP' );
+
+ $is_same_plugin = true; // Let's consider only these rows.
+
+ foreach ( $fields as $field ) {
+ $old_value = ! empty( $current_plugin_data[ $field ] ) ? (string) $current_plugin_data[ $field ] : '-';
+ $new_value = ! empty( $new_plugin_data[ $field ] ) ? (string) $new_plugin_data[ $field ] : '-';
+
+ $is_same_plugin = $is_same_plugin && ( $old_value === $new_value );
+
+ $rows[ $field ] = array( wp_strip_all_tags( $old_value ), wp_strip_all_tags( $new_value ) );
+
+ }
+
+ return $rows;
+ }
+
/**
* Stores an error message about the upgrade.
*
diff --git a/src/wp-admin/includes/plugin-install.php b/src/wp-admin/includes/plugin-install.php
index 7e5a04c0a0aa5..665f97e76cc41 100644
--- a/src/wp-admin/includes/plugin-install.php
+++ b/src/wp-admin/includes/plugin-install.php
@@ -340,20 +340,93 @@ function install_search_form( $deprecated = true ) {
* @since 2.8.0
*/
function install_plugins_upload() {
+ wp_enqueue_script( 'plupload-handlers' );
+ wp_enqueue_script( 'plugin-upload' );
+ add_thickbox();
+ $max_upload_size = wp_max_upload_size();
+ if ( ! $max_upload_size ) {
+ $max_upload_size = 0;
+ }
+ $upload_plugin_nonce = wp_create_nonce( 'upload-plugin' );
+ $activate_plugin_nonce = wp_create_nonce( 'updates' );
+ $cancel_overwrite_nonce = wp_create_nonce( 'plugin-upload-cancel-overwrite' );
+ $plupload_init = array(
+ 'browse_button' => 'plupload-browse-button',
+ 'container' => 'plupload-upload-ui',
+ 'drop_element' => 'drag-drop-area',
+ 'file_data_name' => 'pluginzip',
+ 'url' => admin_url( 'admin-ajax.php' ),
+ 'filters' => array(
+ 'max_file_size' => $max_upload_size . 'b',
+ 'prevent_duplicates' => true,
+ 'mime_types' => array( array( 'extensions' => 'zip' ) ),
+ ),
+ 'multipart_params' => array(
+ 'post_id' => 0,
+ 'action' => 'upload-plugin',
+ '_wpnonce' => $upload_plugin_nonce,
+ ),
+ );
?>
add( 'plugin-install', "/wp-admin/js/plugin-install$suffix.js", array( 'jquery', 'jquery-ui-core', 'thickbox' ), false, 1 );
$scripts->set_translations( 'plugin-install' );
+ $scripts->add( 'plugin-upload', "/wp-admin/js/plugin-upload$suffix.js", array( 'jquery', 'jquery-ui-core', 'thickbox' ), false, 1 );
+ $scripts->set_translations( 'plugin-upload' );
+
$scripts->add( 'site-health', "/wp-admin/js/site-health$suffix.js", array( 'clipboard', 'jquery', 'wp-util', 'wp-a11y', 'wp-api-request', 'wp-url', 'wp-i18n', 'wp-hooks' ), false, 1 );
$scripts->set_translations( 'site-health' );