PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 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 All 84 releases
← All changes | app/Core/Handlers/BasePageHandler.php +192 -1 3.0.2.8 → 3.0.16 View file →
@@ -3,8 +3,11 @@
3 3 declare(strict_types=1);
4 4
5 5 namespace Yatra\Core\Handlers;
6 6
7 +use Yatra\Core\Assets\BaseAssetManager;
8 +use Yatra\Core\Routing\PageContext;
9 +
7 10 /**
8 11 * Base Page Handler
9 12 *
10 13 * Provides common functionality for all page handlers
@@ -55,8 +58,147 @@
55 58 status_header(200);
56 59 }
57 60
58 61 /**
62 + * Configure $wp_query and globals so the current request looks like a real WP page to
63 + * the theme — required for FSE/block themes to resolve the correct block template
64 + * (singular/archive/index) instead of falling back to 404.html.
65 + *
66 + * @param string $context 'singular' for single-trip / booking-confirmation / account, 'archive' for listings.
67 + * @param array $args {
68 + * @type string $title Page title used in <title> and queried-object stub.
69 + * @type int $object_id Optional ID for the virtual queried object (defaults to 0).
70 + * @type string $post_type Post type for the faux WP_Post (default 'page').
71 + * @type string $post_name Slug for the faux WP_Post.
72 + * @type string|null $content Optional post content placeholder.
73 + * }
74 + */
75 + protected function setupPageEnvironment(string $context = 'singular', array $args = []): void
76 + {
77 + global $wp_query, $post;
78 +
79 + $title = (string) ($args['title'] ?? '');
80 + $object_id = (int) ($args['object_id'] ?? 0);
81 + $post_type = (string) ($args['post_type'] ?? 'page');
82 + $post_name = (string) ($args['post_name'] ?? '');
83 + $content = (string) ($args['content'] ?? '');
84 +
85 + // Clear the conditional-tag state — WP_Query keeps stale flags from the main query.
86 + $wp_query->init_query_flags();
87 + $wp_query->is_404 = false;
88 + $wp_query->is_home = false;
89 + $wp_query->is_front_page = false;
90 +
91 + if ($context === 'archive') {
92 + $wp_query->is_archive = true;
93 + // Do NOT set is_post_type_archive — we are a virtual page, not a
94 + // real WP post-type archive. Setting it triggers WP code paths
95 + // (feed_links_extra, post_type_archive_title, …) that call
96 + // get_post_type_object() and crash on null when no CPT is set.
97 + } else {
98 + $wp_query->is_singular = true;
99 + $wp_query->is_page = ($post_type === 'page');
100 + $wp_query->is_single = ($post_type !== 'page');
101 + }
102 +
103 + // Build a virtual WP_Post so the_post(), get_the_ID(), body_class(), and FSE
104 + // block-template selection have something coherent to read.
105 + $virtual = new \WP_Post((object) [
106 + 'ID' => $object_id,
107 + 'post_author' => 0,
108 + 'post_date' => current_time('mysql'),
109 + 'post_date_gmt' => current_time('mysql', true),
110 + 'post_content' => $content,
111 + 'post_title' => $title,
112 + 'post_excerpt' => '',
113 + 'post_status' => 'publish',
114 + 'comment_status' => 'closed',
115 + 'ping_status' => 'closed',
116 + 'post_password' => '',
117 + 'post_name' => $post_name,
118 + 'to_ping' => '',
119 + 'pinged' => '',
120 + 'post_modified' => current_time('mysql'),
121 + 'post_modified_gmt' => current_time('mysql', true),
122 + 'post_content_filtered' => '',
123 + 'post_parent' => 0,
124 + 'guid' => home_url($post_name ? '/' . $post_name : '/'),
125 + 'menu_order' => 0,
126 + 'post_type' => $post_type,
127 + 'post_mime_type' => '',
128 + 'comment_count' => 0,
129 + 'filter' => 'raw',
130 + ]);
131 +
132 + $wp_query->queried_object = $virtual;
133 + $wp_query->queried_object_id = $object_id;
134 + $wp_query->post = $virtual;
135 + $wp_query->posts = [$virtual];
136 + $wp_query->post_count = 1;
137 + $wp_query->found_posts = 1;
138 + $wp_query->max_num_pages = 1;
139 + $wp_query->current_post = -1;
140 +
141 + $post = $virtual;
142 +
143 + // ── Strip WP core hooks that don't make sense for virtual pages ─────────────
144 + //
145 + // Our queried-object ID is a Yatra trips-table row id, NOT a wp_posts row id.
146 + // Several WP core hooks call `get_post( get_queried_object_id() )` expecting a
147 + // real WP_Post — they receive null and crash with
148 + // "Attempt to read property 'post_type' on null".
149 + //
150 + // wp_shortlink_wp_head → calls wp_get_shortlink('query') → get_post(id) → null
151 + // wp_shortlink_header → same code path, called on template_redirect
152 + // adjacent_posts_rel_link_wp_head → walks $post->post_type for prev/next links
153 + //
154 + // None of these emit anything meaningful for a virtual Yatra page, so we strip
155 + // them at the start of the render. Re-checking they exist (rather than calling
156 + // remove_action blindly) keeps us forward-compatible if WP core retires a hook.
157 + if (function_exists('wp_shortlink_wp_head')) {
158 + remove_action('wp_head', 'wp_shortlink_wp_head', 10);
159 + }
160 + if (function_exists('wp_shortlink_header')) {
161 + remove_action('template_redirect', 'wp_shortlink_header', 11);
162 + }
163 + if (function_exists('adjacent_posts_rel_link_wp_head')) {
164 + remove_action('wp_head', 'adjacent_posts_rel_link_wp_head', 10);
165 + }
166 +
167 + // WP admin-bar "Edit Page" link — wp_admin_bar_edit_menu reads our virtual
168 + // $post->post_type ('page') and adds an Edit link pointing at
169 + // wp-admin/post.php?post=<our_virtual_id>&action=edit. That ID is a Yatra trip
170 + // row id, not a wp_posts row, so the link 404s (or worse, edits an unrelated
171 + // post that happens to share the ID). Yatra registers its own "Edit Trip"
172 + // admin-bar item via AdminBarProvider, which is the correct link for trips.
173 + if (function_exists('wp_admin_bar_edit_menu')) {
174 + remove_action('admin_bar_menu', 'wp_admin_bar_edit_menu', 80);
175 + }
176 +
177 + // Belt-and-braces: short-circuit `pre_get_shortlink` for THIS request so any
178 + // other plugin/theme code that calls `wp_get_shortlink( 0, 'query' )` directly
179 + // also bails out cleanly instead of feeding our virtual ID to `get_post()`.
180 + if (!has_filter('pre_get_shortlink', [self::class, 'suppressVirtualShortlink'])) {
181 + add_filter('pre_get_shortlink', [self::class, 'suppressVirtualShortlink'], 1);
182 + }
183 +
184 + status_header(200);
185 + nocache_headers();
186 + }
187 +
188 + /**
189 + * Returning an empty string from `pre_get_shortlink` short-circuits
190 + * {@see wp_get_shortlink()} BEFORE it calls `get_post()` on our virtual ID. Used
191 + * to silence "Attempt to read property 'post_type' on null" warnings on Yatra
192 + * trip / listing / account pages where the queried-object ID is a Yatra row id,
193 + * not a wp_posts row id.
194 + */
195 + public static function suppressVirtualShortlink($shortlink): string
196 + {
197 + return '';
198 + }
199 +
200 + /**
59 201 * Set global variable
60 202 *
61 203 * @param string $name Variable name
62 204 * @param mixed $value Variable value
@@ -77,9 +219,58 @@
77 219 }
78 220 }
79 221
80 222 /**
81 - * Exit with optional message
223 + * Hand off a Yatra template to the `template_include` filter and return
224 + * a routing result. Replaces the legacy `include + exit` pattern.
225 + *
226 + * The handler should call this as the last step in handle(). It does NOT
227 + * render the template — that happens later via WordPress's normal
228 + * template-loader pipeline, so wp_head/wp_footer fire, other plugins can
229 + * hook in, and child themes can override.
230 + *
231 + * @param string $templateName Template filename relative to /templates/, with or without .php.
232 + * @param BaseAssetManager|null $assetManager Optional asset manager whose assets to enqueue.
233 + * @param string $pageType Logical page type for body_class hints + FSE template slug.
234 + * @return bool true if the template was queued, false if missing.
235 + */
236 + protected function selectTemplate(string $templateName, ?BaseAssetManager $assetManager = null, string $pageType = ''): bool
237 + {
238 + $name = ltrim($templateName, '/');
239 + if (substr($name, -4) !== '.php') {
240 + $name .= '.php';
241 + }
242 +
243 + $absolute = YATRA_PLUGIN_PATH . 'templates/' . $name;
244 +
245 + // Allow themes/child-themes to override via templates/yatra/<name>.
246 + $located = locate_template(['yatra/' . $name], false, false);
247 + if (is_string($located) && $located !== '') {
248 + $absolute = $located;
249 + }
250 +
251 + $absolute = (string) apply_filters('yatra_template_path', $absolute, $name, $pageType);
252 +
253 + if (!is_readable($absolute)) {
254 + $this->logError("Template not readable: {$absolute}");
255 + return false;
256 + }
257 +
258 + if ($assetManager !== null) {
259 + $assetManager->enqueueAssets();
260 + }
261 +
262 + $ctx = PageContext::instance();
263 + $ok = $ctx->select($absolute, $pageType);
264 + if ($ok && $pageType !== '') {
265 + $ctx->addBodyClass('yatra-page-' . sanitize_html_class($pageType));
266 + }
267 + return $ok;
268 + }
269 +
270 + /**
271 + * @deprecated Kept only for backwards-compat with handlers not yet migrated.
272 + * New handlers should call selectTemplate() and return.
82 273 *
83 274 * @param string|null $message Optional exit message
84 275 */
85 276 protected function exit(?string $message = null): void