Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions src/wp-includes/block-template-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Copy link
Contributor

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.

Copy link
Member Author

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 don't know how we organize things in general in Core

I'm also not sure about this. I talked with @noisysocks, and we decided to keep these methods in the utils file for now.

Copy link
Contributor

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...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

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.

$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.
*
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* 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.
*/
/**
* Generate export file for block templates.
*
* 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.
*/

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the UI hidden if ZipArchive is unavailable?

This line will be needed even if it is but I just want to make sure it is.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 ZipArchive is available.

Copy link
Member

Choose a reason for hiding this comment

The 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;
}
4 changes: 4 additions & 0 deletions src/wp-includes/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,10 @@ function create_initial_rest_routes() {
// Menu Locations.
$controller = new WP_REST_Menu_Locations_Controller();
$controller->register_routes();

// Site Editor Export.
$controller = new WP_REST_Edit_Site_Export_Controller();
$controller->register_routes();
}

/**
Expand Down
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* Output a ZIP file with an export of the current templates
/**
* Output ZIP archive of export.
*
* Output a ZIP file with an export of the current templates

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;
}
}
1 change: 1 addition & 0 deletions src/wp-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-themes-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-plugins-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-block-directory-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-edit-site-export-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-application-passwords-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-site-health-controller.php';
Expand Down
65 changes: 65 additions & 0 deletions tests/phpunit/tests/block-template-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$this->assertEquals( $expected, _remove_theme_attribute_in_block_template_content( $template_content ) );
$this->assertSame( $expected, _remove_theme_attribute_in_block_template_content( $template_content ) );

}

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.
*/
Expand Down Expand Up @@ -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' );
}
}
21 changes: 21 additions & 0 deletions tests/qunit/fixtures/wp-api-generated.js
Original file line number Diff line number Diff line change
Expand Up @@ -10642,6 +10642,27 @@ mockedApiResponse.Schema = {
}
}
]
},
"/wp-block-editor/v1/export": {
"namespace": "wp-block-editor/v1",
"methods": [
"GET"
],
"endpoints": [
{
"methods": [
"GET"
],
"args": []
}
],
"_links": {
"self": [
{
"href": "http://example.org/index.php?rest_route=/wp-block-editor/v1/export"
}
]
}
}
},
"site_logo": 0,
Expand Down