| 1 |
<?php |
| 2 |
|
| 3 |
declare(strict_types=1); |
| 4 |
|
| 5 |
namespace Yatra\Compatibility\Wanderland; |
| 6 |
|
| 7 |
/** |
| 8 |
* Wanderland Theme Header Compatibility |
| 9 |
* |
| 10 |
* Solves two interrelated problems that occur on Yatra custom-routed pages: |
| 11 |
* |
| 12 |
* Problem 1 – Wrong page context at the `wp` hook: |
| 13 |
* Yatra routes produce queried_object_id = 0 (no real WP page). |
| 14 |
* Wanderland's wanderland_mikado_check_is_header_type_enabled() has a branch for |
| 15 |
* page_id = 0 (PHP empty(0) = true) that looks up ALL per-page header-type meta |
| 16 |
* values across the entire site. If any page uses e.g. `header-bottom`, the check |
| 17 |
* returns true even when the global setting is `header-standard`, causing the header |
| 18 |
* action to be remapped to a hook Yatra templates never call. |
| 19 |
* |
| 20 |
* Problem 2 – Remapped action never fired by Yatra templates: |
| 21 |
* Header types like `header-bottom`, `header-bottom-centered`, and |
| 22 |
* `header-bottom-minimal` move `wanderland_mikado_get_header` from |
| 23 |
* `wanderland_mikado_action_after_wrapper_inner` to |
| 24 |
* `wanderland_mikado_action_before_main_content`. Yatra's templates never call the |
| 25 |
* latter, so the header simply disappears. |
| 26 |
* |
| 27 |
* Fixes: |
| 28 |
* 1. At `wp` priority 0, supply a real WordPress page ID as the queried object so |
| 29 |
* Wanderland's header-type checks evaluate correctly (only against that specific |
| 30 |
* page, not all pages on the site). |
| 31 |
* 2. Hook into `wanderland_mikado_action_after_wrapper_inner` and fire |
| 32 |
* `wanderland_mikado_action_before_main_content` from there once per request, |
| 33 |
* ensuring bottom-style headers still render inside `wanderland/header.php`. |
| 34 |
* |
| 35 |
* Safety guarantees: |
| 36 |
* - Executes only when the Wanderland theme (or a child theme of it) is active, verified |
| 37 |
* via get_template() === 'wanderland'. |
| 38 |
* - Has zero interaction with Elementor compatibility (Elementor\Assets uses separate |
| 39 |
* hooks: wp_enqueue_scripts, wp_head, body_class – none of which this class touches). |
| 40 |
* - `maybeFireBeforeMainContent` is guarded by a per-request static flag so |
| 41 |
* `wanderland_mikado_get_header` can never be rendered more than once per request, |
| 42 |
* regardless of how many times the outer action fires. |
| 43 |
* |
| 44 |
* Registration timing note: |
| 45 |
* Yatra's Compatibility loader calls register() at `plugins_loaded` priority 20. |
| 46 |
* The Wanderland theme loads its functions.php AFTER `plugins_loaded`, so we must |
| 47 |
* NOT gate registration on theme function existence here; the callbacks check |
| 48 |
* isWanderlandActive() lazily when they actually fire (after `after_setup_theme`). |
| 49 |
*/ |
| 50 |
final class Header |
| 51 |
{ |
| 52 |
/** Prevents register() from wiring hooks more than once. */ |
| 53 |
private static bool $registered = false; |
| 54 |
|
| 55 |
/** |
| 56 |
* Prevents maybeFireBeforeMainContent from firing more than once per request. |
| 57 |
* This is the hard guard against double-header rendering. |
| 58 |
*/ |
| 59 |
private static bool $beforeMainContentFired = false; |
| 60 |
|
| 61 |
// ------------------------------------------------------------------------- |
| 62 |
// Registration |
| 63 |
// ------------------------------------------------------------------------- |
| 64 |
|
| 65 |
/** |
| 66 |
* Register all compatibility hooks. |
| 67 |
* Called by Yatra\Compatibility\Compatibility at plugins_loaded priority 20. |
| 68 |
*/ |
| 69 |
public static function register(): void |
| 70 |
{ |
| 71 |
if (self::$registered) { |
| 72 |
return; |
| 73 |
} |
| 74 |
|
| 75 |
self::$registered = true; |
| 76 |
|
| 77 |
/* |
| 78 |
* Priority 0: run before wanderland_mikado_set_header_object (priority 1) |
| 79 |
* AND before wanderland_mikado_include_header_types_after_load (priority 11). |
| 80 |
* Both read wanderland_mikado_get_page_id() → get_queried_object_id(), so we |
| 81 |
* must supply a valid queried object ID before they execute. |
| 82 |
*/ |
| 83 |
add_action('wp', [self::class, 'applyRequestContext'], 0); |
| 84 |
|
| 85 |
/* |
| 86 |
* Fires inside wanderland/header.php → do_action('wanderland_mikado_action_after_wrapper_inner'). |
| 87 |
* Priority 5 runs BEFORE the default header hook (priority 10). For bottom-style |
| 88 |
* header types that moved wanderland_mikado_get_header to |
| 89 |
* wanderland_mikado_action_before_main_content, we fire that action here so the |
| 90 |
* header renders inside the header.php template that Yatra's get_header() calls. |
| 91 |
* The static $beforeMainContentFired flag ensures this fires at most once. |
| 92 |
*/ |
| 93 |
add_action('wanderland_mikado_action_after_wrapper_inner', [self::class, 'maybeFireBeforeMainContent'], 5); |
| 94 |
} |
| 95 |
|
| 96 |
// ------------------------------------------------------------------------- |
| 97 |
// Hook callbacks |
| 98 |
// ------------------------------------------------------------------------- |
| 99 |
|
| 100 |
/** |
| 101 |
* Set a real WordPress page ID as the queried object on Yatra-routed requests. |
| 102 |
* |
| 103 |
* This prevents the page_id = 0 path in wanderland_mikado_check_is_header_type_enabled(), |
| 104 |
* which can incorrectly enable non-global header types (because PHP's empty(0) = true |
| 105 |
* triggers a fallback that checks ALL per-page meta across the whole site). |
| 106 |
* |
| 107 |
* Hooked: wp, priority 0 |
| 108 |
*/ |
| 109 |
public static function applyRequestContext(): void |
| 110 |
{ |
| 111 |
if (!self::isWanderlandActive()) { |
| 112 |
return; |
| 113 |
} |
| 114 |
|
| 115 |
if (!self::isYatraRoutedRequest()) { |
| 116 |
return; |
| 117 |
} |
| 118 |
|
| 119 |
$contextId = self::resolveContextPageId(); |
| 120 |
if ($contextId <= 0) { |
| 121 |
return; |
| 122 |
} |
| 123 |
|
| 124 |
$contextPost = get_post($contextId); |
| 125 |
if (!($contextPost instanceof \WP_Post)) { |
| 126 |
return; |
| 127 |
} |
| 128 |
|
| 129 |
global $wp_query; |
| 130 |
if (!($wp_query instanceof \WP_Query)) { |
| 131 |
return; |
| 132 |
} |
| 133 |
|
| 134 |
// Clear any stale 404 state so wanderland_mikado_is_default_wp_template() |
| 135 |
// returns false (otherwise get_page_id() returns -1 which skips per-page meta |
| 136 |
// entirely and forces global-only resolution). |
| 137 |
$wp_query->is_404 = false; |
| 138 |
$wp_query->queried_object = $contextPost; |
| 139 |
$wp_query->queried_object_id = $contextId; |
| 140 |
} |
| 141 |
|
| 142 |
/** |
| 143 |
* On Yatra pages, fire wanderland_mikado_action_before_main_content from inside |
| 144 |
* wanderland_mikado_action_after_wrapper_inner. |
| 145 |
* |
| 146 |
* Header types that remap the header hook (header-bottom, header-bottom-centered, |
| 147 |
* header-bottom-minimal) attach wanderland_mikado_get_header to |
| 148 |
* wanderland_mikado_action_before_main_content. Yatra templates only call |
| 149 |
* get_header() → wanderland/header.php → wanderland_mikado_action_after_wrapper_inner, |
| 150 |
* never the before_main_content action. This bridge ensures those header types |
| 151 |
* still render their output. |
| 152 |
* |
| 153 |
* The static flag guarantees this bridge fires at most once per request, so the |
| 154 |
* header is never duplicated even if the action fires more than once. |
| 155 |
* |
| 156 |
* Hooked: wanderland_mikado_action_after_wrapper_inner, priority 5 |
| 157 |
*/ |
| 158 |
public static function maybeFireBeforeMainContent(): void |
| 159 |
{ |
| 160 |
// Hard guard: fire at most once per request. |
| 161 |
if (self::$beforeMainContentFired) { |
| 162 |
return; |
| 163 |
} |
| 164 |
|
| 165 |
if (!self::isWanderlandActive()) { |
| 166 |
return; |
| 167 |
} |
| 168 |
|
| 169 |
if (!self::isYatraRoutedRequest()) { |
| 170 |
return; |
| 171 |
} |
| 172 |
|
| 173 |
self::$beforeMainContentFired = true; |
| 174 |
|
| 175 |
do_action('wanderland_mikado_action_before_main_content'); |
| 176 |
} |
| 177 |
|
| 178 |
// ------------------------------------------------------------------------- |
| 179 |
// Internal helpers |
| 180 |
// ------------------------------------------------------------------------- |
| 181 |
|
| 182 |
/** |
| 183 |
* Returns true when the Wanderland theme (or a child theme of it) is active. |
| 184 |
* |
| 185 |
* Uses get_template() which returns the PARENT theme slug even for child themes. |
| 186 |
* This is checked lazily (inside callbacks) so it works correctly regardless of |
| 187 |
* whether the theme was loaded before or after register() ran. |
| 188 |
*/ |
| 189 |
private static function isWanderlandActive(): bool |
| 190 |
{ |
| 191 |
return get_template() === 'wanderland'; |
| 192 |
} |
| 193 |
|
| 194 |
/** |
| 195 |
* Detect whether the current request is a Yatra-routed page. |
| 196 |
* Relies only on registered WP query vars, which are available from `wp` onwards. |
| 197 |
*/ |
| 198 |
private static function isYatraRoutedRequest(): bool |
| 199 |
{ |
| 200 |
$yatraQueryVars = [ |
| 201 |
'yatra_page', |
| 202 |
'yatra_booking_confirmation', |
| 203 |
'yatra_remaining_checkout', |
| 204 |
'yatra_verify_email', |
| 205 |
]; |
| 206 |
|
| 207 |
foreach ($yatraQueryVars as $var) { |
| 208 |
if ((string) get_query_var($var, '') !== '') { |
| 209 |
return true; |
| 210 |
} |
| 211 |
} |
| 212 |
|
| 213 |
return false; |
| 214 |
} |
| 215 |
|
| 216 |
/** |
| 217 |
* Find the best WordPress page ID to use as the request context. |
| 218 |
* |
| 219 |
* The chosen page must NOT have a per-page header-type override so Wanderland |
| 220 |
* falls back to the global header option — which is what should apply to Yatra pages. |
| 221 |
* |
| 222 |
* Selection order (first non-zero result wins): |
| 223 |
* 1. Static front page (page_on_front) — present on most sites, rarely has a |
| 224 |
* per-page header override. |
| 225 |
* 2. Posts page (page_for_posts). |
| 226 |
* 3. The first published WP page (by ID ascending) without mkdf_header_type_meta set. |
| 227 |
*/ |
| 228 |
private static function resolveContextPageId(): int |
| 229 |
{ |
| 230 |
// 1. Static front page |
| 231 |
$id = (int) get_option('page_on_front', 0); |
| 232 |
if ($id > 0) { |
| 233 |
return $id; |
| 234 |
} |
| 235 |
|
| 236 |
// 2. Posts page |
| 237 |
$id = (int) get_option('page_for_posts', 0); |
| 238 |
if ($id > 0) { |
| 239 |
return $id; |
| 240 |
} |
| 241 |
|
| 242 |
// 3. Any published page without a per-page header override |
| 243 |
$pages = get_posts([ |
| 244 |
'post_type' => 'page', |
| 245 |
'post_status' => 'publish', |
| 246 |
'posts_per_page' => 1, |
| 247 |
'orderby' => 'ID', |
| 248 |
'order' => 'ASC', |
| 249 |
'meta_query' => [ |
| 250 |
[ |
| 251 |
'key' => 'mkdf_header_type_meta', |
| 252 |
'compare' => 'NOT EXISTS', |
| 253 |
], |
| 254 |
], |
| 255 |
'no_found_rows' => true, |
| 256 |
]); |
| 257 |
|
| 258 |
if (!empty($pages) && $pages[0] instanceof \WP_Post) { |
| 259 |
return $pages[0]->ID; |
| 260 |
} |
| 261 |
|
| 262 |
return 0; |
| 263 |
} |
| 264 |
} |
| 265 |
|