PluginProbe
Gutenberg / 9.1.0
Gutenberg v9.1.0
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / lib / template-loader.php

template-loader.php in Gutenberg 9.1.0, at lib/template-loader.php

470 lines 16.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Block template loader functions.
4 *
5 * @package gutenberg
6 */
7
8 /**
9 * Return a list of all overrideable default template types.
10 *
11 * @see get_query_template
12 *
13 * @return string[] List of all overrideable default template types.
14 */
15 function get_template_types() {
16 return array(
17 'index',
18 '404',
19 'archive',
20 'author',
21 'category',
22 'tag',
23 'taxonomy',
24 'date',
25 'embed',
26 'home',
27 'front-page',
28 'privacy-policy',
29 'page',
30 'search',
31 'single',
32 'singular',
33 'attachment',
34 );
35 }
36
37 /**
38 * Adds necessary filters to use 'wp_template' posts instead of theme template files.
39 */
40 function gutenberg_add_template_loader_filters() {
41 foreach ( get_template_types() as $template_type ) {
42 if ( 'embed' === $template_type ) { // Skip 'embed' for now because it is not a regular template type.
43 continue;
44 }
45 add_filter( str_replace( '-', '', $template_type ) . '_template', 'gutenberg_override_query_template', 20, 3 );
46 }
47 }
48 add_action( 'wp_loaded', 'gutenberg_add_template_loader_filters' );
49
50 /**
51 * Get the template hierarchy for a given template type.
52 *
53 * Internally, this filters into the "{$type}_template_hierarchy" hook to record the type-specific template hierarchy.
54 *
55 * @param string $template_type A template type.
56 * @return string[] A list of template candidates, in descending order of priority.
57 */
58 function get_template_hierarchy( $template_type ) {
59 if ( ! in_array( $template_type, get_template_types(), true ) ) {
60 return array();
61 }
62
63 $get_template_function = 'get_' . str_replace( '-', '_', $template_type ) . '_template'; // front-page -> get_front_page_template.
64 $template_hierarchy_filter = str_replace( '-', '', $template_type ) . '_template_hierarchy'; // front-page -> frontpage_template_hierarchy.
65
66 $result = array();
67 $template_hierarchy_filter_function = function( $templates ) use ( &$result ) {
68 $result = $templates;
69 return $templates;
70 };
71
72 add_filter( $template_hierarchy_filter, $template_hierarchy_filter_function, 20, 1 );
73 call_user_func( $get_template_function ); // This invokes template_hierarchy_filter.
74 remove_filter( $template_hierarchy_filter, $template_hierarchy_filter_function, 20 );
75
76 return $result;
77 }
78
79 /**
80 * Filters into the "{$type}_template" hooks to redirect them to the Full Site Editing template canvas.
81 *
82 * Internally, this communicates the block content that needs to be used by the template canvas through a global variable.
83 *
84 * @param string $template Path to the template. See locate_template().
85 * @param string $type Sanitized filename without extension.
86 * @param array $templates A list of template candidates, in descending order of priority.
87 * @return string The path to the Full Site Editing template canvas file.
88 */
89 function gutenberg_override_query_template( $template, $type, array $templates = array() ) {
90 global $_wp_current_template_content;
91
92 $current_template = gutenberg_find_template_post_and_parts( $type, $templates );
93
94 if ( $current_template ) {
95 $_wp_current_template_content = empty( $current_template['template_post']->post_content ) ? __( 'Empty template.', 'gutenberg' ) : $current_template['template_post']->post_content;
96
97 if ( isset( $_GET['_wp-find-template'] ) ) {
98 wp_send_json_success( $current_template['template_post'] );
99 }
100 } else {
101 if ( 'index' === $type ) {
102 if ( isset( $_GET['_wp-find-template'] ) ) {
103 wp_send_json_error( array( 'message' => __( 'No matching template found.', 'gutenberg' ) ) );
104 }
105 } else {
106 return false; // So that the template loader keeps looking for templates.
107 }
108 }
109
110 // Add hooks for template canvas.
111 // Add viewport meta tag.
112 add_action( 'wp_head', 'gutenberg_viewport_meta_tag', 0 );
113
114 // Render title tag with content, regardless of whether theme has title-tag support.
115 remove_action( 'wp_head', '_wp_render_title_tag', 1 ); // Remove conditional title tag rendering...
116 add_action( 'wp_head', 'gutenberg_render_title_tag', 1 ); // ...and make it unconditional.
117
118 // This file will be included instead of the theme's template file.
119 return gutenberg_dir_path() . 'lib/template-canvas.php';
120 }
121
122 /**
123 * Recursively traverses a block tree, creating auto drafts
124 * for any encountered template parts without a fixed post.
125 *
126 * @access private
127 *
128 * @param array $block The root block to start traversing from.
129 * @return int[] A list of template parts IDs for the given block.
130 */
131 function create_auto_draft_for_template_part_block( $block ) {
132 $template_part_ids = array();
133
134 if ( 'core/template-part' === $block['blockName'] && isset( $block['attrs']['slug'] ) ) {
135 if ( isset( $block['attrs']['postId'] ) ) {
136 // Template part is customized.
137 $template_part_id = $block['attrs']['postId'];
138 } else {
139 // A published post might already exist if this template part
140 // was customized elsewhere or if it's part of a customized
141 // template. We also check if an auto-draft was already created
142 // because preloading can make this run twice, so, different code
143 // paths can end up with different posts for the same template part.
144 // E.g. The server could send back post ID 1 to the client, preload,
145 // and create another auto-draft. So, if the client tries to resolve the
146 // post ID from the slug and theme, it won't match with what the server sent.
147 $template_part_query = new WP_Query(
148 array(
149 'post_type' => 'wp_template_part',
150 'post_status' => array( 'publish', 'auto-draft' ),
151 'title' => $block['attrs']['slug'],
152 'meta_key' => 'theme',
153 'meta_value' => $block['attrs']['theme'],
154 'posts_per_page' => 1,
155 'no_found_rows' => true,
156 )
157 );
158 $template_part_post = $template_part_query->have_posts() ? $template_part_query->next_post() : null;
159 if ( $template_part_post && 'auto-draft' !== $template_part_post->post_status ) {
160 $template_part_id = $template_part_post->ID;
161 } else {
162 // Template part is not customized, get it from a file and make an auto-draft for it, unless one already exists
163 // and the underlying file hasn't changed.
164 $template_part_file_path =
165 get_stylesheet_directory() . '/block-template-parts/' . $block['attrs']['slug'] . '.html';
166 if ( ! file_exists( $template_part_file_path ) ) {
167 if ( gutenberg_is_experiment_enabled( 'gutenberg-full-site-editing-demo' ) ) {
168 $template_part_file_path =
169 dirname( __FILE__ ) . '/demo-block-template-parts/' . $block['attrs']['slug'] . '.html';
170 if ( ! file_exists( $template_part_file_path ) ) {
171 $template_part_file_path = false;
172 }
173 } else {
174 $template_part_file_path = false;
175 }
176 }
177
178 if ( $template_part_file_path ) {
179 $file_contents = file_get_contents( $template_part_file_path );
180 if ( $template_part_post && $template_part_post->post_content === $file_contents ) {
181 $template_part_id = $template_part_post->ID;
182 } else {
183 $template_part_id = wp_insert_post(
184 array(
185 'post_content' => $file_contents,
186 'post_title' => $block['attrs']['slug'],
187 'post_status' => 'auto-draft',
188 'post_type' => 'wp_template_part',
189 'post_name' => $block['attrs']['slug'],
190 'meta_input' => array(
191 'theme' => $block['attrs']['theme'],
192 ),
193 )
194 );
195 }
196 }
197 }
198 }
199 $template_part_ids[ $block['attrs']['slug'] ] = $template_part_id;
200 }
201
202 foreach ( $block['innerBlocks'] as $inner_block ) {
203 $template_part_ids = array_merge( $template_part_ids, create_auto_draft_for_template_part_block( $inner_block ) );
204 }
205 return $template_part_ids;
206 }
207
208 /**
209 * Return the correct 'wp_template' post and template part IDs for the current template.
210 *
211 * Accepts an optional $template_hierarchy argument as a hint.
212 *
213 * @param string $template_type The current template type.
214 * @param string[] $template_hierarchy (optional) The current template hierarchy, ordered by priority.
215 * @return null|array {
216 * @type WP_Post|null template_post A template post object, or null if none could be found.
217 * @type int[] A list of template parts IDs for the template.
218 * }
219 */
220 function gutenberg_find_template_post_and_parts( $template_type, $template_hierarchy = array() ) {
221 if ( ! $template_type ) {
222 return null;
223 }
224
225 if ( empty( $template_hierarchy ) ) {
226 if ( 'index' === $template_type ) {
227 $template_hierarchy = get_template_hierarchy( 'index' );
228 } else {
229 $template_hierarchy = array_merge( get_template_hierarchy( $template_type ), get_template_hierarchy( 'index' ) );
230 }
231 }
232
233 $slugs = array_map(
234 'gutenberg_strip_php_suffix',
235 $template_hierarchy
236 );
237
238 // Find most specific 'wp_template' post matching the hierarchy.
239 $template_query = new WP_Query(
240 array(
241 'post_type' => 'wp_template',
242 'post_status' => 'publish',
243 'post_name__in' => $slugs,
244 'orderby' => 'post_name__in',
245 'posts_per_page' => 1,
246 'no_found_rows' => true,
247 )
248 );
249
250 $current_template_post = $template_query->have_posts() ? $template_query->next_post() : null;
251
252 // Build map of template slugs to their priority in the current hierarchy.
253 $slug_priorities = array_flip( $slugs );
254
255 // See if there is a theme block template with higher priority than the resolved template post.
256 $higher_priority_block_template_path = null;
257 $higher_priority_block_template_priority = PHP_INT_MAX;
258 $block_template_files = glob( get_stylesheet_directory() . '/block-templates/*.html' );
259 $block_template_files = is_array( $block_template_files ) ? $block_template_files : array();
260 if ( is_child_theme() ) {
261 $child_block_template_files = glob( get_template_directory() . '/block-templates/*.html' );
262 $child_block_template_files = is_array( $child_block_template_files ) ? $child_block_template_files : array();
263 $block_template_files = array_merge( $block_template_files, $child_block_template_files );
264 }
265 if ( gutenberg_is_experiment_enabled( 'gutenberg-full-site-editing-demo' ) ) {
266 $demo_block_template_files = glob( dirname( __FILE__ ) . '/demo-block-templates/*.html' );
267 $demo_block_template_files = is_array( $demo_block_template_files ) ? $demo_block_template_files : array();
268 $block_template_files = array_merge( $block_template_files, $demo_block_template_files );
269 }
270 foreach ( $block_template_files as $path ) {
271 if ( ! isset( $slug_priorities[ basename( $path, '.html' ) ] ) ) {
272 continue;
273 }
274 $theme_block_template_priority = $slug_priorities[ basename( $path, '.html' ) ];
275 if (
276 $theme_block_template_priority < $higher_priority_block_template_priority &&
277 ( empty( $current_template_post ) || $theme_block_template_priority < $slug_priorities[ $current_template_post->post_name ] )
278 ) {
279 $higher_priority_block_template_path = $path;
280 $higher_priority_block_template_priority = $theme_block_template_priority;
281 }
282 }
283
284 // If there is, use it instead.
285 if ( isset( $higher_priority_block_template_path ) ) {
286 $post_name = basename( $higher_priority_block_template_path, '.html' );
287 $file_contents = file_get_contents( $higher_priority_block_template_path );
288 $current_template_post = array(
289 'post_content' => $file_contents,
290 'post_title' => $post_name,
291 'post_status' => 'auto-draft',
292 'post_type' => 'wp_template',
293 'post_name' => $post_name,
294 );
295 if ( is_admin() || defined( 'REST_REQUEST' ) ) {
296 $template_query = new WP_Query(
297 array(
298 'post_type' => 'wp_template',
299 'post_status' => 'auto-draft',
300 'name' => $post_name,
301 'posts_per_page' => 1,
302 'no_found_rows' => true,
303 )
304 );
305 $current_template_post = $template_query->have_posts() ? $template_query->next_post() : $current_template_post;
306
307 // Only create auto-draft of block template for editing
308 // in admin screens, when necessary, because the underlying
309 // file has changed.
310 if ( is_array( $current_template_post ) || $current_template_post->post_content !== $file_contents ) {
311 if ( ! is_array( $current_template_post ) ) {
312 $current_template_post->post_content = $file_contents;
313 }
314 $current_template_post = get_post(
315 wp_insert_post( $current_template_post )
316 );
317 }
318 } else {
319 $current_template_post = new WP_Post(
320 (object) $current_template_post
321 );
322 }
323 }
324
325 // If we haven't found any template post by here, it means that this theme doesn't even come with a fallback
326 // `index.html` block template. We create one so that people that are trying to access the editor are greeted
327 // with a blank page rather than an error.
328 if ( ! $current_template_post && ( is_admin() || defined( 'REST_REQUEST' ) ) ) {
329 $current_template_post = array(
330 'post_title' => 'index',
331 'post_status' => 'auto-draft',
332 'post_type' => 'wp_template',
333 'post_name' => 'index',
334 );
335 $current_template_post = get_post(
336 wp_insert_post( $current_template_post )
337 );
338 }
339
340 if ( $current_template_post ) {
341 $template_part_ids = array();
342 if ( is_admin() || defined( 'REST_REQUEST' ) ) {
343 foreach ( parse_blocks( $current_template_post->post_content ) as $block ) {
344 $template_part_ids = array_merge( $template_part_ids, create_auto_draft_for_template_part_block( $block ) );
345 }
346 }
347 return array(
348 'template_post' => $current_template_post,
349 'template_part_ids' => $template_part_ids,
350 );
351 }
352 return null;
353 }
354
355 /**
356 * Displays title tag with content, regardless of whether theme has title-tag support.
357 *
358 * @see _wp_render_title_tag()
359 */
360 function gutenberg_render_title_tag() {
361 echo '<title>' . wp_get_document_title() . '</title>' . "\n";
362 }
363
364 /**
365 * Renders the markup for the current template.
366 */
367 function gutenberg_render_the_template() {
368 global $_wp_current_template_content;
369 global $wp_embed;
370
371 if ( ! $_wp_current_template_content ) {
372 echo '<h1>' . esc_html__( 'No matching template found', 'gutenberg' ) . '</h1>';
373 return;
374 }
375
376 $content = $wp_embed->run_shortcode( $_wp_current_template_content );
377 $content = $wp_embed->autoembed( $content );
378 $content = do_blocks( $content );
379 $content = wptexturize( $content );
380 if ( function_exists( 'wp_filter_content_tags' ) ) {
381 $content = wp_filter_content_tags( $content );
382 } else {
383 $content = wp_make_content_images_responsive( $content );
384 }
385 $content = str_replace( ']]>', ']]&gt;', $content );
386
387 // Wrap block template in .wp-site-blocks to allow for specific descendant styles
388 // (e.g. `.wp-site-blocks > *`).
389 echo '<div class="wp-site-blocks">';
390 echo $content; // phpcs:ignore WordPress.Security.EscapeOutput
391 echo '</div>';
392 }
393
394 /**
395 * Renders a 'viewport' meta tag.
396 *
397 * This is hooked into {@see 'wp_head'} to decouple its output from the default template canvas.
398 */
399 function gutenberg_viewport_meta_tag() {
400 echo '<meta name="viewport" content="width=device-width, initial-scale=1" />' . "\n";
401 }
402
403 /**
404 * Strips .php suffix from template file names.
405 *
406 * @access private
407 *
408 * @param string $template_file Template file name.
409 * @return string Template file name without extension.
410 */
411 function gutenberg_strip_php_suffix( $template_file ) {
412 return preg_replace( '/\.php$/', '', $template_file );
413 }
414
415 /**
416 * Extends default editor settings to enable template and template part editing.
417 *
418 * @param array $settings Default editor settings.
419 *
420 * @return array Filtered editor settings.
421 */
422 function gutenberg_template_loader_filter_block_editor_settings( $settings ) {
423 global $post;
424
425 if ( ! $post ) {
426 return $settings;
427 }
428
429 // If this is the Site Editor, auto-drafts for template parts have already been generated
430 // through `filter_rest_wp_template_part_query`, when called via the REST API.
431 if ( isset( $settings['editSiteInitialState'] ) ) {
432 return $settings;
433 }
434
435 // Otherwise, create template part auto-drafts for the edited post.
436 $post = get_post();
437 foreach ( parse_blocks( $post->post_content ) as $block ) {
438 create_auto_draft_for_template_part_block( $block );
439 }
440
441 // TODO: Set editing mode and current template ID for editing modes support.
442 return $settings;
443 }
444 add_filter( 'block_editor_settings', 'gutenberg_template_loader_filter_block_editor_settings' );
445
446 /**
447 * Removes post details from block context when rendering a block template.
448 *
449 * @param array $context Default context.
450 *
451 * @return array Filtered context.
452 */
453 function gutenberg_template_render_without_post_block_context( $context ) {
454 /*
455 * When loading a template or template part directly and not through a page
456 * that resolves it, the top-level post ID and type context get set to that
457 * of the template part. Templates are just the structure of a site, and
458 * they should not be available as post context because blocks like Post
459 * Content would recurse infinitely.
460 */
461 if ( isset( $context['postType'] ) &&
462 ( 'wp_template' === $context['postType'] || 'wp_template_part' === $context['postType'] ) ) {
463 unset( $context['postId'] );
464 unset( $context['postType'] );
465 }
466
467 return $context;
468 }
469 add_filter( 'render_block_context', 'gutenberg_template_render_without_post_block_context' );
470