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( ` +
+
+
0%
+
+
+
+

${ fileObj.name }

+
+
+
+ ` ); +} + +function buildComparisonTable( fileObj, data ) { + return ` + + + `; +} +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( + `

Installed

` + ); + 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' );