diff --git a/Gruntfile.js b/Gruntfile.js
index 216dbfae64945..eb1dabd45302c 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/theme-upload.js' ]: [ './src/js/_enqueues/admin/theme-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' ],
@@ -967,6 +968,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/theme-upload.js': 'src/js/_enqueues/admin/theme-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/theme-upload.js b/src/js/_enqueues/admin/theme-upload.js
new file mode 100644
index 0000000000000..1cab0cbb00493
--- /dev/null
+++ b/src/js/_enqueues/admin/theme-upload.js
@@ -0,0 +1,290 @@
+/**
+ * @file Functionality for the theme upload screen.
+ *
+ * @output wp-admin/js/theme-upload.js
+ */
+
+/* global theme_upload_intl, plupload, themeUploaderInit, ajaxurl, upload_theme_nonce, cancel_theme_overwrite_nonce, customize_url*/
+
+function themeFileQueued( fileObj ) {
+ jQuery( '#theme-upload-list' ).append( `
+
+ ` );
+}
+
+function buildComparisonTable( fileObj, data ) {
+ return `
+
+
+
+
+ |
+ ${ theme_upload_intl.current } |
+ ${ theme_upload_intl.uploaded } |
+
+
+ ${ theme_upload_intl.theme_name } |
+ ${ data.Name[ 0 ] } |
+ ${ data.Name[ 1 ] } |
+
+
+ ${ theme_upload_intl.version } |
+ ${ data.Version[ 0 ] } |
+ ${ data.Version[ 1 ] } |
+
+
+ ${ theme_upload_intl.author } |
+ ${ data.Author[ 0 ] } |
+ ${ data.Author[ 1 ] } |
+
+
+ ${ theme_upload_intl.required_wordpress_version } |
+ ${ data.RequiresWP[ 0 ] } |
+ ${ data.RequiresWP[ 1 ] } |
+
+
+ ${ theme_upload_intl.required_php_version } |
+ ${ data.RequiresPHP[ 0 ] } |
+ ${ data.RequiresPHP[ 1 ] } |
+
+
+
+
+
+ `;
+}
+function themeUploadProgress( up, file ) {
+ const item = jQuery( '#theme-item-' + file.id );
+
+ jQuery( '.bar', item ).width( ( 200 * file.loaded ) / file.size );
+ if ( 100 === file.percent ) {
+ jQuery( '.percent', item ).html( theme_upload_intl.processing + '...' );
+ } else {
+ jQuery( '.percent', item ).html( file.percent + '%' );
+ }
+}
+
+function themeUploadSuccess( fileObj, serverData ) {
+ const item = jQuery( '#theme-item-' + fileObj.id );
+ const action_selector = jQuery( '.theme-actions', item );
+
+ const response_json = JSON.parse( serverData );
+ const data = response_json.data;
+
+ jQuery( '.theme-author', item ).text( 'By ' + data.theme.Author );
+
+ jQuery( '.theme-name', item ).text( data.theme.Name );
+ jQuery( '.theme-screenshot', item ).html( '' ).removeClass( 'uploading' );
+
+ item.append(
+ ``
+ );
+ if ( 'can_override' === data.successCode ) {
+ if ( data.comparisonMessage.Downgrade ) {
+ action_selector.append(
+ ``
+ );
+ } else {
+ action_selector.append(
+ ``
+ );
+ }
+ action_selector.append(
+ buildComparisonTable( fileObj, data.comparisonMessage )
+ );
+ action_selector.append(
+ ``
+ );
+
+ item.append(
+ `${theme_upload_intl.details}`
+ );
+ jQuery( '.button.overwrite-theme' )
+ .unbind()
+ .click( function () {
+ const button = jQuery( this );
+ const attachment_id = button.data( 'attachment' );
+ overwriteTheme( 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' );
+ cancelOverwriteTheme( file_id, attachment_id, button );
+ } );
+ } else {
+ if ( data.screenshot ) {
+ jQuery( '.theme-screenshot', item ).html(
+ `
`
+ );
+ }
+ // The activate nonce must be in the format 'switch-theme_' . $_GET['stylesheet']. It is a bit tricky to generate this dynamically via javascript. So I will comment this out till I find a switable solution
+ // action_selector.append(`${ theme_upload_intl.activate } `);
+
+ action_selector.append(
+ `${theme_upload_intl.live_preview}`
+ );
+ }
+}
+
+function themeUploadError( fileObj, errorCode, message ) {
+ if ( message ) {
+ const item = jQuery( '#theme-item-' + fileObj.id );
+ const selector = jQuery(
+ '.theme-screenshot',
+ item
+ );
+
+ const responseJSON = JSON.parse( message );
+ if (
+ responseJSON &&
+ responseJSON.data &&
+ responseJSON.data.errorMessage
+ ) {
+ selector.html(
+ `${ responseJSON.data.errorMessage }
`
+ );
+ } else {
+ selector.html(
+ `${ theme_upload_intl.generic_error }
`
+ );
+ }
+ }
+}
+
+function overwriteTheme( attachment_id, button ) {
+ const formData = new FormData();
+ formData.append( '_wpnonce', upload_theme_nonce );
+ formData.append( 'action', 'upload-theme' );
+ button.prop( 'disabled', true );
+ button.text( theme_upload_intl.processing + '...' );
+ const cancel_button = button.parent().find('.cancel-overwrite');
+ cancel_button.prop( 'disabled', true );
+
+
+ jQuery.ajax( {
+ type: 'POST',
+ url: ajaxurl + '?package=' + attachment_id + '&overwrite=update-theme',
+ 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( theme_upload_intl.updated );
+ },
+ error: function () {
+ button.prop( 'disabled', false );
+ cancel_button.prop( 'disabled', false );
+ button.text( theme_upload_intl.activation_failed );
+ },
+ } );
+}
+
+function cancelOverwriteTheme( file_id, attachment_id, button ) {
+ const formData = new FormData();
+ formData.append( '_wpnonce', cancel_theme_overwrite_nonce );
+ formData.append( 'action', 'cancel-theme-overwrite' );
+ button.prop( 'disabled', true );
+ button.text( theme_upload_intl.processing + '...' );
+ const overwrite_button = button.parent().find('.overwrite-theme');
+ 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( '#theme-item-' + file_id ).remove();
+ },
+ error: function () {
+ button.prop( 'disabled', false );
+ overwrite_button.prop( 'disabled', false );
+ button.text( theme_upload_intl.cancel_failed );
+ },
+ } );
+}
+jQuery( function ( $ ) {
+ const uploader_init = function () {
+ const uploader = new plupload.Uploader( themeUploaderInit );
+
+ 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;
+ }
+
+ themeFileQueued( file );
+ } );
+
+ up.refresh();
+ up.start();
+ } );
+
+ uploader.bind( 'UploadProgress', function ( up, file ) {
+ themeUploadProgress( up, file );
+ } );
+
+ uploader.bind( 'Error', function ( up, error ) {
+ themeUploadError( error.file, error.code, error.response );
+ up.refresh();
+ } );
+
+ uploader.bind( 'FileUploaded', function ( up, file, response ) {
+ themeUploadSuccess( file, response.response );
+ } );
+ };
+
+ uploader_init();
+} );
diff --git a/src/wp-admin/admin-ajax.php b/src/wp-admin/admin-ajax.php
index 3ad60f95766e3..18d2b9fe867a9 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-theme',
+ 'cancel-theme-overwrite',
'activate-plugin',
'update-plugin',
'crop-image',
diff --git a/src/wp-admin/css/themes.css b/src/wp-admin/css/themes.css
index 7d4bb848a129a..c657a95398638 100644
--- a/src/wp-admin/css/themes.css
+++ b/src/wp-admin/css/themes.css
@@ -146,6 +146,12 @@ body.js .theme-browser.search-loading {
transition: opacity 0.2s ease-in-out;
}
+.theme-browser .theme .theme-screenshot.uploading {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
.theme-browser .theme:hover .theme-screenshot,
.theme-browser .theme.focus .theme-screenshot {
background: #fff;
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..ae5d1c4d4b224 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,83 @@ public function get_error_messages() {
return implode( ', ', $messages );
}
+ /**
+ * Checks if the theme can be overwritten and returns an array of changes for overwriting a theme on upload.
+ *
+ * @since 6.9.0
+ *
+ * @return bool|array Whether the theme can be overwritten and an array of changes returned.
+ */
+ public function can_overwrite_theme() {
+ if ( ! is_wp_error( $this->result ) || 'folder_exists' !== $this->result->get_error_code() ) {
+ return false;
+ }
+
+ $folder = $this->result->get_error_data( 'folder_exists' );
+ $folder = rtrim( $folder, '/' );
+
+ $current_theme_data = false;
+ $all_themes = wp_get_themes( array( 'errors' => null ) );
+
+ foreach ( $all_themes as $theme ) {
+ $stylesheet_dir = wp_normalize_path( $theme->get_stylesheet_directory() );
+
+ if ( rtrim( $stylesheet_dir, '/' ) !== $folder ) {
+ continue;
+ }
+
+ $current_theme_data = $theme;
+ }
+
+ $new_theme_data = $this->upgrader->new_theme_data;
+
+ if ( ! $current_theme_data || ! $new_theme_data ) {
+ return false;
+ }
+
+ $rows = array(
+ 'Downgrade' => version_compare( $current_theme_data['Version'], $new_theme_data['Version'], '>' ),
+ );
+
+ $is_invalid_parent = false;
+ if ( ! empty( $new_theme_data['Template'] ) ) {
+ $is_invalid_parent = ! in_array( $new_theme_data['Template'], array_keys( $all_themes ), true );
+ }
+
+ $fields = array(
+ 'Name',
+ 'Version',
+ 'Author',
+ 'RequiresWP',
+ 'RequiresPHP',
+ 'Template',
+ );
+
+ $is_same_theme = true; // Let's consider only these rows.
+
+ foreach ( $fields as $field ) {
+ $old_value = $current_theme_data->display( $field, false );
+ $old_value = $old_value ? (string) $old_value : '-';
+
+ $new_value = ! empty( $new_theme_data[ $field ] ) ? (string) $new_theme_data[ $field ] : '-';
+
+ if ( $old_value === $new_value && '-' === $new_value && 'Template' === $field ) {
+ continue;
+ }
+
+ $is_same_theme = $is_same_theme && ( $old_value === $new_value );
+
+ if ( 'Template' === $field && $is_invalid_parent ) {
+ $new_value .= ' ' . __( '(not found)' );
+ }
+
+ $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/theme-install.php b/src/wp-admin/includes/theme-install.php
index 6faedc94fb921..228bfd7719d22 100644
--- a/src/wp-admin/includes/theme-install.php
+++ b/src/wp-admin/includes/theme-install.php
@@ -193,8 +193,65 @@ function install_themes_dashboard() {
* @since 2.8.0
*/
function install_themes_upload() {
+ wp_enqueue_script( 'plupload-handlers' );
+ wp_enqueue_script( 'theme-upload' );
+ add_thickbox();
+ $max_upload_size = wp_max_upload_size();
+ if ( ! $max_upload_size ) {
+ $max_upload_size = 0;
+ }
+ $upload_theme_nonce = wp_create_nonce( 'upload-theme' );
+ $cancel_theme_overwrite_nonce = wp_create_nonce( 'theme-upload-cancel-overwrite' );
+ $plupload_init = array(
+ 'browse_button' => 'plupload-browse-button',
+ 'container' => 'plupload-upload-ui',
+ 'drop_element' => 'drag-drop-area',
+ 'file_data_name' => 'themezip',
+ '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-theme',
+ '_wpnonce' => $upload_theme_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( 'theme-upload', "/wp-admin/js/theme-upload$suffix.js", array( 'jquery', 'jquery-ui-core', 'thickbox' ), false, 1 );
+ $scripts->set_translations( 'theme-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' );