PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 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 All 85 releases
← All changes | app/Providers/AdminAssetsProvider.php +159 -13 3.0.6 → 3.0.17 View file →
@@ -63,8 +63,11 @@
63 63 'currentUserAvatar' => get_avatar($current_user->ID, 96),
64 64 'siteUrl' => home_url(),
65 65 'adminUrl' => admin_url('admin.php'),
66 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(),
67 70 // Brand-name and brand-logo helpers are filter-backed (defaults
68 71 // wired in includes/helpers.php). Pro's WhiteLabel module
69 72 // overrides the filters when Agency white-label is active.
70 73 'brandLogoUrl' => function_exists('yatra_get_brand_icon_url') ? yatra_get_brand_icon_url() : '',
@@ -153,9 +156,9 @@
153 156 'locale' => get_locale(),
154 157 'currency' => \Yatra\Services\SettingsService::getCurrency(),
155 158 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
156 159 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
157 - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'),
160 + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
158 161 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
159 162 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
160 163 'date_format' => \Yatra\Services\SettingsService::get('date_format', 'Y-m-d'),
161 164 'time_format' => \Yatra\Services\SettingsService::get('time_format', 'H:i'),
@@ -405,9 +408,17 @@
405 408 // Use built assets in production
406 409 $appJs = YATRA_PLUGIN_PATH . 'assets/admin/dist/js/app.js';
407 410
408 411 if (file_exists($appJs)) {
409 - $jsVersion = YATRA_VERSION . '.' . filemtime($appJs) . '.view-icon-fix.' . time() . '.' . microtime(true);
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);
410 421
411 422 $localized_data = $this->buildAdminLocalizedData();
412 423
413 424 // Enqueue our script with media library as dependency
@@ -428,11 +439,32 @@
428 439 $jsVersion,
429 440 true
430 441 );
431 442
443 + // The bundle calls the global wp.i18n.__() (it never ships its
444 + // own copy), and scripts/extract-js-pot.mjs writes every admin
445 + // string's `#:` reference as this bundle's path precisely so
446 + // WordPress's md5(handle src) JSON lookup matches. This call is
447 + // the missing last link: it tells WordPress to load
448 + // i18n/languages/yatra-{locale}-{md5}.json (or the copy under
449 + // WP_LANG_DIR/plugins) for the admin UI. Without it, translated
450 + // admin strings never reach the SPA. Mirrors FrontendAssetsProvider.
451 + if (function_exists('wp_set_script_translations')) {
452 + wp_set_script_translations('yatra-admin', 'yatra', YATRA_PLUGIN_PATH . 'i18n/languages');
453 + $this->backfillScriptTranslations('yatra-admin');
454 + }
455 +
432 456 // Localize script data
433 457 wp_localize_script('yatra-admin', 'yatraAdmin', $localized_data);
434 458
459 + // Phone dataset for admin displays (flag + dial-code detection of
460 + // stored "+<code><number>" values in booking details).
461 + wp_localize_script('yatra-admin', 'yatraPhoneData', [
462 + 'countries' => \Yatra\Helpers\FormatHelper::getPhoneCountries(),
463 + 'priority' => \Yatra\Helpers\FormatHelper::getPhonePriority(),
464 + 'flagBase' => YATRA_PLUGIN_URL . 'assets/img/flags/',
465 + ]);
466 +
435 467 // Start fetching the ES module as early as possible (helps shorten white/splash time before React runs)
436 468 $app_js_url = YATRA_PLUGIN_URL . 'assets/admin/dist/js/app.js';
437 469 add_action('admin_head', static function () use ($app_js_url, $jsVersion): void {
438 470 $href = esc_url(add_query_arg('ver', rawurlencode((string) $jsVersion), $app_js_url));
@@ -449,20 +481,23 @@
449 481 * @return bool
450 482 */
451 483 private function isViteDevServerRunning(string $url): bool
452 484 {
453 - // Check the actual asset URL, not the root
485 + // Check the actual asset URL, not the root. Uses the WP HTTP API
486 + // (not raw cURL) per WP.org guidelines. Only ever called in dev mode
487 + // (WP_DEBUG && YATRA_DEV_MODE), so it never runs on production loads.
454 488 $assetUrl = $url . '/assets/admin/dist/js/app.js';
455 - $ch = curl_init($assetUrl);
456 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
457 - curl_setopt($ch, CURLOPT_TIMEOUT, 2); // 2 second timeout
458 - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1); // 1 second connection timeout
459 - curl_setopt($ch, CURLOPT_NOBODY, true); // HEAD request only
460 - curl_exec($ch);
461 - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
462 - curl_close($ch);
463 -
464 - return $httpCode === 200;
489 +
490 + $response = wp_remote_head($assetUrl, [
491 + 'timeout' => 2,
492 + 'redirection' => 0,
493 + ]);
494 +
495 + if (is_wp_error($response)) {
496 + return false;
497 + }
498 +
499 + return (int) wp_remote_retrieve_response_code($response) === 200;
465 500 }
466 501
467 502 /**
468 503 * Add inline script for media library compatibility
@@ -513,8 +548,119 @@
513 548 * Enqueue setup wizard assets
514 549 *
515 550 * @return void
516 551 */
552 + /**
553 + * Feed the admin bundle its translations when no JSON file exists.
554 + *
555 + * wp_set_script_translations() can only read a `yatra-{locale}-{md5}.json`
556 + * file. WordPress.org language packs ship one, so translations from
557 + * translate.wordpress.org simply work — but a site translated by hand, with
558 + * Loco Translate or a .po/.mo dropped into wp-content/languages, has only
559 + * the PHP catalogue. For those sites every string in the React admin stayed
560 + * in English no matter how complete the translation was, which looked like
561 + * the plugin ignoring the translation altogether.
562 + *
563 + * The strings are the same ones the PHP catalogue already holds, so they are
564 + * handed to wp.i18n directly. Core is asked first and left in charge
565 + * whenever it can find a JSON file: that path is cached by the browser as a
566 + * separate request, and this one is not.
567 + *
568 + * Nothing is emitted on an untranslated site — the map comes back empty and
569 + * English sites carry no extra weight.
570 + */
571 + private function backfillScriptTranslations(string $handle): void
572 + {
573 + if (!function_exists('load_script_textdomain')) {
574 + return;
575 + }
576 +
577 + // A real JSON file beats this: let WordPress load it as it normally would.
578 + if (load_script_textdomain($handle, 'yatra', YATRA_PLUGIN_PATH . 'i18n/languages')) {
579 + return;
580 + }
581 +
582 + $localeData = $this->localeDataFromTextdomain('yatra');
583 + if ($localeData === []) {
584 + return;
585 + }
586 +
587 + wp_add_inline_script(
588 + $handle,
589 + 'wp.i18n.setLocaleData(' . wp_json_encode($localeData) . ', "yatra");',
590 + 'before'
591 + );
592 + }
593 +
594 + /**
595 + * The loaded PHP catalogue, in the shape wp.i18n.setLocaleData() expects.
596 + *
597 + * Entries that were never translated are left out: they would only restate
598 + * the English the bundle already carries. Contexts use the same NUL
599 + * separator Jed and gettext use, so _x() resolves too.
600 + *
601 + * @return array<string, mixed>
602 + */
603 + private function localeDataFromTextdomain(string $domain): array
604 + {
605 + $translations = get_translations_for_domain($domain);
606 + if (!is_object($translations)) {
607 + return [];
608 + }
609 + // Deliberately duck-typed, and deliberately not isset()/??. WordPress
610 + // returns Translations, NOOP_Translations or — since the performant
611 + // translations work — WP_Translations, which neither extends
612 + // Translations nor declares `entries`: it serves that property through
613 + // __get() and defines no __isset(), so both instanceof and isset()
614 + // report nothing is there and quietly disable this fallback. Only a
615 + // direct read reaches the magic getter.
616 + if (!property_exists($translations, 'entries') && !method_exists($translations, '__get')) {
617 + return [];
618 + }
619 +
620 + $entries = $translations->entries;
621 + if (!is_array($entries) || $entries === []) {
622 + return [];
623 + }
624 +
625 + $data = [];
626 + foreach ($entries as $entry) {
627 + if (!is_object($entry) || (string) $entry->singular === '') {
628 + continue;
629 + }
630 +
631 + $forms = array_values(array_filter(
632 + (array) $entry->translations,
633 + static fn($t) => is_string($t) && $t !== ''
634 + ));
635 + if ($forms === []) {
636 + continue;
637 + }
638 + // Untranslated entries come back as the original string.
639 + if (count($forms) === 1 && $forms[0] === $entry->singular) {
640 + continue;
641 + }
642 +
643 + $key = ($entry->context !== null && $entry->context !== '')
644 + ? $entry->context . "\u{0004}" . $entry->singular
645 + : $entry->singular;
646 +
647 + $data[$key] = $forms;
648 + }
649 +
650 + if ($data === []) {
651 + return [];
652 + }
653 +
654 + $data[''] = [
655 + 'domain' => $domain,
656 + 'lang' => determine_locale(),
657 + 'plural-forms' => 'nplurals=2; plural=(n != 1);',
658 + ];
659 +
660 + return $data;
661 + }
662 +
517 663 public function enqueueSetupWizardAssets(): void
518 664 {
519 665 // Enqueue setup wizard CSS
520 666 $cssPath = YATRA_PLUGIN_PATH . 'assets/admin/css/setup-wizard.css';