PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.13
Yatra – Travel Booking & Tour Operator Software v3.0.13
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 / Providers / AdminAssetsProvider.php

AdminAssetsProvider.php in Yatra – Travel Booking & Tour Operator Software 3.0.13, at app/Providers/AdminAssetsProvider.php

571 lines 23.6 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\Providers;
6
7 /**
8 * Admin Assets Provider
9 *
10 * Handles enqueuing of all admin-related CSS and JavaScript assets
11 * Centralizes admin asset management for better organization and maintainability
12 *
13 * @package Yatra\Providers
14 * @since 3.0.0
15 */
16 class AdminAssetsProvider
17 {
18 /**
19 * Full `window.yatraAdmin` payload — must match what addMediaLibraryCompatScript used to merge
20 * (permalinkStructure, tripBase, locale, etc.) so View links and REST helpers work in all modes.
21 *
22 * @return array<string, mixed>
23 */
24 private function buildAdminLocalizedData(): array
25 {
26 $current_user = wp_get_current_user();
27 $capabilities = [];
28 if ($current_user->ID > 0) {
29 $user_caps = $current_user->allcaps;
30 foreach ($user_caps as $cap => $has_cap) {
31 if (!$has_cap) continue;
32 // Mirror every `yatra_*` cap into the JS-side map (these
33 // are what React's `can()` checks against). Also
34 // explicitly include `manage_options` so the React-side
35 // admin fallback has a server-confirmed signal even on
36 // exotic installs where `isWpAdmin` or `roles` were
37 // filtered out by a third-party plugin.
38 $capStr = (string) $cap;
39 if (strpos($capStr, 'yatra_') === 0 || $capStr === 'manage_options') {
40 $capabilities[$capStr] = true;
41 }
42 }
43 }
44
45 return apply_filters('yatra_admin_localized_data', [
46 'timeZoneIdentifiers' => self::buildTimezoneIdentifierList(),
47 'wordPressTimezone' => function_exists('wp_timezone_string')
48 ? (string) wp_timezone_string()
49 : 'UTC',
50 'timezone' => \Yatra\Services\SettingsService::getString('timezone', 'UTC'),
51 'apiUrl' => rest_url('yatra/v1'),
52 'licenseStatus' => (function () {
53 $all = get_option('yatra_license', []);
54 $status = $all['yatra-pro']['status'] ?? 'inactive';
55 return (string) $status;
56 })(),
57 'restUrl' => rest_url(),
58 'nonce' => wp_create_nonce('wp_rest'),
59 'currentUser' => $current_user->ID,
60 'currentUserEmail' => $current_user->user_email,
61 'currentUserDisplayName' => $current_user->display_name,
62 'currentUserLogin' => $current_user->user_login,
63 'currentUserAvatar' => get_avatar($current_user->ID, 96),
64 'siteUrl' => home_url(),
65 'adminUrl' => admin_url('admin.php'),
66 'pluginUrl' => YATRA_PLUGIN_URL,
67 // Public URL of the Yatra sitemap (handles plain vs pretty
68 // permalinks), shown in the SEO settings tab.
69 'sitemapUrl' => \Yatra\Sitemap\SitemapRouter::sitemapUrl(),
70 // Brand-name and brand-logo helpers are filter-backed (defaults
71 // wired in includes/helpers.php). Pro's WhiteLabel module
72 // overrides the filters when Agency white-label is active.
73 'brandLogoUrl' => function_exists('yatra_get_brand_icon_url') ? yatra_get_brand_icon_url() : '',
74 'brandName' => function_exists('yatra_get_brand_name') ? yatra_get_brand_name() : 'Yatra',
75 // White-label-specific window.yatraAdmin keys (brandMenuOverrides,
76 // brandMenuOrder, brandUiChrome, brandPrimaryColor) are injected
77 // by Pro via the `yatra_admin_localized_data` filter applied at
78 // the bottom of this method. They are NOT set here because option
79 // storage is owned by Pro's WhiteLabel module.
80 'permalinkStructure' => (get_option('permalink_structure') ?: '') ?: 'plain',
81 'tripBase' => \Yatra\Services\SettingsService::getTripBase(),
82 'bookingBase' => \Yatra\Services\SettingsService::getBookingBase(),
83 'capabilities' => $capabilities,
84 'roles' => $current_user->roles,
85 // Cap-gating fallback flag. ALWAYS injected (not just by the
86 // Team module) because the React `usePermissions.can()` helper
87 // uses it as the last-resort allow for site owners: anyone
88 // with `manage_options` passes any cap check, mirroring the
89 // server-side admin fallback in Team's Capabilities filter.
90 //
91 // Without this, free-plugin installs (or Pro installs where
92 // Team is off) silently fail every `can("yatra_*")` check —
93 // even for site owners — because the cap isn't on the
94 // administrator role record. The Team module overwrites
95 // this same key when active; semantics are identical, so
96 // the overwrite is safe.
97 'isWpAdmin' => current_user_can('manage_options'),
98 'isPro' => defined('YATRA_PRO_VERSION'),
99 // Agency-tier flag — drives the sidebar's White Label entry visibility
100 // and any other Agency-only UI affordances. Pro registers the filter
101 // unconditionally so the value is always trustworthy.
102 'isAgency' => (bool) apply_filters('yatra_is_agency_active', false),
103 // AI-eligibility flag (Growth + Agency). Drives the AI Assistant
104 // sidebar entry visibility and the per-field sparkle affordances
105 // in the trip / SEO editors.
106 'isAiEligible' => (bool) apply_filters('yatra_is_ai_eligible', false),
107 'whiteLabelEnabled' => class_exists('\\Yatra\\Core\\Modules\\ModuleManager')
108 ? \Yatra\Core\Modules\ModuleManager::isModuleEnabled('white_label')
109 : false,
110 'aiAssistantEnabled' => class_exists('\\Yatra\\Core\\Modules\\ModuleManager')
111 ? \Yatra\Core\Modules\ModuleManager::isModuleEnabled('ai_assistant')
112 : false,
113 'whatsappEnabled' => class_exists('\\Yatra\\Core\\Modules\\ModuleManager')
114 ? \Yatra\Core\Modules\ModuleManager::isModuleEnabled('whatsapp')
115 : false,
116 'channelManagerEnabled' => class_exists('\\Yatra\\Core\\Modules\\ModuleManager')
117 ? \Yatra\Core\Modules\ModuleManager::isModuleEnabled('channel_manager')
118 : false,
119 'webhooksEnabled' => class_exists('\\Yatra\\Core\\Modules\\ModuleManager')
120 ? \Yatra\Core\Modules\ModuleManager::isModuleEnabled('webhooks')
121 : false,
122 // Settings → Pricing (Discount Stacking) drives off these
123 // two. Setting them here (free plugin, AdminAssetsProvider)
124 // matches the pattern used by every other Pro-module flag
125 // above and decouples the React UI from Pro module boot
126 // timing — Pro's init.php is conditionally loaded by
127 // ProModuleManager only when the module is enabled, so any
128 // filter-based exposure could fail silently if boot order
129 // shifts. Reading from the canonical ModuleManager here is
130 // the source of truth.
131 'dynamicPricingEnabled' => class_exists('\\Yatra\\Core\\Modules\\ModuleManager')
132 ? \Yatra\Core\Modules\ModuleManager::isModuleEnabled('dynamic_pricing')
133 : false,
134 'advancedDiscountEnabled' => class_exists('\\Yatra\\Core\\Modules\\ModuleManager')
135 ? \Yatra\Core\Modules\ModuleManager::isModuleEnabled('advanced_discount')
136 : false,
137 // Single source of truth for every country dropdown in the
138 // React admin. Pulled from the canonical FormatHelper —
139 // operators that want a curated or reordered list apply
140 // the `yatra_countries_list` filter once and it propagates
141 // to every dropdown automatically.
142 'countries' => class_exists('\\Yatra\\Helpers\\FormatHelper')
143 ? \Yatra\Helpers\FormatHelper::getCountries()
144 : [],
145 'customLandingPagesModuleEnabled' => class_exists('\\Yatra\\Core\\Modules\\ModuleManager')
146 ? \Yatra\Core\Modules\ModuleManager::isModuleEnabled('custom_landing_pages')
147 : false,
148 // Per-trip Deposit & Payment Terms is a Pro feature (FlexiblePayments).
149 // Default false; Pro's FlexiblePaymentsModule::addAdminData() flips this
150 // to true via the `yatra_admin_localized_data` filter when active, and
151 // the React TripForm hides/shows the section based on this flag.
152 'flexiblePaymentsEnabled' => false,
153 'version' => defined('YATRA_VERSION') ? YATRA_VERSION : '1.0.0',
154 'proVersion' => defined('YATRA_PRO_VERSION') ? YATRA_PRO_VERSION : null,
155
156 'locale' => get_locale(),
157 'currency' => \Yatra\Services\SettingsService::getCurrency(),
158 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
159 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
160 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
161 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
162 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
163 'date_format' => \Yatra\Services\SettingsService::get('date_format', 'Y-m-d'),
164 'time_format' => \Yatra\Services\SettingsService::get('time_format', 'H:i'),
165 'geocodingNonce' => wp_create_nonce('yatra_geocoding_nonce'),
166 'ajaxUrl' => admin_url('admin-ajax.php'),
167 ]);
168 }
169
170 /**
171 * Sorted IANA identifiers for the admin timezone control (matches PHP {@see DateTimeZone}).
172 *
173 * @return list<string>
174 */
175 private static function buildTimezoneIdentifierList(): array
176 {
177 if (!function_exists('timezone_identifiers_list')) {
178 return ['UTC'];
179 }
180
181 $ids = timezone_identifiers_list();
182 if (!is_array($ids) || $ids === []) {
183 return ['UTC'];
184 }
185
186 $ids = array_values(array_filter($ids, static function ($id): bool {
187 return is_string($id) && $id !== '';
188 }));
189
190 sort($ids, SORT_STRING);
191
192 return $ids;
193 }
194
195 /**
196 * Enqueue all admin assets
197 *
198 * @param string $hook Current admin page hook
199 * @return void
200 */
201 public function enqueueAssets(string $hook): void
202 {
203 // Only load on our admin page
204 if ($hook !== 'toplevel_page_yatra') {
205 return;
206 }
207
208 // Prevent problematic scripts that cause initialization errors
209 $this->preventProblematicScripts();
210
211 // Enqueue WordPress media library dependencies
212 $this->enqueueWordPressMedia();
213
214 // Enqueue admin React app assets
215 $this->enqueueAdminReactApp();
216
217 // Do not strip styles required by wp_enqueue_media() / wp.media(): stripping `media-views`
218 // (and replacing `forms` with an empty handle) makes the media modal invisible or broken.
219 // See: TripForm gallery / featured image / downloadable file pickers.
220
221 // Aggressive WordPress Admin CSS removal (keep media modal + its dependency chain intact)
222 $admin_css_handles = [
223 'admin-bar',
224 'admin-menu',
225 'dashboard',
226 'list-tables',
227 'edit',
228 'revisions',
229 'themes',
230 'about',
231 'nav-menus',
232 'wp-pointer',
233 'widgets',
234 'site-icon',
235 'l10n',
236 'wp-auth-check',
237 'wp-components',
238 'wp-commands',
239 'login',
240 'install',
241 'wp-reset-editor-styles',
242 'wp-admin',
243 'colors',
244 ];
245
246 // Dequeue all WordPress admin CSS and register empty placeholders
247 foreach ($admin_css_handles as $handle) {
248 wp_dequeue_style($handle);
249 wp_deregister_style($handle);
250 // Register as empty style to satisfy dependencies
251 wp_register_style($handle, false);
252 }
253
254 // Final safety dequeue at print time
255 add_action('wp_print_styles', function () use ($admin_css_handles) {
256 foreach ($admin_css_handles as $handle) {
257 wp_dequeue_style($handle);
258 }
259 }, 999);
260
261 // Add aggressive style loader filter to prevent WordPress admin CSS
262 add_filter('style_loader_src', function($src, $handle) use ($admin_css_handles) {
263 if (in_array($handle, $admin_css_handles)) {
264 return false; // Prevent loading actual CSS files
265 }
266 // Allow load-styles.php but individual handles will be blocked above
267 return $src;
268 }, 999, 2);
269
270 // Add inline script for media library compatibility
271 $this->addMediaLibraryCompatScript();
272 }
273
274 /**
275 * Prevent problematic WordPress scripts
276 *
277 * @return void
278 */
279 private function preventProblematicScripts(): void
280 {
281 // These scripts try to access wp.media.view before it's initialized
282 wp_dequeue_script('svg-painter');
283 wp_deregister_script('svg-painter');
284 wp_dequeue_script('image-edit');
285 wp_deregister_script('image-edit');
286 }
287
288 /**
289 * Enqueue WordPress media library dependencies
290 *
291 * @return void
292 */
293 private function enqueueWordPressMedia(): void
294 {
295 // Enqueue WordPress media library
296 wp_enqueue_media();
297
298 // Ensure wp-mediaelement is loaded
299 wp_enqueue_script('wp-mediaelement');
300
301 // Ensure media-audiovideo is loaded
302 wp_enqueue_script('media-audiovideo');
303
304 // Keep media-editor loaded - it's required by media-audiovideo
305 // Note: The initialization errors were caused by svg-painter and image-edit, not media-editor
306
307 // Ensure all required dependencies are loaded
308 wp_enqueue_script('jquery');
309 wp_enqueue_script('underscore');
310 wp_enqueue_script('backbone');
311 }
312
313 /**
314 * Enqueue admin React app assets
315 *
316 * @return void
317 */
318 private function enqueueAdminReactApp(): void
319 {
320 // Enqueue compiled React app CSS files
321 $this->enqueueAdminReactCss();
322 $this->enqueueAdminReactJs();
323 }
324
325 /**
326 * Enqueue admin React CSS files
327 *
328 * @return void
329 */
330 private function enqueueAdminReactCss(): void
331 {
332 $faPath = YATRA_PLUGIN_PATH . 'assets/vendor/fontawesome/css/all.min.css';
333 if (file_exists($faPath)) {
334 wp_enqueue_style(
335 'yatra-fontawesome-6-admin',
336 YATRA_PLUGIN_URL . 'assets/vendor/fontawesome/css/all.min.css',
337 [],
338 '6.7.2.' . filemtime($faPath)
339 );
340 }
341
342 $basePath = YATRA_PLUGIN_PATH . 'assets/admin/dist/css/';
343 $faHandle = file_exists($faPath) ? 'yatra-fontawesome-6-admin' : false;
344
345 // React vendor CSS (contains react-draft-wysiwyg CSS)
346 $reactVendorCss = $basePath . 'react-vendor.css';
347 if (file_exists($reactVendorCss)) {
348 $cssVersion = YATRA_VERSION . '.' . filemtime($reactVendorCss);
349 wp_enqueue_style(
350 'yatra-react-vendor',
351 YATRA_PLUGIN_URL . 'assets/admin/dist/css/react-vendor.css',
352 $faHandle ? [$faHandle] : [],
353 $cssVersion
354 );
355 }
356
357 // Index CSS (contains main component styles)
358 $indexCss = $basePath . 'index.css';
359 if (file_exists($indexCss)) {
360 $cssVersion = YATRA_VERSION . '.' . filemtime($indexCss);
361 wp_enqueue_style(
362 'yatra-index',
363 YATRA_PLUGIN_URL . 'assets/admin/dist/css/index.css',
364 ['yatra-react-vendor'],
365 $cssVersion
366 );
367 }
368 }
369
370 /**
371 * Enqueue admin React JS files
372 *
373 * @return void
374 */
375 private function enqueueAdminReactJs(): void
376 {
377 // Check if we're in development mode and Vite dev server is running
378 $isDevMode = defined('WP_DEBUG') && WP_DEBUG && defined('YATRA_DEV_MODE') && YATRA_DEV_MODE;
379 $viteDevServer = 'http://localhost:5173';
380
381 if ($isDevMode && $this->isViteDevServerRunning($viteDevServer)) {
382 // In dev mode, inject localized data and Vite's HMR client
383 add_action('admin_head', function() use ($viteDevServer) {
384 $localized_data = $this->buildAdminLocalizedData();
385
386 ?>
387 <script>
388 window.yatraAdmin = <?php echo wp_json_encode($localized_data); ?>;
389 </script>
390 <script type="module">
391 import { injectIntoGlobalHook } from "<?php echo $viteDevServer; ?>/@react-refresh";
392 injectIntoGlobalHook(window);
393 window.$RefreshReg$ = () => {};
394 window.$RefreshSig$ = () => (type) => type;
395 </script>
396 <script type="module" src="<?php echo $viteDevServer; ?>/@vite/client"></script>
397 <?php
398 }, 1);
399
400 // Load the entry point as ES module
401 add_action('admin_footer', function() use ($viteDevServer) {
402 ?>
403 <script type="module" src="<?php echo $viteDevServer; ?>/resources/js/main.tsx"></script>
404 <?php
405 }, 1);
406
407 } else {
408 // Use built assets in production
409 $appJs = YATRA_PLUGIN_PATH . 'assets/admin/dist/js/app.js';
410
411 if (file_exists($appJs)) {
412 // Version on the plugin version + the bundle's own mtime. That
413 // already changes on every update or rebuild, which is exactly
414 // when the cache must be busted.
415 //
416 // This previously appended time() . microtime(true), making the
417 // URL unique on every single request — so the ~3 MB admin bundle
418 // was re-downloaded on every admin page view and could never be
419 // cached by the browser.
420 $jsVersion = YATRA_VERSION . '.' . filemtime($appJs);
421
422 $localized_data = $this->buildAdminLocalizedData();
423
424 // Enqueue our script with media library as dependency
425 wp_enqueue_script(
426 'yatra-admin',
427 YATRA_PLUGIN_URL . 'assets/admin/dist/js/app.js',
428 [
429 'jquery',
430 'underscore',
431 'backbone',
432 'media-models',
433 'wp-mediaelement',
434 'media-editor',
435 'media-audiovideo',
436 'media-views',
437 'wp-i18n'
438 ],
439 $jsVersion,
440 true
441 );
442
443 // Localize script data
444 wp_localize_script('yatra-admin', 'yatraAdmin', $localized_data);
445
446 // Phone dataset for admin displays (flag + dial-code detection of
447 // stored "+<code><number>" values in booking details).
448 wp_localize_script('yatra-admin', 'yatraPhoneData', [
449 'countries' => \Yatra\Helpers\FormatHelper::getPhoneCountries(),
450 'priority' => \Yatra\Helpers\FormatHelper::getPhonePriority(),
451 'flagBase' => YATRA_PLUGIN_URL . 'assets/img/flags/',
452 ]);
453
454 // Start fetching the ES module as early as possible (helps shorten white/splash time before React runs)
455 $app_js_url = YATRA_PLUGIN_URL . 'assets/admin/dist/js/app.js';
456 add_action('admin_head', static function () use ($app_js_url, $jsVersion): void {
457 $href = esc_url(add_query_arg('ver', rawurlencode((string) $jsVersion), $app_js_url));
458 echo '<link rel="modulepreload" href="' . $href . '" />' . "\n";
459 }, 0);
460 }
461 }
462 }
463
464 /**
465 * Check if Vite dev server is running
466 *
467 * @param string $url
468 * @return bool
469 */
470 private function isViteDevServerRunning(string $url): bool
471 {
472 // Check the actual asset URL, not the root. Uses the WP HTTP API
473 // (not raw cURL) per WP.org guidelines. Only ever called in dev mode
474 // (WP_DEBUG && YATRA_DEV_MODE), so it never runs on production loads.
475 $assetUrl = $url . '/assets/admin/dist/js/app.js';
476
477 $response = wp_remote_head($assetUrl, [
478 'timeout' => 2,
479 'redirection' => 0,
480 ]);
481
482 if (is_wp_error($response)) {
483 return false;
484 }
485
486 return (int) wp_remote_retrieve_response_code($response) === 200;
487 }
488
489 /**
490 * Add inline script for media library compatibility
491 *
492 * @return void
493 */
494 private function addMediaLibraryCompatScript(): void
495 {
496 // `yatraAdmin` is localized once in enqueueAdminReactJs via buildAdminLocalizedData().
497 // Load WordPress translation data for the yatra domain
498 $this->loadWordPressTranslations();
499 }
500
501 /**
502 * Load WordPress translation data for JavaScript
503 *
504 * @return void
505 */
506 private function loadWordPressTranslations(): void
507 {
508 // Use WordPress built-in function to load script translations.
509 // The third argument MUST be an absolute path to the directory
510 // that contains the per-locale .json translation files.
511 //
512 // Previously this passed YATRA_PLUGIN_FILE — i.e. the main
513 // plugin PHP FILE path, not its directory. Appending
514 // "/i18n/languages" yielded ".../plugin/yatra.php/i18n/languages",
515 // a path that doesn't exist, so WordPress silently fell back to
516 // shipping source-English strings to the React admin regardless
517 // of the operator's WP locale.
518 //
519 // Use YATRA_PLUGIN_PATH (the directory, ending in /) instead,
520 // matching the block-editor side that has always worked.
521 //
522 // The actual JSON file shipped here is generated at BUILD time
523 // by scripts/build-translation-json.mjs from each locale's .po
524 // file. That script writes ONE consolidated JSON per locale
525 // named `yatra-{locale}-{md5(bundle src path)}.json`, so
526 // WordPress's native script-translation loader finds it on
527 // first try — no runtime filter / merge needed.
528 if (function_exists('wp_set_script_translations')) {
529 wp_set_script_translations('yatra-admin', 'yatra', YATRA_PLUGIN_PATH . 'i18n/languages');
530 }
531 }
532
533
534 /**
535 * Enqueue setup wizard assets
536 *
537 * @return void
538 */
539 public function enqueueSetupWizardAssets(): void
540 {
541 // Enqueue setup wizard CSS
542 $cssPath = YATRA_PLUGIN_PATH . 'assets/admin/css/setup-wizard.css';
543 if (file_exists($cssPath)) {
544 wp_enqueue_style(
545 'yatra-setup-wizard',
546 YATRA_PLUGIN_URL . 'assets/admin/css/setup-wizard.css',
547 [],
548 YATRA_VERSION
549 );
550 }
551
552 // Enqueue setup wizard JS
553 $jsPath = YATRA_PLUGIN_PATH . 'assets/admin/js/setup-wizard.js';
554 if (file_exists($jsPath)) {
555 wp_enqueue_script(
556 'yatra-setup-wizard',
557 YATRA_PLUGIN_URL . 'assets/admin/js/setup-wizard.js',
558 ['jquery'],
559 YATRA_VERSION,
560 true
561 );
562
563 // Localize setup wizard
564 wp_localize_script('yatra-setup-wizard', 'yatraSetupWizard', [
565 'ajaxurl' => admin_url('admin-ajax.php'),
566 'nonce' => wp_create_nonce('yatra-setup-wizard'),
567 ]);
568 }
569 }
570 }
571