PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.7
Yatra – Travel Booking & Tour Operator Software v3.0.7
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
yatra / app / Providers / AdminAssetsProvider.php

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

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