diff --git a/lib/compat/wordpress-6.0/block-template-utils.php b/lib/compat/wordpress-6.0/block-template-utils.php index c03a559af5f317..969e59e2a6094e 100644 --- a/lib/compat/wordpress-6.0/block-template-utils.php +++ b/lib/compat/wordpress-6.0/block-template-utils.php @@ -96,6 +96,19 @@ function gutenberg_generate_block_templates_export_file() { ); } + // Add the theme.json file to the zip. + $zip->addFromString( + 'theme.json', + gutenberg_export_theme_json() + ); + + // Save changes to the zip file. + $zip->close(); + + return $filename; +} + +function gutenberg_export_theme_json() { // Load theme.json into the zip file. $tree = WP_Theme_JSON_Resolver_Gutenberg::get_theme_data( array(), array( 'with_supports' => false ) ); // Merge with user data. @@ -117,16 +130,5 @@ function gutenberg_generate_block_templates_export_file() { $theme_json_encoded = wp_json_encode( $theme_json_raw, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); // Replace 4 spaces with a tab. - $theme_json_tabbed = preg_replace( '~(?:^|\G)\h{4}~m', "\t", $theme_json_encoded ); - - // Add the theme.json file to the zip. - $zip->addFromString( - 'theme.json', - $theme_json_tabbed - ); - - // Save changes to the zip file. - $zip->close(); - - return $filename; + return preg_replace( '~(?:^|\G)\h{4}~m', "\t", $theme_json_encoded ); } diff --git a/lib/compat/wordpress-6.1/class-gutenberg-rest-edit-site-export-controller.php b/lib/compat/wordpress-6.1/class-gutenberg-rest-edit-site-export-controller.php index 2bf6a4cda04bd7..480903cee8acf0 100644 --- a/lib/compat/wordpress-6.1/class-gutenberg-rest-edit-site-export-controller.php +++ b/lib/compat/wordpress-6.1/class-gutenberg-rest-edit-site-export-controller.php @@ -7,8 +7,9 @@ */ /** - * Controller which provides REST endpoint for exporting current templates - * and template parts. + * Controller which provides REST endpoints for exporting current templates + * and template parts either as a zip bundle or as edits to the current enabled + * theme's files. * * @since 5.9.0 * @@ -17,18 +18,14 @@ class Gutenberg_REST_Edit_Site_Export_Controller_6_1 extends Gutenberg_REST_Edit_Site_Export_Controller { - - /** - * Registers the necessary REST API routes. - */ public function register_routes() { register_rest_route( $this->namespace, - '/' . $this->rest_base . '/update_theme', + '/' . $this->rest_base . '/export_to_theme_files', array( array( 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'update_theme' ), + 'callback' => array( $this, 'export_to_theme_files' ), 'permission_callback' => array( $this, 'permissions_check' ), ), ), @@ -39,178 +36,124 @@ public function register_routes() { } /** - * Checks whether a given request has permission to export. - * - * @return WP_Error|bool True if the request has access, or WP_Error object. - */ - public function permissions_check() { - if ( current_user_can( 'edit_theme_options' ) ) { - return true; - } - - return new WP_Error( - 'rest_cannot_export_templates', - __( 'Sorry, you are not allowed to export templates and template parts.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - /** - * Update the files of a block theme with the template customisations - * made in the site editor. + * Update the files of the currently enabled block theme with the template and + * template part customisations and additions made in the site editor. * * @return WP_Error|void */ - public function update_theme() { - if ( is_child_theme() ) { - $this->save_theme_locally( 'current' ); - } - else { - $this->save_theme_locally( 'all' ); - } + public function export_to_theme_files() { + $this->update_template_and_parts_files(); + $this->update_theme_json_file(); $this->clear_user_customizations(); return rest_ensure_response( array( "update_theme" => true ) ); } - function save_theme_locally( $export_type ) { - $this->add_templates_to_local( $export_type ); - $this->add_theme_json_to_local( $export_type ); - } - - function clear_user_customizations() { + protected function update_template_and_parts_files() { - // Clear all values in the user theme.json - $user_custom_post_type_id = WP_Theme_JSON_Resolver::get_user_global_styles_post_id(); - $global_styles_controller = new Gutenberg_REST_Global_Styles_Controller(); - $update_request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' ); - $update_request->set_param( 'id', $user_custom_post_type_id ); - $update_request->set_param( 'settings', [] ); - $update_request->set_param( 'styles', [] ); - $updated_global_styles = $global_styles_controller->update_item( $update_request ); - delete_transient( 'global_styles' ); - delete_transient( 'global_styles_' . get_stylesheet() ); - delete_transient( 'gutenberg_global_styles' ); - delete_transient( 'gutenberg_global_styles_' . get_stylesheet() ); + $user_customisations = $this->get_user_customisations(); - //remove all user templates (they have been saved in the theme) - $templates = gutenberg_get_block_templates(); - $template_parts = gutenberg_get_block_templates( array(), 'wp_template_part' ); - foreach ( $template_parts as $template ) { - if ( $template->source !== 'custom' ) { - continue; - } - wp_delete_post($template->wp_id, true); + foreach ( $user_customisations['templates'] as $template ) { + file_put_contents( + get_template_directory() . '/templates/' . $template->slug . '.html', + $template->content + ); } - foreach ( $templates as $template ) { - if ( $template->source !== 'custom' ) { - continue; - } - wp_delete_post($template->wp_id, true); + foreach ( $user_customisations['template_parts'] as $template_part ) { + file_put_contents( + get_template_directory() . '/parts/' . $template_part->slug . '.html', + $template_part->content + ); } - } - /* - * Filter a template out (return false) based on the export_type expected and the templates origin. - * Templates not filtered out are modified based on the slug information provided and cleaned up - * to have the expected exported value. - */ - function filter_theme_template( $template, $export_type, $path, $old_slug, $new_slug ) { - if ($template->source === 'theme' && $export_type === 'user') { - return false; - } - if ( - $template->source === 'theme' && - $export_type === 'current' && - ! file_exists( $path . $template->slug . '.html' ) - ) { - return false; - } - - $template->content = _remove_theme_attribute_in_block_template_content( $template->content ); - - // NOTE: Dashes are encoded as \u002d in the content that we get (noteably in things like css variables used in templates) - // This replaces that with dashes again. We should consider decoding the entire string but that is proving difficult. - $template->content = str_replace( '\u002d', '-', $template->content ); - - if ( $new_slug ) { - $template->content = str_replace( $old_slug, $new_slug, $template->content ); - } - - return $template; + protected function update_theme_json_file() { + file_put_contents( + get_template_directory() . '/theme.json', + gutenberg_export_theme_json() + ); } - function get_theme_templates( $export_type, $new_slug ) { + protected function get_user_customisations() { - $old_slug = wp_get_theme()->get( 'TextDomain' ); $templates = gutenberg_get_block_templates(); $template_parts = gutenberg_get_block_templates ( array(), 'wp_template_part' ); - $exported_templates = []; - $exported_parts = []; - // build collection of templates/parts in currently activated theme - $templates_paths = get_block_theme_folders(); - $templates_path = get_stylesheet_directory() . '/' . $templates_paths['wp_template'] . '/'; - $parts_path = get_stylesheet_directory() . '/' . $templates_paths['wp_template_part'] . '/'; + $exported_templates = []; + $exported_template_parts = []; foreach ( $templates as $template ) { - $template = $this->filter_theme_template( - $template, - $export_type, - $templates_path, - $old_slug, - $new_slug - ); - if ( $template ) { - $exported_templates[] = $template; + if ( $template->source !== 'custom' ) { + continue; } + $template = $this->clean_template( $template ); + $exported_templates[] = $template; } - foreach ( $template_parts as $template ) { - $template = $this->filter_theme_template( - $template, - $export_type, - $parts_path, - $old_slug, - $new_slug - - ); - if ( $template ) { - $exported_parts[] = $template; + foreach ( $template_parts as $template_part ) { + if ( $template_part->source !== 'custom' ) { + continue; } + $template_part = $this->clean_template( $template_part ); + $exported_template_parts[] = $template_part; } - return (object)[ + return array( 'templates'=>$exported_templates, - 'parts'=>$exported_parts - ]; + 'template_parts'=>$exported_template_parts + ); } - function add_templates_to_local( $export_type ) { - - $theme_templates = $this->get_theme_templates( $export_type, null ); + /* + * Clean a template or template part by reversing unicode escaping + * and removing the theme attribute from the content + */ + function clean_template( $template ) { + $template->content = _remove_theme_attribute_in_block_template_content( + $template->content + ); - foreach ( $theme_templates->templates as $template ) { - file_put_contents( - get_template_directory() . '/templates/' . $template->slug . '.html', - $template->content - ); - } + // `serialize_block_attributes` has "unicode escape sequence + // substitution for characters which might otherwise interfere with embedding + // the result in an HTML comment". + // This reverses that escaping for saving back to files. + // @see `wp-includes/blocks.php` + $template->content = str_replace( '\\u002d\\u002d', '--', $template->content ); + $template->content = str_replace( '\\u003c', '<', $template->content ); + $template->content = str_replace( '\\u003e', '>', $template->content ); + $template->content = str_replace( '\\u0026', '&', $template->content ); + $template->content = str_replace( '\\u0022', '"', $template->content ); - foreach ( $theme_templates->parts as $template_part ) { - file_put_contents( - get_template_directory() . '/parts/' . $template_part->slug . '.html', - $template_part->content - ); - } + return $template; } - function add_theme_json_to_local ( $export_type ) { - file_put_contents( - get_template_directory() . '/theme.json', - WP_Theme_JSON_Resolver_6_1::export_theme_data( $export_type ) + function clear_user_customizations() { + + // clear global styles + $global_styles_controller = new Gutenberg_REST_Global_Styles_Controller(); + $update_request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' ); + $update_request->set_param( + 'id', + WP_Theme_JSON_Resolver::get_user_global_styles_post_id() ); + $update_request->set_param( 'settings', [] ); + $update_request->set_param( 'styles', [] ); + $global_styles_controller->update_item( $update_request ); + + // delete global styles transients + delete_transient( 'global_styles' ); + delete_transient( 'global_styles_' . get_stylesheet() ); + delete_transient( 'gutenberg_global_styles' ); + delete_transient( 'gutenberg_global_styles_' . get_stylesheet() ); + + // remove all user templates and template parts + $user_customisations = $this->get_user_customisations(); + foreach ( $user_customisations['templates'] as $template ) { + wp_delete_post($template->wp_id, true); + } + foreach ( $user_customisations['template_parts'] as $template_part ) { + wp_delete_post($template->wp_id, true); + } } } diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php deleted file mode 100644 index 09924d43de7870..00000000000000 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php +++ /dev/null @@ -1,109 +0,0 @@ - $node_value ) { - $data[ $node_name ] = WP_Theme_JSON_Resolver_6_1::flatten_theme_json( $node_value, $node_name ); - } - } - - return $data; - } - - /** - * Export the combined (and flattened) THEME and CUSTOM data. - * - * @param string $content ['all', 'current', 'user'] Determines which settings content to include in the export. - * All options include user settings. - * 'current' will include settings from the currently installed theme but NOT from the parent theme. - * 'all' will include settings from the current theme as well as the parent theme (if it has one) - */ - public static function export_theme_data( $content ) { - $theme = new WP_Theme_JSON(); - - if ( $content === 'all' && wp_get_theme()->parent() ) { - // Get parent theme.json. - $parent_theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json', true ) ); - $parent_theme_json_data = static::translate( $parent_theme_json_data, wp_get_theme()->parent()->get( 'TextDomain' ) ); - $parent_theme = new WP_Theme_JSON( $parent_theme_json_data ); - $theme->merge ($parent_theme); - } - - if ( $content === 'all' || $content === 'current' ) { - $theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json' ) ); - $theme_json_data = static::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) ); - $theme_theme = new WP_Theme_JSON( $theme_json_data ); - $theme->merge( $theme_theme ); - } - - $theme->merge( static::get_user_data() ); - - $data = WP_Theme_JSON_Resolver_6_1::flatten_theme_json($theme->get_raw_data(), null); - - $theme_json = wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); - return preg_replace ( '~(?:^|\G)\h{4}~m', "\t", $theme_json ); - - } - -} diff --git a/lib/load.php b/lib/load.php index 5c50a30a82862d..f8b5c42c379075 100644 --- a/lib/load.php +++ b/lib/load.php @@ -119,7 +119,6 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php'; require __DIR__ . '/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php'; require __DIR__ . '/compat/wordpress-6.1/class-gutenberg-rest-edit-site-export-controller.php'; -require __DIR__ . '/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php'; require __DIR__ . '/compat/wordpress-6.1/rest-api.php'; require __DIR__ . '/compat/wordpress-6.0/block-patterns.php'; require __DIR__ . '/compat/wordpress-6.0/block-template.php'; diff --git a/packages/edit-site/src/components/header/theme-udpdater/index.js b/packages/edit-site/src/components/header/theme-udpdater/index.js index db9ef42bed36b5..7dfa0726ce2838 100644 --- a/packages/edit-site/src/components/header/theme-udpdater/index.js +++ b/packages/edit-site/src/components/header/theme-udpdater/index.js @@ -39,7 +39,6 @@ export default function ThemeUpdater() { isResolving: isTemplateListLoading, } = useEntityRecords( 'postType', 'wp_template', { per_page: -1, - source: 'custom', } ); const { @@ -63,35 +62,30 @@ export default function ThemeUpdater() { }; }, [] ); - if ( ! templates || isTemplateListLoading ) { - return null; - } - - if ( ! templateParts || isTemplatePartListLoading ) { - return null; - } - const customSourceFilter = ( tpl ) => tpl.source === 'custom'; - const unModifiedTheme = - ! filter( templates, customSourceFilter ).length && - ! filter( templateParts, customSourceFilter ).length; + const customTemplates = filter( templates, customSourceFilter ); + const customTemplateParts = filter( templateParts, customSourceFilter ); - if ( ! enableThemeSaving ) { + const unModifiedTheme = + ! customTemplates.length && ! customTemplateParts.length; + const isLoading = isTemplateListLoading || isTemplatePartListLoading; + + if ( + ! enableThemeSaving || + isLoading || + unModifiedTheme || + isDirty || + isSaving + ) { return null; } const handleUpdateTheme = async () => { try { await apiFetch( { - path: '/wp-block-editor/v1/export/update_theme', + path: '/wp-block-editor/v1/export/export_to_theme_files', } ); - const customTemplates = filter( templates, customSourceFilter ); - const customTemplateParts = filter( - templateParts, - customSourceFilter - ); - const revertTemplateOrPart = ( name ) => { if ( isTemplateRevertable( name ) ) { revertTemplate( name ); @@ -100,7 +94,7 @@ export default function ThemeUpdater() { customTemplates.forEach( revertTemplateOrPart ); customTemplateParts.forEach( revertTemplateOrPart ); - createInfoNotice( __( 'Customisations saved to theme' ), { + createInfoNotice( __( 'Customisations exported to theme files' ), { speak: true, type: 'snackbar', } ); @@ -117,10 +111,6 @@ export default function ThemeUpdater() { } }; - if ( unModifiedTheme || isDirty || isSaving ) { - return null; - } - return (