diff --git a/wp-includes/block-template-utils.php b/wp-includes/block-template-utils.php index 2c2fd66d7c33..b0086f5d301f 100644 --- a/wp-includes/block-template-utils.php +++ b/wp-includes/block-template-utils.php @@ -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. + */ +function wp_generate_block_templates_export_file() { + if ( ! class_exists( 'ZipArchive' ) ) { + return new WP_Error( __( 'Zip Export not supported.' ) ); + } + + $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; +} diff --git a/wp-includes/rest-api.php b/wp-includes/rest-api.php index 25f23fae6439..9617c81c91d9 100644 --- a/wp-includes/rest-api.php +++ b/wp-includes/rest-api.php @@ -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(); } /** diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-edit-site-export-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-edit-site-export-controller.php new file mode 100644 index 000000000000..1bd40cd637c1 --- /dev/null +++ b/wp-includes/rest-api/endpoints/class-wp-rest-edit-site-export-controller.php @@ -0,0 +1,93 @@ +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 + * 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; + } +} diff --git a/wp-includes/version.php b/wp-includes/version.php index d850bdf5464e..fc59b9fa7674 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '5.9-alpha-52285'; +$wp_version = '5.9-alpha-52286'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. diff --git a/wp-settings.php b/wp-settings.php index 3b0e179baf14..85b2f2146a68 100644 --- a/wp-settings.php +++ b/wp-settings.php @@ -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';