PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 2.0.11 All 82 releases
yatra / app / Core / TemplateLoader.php

TemplateLoader.php in Yatra – Travel Booking & Tour Operator Software trunk, at app/Core/TemplateLoader.php

512 lines 18.8 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;
6
7 use Yatra\Core\Routing\Router;
8 use Yatra\Core\Routing\PermalinkCanonical;
9 use Yatra\Core\Routing\UrlParser;
10 use Yatra\Core\Routing\PageContext;
11 use Yatra\Core\Template\FseTemplates;
12 use Yatra\Services\SettingsService;
13 use Yatra\Core\Handlers\BookingConfirmationPageHandler;
14
15 /**
16 * Template Loader
17 *
18 * Handles all frontend template loading and routing for Yatra pages.
19 * Uses a modern, modular architecture with dedicated routing and asset management.
20 *
21 * @package Yatra\Core
22 * @since 3.0.0
23 */
24 class TemplateLoader
25 {
26 /**
27 * Router instance
28 */
29 private static ?Router $router = null;
30
31 /**
32 * Initialize template loading hooks
33 */
34 public static function init(): void
35 {
36 // Initialize rewrite rules and query vars first
37 add_action('init', [self::class, 'addTripRewriteRules'], 10);
38 add_filter('query_vars', [self::class, 'addCustomQueryVars']);
39
40 // FSE Site Editor integration — virtual block templates + page-content block.
41 // No-ops on classic themes.
42 FseTemplates::init();
43
44 // Prevent WP::handle_404() from marking Yatra URLs as 404 in the first place.
45 // This is the proper WordPress way — fires before status_header(404) is sent and
46 // before FSE locate_block_template() reads is_404() to pick 404.html.
47 add_filter('pre_handle_404', [self::class, 'preventCore404'], 10, 2);
48
49 // Strip the `error404` body class for Yatra requests so FSE themes don't apply
50 // 404-specific styling to plugin pages.
51 add_filter('body_class', [self::class, 'filterBodyClass'], 20);
52
53 add_action('template_redirect', [PermalinkCanonical::class, 'enforce'], 0);
54
55 // Initialize the main router for template handling. Handlers configure
56 // $wp_query + a virtual WP_Post here and stash the chosen template in
57 // PageContext; they do NOT include or exit, so the full template-loader
58 // pipeline (template hierarchy resolution, wp_head, wp_footer, plugin
59 // template_include hooks) continues to run normally.
60 add_action('template_redirect', [self::class, 'handleTemplateRedirect'], 1);
61
62 // Hand off to WordPress's template-loader: if a Yatra handler queued a
63 // template via PageContext, swap it in here. Priority 99 ensures we run
64 // after FSE's locate_block_template() (default priority 10) so we win
65 // over the theme's block-template choice without disabling FSE for the
66 // rest of the site.
67 add_filter('template_include', [self::class, 'filterTemplateInclude'], 99);
68 }
69
70 /**
71 * Short-circuit WP::handle_404() for Yatra-owned URLs.
72 *
73 * Returning true tells WordPress core to skip setting is_404 / status_header(404).
74 * This keeps FSE/block themes from resolving to 404.html and pulling the wrong
75 * header template part. The Router still runs on template_redirect to actually
76 * render the page; this filter only prevents the premature 404 marking.
77 *
78 * @param bool $preempt Whether to short-circuit default 404 handling.
79 * @param \WP_Query $wp_query The main WP_Query instance (unused — detection uses globals).
80 * @return bool
81 */
82 public static function preventCore404($preempt, /** @noinspection PhpUnusedParameterInspection */ $wp_query)
83 {
84 if ($preempt) {
85 return $preempt;
86 }
87 if (!self::isYatraRequest()) {
88 return $preempt;
89 }
90 // Mirror what WP::handle_404() does on success: do NOT set is_404, but ensure
91 // status header is 200 so downstream caches / CDNs behave correctly.
92 status_header(200);
93 nocache_headers();
94 return true;
95 }
96
97 /**
98 * Remove the `error404` body class for Yatra pages so FSE themes render normal page chrome.
99 *
100 * @param array $classes
101 * @return array
102 */
103 public static function filterBodyClass(array $classes): array
104 {
105 $ctx = PageContext::instance();
106 $isYatra = self::isYatraRequest() || $ctx->isHandled();
107 if (!$isYatra) {
108 return $classes;
109 }
110
111 $classes = array_values(array_filter($classes, static function ($c) {
112 return $c !== 'error404' && $c !== 'error-404';
113 }));
114
115 if (!in_array('yatra-page', $classes, true)) {
116 $classes[] = 'yatra-page';
117 }
118
119 foreach ($ctx->getBodyClasses() as $extra) {
120 if (!in_array($extra, $classes, true)) {
121 $classes[] = $extra;
122 }
123 }
124
125 return $classes;
126 }
127
128 /**
129 * Public detection helper — true when the current request belongs to a Yatra route.
130 * Centralises the logic previously in shouldClear404ForYatraRouting() so it can be
131 * reused by pre_handle_404 and body_class filters.
132 */
133 public static function isYatraRequest(): bool
134 {
135 return self::shouldClear404ForYatraRouting();
136 }
137
138 /**
139 * `template_include` filter — choose between PHP template and FSE block template.
140 *
141 * Decision flow when a Yatra handler has queued a template:
142 * 1. If the admin has customised the matching virtual block template in the
143 * Site Editor (wp_template post with source = 'custom'), defer to FSE —
144 * WP renders the customised block template, which embeds Yatra content
145 * via the `yatra/page-content` server block. Their edits take effect.
146 * 2. Otherwise return Yatra's PHP template. It's faster, cache-friendly,
147 * and the path the plugin tested most.
148 *
149 * For non-Yatra requests, return $template unchanged so the theme/FSE keep control.
150 */
151 public static function filterTemplateInclude(string $template): string
152 {
153 $ctx = PageContext::instance();
154 if (!$ctx->hasTemplate()) {
155 return $template;
156 }
157
158 // If the admin has saved a Site-Editor customisation for this page
159 // type, render via WordPress's block-template canvas with the saved
160 // content. loadCustomisedCanvas() returns null if no customisation
161 // exists (the common case) or returns template-canvas.php with the
162 // canvas globals primed.
163 $pageType = $ctx->getPageType();
164 if ($pageType !== '') {
165 $canvas = FseTemplates::loadCustomisedCanvas($pageType);
166 if ($canvas !== null) {
167 return $canvas;
168 }
169 }
170
171 $selected = $ctx->getTemplate();
172 return $selected !== null ? $selected : $template;
173 }
174
175 /**
176 * Handle template_redirect:
177 * 1. Clear residual is_404 (paged-home quirk; pre_handle_404 catches the rest).
178 * 2. Run the Router — handlers configure $wp_query and queue a template.
179 * 3. Fall through to WordPress's template-loader. Our template_include
180 * filter at priority 99 swaps in the Yatra template if one was queued.
181 *
182 * No include + exit here — that was the source of the FSE breakage.
183 */
184 public static function handleTemplateRedirect(): void
185 {
186 global $wp_query;
187
188 if (!empty($wp_query->is_404) && self::shouldClear404ForYatraRouting()) {
189 $wp_query->is_404 = false;
190 status_header(200);
191 }
192
193 if (!empty($wp_query->is_404)) {
194 return;
195 }
196
197 if (!self::$router) {
198 self::$router = new Router();
199 }
200
201 $handled = self::$router->route();
202
203 // Plain-permalink fallback: ?yatra_booking_confirmation=... still needs
204 // an explicit dispatch because PlainPageMatcher doesn't know about it.
205 if (!$handled) {
206 $confirmationId = get_query_var('yatra_booking_confirmation')
207 ?: ($_GET['yatra_booking_confirmation'] ?? ($_GET['reference'] ?? ($_GET['booking_id'] ?? '')));
208 if (!empty($confirmationId)) {
209 $handler = new BookingConfirmationPageHandler();
210 $handler->handle([
211 'confirmation_id' => sanitize_text_field((string) $confirmationId),
212 ]);
213 }
214 }
215
216 // No exit. The selected Yatra template (if any) is in PageContext;
217 // filterTemplateInclude() will return it from the template_include filter.
218 }
219
220 /**
221 * True when this request should be routed by Yatra even if WP marked it 404 (paged home quirk).
222 */
223 private static function shouldClear404ForYatraRouting(): bool
224 {
225 if (is_admin() || wp_doing_ajax() || wp_doing_cron()) {
226 return false;
227 }
228
229 $yatraPage = isset($_GET['yatra_page']) ? trim((string) wp_unslash($_GET['yatra_page'])) : '';
230 if ($yatraPage !== '') {
231 return true;
232 }
233
234 $qv = (string) get_query_var('yatra_page');
235 if ($qv !== '') {
236 return true;
237 }
238
239 // Plain trip / taxonomy slug keys (same idea as {@see PermalinkCanonical::requestHasPlainYatraRoutingQuery})
240 $tripKey = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) SettingsService::getTripBase()) ?: 'trip';
241 if (isset($_GET[$tripKey]) && is_string($_GET[$tripKey]) && trim(wp_unslash($_GET[$tripKey])) !== '') {
242 return true;
243 }
244
245 foreach (
246 [
247 SettingsService::getDestinationBase(),
248 SettingsService::getActivityBase(),
249 SettingsService::getTripCategoryBase(),
250 ] as $base
251 ) {
252 $bk = preg_replace('/[^a-zA-Z0-9_-]/', '', $base) ?: '';
253 if ($bk === '' || $bk === $tripKey) {
254 continue;
255 }
256 if (isset($_GET[$bk]) && is_string($_GET[$bk]) && trim(wp_unslash($_GET[$bk])) !== '') {
257 return true;
258 }
259 }
260
261 foreach (['yatra_trip', 'yatra_trip_slug', 'yatra_destination_slug', 'yatra_activity_slug', 'yatra_category_slug', 'yatra_booking_confirmation', 'yatra_verify_email'] as $key) {
262 if (!isset($_GET[$key])) {
263 continue;
264 }
265 $v = wp_unslash($_GET[$key]);
266 if (is_string($v) && trim($v) !== '') {
267 return true;
268 }
269 }
270
271 if ((string) get_query_var('yatra_verify_email') !== '') {
272 return true;
273 }
274
275 $verifyPath = trim(UrlParser::getCleanRequestPath(), '/');
276 $verifyPrefix = SettingsService::getPermalinkBases()['email_verification_prefix'];
277 if ($verifyPath !== '' && strpos($verifyPath, $verifyPrefix . '/') === 0) {
278 return true;
279 }
280
281 return (bool) apply_filters('yatra_clear_404_for_routing', false);
282 }
283
284 /**
285 * Add rewrite rules for trip permalinks and listing pages
286 */
287 public static function addTripRewriteRules(): void
288 {
289 $bases = SettingsService::getPermalinkBases();
290 $trip_base = $bases['trip_base'];
291 $booking_base = $bases['booking_base'];
292 $account_base = $bases['account_base'];
293 $destination_base = $bases['destination_base'];
294 $activity_base = $bases['activity_base'];
295 $trip_category_base = $bases['trip_category_base'];
296 $bookingConfirmSeg = $bases['booking_flow_confirmation_segment'];
297 $legacyBookingConfirmation = $bases['legacy_booking_confirmation_prefix'];
298 $remainingCheckout = $bases['remaining_checkout_prefix'];
299 $emailVerifyPrefix = $bases['email_verification_prefix'];
300
301 // Add query vars first (must be registered before rewrite rules)
302 // Single-trip slug query var matches trip URL base (e.g. trip=, tours=)
303 add_rewrite_tag('%' . $trip_base . '%', '([^&]+)');
304 add_rewrite_tag('%yatra_booking_confirmation%', '([^&]+)');
305 add_rewrite_tag('%yatra_remaining_checkout%', '([^&]+)');
306 add_rewrite_tag('%yatra_verify_email%', '([^&]+)');
307 // Single taxonomy page tags
308 add_rewrite_tag('%yatra_destination_slug%', '([^&]+)');
309 add_rewrite_tag('%yatra_activity_slug%', '([^&]+)');
310 add_rewrite_tag('%yatra_category_slug%', '([^&]+)');
311 add_rewrite_tag('%yatra_page%', '([a-zA-Z0-9_-]+)');
312 add_rewrite_tag('%paged%', '([0-9]+)');
313
314 // Add rewrite rule for email verification: /{email_verification_prefix}/{token}/
315 add_rewrite_rule(
316 '^' . $emailVerifyPrefix . '/([a-zA-Z0-9_-]+)/?$',
317 'index.php?yatra_verify_email=$matches[1]',
318 'top'
319 );
320
321 // Trip listing pagination: {trip_base}/page/{n}/
322 add_rewrite_rule(
323 '^' . $trip_base . '/page/([0-9]+)/?$',
324 'index.php?yatra_page=' . $trip_base . '&paged=$matches[1]',
325 'top'
326 );
327
328 // Trip listing (page 1): {trip_base}/
329 add_rewrite_rule(
330 '^' . $trip_base . '/?$',
331 'index.php?yatra_page=' . $trip_base,
332 'top'
333 );
334
335 // Customer account (pageless): /{account_base}/ and /{account_base}/{tab}/
336 // Must be real rewrite rules so WordPress doesn't 404 before Yatra Router runs.
337 add_rewrite_rule(
338 '^' . $account_base . '/?$',
339 'index.php?yatra_page=' . $account_base . '&yatra_account_page=dashboard',
340 'top'
341 );
342
343 add_rewrite_rule(
344 '^' . $account_base . '/([^/]+)/?$',
345 'index.php?yatra_page=' . $account_base . '&yatra_account_page=$matches[1]',
346 'top'
347 );
348
349 // Single trip: {trip_base}/{trip_slug}/
350 add_rewrite_rule(
351 '^' . $trip_base . '/([^/]+)/?$',
352 'index.php?yatra_page=' . $trip_base . '&' . $trip_base . '=$matches[1]',
353 'top'
354 );
355
356 // Taxonomy: single + pagination (yatra_page = base from settings; paged = WP pagination)
357 add_rewrite_rule(
358 '^' . $destination_base . '/([^/]+)/page/([0-9]+)/?$',
359 'index.php?yatra_page=' . $destination_base . '&yatra_destination_slug=$matches[1]&paged=$matches[2]',
360 'top'
361 );
362
363 add_rewrite_rule(
364 '^' . $destination_base . '/([^/]+)/?$',
365 'index.php?yatra_page=' . $destination_base . '&yatra_destination_slug=$matches[1]',
366 'top'
367 );
368
369 add_rewrite_rule(
370 '^' . $destination_base . '/?$',
371 'index.php?yatra_page=' . $destination_base,
372 'top'
373 );
374
375 add_rewrite_rule(
376 '^' . $activity_base . '/([^/]+)/page/([0-9]+)/?$',
377 'index.php?yatra_page=' . $activity_base . '&yatra_activity_slug=$matches[1]&paged=$matches[2]',
378 'top'
379 );
380
381 add_rewrite_rule(
382 '^' . $activity_base . '/([^/]+)/?$',
383 'index.php?yatra_page=' . $activity_base . '&yatra_activity_slug=$matches[1]',
384 'top'
385 );
386
387 add_rewrite_rule(
388 '^' . $activity_base . '/?$',
389 'index.php?yatra_page=' . $activity_base,
390 'top'
391 );
392
393 add_rewrite_rule(
394 '^' . $trip_category_base . '/([^/]+)/page/([0-9]+)/?$',
395 'index.php?yatra_page=' . $trip_category_base . '&yatra_category_slug=$matches[1]&paged=$matches[2]',
396 'top'
397 );
398
399 add_rewrite_rule(
400 '^' . $trip_category_base . '/([^/]+)/?$',
401 'index.php?yatra_page=' . $trip_category_base . '&yatra_category_slug=$matches[1]',
402 'top'
403 );
404
405 add_rewrite_rule(
406 '^' . $trip_category_base . '/?$',
407 'index.php?yatra_page=' . $trip_category_base,
408 'top'
409 );
410
411 // Pageless booking confirmation: /{booking_base}/{confirmation_segment}/{reference}/ (before trip slug rule)
412 add_rewrite_rule(
413 '^' . $booking_base . '/' . $bookingConfirmSeg . '/([a-zA-Z0-9_-]+)/?$',
414 'index.php?yatra_booking_confirmation=$matches[1]',
415 'top'
416 );
417
418 // Booking with trip slug: /{booking_base}/{trip}/
419 add_rewrite_rule(
420 '^' . $booking_base . '/([^/]+)/?$',
421 'index.php?yatra_page=' . $booking_base . '&trip=$matches[1]',
422 'top'
423 );
424
425 // Booking hub (Settings → booking base, e.g. /book/)
426 add_rewrite_rule(
427 '^' . $booking_base . '/?$',
428 'index.php?yatra_page=' . $booking_base,
429 'top'
430 );
431
432 // Legacy booking confirmation: /{legacy_booking_confirmation_prefix}/{reference}
433 add_rewrite_rule(
434 '^' . $legacyBookingConfirmation . '/([a-zA-Z0-9_-]+)/?$',
435 'index.php?yatra_booking_confirmation=$matches[1]',
436 'top'
437 );
438
439 // Remaining checkout: /{remaining_checkout_prefix}/{token}/
440 add_rewrite_rule(
441 '^' . $remainingCheckout . '/([a-zA-Z0-9_-]+)/?$',
442 'index.php?yatra_remaining_checkout=$matches[1]',
443 'top'
444 );
445
446 /**
447 * Fires after Yatra registers its core rewrite tags/rules.
448 *
449 * Use {@see \Yatra\Services\SettingsService::getPermalinkBases()} for the same slugs/helpers use.
450 *
451 * @param array<string, string> $bases
452 */
453 do_action('yatra_register_rewrite_rules', $bases);
454
455 // Check if rewrite rules need flushing (only flush once after plugin update/activation)
456 $rewrite_version = get_option('yatra_rewrite_rules_version', '0');
457 $current_version = '1.0.9'; // Increment this when rewrite rules change
458 if ($rewrite_version !== $current_version) {
459 flush_rewrite_rules(false);
460 update_option('yatra_rewrite_rules_version', $current_version);
461 }
462 }
463
464 /**
465 * Add custom query vars for Yatra functionality
466 */
467 public static function addCustomQueryVars(array $vars): array
468 {
469 $yatra_vars = [
470 'yatra_trip', // legacy plain/pretty query var for trip slug
471 'yatra_booking_confirmation',
472 'yatra_remaining_checkout',
473 'yatra_verify_email',
474 'yatra_account_page',
475 'yatra_destination_slug',
476 'yatra_activity_slug',
477 'yatra_category_slug',
478 'yatra_page',
479 'paged',
480 ];
481
482 // Add dynamic base names for plain permalink support
483 $pb = SettingsService::getPermalinkBases();
484 $yatra_vars[] = $pb['trip_base'];
485 $yatra_vars[] = $pb['destination_base'];
486 $yatra_vars[] = $pb['activity_base'];
487 $yatra_vars[] = $pb['trip_category_base'];
488
489 return array_merge($vars, $yatra_vars);
490 }
491
492 /**
493 * Get client IP address
494 */
495 private static function getClientIp(): string
496 {
497 $ip_keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'];
498
499 foreach ($ip_keys as $key) {
500 if (!empty($_SERVER[$key])) {
501 $ips = explode(',', $_SERVER[$key]);
502 $ip = trim($ips[0]);
503 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
504 return $ip;
505 }
506 }
507 }
508
509 return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
510 }
511 }
512