Skip to content
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

Fix REST API auto-draft creation #26650

Merged
merged 13 commits into from
Nov 9, 2020
130 changes: 130 additions & 0 deletions lib/class-wp-rest-template-parts-controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php
/**
* REST API: WP_REST_Template_Parts_Controller class
*
* @subpackage REST_API
* @package WordPress
*/

/**
* Creates a template part auto-draft if it doesn't exist yet.
*
* @access private
*
* @param string $slug Template part slug.
* @param string $theme Template part theme.
* @param string $content Template part content.
*/
function create_auto_draft_for_template_part_block( $slug, $theme, $content ) {
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
// A published post might already exist if this template part
// was customized elsewhere or if it's part of a customized
// template. We also check if an auto-draft was already created
// because preloading can make this run twice, so, different code
// paths can end up with different posts for the same template part.
// E.g. The server could send back post ID 1 to the client, preload,
// and create another auto-draft. So, if the client tries to resolve the
// post ID from the slug and theme, it won't match with what the server sent.
$template_part_query = new WP_Query(
array(
'post_type' => 'wp_template_part',
'post_status' => array( 'publish', 'auto-draft' ),
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
'title' => $slug,
'meta_key' => 'theme',
'meta_value' => $theme,
'posts_per_page' => 1,
'no_found_rows' => true,
)
);
$template_part_post = $template_part_query->have_posts() ? $template_part_query->next_post() : null;
if ( ! $template_part_post ) {
wp_insert_post(
array(
'post_content' => $content,
'post_title' => $slug,
'post_status' => 'auto-draft',
'post_type' => 'wp_template_part',
'post_name' => $slug,
'meta_input' => array(
'theme' => $theme,
),
)
);
} else {
// Potentially we could decide to update the content if different.
Copy link
Contributor

Choose a reason for hiding this comment

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

If we aren't comparing content as in the previous implementation, @david-szabo97 has been looking into re-writing the auto-drafts if the theme version changes - #26383

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, with this simpler syncing function, it's just a matter of comparing content here and updating the auto-draft if needed.

Choose a reason for hiding this comment

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

I think the updating of the auto-draft was already being done prior to #26383. The purpose of #26383 was to stop that code from running whenever it was not necessary to run it (i.e. theme version hasn't changed.) This PR seems to go backwards in that respect.

Choose a reason for hiding this comment

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

It's because gutenberg_resolve_template calls file_get_contents on every template in the theme while gutenberg_find_template_post_and_parts only called it on the one being requested. That means if you are loading 10 templates in the site editor then file_get_contents will get called 100 times.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Where do you see the template part update was being done before? By my own reading of the code, it was not being done anywhere.

Choose a reason for hiding this comment

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

I was talking about templates actually. They were being updated inside gutenberg_find_template_post_and_parts if the file contents had changed. I don't know about template parts.

Copy link
Contributor

@Addison-Stavlo Addison-Stavlo Nov 6, 2020

Choose a reason for hiding this comment

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

Where do you see the template part update was being done before?

From what I can tell, the following was the closest to this. It looks like it didn't so much update the auto-draft itself, but create a new one. The current code only gets to this point if a publish version isn't found, but if the contents of the auto-draft don't match the contents of the file then a new auto-draft is created:

$file_contents = file_get_contents( $template_part_file_path );
if ( $template_part_post && $template_part_post->post_content === $file_contents ) {
$template_part_id = $template_part_post->ID;
} else {
$template_part_id = wp_insert_post(

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I can see that difference and it's very problematic IMO. Creating a new auto-draft when the post_content changes is probably not the best approach to synchronization.

Let's leave this syncing behavior to its own dedicated PR since it's broken in master anyway (proliferation of auto-drafts).

Copy link
Contributor

Choose a reason for hiding this comment

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

it's very problematic IMO.
Let's leave this syncing behavior to its own dedicated PR

Agreed, and that sounds like a good plan!

}
}

/**
* Create the template parts auto-drafts for the current theme.
*
* @access private
*/
function create_theme_template_parts() {
/**
* Finds all nested template part file paths in a theme's directory.
*
* @param string $base_directory The theme's file path.
* @return array $path_list A list of paths to all template part files.
*/
function get_template_part_paths( $base_directory ) {
$path_list = array();
if ( file_exists( $base_directory . '/block-template-parts' ) ) {
$nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory . '/block-template-parts' ) );
$nested_html_files = new RegexIterator( $nested_files, '/^.+\.html$/i', RecursiveRegexIterator::GET_MATCH );
foreach ( $nested_html_files as $path => $file ) {
$path_list[] = $path;
}
}
return $path_list;
}

// Get file paths for all theme supplied template parts.
$template_part_files = get_template_part_paths( get_stylesheet_directory() );
if ( is_child_theme() ) {
$template_part_files = array_merge( $template_part_files, get_template_part_paths( get_template_directory() ) );
}
// Build and save each template part.
foreach ( $template_part_files as $template_part_file ) {
$content = file_get_contents( $template_part_file );
$slug = substr(
$template_part_file,
// Starting position of slug.
strpos( $template_part_file, 'block-template-parts/' ) + 21,
// Subtract ending '.html'.
-5
);
create_auto_draft_for_template_part_block( $slug, wp_get_theme()->get( 'TextDomain' ), $content );
}
}

/**
* Core class used to access menu templte parts via the REST API.
*
* @see WP_REST_Controller
*/
class WP_REST_Template_Parts_Controller extends WP_REST_Posts_Controller {

/**
* Retrieves a list of template parts.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
create_theme_template_parts();

return parent::get_items( $request );
}

/**
* Retrieves a single template parat.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
create_theme_template_parts();

return parent::get_items( $request );
}
}
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 5 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,14 @@ function gutenberg_is_experiment_enabled( $name ) {
if ( ! class_exists( 'WP_Block_List' ) ) {
require dirname( __FILE__ ) . '/class-wp-block-list.php';
}

if ( ! class_exists( 'WP_Widget_Block' ) ) {
require_once dirname( __FILE__ ) . '/class-wp-widget-block.php';
}

if ( ! class_exists( 'WP_REST_Template_Parts_Controller' ) ) {
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
require_once dirname( __FILE__ ) . '/class-wp-rest-template-parts-controller.php';
}
require_once dirname( __FILE__ ) . '/widgets-page.php';

require dirname( __FILE__ ) . '/compat.php';
Expand Down
112 changes: 2 additions & 110 deletions lib/template-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,83 +123,6 @@ function gutenberg_override_query_template( $template, $type, array $templates =
return gutenberg_dir_path() . 'lib/template-canvas.php';
}

/**
* Recursively traverses a block tree, creating auto drafts
* for any encountered template parts without a fixed post.
*
* @access private
*
* @param array $block The root block to start traversing from.
* @return int[] A list of template parts IDs for the given block.
*/
function create_auto_draft_for_template_part_block( $block ) {
$template_part_ids = array();

if ( 'core/template-part' === $block['blockName'] && isset( $block['attrs']['slug'] ) ) {
if ( isset( $block['attrs']['postId'] ) ) {
// Template part is customized.
$template_part_id = $block['attrs']['postId'];
} else {
// A published post might already exist if this template part
// was customized elsewhere or if it's part of a customized
// template. We also check if an auto-draft was already created
// because preloading can make this run twice, so, different code
// paths can end up with different posts for the same template part.
// E.g. The server could send back post ID 1 to the client, preload,
// and create another auto-draft. So, if the client tries to resolve the
// post ID from the slug and theme, it won't match with what the server sent.
$template_part_query = new WP_Query(
array(
'post_type' => 'wp_template_part',
'post_status' => array( 'publish', 'auto-draft' ),
'title' => $block['attrs']['slug'],
'meta_key' => 'theme',
'meta_value' => $block['attrs']['theme'],
'posts_per_page' => 1,
'no_found_rows' => true,
)
);
$template_part_post = $template_part_query->have_posts() ? $template_part_query->next_post() : null;
if ( $template_part_post && 'auto-draft' !== $template_part_post->post_status ) {
$template_part_id = $template_part_post->ID;
} else {
// Template part is not customized, get it from a file and make an auto-draft for it, unless one already exists
// and the underlying file hasn't changed.
$template_part_file_path = get_stylesheet_directory() . '/block-template-parts/' . $block['attrs']['slug'] . '.html';
if ( ! file_exists( $template_part_file_path ) ) {
$template_part_file_path = false;
}

if ( $template_part_file_path ) {
$file_contents = file_get_contents( $template_part_file_path );
if ( $template_part_post && $template_part_post->post_content === $file_contents ) {
$template_part_id = $template_part_post->ID;
} else {
$template_part_id = wp_insert_post(
array(
'post_content' => $file_contents,
'post_title' => $block['attrs']['slug'],
'post_status' => 'auto-draft',
'post_type' => 'wp_template_part',
'post_name' => $block['attrs']['slug'],
'meta_input' => array(
'theme' => $block['attrs']['theme'],
),
)
);
}
}
}
}
$template_part_ids[ $block['attrs']['slug'] ] = $template_part_id;
}

foreach ( $block['innerBlocks'] as $inner_block ) {
$template_part_ids = array_merge( $template_part_ids, create_auto_draft_for_template_part_block( $inner_block ) );
}
return $template_part_ids;
}

/**
* Return the correct 'wp_template' post and template part IDs for the current template.
*
Expand Down Expand Up @@ -323,11 +246,11 @@ function gutenberg_find_template_post_and_parts( $template_type, $template_hiera

if ( $current_template_post ) {
$template_part_ids = array();
if ( is_admin() || defined( 'REST_REQUEST' ) ) {
/* if ( is_admin() || defined( 'REST_REQUEST' ) ) {
foreach ( parse_blocks( $current_template_post->post_content ) as $block ) {
$template_part_ids = array_merge( $template_part_ids, create_auto_draft_for_template_part_block( $block ) );
}
}
} */
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
return array(
'template_post' => $current_template_post,
'template_part_ids' => $template_part_ids,
Expand Down Expand Up @@ -396,37 +319,6 @@ function gutenberg_strip_php_suffix( $template_file ) {
return preg_replace( '/\.php$/', '', $template_file );
}

/**
* Extends default editor settings to enable template and template part editing.
*
* @param array $settings Default editor settings.
*
* @return array Filtered editor settings.
*/
function gutenberg_template_loader_filter_block_editor_settings( $settings ) {
global $post;

if ( ! $post ) {
return $settings;
}

// If this is the Site Editor, auto-drafts for template parts have already been generated
// through `filter_rest_wp_template_part_query`, when called via the REST API.
if ( isset( $settings['editSiteInitialState'] ) ) {
return $settings;
}

// Otherwise, create template part auto-drafts for the edited post.
$post = get_post();
foreach ( parse_blocks( $post->post_content ) as $block ) {
create_auto_draft_for_template_part_block( $block );
}

// TODO: Set editing mode and current template ID for editing modes support.
return $settings;
}
add_filter( 'block_editor_settings', 'gutenberg_template_loader_filter_block_editor_settings' );
youknowriad marked this conversation as resolved.
Show resolved Hide resolved

/**
* Removes post details from block context when rendering a block template.
*
Expand Down
66 changes: 12 additions & 54 deletions lib/template-parts.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,18 @@ function gutenberg_register_template_part_post_type() {
);

$args = array(
'labels' => $labels,
'description' => __( 'Template parts to include in your templates.', 'gutenberg' ),
'public' => false,
'has_archive' => false,
'show_ui' => true,
'show_in_menu' => 'themes.php',
'show_in_admin_bar' => false,
'show_in_rest' => true,
'rest_base' => 'template-parts',
'map_meta_cap' => true,
'supports' => array(
'labels' => $labels,
'description' => __( 'Template parts to include in your templates.', 'gutenberg' ),
'public' => false,
'has_archive' => false,
'show_ui' => true,
'show_in_menu' => 'themes.php',
'show_in_admin_bar' => false,
'show_in_rest' => true,
'rest_base' => 'template-parts',
'rest_controller_class' => 'WP_REST_Template_Parts_Controller',
'map_meta_cap' => true,
'supports' => array(
'title',
'slug',
'editor',
Expand Down Expand Up @@ -217,49 +218,6 @@ function filter_rest_wp_template_part_query( $args, $request ) {
'value' => $request['theme'],
);

// Ensure auto-drafts of all theme supplied template parts are created.
if ( wp_get_theme()->stylesheet === $request['theme'] ) {
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
/**
* Finds all nested template part file paths in a theme's directory.
*
* @param string $base_directory The theme's file path.
* @return array $path_list A list of paths to all template part files.
*/
function get_template_part_paths( $base_directory ) {
$path_list = array();
if ( file_exists( $base_directory . '/block-template-parts' ) ) {
$nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory . '/block-template-parts' ) );
$nested_html_files = new RegexIterator( $nested_files, '/^.+\.html$/i', RecursiveRegexIterator::GET_MATCH );
foreach ( $nested_html_files as $path => $file ) {
$path_list[] = $path;
}
}
return $path_list;
}

// Get file paths for all theme supplied template parts.
$template_part_files = get_template_part_paths( get_stylesheet_directory() );
if ( is_child_theme() ) {
$template_part_files = array_merge( $template_part_files, get_template_part_paths( get_template_directory() ) );
}
// Build and save each template part.
foreach ( $template_part_files as $template_part_file ) {
$content = file_get_contents( $template_part_file );
// Infer slug from filepath.
$slug = substr(
$template_part_file,
// Starting position of slug.
strpos( $template_part_file, 'block-template-parts/' ) + 21,
// Subtract ending '.html'.
-5
);
// Wrap content with the template part block, parse, and create auto-draft.
$template_part_string = '<!-- wp:template-part {"slug":"' . $slug . '","theme":"' . wp_get_theme()->get( 'TextDomain' ) . '"} -->' . $content . '<!-- /wp:template-part -->';
$template_part_block = parse_blocks( $template_part_string )[0];
create_auto_draft_for_template_part_block( $template_part_block );
}
};

$args['meta_query'] = $meta_query;
}

Expand Down