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 / Compatibility / Elementor / Assets.php

Assets.php in Yatra – Travel Booking & Tour Operator Software trunk, at app/Compatibility/Elementor/Assets.php

461 lines 17.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\Compatibility\Elementor;
6
7 /**
8 * Elementor compatibility – Assets & Body Class
9 *
10 * Ensures Elementor Theme Builder header/footer styling (kit typography, spacing,
11 * Google Fonts) applies on all Yatra routed pages (trip, booking, destination,
12 * activity, category and listing pages).
13 *
14 * Boot order: registered on `plugins_loaded` (priority 20) via Bootstrap so that
15 * Elementor classes are guaranteed to be loaded before we check for them.
16 *
17 * === Font loading architecture ===
18 *
19 * Elementor loads Google Fonts in two stages:
20 * 1. CSS enqueue : `\Elementor\Core\Files\CSS\Post::create($id)->enqueue()` enqueues the
21 * pre-generated CSS file AND queues each font in
22 * `$frontend->fonts_to_enqueue` via `$frontend->enqueue_font()`.
23 * 2. wp_head print : `$frontend->print_fonts_links()` (hooked by `Frontend::init()` at
24 * priority 7) reads `fonts_to_enqueue` and outputs `<link>` tags.
25 *
26 * On Yatra pages (custom routing), Elementor does NOT detect the page as its own,
27 * so neither step runs automatically. We trigger both:
28 * – Step 1: call `Post::create($id)->enqueue()` for the active Kit and every
29 * active Theme Builder header/footer template.
30 * – Step 2: hook an explicit `wp_head` callback at priority 9 as a safety-net
31 * in case `Frontend::init()` did not register its own priority-7 hook
32 * (edge-case: some caching/proxy setups skip template_redirect).
33 */
34 final class Assets
35 {
36 /**
37 * Per-request cache (avoid duplicate DB/API calls within the same request).
38 *
39 * @var array<string,mixed>
40 */
41 private static array $cache = [];
42
43 // -------------------------------------------------------------------------
44 // Registration
45 // -------------------------------------------------------------------------
46
47 public static function register(): void
48 {
49 if (!class_exists('\Elementor\Plugin')) {
50 return;
51 }
52
53 // Primary: late priority so kit typography wins over theme base styles.
54 add_action('wp_enqueue_scripts', [self::class, 'enqueue'], 100);
55
56 // Safety-net: wp_enqueue_scripts sometimes fires before Yatra sets its
57 // routing query-vars (especially with caching plugins). Re-run on wp_head
58 // priority 1 as a second chance; WP deduplicates enqueued handles.
59 add_action('wp_head', [self::class, 'enqueue'], 1);
60
61 // Safety-net for Google Fonts: in edge-cases where Frontend::init() did not
62 // register its own print_fonts_links hook, call it ourselves at priority 9
63 // (just after Elementor's priority 7). Calling it when fonts_to_enqueue is
64 // already empty is a no-op, so this is always safe.
65 add_action('wp_head', [self::class, 'printFonts'], 9);
66
67 // Inject the active Elementor kit body-class.
68 add_filter('body_class', [self::class, 'bodyClass'], 20, 1);
69 }
70
71 // -------------------------------------------------------------------------
72 // Yatra page detection
73 // -------------------------------------------------------------------------
74
75 /**
76 * Returns true when the current request is a Yatra-routed frontend page.
77 *
78 * Uses routing query-vars (available early, before TemplateLoader sets globals)
79 * and falls back to REQUEST_URI path matching + late globals-based helpers.
80 */
81 private static function isYatraRoutedPage(): bool
82 {
83 // 1. WordPress query-var set by Yatra rewrite rules (pretty permalinks).
84 if ((string) get_query_var('yatra_page') !== '') {
85 return true;
86 }
87
88 // 2. Plain-permalink GET param fallback.
89 $rawYatraPage = $_GET['yatra_page'] ?? '';
90 if (is_string($rawYatraPage) && trim(wp_unslash($rawYatraPage)) !== '') {
91 return true;
92 }
93
94 // 3. Individual taxonomy / special-page query-vars.
95 foreach ([
96 'yatra_booking_confirmation',
97 'yatra_remaining_checkout',
98 'yatra_verify_email',
99 'yatra_destination_slug',
100 'yatra_activity_slug',
101 'yatra_category_slug',
102 ] as $qv) {
103 $v = get_query_var($qv);
104 if (is_string($v) && trim($v) !== '') {
105 return true;
106 }
107 }
108
109 // 4. Dynamic single-trip query-var (key = trip base, e.g. "trip" or "tours").
110 if (class_exists('\Yatra\Services\SettingsService')) {
111 $tripBase = preg_replace(
112 '/[^a-zA-Z0-9_-]/',
113 '',
114 (string) \Yatra\Services\SettingsService::getTripBase()
115 ) ?: 'trip';
116
117 if (trim((string) get_query_var($tripBase)) !== '') {
118 return true;
119 }
120
121 // Plain-permalink version: ?{tripBase}=slug
122 $rawTrip = $_GET[$tripBase] ?? '';
123 if (is_string($rawTrip) && trim(wp_unslash($rawTrip)) !== '') {
124 return true;
125 }
126
127 // 5. Last resort: match REQUEST_URI path against Yatra URL bases.
128 // Handles edge-cases where caching/proxies strip query-vars.
129 $requestUri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
130 $path = $requestUri !== ''
131 ? trim((string) wp_parse_url($requestUri, PHP_URL_PATH), '/')
132 : '';
133
134 if ($path !== '') {
135 $bases = [
136 $tripBase,
137 preg_replace('/[^a-zA-Z0-9_-]/', '', (string) \Yatra\Services\SettingsService::getDestinationBase()),
138 preg_replace('/[^a-zA-Z0-9_-]/', '', (string) \Yatra\Services\SettingsService::getActivityBase()),
139 preg_replace('/[^a-zA-Z0-9_-]/', '', (string) \Yatra\Services\SettingsService::getTripCategoryBase()),
140 preg_replace('/[^a-zA-Z0-9_-]/', '', (string) \Yatra\Services\SettingsService::getBookingBase()),
141 preg_replace('/[^a-zA-Z0-9_-]/', '', (string) \Yatra\Services\SettingsService::getAccountBase()),
142 ];
143 foreach (array_filter($bases) as $base) {
144 if ($path === $base || str_starts_with($path, $base . '/')) {
145 return true;
146 }
147 }
148 }
149 }
150
151 // 6. Fallback: globals-based helpers (available after TemplateLoader runs).
152 if (function_exists('yatra_is_yatra_page')) {
153 return yatra_is_yatra_page()
154 || (function_exists('yatra_is_booking_page') && yatra_is_booking_page())
155 || (function_exists('yatra_is_trip_listing') && yatra_is_trip_listing())
156 || (function_exists('yatra_is_single_trip') && yatra_is_single_trip());
157 }
158
159 return false;
160 }
161
162 // -------------------------------------------------------------------------
163 // Asset enqueue
164 // -------------------------------------------------------------------------
165
166 public static function enqueue(): void
167 {
168 if (!self::isYatraRoutedPage()) {
169 return;
170 }
171
172 // Guard: only run once per request.
173 if (!empty(self::$cache['enqueue_done'])) {
174 return;
175 }
176 self::$cache['enqueue_done'] = true;
177
178 try {
179 $plugin = \Elementor\Plugin::$instance;
180
181 // -- Core Elementor frontend handles -----------------------------------
182 if ($plugin && isset($plugin->frontend)) {
183 if (method_exists($plugin->frontend, 'enqueue_styles')) {
184 $plugin->frontend->enqueue_styles();
185 }
186 if (method_exists($plugin->frontend, 'enqueue_scripts')) {
187 $plugin->frontend->enqueue_scripts();
188 }
189 }
190
191 // Explicit handles some themes depend on directly.
192 foreach ([
193 'elementor-frontend',
194 'elementor-icons',
195 'elementor-animations',
196 'elementor-frontend-google-fonts',
197 ] as $handle) {
198 if (wp_style_is($handle, 'registered') || wp_style_is($handle, 'queued')) {
199 wp_enqueue_style($handle);
200 }
201 }
202
203 // -- Active Kit (Site Settings) CSS ------------------------------------
204 // The kit CSS defines all CSS custom properties (colors, typography variables)
205 // that the header/footer templates inherit.
206 self::enqueuePostCss($plugin, (int) self::getKitId($plugin));
207
208 // -- Theme Builder Header / Footer template CSS ------------------------
209 if (class_exists('\ElementorPro\Plugin')) {
210 self::enqueueThemeBuilderTemplates($plugin);
211
212 // Pro frontend styles & fonts.
213 foreach ([
214 'elementor-pro',
215 'elementor-pro-frontend',
216 'elementor-pro-frontend-google-fonts',
217 ] as $handle) {
218 if (wp_style_is($handle, 'registered') || wp_style_is($handle, 'queued')) {
219 wp_enqueue_style($handle);
220 }
221 }
222
223 $pro = \ElementorPro\Plugin::instance();
224 if ($pro && method_exists($pro, 'get_frontend')) {
225 $fe = $pro->get_frontend();
226 if ($fe) {
227 if (method_exists($fe, 'enqueue_styles')) {
228 $fe->enqueue_styles();
229 }
230 if (method_exists($fe, 'enqueue_scripts')) {
231 $fe->enqueue_scripts();
232 }
233 }
234 }
235 }
236 } catch (\Throwable $e) {
237 // Never break frontend rendering due to optional integration.
238 }
239 }
240
241 // -------------------------------------------------------------------------
242 // Google Fonts safety-net
243 // -------------------------------------------------------------------------
244
245 /**
246 * Explicitly flush `$frontend->fonts_to_enqueue` → Google Fonts <link> tags.
247 *
248 * Elementor normally does this on wp_head priority 7 inside Frontend::init().
249 * init() is hooked to template_redirect, which fires on all pages, so under
250 * normal circumstances this is a no-op (fonts_to_enqueue is already empty).
251 * It protects against edge-cases where template_redirect was skipped (e.g.
252 * via a caching layer) and init() was therefore never called.
253 */
254 public static function printFonts(): void
255 {
256 if (!self::isYatraRoutedPage()) {
257 return;
258 }
259
260 try {
261 $frontend = \Elementor\Plugin::$instance->frontend ?? null;
262 if ($frontend && method_exists($frontend, 'print_fonts_links')) {
263 $frontend->print_fonts_links();
264 }
265 } catch (\Throwable $e) {
266 // Silently skip.
267 }
268 }
269
270 // -------------------------------------------------------------------------
271 // Body class
272 // -------------------------------------------------------------------------
273
274 /**
275 * @param string[] $classes
276 * @return string[]
277 */
278 public static function bodyClass(array $classes): array
279 {
280 if (!self::isYatraRoutedPage()) {
281 return $classes;
282 }
283
284 try {
285 $kitId = (int) self::getKitId(\Elementor\Plugin::$instance);
286 if ($kitId > 0) {
287 $kitClass = 'elementor-kit-' . $kitId;
288 if (!in_array($kitClass, $classes, true)) {
289 $classes[] = $kitClass;
290 }
291 }
292 } catch (\Throwable $e) {
293 // Silently skip.
294 }
295
296 return $classes;
297 }
298
299 // -------------------------------------------------------------------------
300 // Internal helpers
301 // -------------------------------------------------------------------------
302
303 /**
304 * Return the active Elementor kit ID (cached per request).
305 */
306 private static function getKitId(object $plugin): int
307 {
308 if (isset(self::$cache['kit_id'])) {
309 return (int) self::$cache['kit_id'];
310 }
311
312 $kitId = 0;
313 $kits = $plugin->kits_manager ?? null;
314 if ($kits && method_exists($kits, 'get_active_id')) {
315 $kitId = (int) $kits->get_active_id();
316 }
317
318 self::$cache['kit_id'] = $kitId;
319
320 return $kitId;
321 }
322
323 /**
324 * Enqueue an Elementor post CSS file.
325 *
326 * Uses Elementor's internal `Post::create($id)->enqueue()` API which:
327 * a) Enqueues the pre-generated `post-{id}.css` file (or inlines it).
328 * b) Calls `$frontend->enqueue_font($font)` for every font stored in the
329 * post's CSS meta, populating `$frontend->fonts_to_enqueue` so that
330 * Elementor's `print_fonts_links()` (wp_head priority 7) can output
331 * the correct Google Fonts <link> tags.
332 *
333 * Falls back to directly linking the file from uploads if the internal API
334 * doesn't produce a registered handle (belt-and-suspenders for caching setups).
335 *
336 * IMPORTANT: Do NOT call `\Elementor\Core\Files\CSS\Post::enqueue($id)` —
337 * `enqueue()` is an INSTANCE method; calling it statically is a PHP error
338 * that is silently swallowed and leaves fonts_to_enqueue unpopulated.
339 */
340 private static function enqueuePostCss(object $plugin, int $postId): void
341 {
342 if ($postId <= 0) {
343 return;
344 }
345
346 // Ask Elementor to enqueue via its internal system.
347 // Post::create() uses the files_manager for per-request caching.
348 if (class_exists('\Elementor\Core\Files\CSS\Post')) {
349 try {
350 /** @var \Elementor\Core\Files\CSS\Post $cssFile */
351 $cssFile = \Elementor\Core\Files\CSS\Post::create($postId);
352 if ($cssFile && method_exists($cssFile, 'enqueue')) {
353 $cssFile->enqueue();
354 }
355 } catch (\Throwable $e) {
356 // Continue to fallback.
357 }
358 }
359
360 // Ensure Elementor's own registered handle is queued (in case create()
361 // registered but did not enqueue it for some reason).
362 $handle = 'elementor-post-' . $postId;
363 if (wp_style_is($handle, 'registered') || wp_style_is($handle, 'queued')) {
364 wp_enqueue_style($handle);
365 }
366
367 // Absolute fallback: link the generated CSS file from uploads directly.
368 // This fires even when Elementor's internal enqueue silently does nothing
369 // (e.g. the document has no Elementor data flag set). It does NOT populate
370 // fonts_to_enqueue — that path relies on the Post::create() call above.
371 $upload = wp_upload_dir();
372 if (is_array($upload) && !empty($upload['basedir']) && !empty($upload['baseurl'])) {
373 $dir = rtrim((string) $upload['basedir'], '/\\') . '/elementor/css/';
374 $url = rtrim((string) $upload['baseurl'], '/\\') . '/elementor/css/';
375 $file = $dir . 'post-' . $postId . '.css';
376 if (file_exists($file)) {
377 wp_enqueue_style(
378 'yatra-elementor-post-' . $postId,
379 $url . 'post-' . $postId . '.css',
380 [],
381 (string) @filemtime($file)
382 );
383 }
384 }
385 }
386
387 /**
388 * Enqueue CSS for all active Elementor Pro Theme Builder header/footer templates.
389 * Prefers the Theme Builder conditions API; falls back to a DB query.
390 */
391 private static function enqueueThemeBuilderTemplates(object $plugin): void
392 {
393 if (!class_exists('\ElementorPro\Modules\ThemeBuilder\Module')) {
394 return;
395 }
396
397 foreach (['header', 'footer'] as $location) {
398 $ids = self::getThemeBuilderTemplateIds($location);
399 foreach ($ids as $id) {
400 self::enqueuePostCss($plugin, $id);
401 }
402 }
403 }
404
405 /**
406 * Resolve template IDs for a Theme Builder location (cached per request).
407 *
408 * @return int[]
409 */
410 private static function getThemeBuilderTemplateIds(string $location): array
411 {
412 $cacheKey = 'tb_ids_' . $location;
413 if (isset(self::$cache[$cacheKey])) {
414 return (array) self::$cache[$cacheKey];
415 }
416
417 $ids = [];
418
419 // Preferred: Theme Builder conditions manager (only currently active templates).
420 try {
421 $tb = \ElementorPro\Modules\ThemeBuilder\Module::instance();
422 if ($tb && method_exists($tb, 'get_conditions_manager')) {
423 $cm = $tb->get_conditions_manager();
424 if ($cm && method_exists($cm, 'get_documents_for_location')) {
425 foreach ((array) $cm->get_documents_for_location($location) as $doc) {
426 if (is_object($doc) && method_exists($doc, 'get_main_id')) {
427 $ids[] = (int) $doc->get_main_id();
428 } elseif (is_numeric($doc)) {
429 $ids[] = (int) $doc;
430 }
431 }
432 }
433 }
434 } catch (\Throwable $e) {
435 $ids = [];
436 }
437
438 // Fallback: query published Elementor library items for this template type.
439 if (empty($ids)) {
440 $rows = get_posts([
441 'post_type' => 'elementor_library',
442 'post_status' => 'publish',
443 'fields' => 'ids',
444 'numberposts' => -1,
445 'no_found_rows' => true,
446 'meta_query' => [[
447 'key' => '_elementor_template_type',
448 'value' => $location,
449 'compare' => '=',
450 ]],
451 ]);
452 $ids = array_map('intval', (array) $rows);
453 }
454
455 $ids = array_values(array_filter(array_unique($ids)));
456 self::$cache[$cacheKey] = $ids;
457
458 return $ids;
459 }
460 }
461