-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Site Editor: Add missing export endpoint #1936
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
191d85d
e74a881
275656f
f441c4d
55cab98
b02f8f3
0d5916e
2b40531
f7fe3c4
48dd608
0473b9c
8388d27
80bbb6d
8aec83d
7c78e4f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -454,6 +454,39 @@ function _inject_theme_attribute_in_block_template_content( $template_content ) | |||||||||||||||||||||||||||||||||||||||||
| return $template_content; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Parses a block template and removes the theme attribute from each template part. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @access private | ||||||||||||||||||||||||||||||||||||||||||
| * @since 5.9.0 | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @param string $template_content Serialized block template content. | ||||||||||||||||||||||||||||||||||||||||||
| * @return string Updated block template content. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| function _remove_theme_attribute_in_block_template_content( $template_content ) { | ||||||||||||||||||||||||||||||||||||||||||
| $has_updated_content = false; | ||||||||||||||||||||||||||||||||||||||||||
| $new_content = ''; | ||||||||||||||||||||||||||||||||||||||||||
| $template_blocks = parse_blocks( $template_content ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| $blocks = _flatten_blocks( $template_blocks ); | ||||||||||||||||||||||||||||||||||||||||||
| foreach ( $blocks as $key => $block ) { | ||||||||||||||||||||||||||||||||||||||||||
| if ( 'core/template-part' === $block['blockName'] && isset( $block['attrs']['theme'] ) ) { | ||||||||||||||||||||||||||||||||||||||||||
| unset( $blocks[ $key ]['attrs']['theme'] ); | ||||||||||||||||||||||||||||||||||||||||||
| $has_updated_content = true; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if ( ! $has_updated_content ) { | ||||||||||||||||||||||||||||||||||||||||||
| return $template_content; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| foreach ( $template_blocks as $block ) { | ||||||||||||||||||||||||||||||||||||||||||
| $new_content .= serialize_block( $block ); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return $new_content; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Build a unified template object based on a theme file. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -863,3 +896,55 @@ function block_header_area() { | |||||||||||||||||||||||||||||||||||||||||
| function block_footer_area() { | ||||||||||||||||||||||||||||||||||||||||||
| block_template_part( 'footer' ); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Creates an export of the current templates and | ||||||||||||||||||||||||||||||||||||||||||
| * template parts from the site editor at the | ||||||||||||||||||||||||||||||||||||||||||
| * specified path in a ZIP file. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @since 5.9.0 | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @return WP_Error|string Path of the ZIP file or error on failure. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+900
to
+908
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
| function wp_generate_block_templates_export_file() { | ||||||||||||||||||||||||||||||||||||||||||
| if ( ! class_exists( 'ZipArchive' ) ) { | ||||||||||||||||||||||||||||||||||||||||||
| return new WP_Error( __( 'Zip Export not supported.' ) ); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+910
to
+912
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the UI hidden if This line will be needed even if it is but I just want to make sure it is.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently, UI is always visible since we will have to make a new request to check if
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my testing, the export feature did not fail gracefully. Screen.Recording.2021-11-30.at.16.33.44.mov |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| $obscura = wp_generate_password( 12, false, false ); | ||||||||||||||||||||||||||||||||||||||||||
| $filename = get_temp_dir() . 'edit-site-export-' . $obscura . '.zip'; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| $zip = new ZipArchive(); | ||||||||||||||||||||||||||||||||||||||||||
| if ( true !== $zip->open( $filename, ZipArchive::CREATE ) ) { | ||||||||||||||||||||||||||||||||||||||||||
| return new WP_Error( __( 'Unable to open export file (archive) for writing.' ) ); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| $zip->addEmptyDir( 'theme' ); | ||||||||||||||||||||||||||||||||||||||||||
| $zip->addEmptyDir( 'theme/templates' ); | ||||||||||||||||||||||||||||||||||||||||||
| $zip->addEmptyDir( 'theme/parts' ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Load templates into the zip file. | ||||||||||||||||||||||||||||||||||||||||||
| $templates = get_block_templates(); | ||||||||||||||||||||||||||||||||||||||||||
| foreach ( $templates as $template ) { | ||||||||||||||||||||||||||||||||||||||||||
| $template->content = _remove_theme_attribute_in_block_template_content( $template->content ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| $zip->addFromString( | ||||||||||||||||||||||||||||||||||||||||||
| 'theme/templates/' . $template->slug . '.html', | ||||||||||||||||||||||||||||||||||||||||||
| $template->content | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Load template parts into the zip file. | ||||||||||||||||||||||||||||||||||||||||||
| $template_parts = get_block_templates( array(), 'wp_template_part' ); | ||||||||||||||||||||||||||||||||||||||||||
| foreach ( $template_parts as $template_part ) { | ||||||||||||||||||||||||||||||||||||||||||
| $zip->addFromString( | ||||||||||||||||||||||||||||||||||||||||||
| 'theme/parts/' . $template_part->slug . '.html', | ||||||||||||||||||||||||||||||||||||||||||
| $template_part->content | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Save changes to the zip file. | ||||||||||||||||||||||||||||||||||||||||||
| $zip->close(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return $filename; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,93 @@ | ||||||||||||||
| <?php | ||||||||||||||
| /** | ||||||||||||||
| * REST API: WP_REST_Edit_Site_Export_Controller class | ||||||||||||||
| * | ||||||||||||||
| * @package WordPress | ||||||||||||||
| * @subpackage REST_API | ||||||||||||||
| */ | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Controller which provides REST endpoint for exporting current templates | ||||||||||||||
| * and template parts. | ||||||||||||||
| * | ||||||||||||||
| * @since 5.9.0 | ||||||||||||||
| * | ||||||||||||||
| * @see WP_REST_Controller | ||||||||||||||
| */ | ||||||||||||||
| class WP_REST_Edit_Site_Export_Controller extends WP_REST_Controller { | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Constructor. | ||||||||||||||
| * | ||||||||||||||
| * @since 5.9.0 | ||||||||||||||
| */ | ||||||||||||||
| public function __construct() { | ||||||||||||||
| $this->namespace = 'wp-block-editor/v1'; | ||||||||||||||
| $this->rest_base = 'export'; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Registers the site export route. | ||||||||||||||
| * | ||||||||||||||
| * @since 5.9.0 | ||||||||||||||
| */ | ||||||||||||||
| public function register_routes() { | ||||||||||||||
| register_rest_route( | ||||||||||||||
| $this->namespace, | ||||||||||||||
| '/' . $this->rest_base, | ||||||||||||||
| array( | ||||||||||||||
| array( | ||||||||||||||
| 'methods' => WP_REST_Server::READABLE, | ||||||||||||||
| 'callback' => array( $this, 'export' ), | ||||||||||||||
| 'permission_callback' => array( $this, 'permissions_check' ), | ||||||||||||||
| ), | ||||||||||||||
| ) | ||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Checks whether a given request has permission to export. | ||||||||||||||
| * | ||||||||||||||
| * @since 5.9.0 | ||||||||||||||
| * | ||||||||||||||
| * @return WP_Error|true True if the request has access, or WP_Error object. | ||||||||||||||
| */ | ||||||||||||||
| public function permissions_check() { | ||||||||||||||
| if ( ! current_user_can( 'edit_theme_options' ) ) { | ||||||||||||||
| new WP_Error( | ||||||||||||||
| 'rest_cannot_view_url_details', | ||||||||||||||
| __( 'Sorry, you are not allowed to export templates and template parts.' ), | ||||||||||||||
| array( 'status' => rest_authorization_required_code() ) | ||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| return true; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Output a ZIP file with an export of the current templates | ||||||||||||||
|
Comment on lines
+67
to
+68
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Another one that really needs a short summary ahead of the description. The first line is pulled out as a short summary in the developer docs, for example https://developer.wordpress.org/reference/functions/wp_remote_get/ |
||||||||||||||
| * and template parts from the site editor, and close the connection. | ||||||||||||||
| * | ||||||||||||||
| * @since 5.9.0 | ||||||||||||||
| * | ||||||||||||||
| * @return WP_Error|void | ||||||||||||||
| */ | ||||||||||||||
| public function export() { | ||||||||||||||
| // Generate the export file. | ||||||||||||||
| $filename = wp_generate_block_templates_export_file(); | ||||||||||||||
|
|
||||||||||||||
| if ( is_wp_error( $filename ) ) { | ||||||||||||||
| $filename->add_data( array( 'status' => 500 ) ); | ||||||||||||||
|
|
||||||||||||||
| return $filename; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| header( 'Content-Type: application/zip' ); | ||||||||||||||
| header( 'Content-Disposition: attachment; filename=edit-site-export.zip' ); | ||||||||||||||
| header( 'Content-Length: ' . filesize( $filename ) ); | ||||||||||||||
| flush(); | ||||||||||||||
| readfile( $filename ); | ||||||||||||||
| unlink( $filename ); | ||||||||||||||
| exit; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -189,6 +189,38 @@ function test_inject_theme_attribute_in_block_template_content() { | |||||
| $this->assertSame( $content_with_no_template_part, $template_content ); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * @ticket 54448 | ||||||
| * | ||||||
| * @dataProvider data_remove_theme_attribute_in_block_template_content | ||||||
| */ | ||||||
| function test_remove_theme_attribute_in_block_template_content( $template_content, $expected ) { | ||||||
| $this->assertEquals( $expected, _remove_theme_attribute_in_block_template_content( $template_content ) ); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| } | ||||||
|
|
||||||
| function data_remove_theme_attribute_in_block_template_content() { | ||||||
| return array( | ||||||
| array( | ||||||
| '<!-- wp:template-part {"slug":"header","theme":"tt1-blocks","align":"full","tagName":"header","className":"site-header"} /-->', | ||||||
| '<!-- wp:template-part {"slug":"header","align":"full","tagName":"header","className":"site-header"} /-->', | ||||||
| ), | ||||||
| array( | ||||||
| '<!-- wp:group --><!-- wp:template-part {"slug":"header","theme":"tt1-blocks","align":"full","tagName":"header","className":"site-header"} /--><!-- /wp:group -->', | ||||||
| '<!-- wp:group --><!-- wp:template-part {"slug":"header","align":"full","tagName":"header","className":"site-header"} /--><!-- /wp:group -->', | ||||||
| ), | ||||||
| // Does not modify content when there is no existing theme attribute. | ||||||
| array( | ||||||
| '<!-- wp:template-part {"slug":"header","align":"full","tagName":"header","className":"site-header"} /-->', | ||||||
| '<!-- wp:template-part {"slug":"header","align":"full","tagName":"header","className":"site-header"} /-->', | ||||||
| ), | ||||||
| // Does not remove theme when there is no template part. | ||||||
| array( | ||||||
| '<!-- wp:post-content /-->', | ||||||
| '<!-- wp:post-content /-->', | ||||||
| ), | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Should retrieve the template from the theme files. | ||||||
| */ | ||||||
|
|
@@ -311,4 +343,37 @@ function test_flatten_blocks() { | |||||
| $expected = array( $blocks[0] ); | ||||||
| $this->assertSame( $expected, $actual ); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Should generate block templates export file. | ||||||
| * | ||||||
| * @ticket 54448 | ||||||
| */ | ||||||
| function test_wp_generate_block_templates_export_file() { | ||||||
| $filename = wp_generate_block_templates_export_file(); | ||||||
| $this->assertFileExists( $filename, 'zip file is created at the specified path' ); | ||||||
| $this->assertTrue( filesize( $filename ) > 0, 'zip file is larger than 0 bytes' ); | ||||||
|
|
||||||
| // Open ZIP file and make sure the directories exist. | ||||||
| $zip = new ZipArchive(); | ||||||
| $zip->open( $filename ); | ||||||
| $has_theme_dir = $zip->locateName( 'theme/' ) !== false; | ||||||
| $has_block_templates_dir = $zip->locateName( 'theme/templates/' ) !== false; | ||||||
| $has_block_template_parts_dir = $zip->locateName( 'theme/parts/' ) !== false; | ||||||
| $this->assertTrue( $has_theme_dir, 'theme directory exists' ); | ||||||
| $this->assertTrue( $has_block_templates_dir, 'theme/templates directory exists' ); | ||||||
| $this->assertTrue( $has_block_template_parts_dir, 'theme/parts directory exists' ); | ||||||
|
|
||||||
| // ZIP file contains at least one HTML file. | ||||||
| $has_html_files = false; | ||||||
| $num_files = $zip->numFiles; | ||||||
| for ( $i = 0; $i < $num_files; $i++ ) { | ||||||
| $filename = $zip->getNameIndex( $i ); | ||||||
| if ( '.html' === substr( $filename, -5 ) ) { | ||||||
| $has_html_files = true; | ||||||
| break; | ||||||
| } | ||||||
| } | ||||||
| $this->assertTrue( $has_html_files, 'contains at least one html file' ); | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know how we organize things in general in Core but for me, I don't think we should change this file as the added functions are specific to the export behavior and could be moved to the export controller directly (private methods there) or their own file. WDYT?
I see this file as the API to fetch block templates and template parts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't test the private method in PHP, but we can make it public.
Also, make sense to have a remove method when we have an inject one.
I'm also not sure about this. I talked with @noisysocks, and we decided to keep these methods in the utils file for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmm, I don't like that we're kind of changing the role of the "utils" file but I can live with it. We should also do the same in the plugin if we go that road to keep the files similar to easy backports...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done - WordPress/gutenberg#36908
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initially, I was planning to have these methods private, but then I got reminded that public API could be helpful for WP CLI and similar tools - WordPress/gutenberg#36559 (comment).
Maybe we can have a separate file for site export methods once API is more mature in the future.