| @@ -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 |