PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.15
Yatra – Travel Booking & Tour Operator Software v3.0.15
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / app / Core / Template / FseTemplates.php

FseTemplates.php in Yatra – Travel Booking & Tour Operator Software 3.0.15, at app/Core/Template/FseTemplates.php

469 lines 17.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Core\Template;
6
7 use Yatra\Core\Routing\PageContext;
8
9 /**
10 * FseTemplates
11 *
12 * Make Yatra pages first-class citizens in Full Site Editing:
13 *
14 * - Registers virtual block templates (Single Trip, Trip Listing, Booking,
15 * etc.) so admins can find and edit them in Appearance → Editor → Templates.
16 *
17 * - Registers a server-side `yatra/page-content` block that, when present in
18 * a block template, defers to the PHP template selected by Yatra's router.
19 * This lets an admin reorder/replace header/footer/sidebar around Yatra
20 * content using normal FSE tools.
21 *
22 * - When an admin saves a customisation in the Site Editor, WordPress core
23 * stores it as a `wp_template` post with `source = 'custom'`. We detect
24 * that via `should_render_block_template()` and let WP render the block
25 * template instead of our PHP template — so user edits actually take
26 * effect, without us needing to maintain a separate FSE rendering path.
27 *
28 * The class is intentionally inert on classic (non-block) themes: every hook
29 * short-circuits early via wp_is_block_theme() so there is zero cost when not
30 * applicable.
31 *
32 * @package Yatra\Core\Template
33 */
34 final class FseTemplates
35 {
36 /**
37 * Plugin slug used as the namespace for block templates we register.
38 * WordPress stores customisations against `<theme>//<slug>` for theme
39 * templates and `<plugin>//<slug>` for plugin templates.
40 */
41 public const NAMESPACE_SLUG = 'yatra';
42
43 /**
44 * True while a Yatra PHP template is being executed as the body of the
45 * `yatra/page-content` server block (i.e. nested inside a block-template
46 * canvas). yatra_get_header() / yatra_get_footer() consult this so they
47 * don't re-emit <doctype>/<html>/<head>/<body>/header/footer chrome that
48 * the canvas already owns.
49 */
50 private static bool $insideCanvas = false;
51
52 public static function isRenderingInsideCanvas(): bool
53 {
54 return self::$insideCanvas;
55 }
56
57 /**
58 * Map of virtual template slug → metadata.
59 * Adding a new entry here exposes a new editable layout in the Site Editor.
60 */
61 private const TEMPLATES = [
62 'yatra-single-trip' => [
63 'title' => 'Yatra: Single Trip',
64 'description' => 'Layout used for individual trip detail pages.',
65 'page_type' => 'trip',
66 'php_template' => 'single-trip',
67 'is_singular' => true,
68 ],
69 'yatra-trip-listing' => [
70 'title' => 'Yatra: Trip Listing',
71 'description' => 'Layout used for the main trip archive.',
72 'page_type' => 'listing-trip',
73 'php_template' => 'listing-trip',
74 'is_singular' => false,
75 ],
76 'yatra-destination' => [
77 'title' => 'Yatra: Destination',
78 'description' => 'Layout used for destination taxonomy pages.',
79 'page_type' => 'taxonomy-destination',
80 'php_template' => 'single-taxonomy',
81 'is_singular' => false,
82 ],
83 'yatra-activity' => [
84 'title' => 'Yatra: Activity',
85 'description' => 'Layout used for activity taxonomy pages.',
86 'page_type' => 'taxonomy-activity',
87 'php_template' => 'single-taxonomy',
88 'is_singular' => false,
89 ],
90 'yatra-booking' => [
91 'title' => 'Yatra: Booking',
92 'description' => 'Layout used for the booking flow page.',
93 'page_type' => 'booking',
94 'php_template' => 'booking',
95 'is_singular' => true,
96 ],
97 'yatra-booking-confirmation' => [
98 'title' => 'Yatra: Booking Confirmation',
99 'description' => 'Layout used for the booking confirmation page.',
100 'page_type' => 'booking-confirmation',
101 'php_template' => 'booking-confirmation',
102 'is_singular' => true,
103 ],
104 'yatra-account' => [
105 'title' => 'Yatra: My Account',
106 'description' => 'Layout used for the customer account dashboard.',
107 'page_type' => 'account',
108 'php_template' => 'account-page',
109 'is_singular' => true,
110 ],
111 ];
112
113 public static function init(): void
114 {
115 add_action('init', [self::class, 'registerBlock']);
116 add_action('enqueue_block_editor_assets', [self::class, 'enqueueEditorAssets']);
117 add_filter('get_block_templates', [self::class, 'injectVirtualTemplates'], 10, 3);
118 add_filter('get_block_template', [self::class, 'resolveVirtualTemplate'], 10, 3);
119 }
120
121 /**
122 * Enqueue the JS that registers `yatra/page-content` in the block editor.
123 *
124 * Without this, the editor's JS-side block registry doesn't know about the
125 * block, the deserializer fails on `<!-- wp:yatra/page-content /-->` markup
126 * inside saved templates, and the Site Editor shows the "Your site doesn't
127 * include support for the X block" warning.
128 */
129 public static function enqueueEditorAssets(): void
130 {
131 $handle = 'yatra-block-page-content';
132 $relPath = 'assets/js/blocks/page-content.js';
133 $absPath = YATRA_PLUGIN_PATH . $relPath;
134 if (!is_readable($absPath)) {
135 return;
136 }
137 wp_enqueue_script(
138 $handle,
139 YATRA_PLUGIN_URL . $relPath,
140 ['wp-blocks', 'wp-element', 'wp-block-editor', 'wp-i18n'],
141 (string) filemtime($absPath),
142 true
143 );
144 if (function_exists('wp_set_script_translations')) {
145 wp_set_script_translations($handle, 'yatra');
146 }
147 }
148
149 /**
150 * Server-side render: emit the Yatra PHP template inline.
151 *
152 * The template path comes from PageContext (set by the router) when
153 * available; otherwise the block's `template` attribute is used as a
154 * fallback so the block can be previewed in the editor with explicit
155 * configuration.
156 *
157 * Output buffering captures the included template safely even if it
158 * echoes directly.
159 *
160 * @param array $attributes Block attributes (supports 'template').
161 * @return string Rendered HTML.
162 */
163 public static function renderPageContentBlock(array $attributes): string
164 {
165 $ctx = PageContext::instance();
166 $path = $ctx->getTemplate();
167
168 if (($path === null || $path === '') && !empty($attributes['template'])) {
169 $name = ltrim((string) $attributes['template'], '/');
170 if (substr($name, -4) !== '.php') {
171 $name .= '.php';
172 }
173 $candidate = YATRA_PLUGIN_PATH . 'templates/' . $name;
174 if (is_readable($candidate)) {
175 $path = $candidate;
176 }
177 }
178
179 if ($path === null || $path === '' || !is_readable($path)) {
180 return '';
181 }
182
183 // Mark "inside canvas" for the duration of the include so that
184 // yatra_get_header() / yatra_get_footer() inside the template don't
185 // re-emit document chrome. try/finally guarantees the flag is reset
186 // even if the template throws.
187 $previous = self::$insideCanvas;
188 self::$insideCanvas = true;
189 ob_start();
190 try {
191 include $path;
192 } finally {
193 self::$insideCanvas = $previous;
194 }
195 return (string) ob_get_clean();
196 }
197
198 /**
199 * Register the dynamic block. Safe to call on every request — register_block_type
200 * is idempotent against re-registration of the same name (it returns false).
201 */
202 public static function registerBlock(): void
203 {
204 if (!function_exists('register_block_type')) {
205 return;
206 }
207 if (\WP_Block_Type_Registry::get_instance()->is_registered('yatra/page-content')) {
208 return;
209 }
210 register_block_type('yatra/page-content', [
211 'api_version' => 2,
212 'title' => 'Yatra Page Content',
213 'description' => 'Renders the current Yatra page (single trip, listing, booking, etc.) inside a block template.',
214 'category' => 'theme',
215 'supports' => [
216 'html' => false,
217 'reusable' => false,
218 ],
219 'attributes' => [
220 'template' => [
221 'type' => 'string',
222 'default' => '',
223 ],
224 ],
225 'render_callback' => [self::class, 'renderPageContentBlock'],
226 ]);
227 }
228
229 /**
230 * Inject virtual templates into the Site Editor's template list.
231 * Skipped for classic themes — they don't use block templates.
232 *
233 * @param \WP_Block_Template[] $query_result
234 * @param array $query
235 * @param string $template_type 'wp_template' or 'wp_template_part'.
236 * @return \WP_Block_Template[]
237 */
238 public static function injectVirtualTemplates(array $query_result, array $query, string $template_type): array
239 {
240 if ($template_type !== 'wp_template' || !function_exists('wp_is_block_theme') || !wp_is_block_theme()) {
241 return $query_result;
242 }
243
244 $existing = [];
245 foreach ($query_result as $tpl) {
246 if (isset($tpl->slug)) {
247 $existing[$tpl->slug] = true;
248 }
249 }
250
251 foreach (self::TEMPLATES as $slug => $meta) {
252 if (isset($existing[$slug])) {
253 continue; // User has already customised this template.
254 }
255 // Respect query filters (slug__in, post_type, etc.) the editor passes.
256 if (!empty($query['slug__in']) && !in_array($slug, (array) $query['slug__in'], true)) {
257 continue;
258 }
259 $query_result[] = self::buildVirtualTemplate($slug, $meta);
260 }
261
262 return $query_result;
263 }
264
265 /**
266 * Resolve a single template by id (`<namespace>//<slug>`) — used when an
267 * admin opens a Yatra template directly in the Site Editor.
268 *
269 * @param \WP_Block_Template|null $template
270 * @param string $id
271 * @param string $template_type
272 * @return \WP_Block_Template|null
273 */
274 public static function resolveVirtualTemplate($template, string $id, string $template_type)
275 {
276 if ($template !== null || $template_type !== 'wp_template') {
277 return $template;
278 }
279 if (!function_exists('wp_is_block_theme') || !wp_is_block_theme()) {
280 return $template;
281 }
282
283 // id format: "<namespace>//<slug>"
284 $parts = explode('//', $id, 2);
285 if (count($parts) !== 2) {
286 return $template;
287 }
288 [$ns, $slug] = $parts;
289 if ($ns !== self::NAMESPACE_SLUG) {
290 return $template;
291 }
292 if (!isset(self::TEMPLATES[$slug])) {
293 return $template;
294 }
295 return self::buildVirtualTemplate($slug, self::TEMPLATES[$slug]);
296 }
297
298 /**
299 * If the admin has saved a Site Editor customisation for the given Yatra
300 * page type, prime the block-template canvas globals with that customised
301 * content and return the absolute path to template-canvas.php so the
302 * caller can return it from the `template_include` filter.
303 *
304 * Implementation note: we deliberately avoid get_block_templates() here.
305 * Core silently restricts that query to wp_theme = get_stylesheet(), but
306 * plugin-namespaced template customisations are saved under wp_theme =
307 * '<plugin-namespace>' (e.g. 'yatra'), so the core query misses them. We
308 * query the wp_template post type directly and accept matches against the
309 * plugin namespace OR the active theme — whichever Gutenberg ended up
310 * using when the admin saved their customisation.
311 *
312 * Returns null when:
313 * - not on a block theme
314 * - this page type has no virtual template
315 * - no `wp_template` post exists for the slug under either namespace
316 */
317 public static function loadCustomisedCanvas(string $pageType): ?string
318 {
319 if (!function_exists('wp_is_block_theme') || !wp_is_block_theme()) {
320 return null;
321 }
322 $slug = self::slugForPageType($pageType);
323 if ($slug === null) {
324 return null;
325 }
326
327 $post = self::findCustomisedTemplatePost($slug);
328 if ($post === null) {
329 return null;
330 }
331
332 $content = (string) $post->post_content;
333 if ($content === '') {
334 return null;
335 }
336
337 // template-canvas.php reads these two globals and renders the blocks.
338 global $_wp_current_template_content, $_wp_current_template_id;
339 $_wp_current_template_id = self::NAMESPACE_SLUG . '//' . $slug;
340 $_wp_current_template_content = $content;
341
342 $canvas = ABSPATH . WPINC . '/template-canvas.php';
343 return is_readable($canvas) ? $canvas : null;
344 }
345
346 /**
347 * Find a saved wp_template customisation for the given slug.
348 *
349 * Looks for a published `wp_template` post with matching post_name whose
350 * wp_theme term is either the plugin namespace ('yatra') or the active
351 * theme. The plugin namespace is checked first because that's where
352 * Gutenberg saves customisations of plugin-registered templates; the
353 * stylesheet fallback handles WP versions/configurations that map the
354 * customisation onto the active theme instead.
355 */
356 private static function findCustomisedTemplatePost(string $slug): ?\WP_Post
357 {
358 $namespaces = [self::NAMESPACE_SLUG];
359 if (function_exists('get_stylesheet')) {
360 $stylesheet = (string) get_stylesheet();
361 if ($stylesheet !== '' && $stylesheet !== self::NAMESPACE_SLUG) {
362 $namespaces[] = $stylesheet;
363 }
364 }
365
366 $query = new \WP_Query([
367 'post_type' => 'wp_template',
368 'name' => $slug,
369 'post_status' => ['publish', 'auto-draft'],
370 'posts_per_page' => -1,
371 'no_found_rows' => true,
372 'ignore_sticky_posts' => true,
373 'suppress_filters' => true,
374 'tax_query' => [
375 [
376 'taxonomy' => 'wp_theme',
377 'field' => 'name',
378 'terms' => $namespaces,
379 ],
380 ],
381 ]);
382
383 if (empty($query->posts)) {
384 return null;
385 }
386
387 // If multiple matches, prefer one whose wp_theme term equals the
388 // plugin namespace (most precise) — that's where Gutenberg saves
389 // plugin-registered template customisations.
390 $preferred = null;
391 foreach ($query->posts as $post) {
392 $terms = wp_get_post_terms($post->ID, 'wp_theme', ['fields' => 'names']);
393 if (is_wp_error($terms)) {
394 continue;
395 }
396 if (in_array(self::NAMESPACE_SLUG, $terms, true)) {
397 return $post;
398 }
399 if ($preferred === null) {
400 $preferred = $post;
401 }
402 }
403
404 return $preferred;
405 }
406
407 /**
408 * @internal Public so TemplateLoader can map page-type → slug for logging.
409 */
410 public static function slugForPageType(string $pageType): ?string
411 {
412 foreach (self::TEMPLATES as $slug => $meta) {
413 if (($meta['page_type'] ?? '') === $pageType) {
414 return $slug;
415 }
416 }
417 return null;
418 }
419
420 /**
421 * Build a WP_Block_Template object that wraps the theme's header/footer
422 * template parts around the `yatra/page-content` server-side block.
423 *
424 * @param string $slug
425 * @param array $meta
426 */
427 private static function buildVirtualTemplate(string $slug, array $meta): \WP_Block_Template
428 {
429 $tpl = new \WP_Block_Template();
430 $tpl->type = 'wp_template';
431 $tpl->theme = self::NAMESPACE_SLUG;
432 $tpl->slug = $slug;
433 $tpl->id = self::NAMESPACE_SLUG . '//' . $slug;
434 $tpl->title = $meta['title'];
435 $tpl->description = $meta['description'];
436 $tpl->source = 'plugin';
437 $tpl->origin = 'plugin';
438 $tpl->status = 'publish';
439 $tpl->has_theme_file = false;
440 $tpl->is_custom = false;
441 $tpl->author = null;
442 $tpl->content = self::defaultBlockTemplateMarkup($meta);
443
444 return $tpl;
445 }
446
447 /**
448 * Default block-template body: header part → page-content → footer part.
449 * Authors can rearrange in the editor; their version supersedes this.
450 */
451 private static function defaultBlockTemplateMarkup(array $meta): string
452 {
453 $phpTemplate = (string) ($meta['php_template'] ?? '');
454 $alignment = !empty($meta['is_singular']) ? 'full' : 'wide';
455
456 return implode("\n", [
457 '<!-- wp:template-part {"slug":"header","theme":"' . esc_attr(get_stylesheet()) . '","tagName":"header"} /-->',
458 '',
459 '<!-- wp:group {"tagName":"main","align":"' . esc_attr($alignment) . '","layout":{"type":"constrained"}} -->',
460 '<main class="wp-block-group align' . esc_attr($alignment) . ' yatra-page-content-wrap">',
461 '<!-- wp:yatra/page-content {"template":"' . esc_attr($phpTemplate) . '"} /-->',
462 '</main>',
463 '<!-- /wp:group -->',
464 '',
465 '<!-- wp:template-part {"slug":"footer","theme":"' . esc_attr(get_stylesheet()) . '","tagName":"footer"} /-->',
466 ]);
467 }
468 }
469