/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "LocaleService.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/DebugOnly.h" #include "mozilla/Omnijar.h" #include "mozilla/Preferences.h" #include "mozilla/Services.h" #include "mozilla/StaticPrefs_privacy.h" #include "mozilla/intl/AppDateTimeFormat.h" #include "mozilla/intl/Locale.h" #include "mozilla/intl/OSPreferences.h" #include "nsDirectoryService.h" #include "nsDirectoryServiceDefs.h" #include "nsIObserverService.h" #include "nsStringEnumerator.h" #include "nsXULAppAPI.h" #include "nsZipArchive.h" #ifdef XP_WIN # include "WinUtils.h" #endif #ifdef MOZ_WIDGET_GTK # include "mozilla/WidgetUtilsGtk.h" #endif #define INTL_SYSTEM_LOCALES_CHANGED "intl:system-locales-changed" #define PSEUDO_LOCALE_PREF "intl.l10n.pseudo" #define REQUESTED_LOCALES_PREF "intl.locale.requested" #define WEB_EXPOSED_LOCALES_PREF "intl.locale.privacy.web_exposed" static const char* kObservedPrefs[] = {REQUESTED_LOCALES_PREF, WEB_EXPOSED_LOCALES_PREF, PSEUDO_LOCALE_PREF, nullptr}; using namespace mozilla::intl::ffi; using namespace mozilla::intl; using namespace mozilla; NS_IMPL_ISUPPORTS(LocaleService, mozILocaleService, nsIObserver, nsISupportsWeakReference) mozilla::StaticRefPtr LocaleService::sInstance; /** * This function splits an input string by `,` delimiter, sanitizes the result * language tags and returns them to the caller. */ static void SplitLocaleListStringIntoArray(nsACString& str, nsTArray& aRetVal) { if (str.Length() > 0) { for (const nsACString& part : str.Split(',')) { nsAutoCString locale(part); if (LocaleService::CanonicalizeLanguageId(locale)) { if (!aRetVal.Contains(locale)) { aRetVal.AppendElement(locale); } } } } } static void ReadRequestedLocales(nsTArray& aRetVal) { nsAutoCString str; nsresult rv = Preferences::GetCString(REQUESTED_LOCALES_PREF, str); // isRepack means this is a version of Firefox specifically // built for one language. const bool isRepack = #ifdef XP_WIN !mozilla::widget::WinUtils::HasPackageIdentity(); #elif defined(MOZ_WIDGET_GTK) !widget::IsRunningUnderSnap(); #else true; #endif // We handle four scenarios here: // // 1) The pref is not set - use default locale // 2) The pref is not set and we're a packaged app - use OS locales // 3) The pref is set to "" - use OS locales // 4) The pref is set to a value - parse the locale list and use it if (NS_SUCCEEDED(rv)) { if (str.Length() == 0) { // Case 3 OSPreferences::GetInstance()->GetSystemLocales(aRetVal); } else { // Case 4 SplitLocaleListStringIntoArray(str, aRetVal); } } // This will happen when either the pref is not set, // or parsing of the pref didn't produce any usable // result. if (aRetVal.IsEmpty()) { if (isRepack) { // Case 1 nsAutoCString defaultLocale; LocaleService::GetInstance()->GetDefaultLocale(defaultLocale); aRetVal.AppendElement(defaultLocale); } else { // Case 2 OSPreferences::GetInstance()->GetSystemLocales(aRetVal); } } } static void ReadWebExposedLocales(nsTArray& aRetVal) { nsAutoCString str; nsresult rv = Preferences::GetCString(WEB_EXPOSED_LOCALES_PREF, str); if (NS_WARN_IF(NS_FAILED(rv)) || str.Length() == 0) { return; } SplitLocaleListStringIntoArray(str, aRetVal); } LocaleService::LocaleService(bool aIsServer) : mIsServer(aIsServer) {} /** * This function performs the actual language negotiation for the API. * * Currently it collects the locale ID used by nsChromeRegistry and * adds hardcoded default locale as a fallback. */ void LocaleService::NegotiateAppLocales(nsTArray& aRetVal) { if (mIsServer) { nsAutoCString defaultLocale; AutoTArray availableLocales; AutoTArray requestedLocales; GetDefaultLocale(defaultLocale); GetAvailableLocales(availableLocales); GetRequestedLocales(requestedLocales); NegotiateLanguages(requestedLocales, availableLocales, defaultLocale, kLangNegStrategyFiltering, aRetVal); } nsAutoCString lastFallbackLocale; GetLastFallbackLocale(lastFallbackLocale); if (!aRetVal.Contains(lastFallbackLocale)) { // This part is used in one of the two scenarios: // // a) We're in a client mode, and no locale has been set yet, // so we need to return last fallback locale temporarily. // b) We're in a server mode, and the last fallback locale was excluded // when negotiating against the requested locales. // Since we currently package it as a last fallback at build // time, we should also add it at the end of the list at // runtime. aRetVal.AppendElement(lastFallbackLocale); } } LocaleService* LocaleService::GetInstance() { if (!sInstance) { sInstance = new LocaleService(XRE_IsParentProcess()); if (sInstance->IsServer()) { // We're going to observe for requested languages changes which come // from prefs. DebugOnly rv = Preferences::AddWeakObservers(sInstance, kObservedPrefs); MOZ_ASSERT(NS_SUCCEEDED(rv), "Adding observers failed."); nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->AddObserver(sInstance, INTL_SYSTEM_LOCALES_CHANGED, true); obs->AddObserver(sInstance, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); } } // DOM might use ICUUtils and LocaleService during UnbindFromTree by // final cycle collection. ClearOnShutdown(&sInstance, ShutdownPhase::CCPostLastCycleCollection); } return sInstance; } static void NotifyAppLocaleChanged() { nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->NotifyObservers(nullptr, "intl:app-locales-changed", nullptr); } // The locale in AppDateTimeFormat is cached statically. AppDateTimeFormat::ClearLocaleCache(); } void LocaleService::RemoveObservers() { if (mIsServer) { Preferences::RemoveObservers(this, kObservedPrefs); nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->RemoveObserver(this, INTL_SYSTEM_LOCALES_CHANGED); obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); } } } void LocaleService::AssignAppLocales(const nsTArray& aAppLocales) { MOZ_ASSERT(!mIsServer, "This should only be called for LocaleService in client mode."); mAppLocales = aAppLocales.Clone(); NotifyAppLocaleChanged(); } void LocaleService::AssignRequestedLocales( const nsTArray& aRequestedLocales) { MOZ_ASSERT(!mIsServer, "This should only be called for LocaleService in client mode."); mRequestedLocales = aRequestedLocales.Clone(); nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->NotifyObservers(nullptr, "intl:requested-locales-changed", nullptr); } } void LocaleService::RequestedLocalesChanged() { MOZ_ASSERT(mIsServer, "This should only be called in the server mode."); nsTArray newLocales; ReadRequestedLocales(newLocales); if (mRequestedLocales != newLocales) { mRequestedLocales = std::move(newLocales); nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->NotifyObservers(nullptr, "intl:requested-locales-changed", nullptr); } LocalesChanged(); } } void LocaleService::WebExposedLocalesChanged() { MOZ_ASSERT(mIsServer, "This should only be called in the server mode."); nsTArray newLocales; ReadWebExposedLocales(newLocales); if (mWebExposedLocales != newLocales) { mWebExposedLocales = std::move(newLocales); } } void LocaleService::LocalesChanged() { MOZ_ASSERT(mIsServer, "This should only be called in the server mode."); // if mAppLocales has not been initialized yet, just return if (mAppLocales.IsEmpty()) { return; } nsTArray newLocales; NegotiateAppLocales(newLocales); if (mAppLocales != newLocales) { mAppLocales = std::move(newLocales); NotifyAppLocaleChanged(); } } bool LocaleService::IsLocaleRTL(const nsACString& aLocale) { return unic_langid_is_rtl(&aLocale); } bool LocaleService::IsAppLocaleRTL() { // Next, check if there is a pseudo locale `bidi` set. nsAutoCString pseudoLocale; if (NS_SUCCEEDED(Preferences::GetCString("intl.l10n.pseudo", pseudoLocale))) { if (pseudoLocale.EqualsLiteral("bidi")) { return true; } if (pseudoLocale.EqualsLiteral("accented")) { return false; } } nsAutoCString locale; GetAppLocaleAsBCP47(locale); return IsLocaleRTL(locale); } NS_IMETHODIMP LocaleService::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { MOZ_ASSERT(mIsServer, "This should only be called in the server mode."); if (!strcmp(aTopic, INTL_SYSTEM_LOCALES_CHANGED)) { RequestedLocalesChanged(); WebExposedLocalesChanged(); } else if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { RemoveObservers(); } else { NS_ConvertUTF16toUTF8 pref(aData); // At the moment the only thing we're observing are settings indicating // user requested locales. if (pref.EqualsLiteral(REQUESTED_LOCALES_PREF)) { RequestedLocalesChanged(); } else if (pref.EqualsLiteral(WEB_EXPOSED_LOCALES_PREF)) { WebExposedLocalesChanged(); } else if (pref.EqualsLiteral(PSEUDO_LOCALE_PREF)) { NotifyAppLocaleChanged(); } } return NS_OK; } bool LocaleService::LanguagesMatch(const nsACString& aRequested, const nsACString& aAvailable) { Locale requested; auto requestedResult = LocaleParser::TryParse(aRequested, requested); Locale available; auto availableResult = LocaleParser::TryParse(aAvailable, available); if (requestedResult.isErr() || availableResult.isErr()) { return false; } if (requested.Canonicalize().isErr() || available.Canonicalize().isErr()) { return false; } return requested.Language().Span() == available.Language().Span(); } bool LocaleService::IsServer() { return mIsServer; } static bool GetGREFileContents(const char* aFilePath, nsCString* aOutString) { // Look for the requested file in omnijar. RefPtr zip = Omnijar::GetReader(Omnijar::GRE); if (zip) { nsZipItemPtr item(zip, aFilePath); if (!item) { return false; } aOutString->Assign(item.Buffer(), item.Length()); return true; } // If we didn't have an omnijar (i.e. we're running a non-packaged // build), then look in the GRE directory. nsCOMPtr path; if (NS_FAILED(nsDirectoryService::gService->Get( NS_GRE_DIR, NS_GET_IID(nsIFile), getter_AddRefs(path)))) { return false; } path->AppendRelativeNativePath(nsDependentCString(aFilePath)); bool result; if (NS_FAILED(path->IsFile(&result)) || !result || NS_FAILED(path->IsReadable(&result)) || !result) { return false; } // This is a small file, only used once, so it's not worth doing some fancy // off-main-thread file I/O or whatever. Just read it. FILE* fp; if (NS_FAILED(path->OpenANSIFileDesc("r", &fp)) || !fp) { return false; } fseek(fp, 0, SEEK_END); long len = ftell(fp); rewind(fp); aOutString->SetLength(len); size_t cc = fread(aOutString->BeginWriting(), 1, len, fp); fclose(fp); return cc == size_t(len); } void LocaleService::InitPackagedLocales() { MOZ_ASSERT(mPackagedLocales.IsEmpty()); nsAutoCString localesString; if (GetGREFileContents("res/multilocale.txt", &localesString)) { localesString.Trim(" \t\n\r"); // This should never be empty in a correctly-built product. MOZ_ASSERT(!localesString.IsEmpty()); SplitLocaleListStringIntoArray(localesString, mPackagedLocales); } // Last resort in case of broken build if (mPackagedLocales.IsEmpty()) { nsAutoCString defaultLocale; GetDefaultLocale(defaultLocale); mPackagedLocales.AppendElement(defaultLocale); } } /** * mozILocaleService methods */ NS_IMETHODIMP LocaleService::GetDefaultLocale(nsACString& aRetVal) { // We don't allow this to change during a session (it's set at build/package // time), so we cache the result the first time we're called. if (mDefaultLocale.IsEmpty()) { nsAutoCString locale; // Try to get the package locale from update.locale in omnijar. If the // update.locale file is not found, item.len will remain 0 and we'll // just use our hard-coded default below. GetGREFileContents("update.locale", &locale); locale.Trim(" \t\n\r"); #ifdef MOZ_UPDATER // This should never be empty. MOZ_ASSERT(!locale.IsEmpty()); #endif if (CanonicalizeLanguageId(locale)) { mDefaultLocale.Assign(locale); } // Hard-coded fallback to allow us to survive even if update.locale was // missing/broken in some way. if (mDefaultLocale.IsEmpty()) { GetLastFallbackLocale(mDefaultLocale); } } aRetVal = mDefaultLocale; return NS_OK; } NS_IMETHODIMP LocaleService::GetLastFallbackLocale(nsACString& aRetVal) { aRetVal.AssignLiteral("en-US"); return NS_OK; } NS_IMETHODIMP LocaleService::GetAppLocalesAsLangTags(nsTArray& aRetVal) { if (mAppLocales.IsEmpty()) { NegotiateAppLocales(mAppLocales); } for (uint32_t i = 0; i < mAppLocales.Length(); i++) { nsAutoCString locale(mAppLocales[i]); if (locale.LowerCaseEqualsASCII("ja-jp-macos")) { aRetVal.AppendElement("ja-JP-mac"); } else { aRetVal.AppendElement(locale); } } return NS_OK; } NS_IMETHODIMP LocaleService::GetAppLocalesAsBCP47(nsTArray& aRetVal) { if (mAppLocales.IsEmpty()) { NegotiateAppLocales(mAppLocales); } aRetVal = mAppLocales.Clone(); return NS_OK; } NS_IMETHODIMP LocaleService::GetAppLocaleAsLangTag(nsACString& aRetVal) { AutoTArray locales; GetAppLocalesAsLangTags(locales); aRetVal = locales[0]; return NS_OK; } NS_IMETHODIMP LocaleService::GetAppLocaleAsBCP47(nsACString& aRetVal) { if (mAppLocales.IsEmpty()) { NegotiateAppLocales(mAppLocales); } aRetVal = mAppLocales[0]; return NS_OK; } NS_IMETHODIMP LocaleService::GetRegionalPrefsLocales(nsTArray& aRetVal) { bool useOSLocales = Preferences::GetBool("intl.regional_prefs.use_os_locales", false); // If the user specified that they want to use OS Regional Preferences // locales, try to retrieve them and use. if (useOSLocales) { if (NS_SUCCEEDED( OSPreferences::GetInstance()->GetRegionalPrefsLocales(aRetVal))) { return NS_OK; } // If we fail to retrieve them, return the app locales. GetAppLocalesAsBCP47(aRetVal); return NS_OK; } // Otherwise, fetch OS Regional Preferences locales and compare the first one // to the app locale. If the language subtag matches, we can safely use // the OS Regional Preferences locale. // // This facilitates scenarios such as Firefox in "en-US" and User sets // regional prefs to "en-GB". nsAutoCString appLocale; AutoTArray regionalPrefsLocales; LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocale); if (NS_FAILED(OSPreferences::GetInstance()->GetRegionalPrefsLocales( regionalPrefsLocales))) { GetAppLocalesAsBCP47(aRetVal); return NS_OK; } if (LocaleService::LanguagesMatch(appLocale, regionalPrefsLocales[0])) { aRetVal = regionalPrefsLocales.Clone(); return NS_OK; } // Otherwise use the app locales. GetAppLocalesAsBCP47(aRetVal); return NS_OK; } NS_IMETHODIMP LocaleService::GetWebExposedLocales(nsTArray& aRetVal) { if (StaticPrefs::privacy_spoof_english() == 2) { aRetVal = nsTArray({"en-US"_ns}); return NS_OK; } if (!mWebExposedLocales.IsEmpty()) { aRetVal = mWebExposedLocales.Clone(); return NS_OK; } return GetRegionalPrefsLocales(aRetVal); } NS_IMETHODIMP LocaleService::NegotiateLanguages(const nsTArray& aRequested, const nsTArray& aAvailable, const nsACString& aDefaultLocale, int32_t aStrategy, nsTArray& aRetVal) { if (aStrategy < 0 || aStrategy > 2) { return NS_ERROR_INVALID_ARG; } #ifdef DEBUG Locale parsedLocale; auto result = LocaleParser::TryParse(aDefaultLocale, parsedLocale); MOZ_ASSERT( aDefaultLocale.IsEmpty() || result.isOk(), "If specified, default locale must be a well-formed BCP47 language tag."); #endif if (aStrategy == kLangNegStrategyLookup && aDefaultLocale.IsEmpty()) { NS_WARNING( "Default locale should be specified when using lookup strategy."); } NegotiationStrategy strategy; switch (aStrategy) { case kLangNegStrategyFiltering: strategy = NegotiationStrategy::Filtering; break; case kLangNegStrategyMatching: strategy = NegotiationStrategy::Matching; break; case kLangNegStrategyLookup: strategy = NegotiationStrategy::Lookup; break; } fluent_langneg_negotiate_languages(&aRequested, &aAvailable, &aDefaultLocale, strategy, &aRetVal); return NS_OK; } NS_IMETHODIMP LocaleService::GetRequestedLocales(nsTArray& aRetVal) { if (mRequestedLocales.IsEmpty()) { ReadRequestedLocales(mRequestedLocales); } aRetVal = mRequestedLocales.Clone(); return NS_OK; } NS_IMETHODIMP LocaleService::GetRequestedLocale(nsACString& aRetVal) { if (mRequestedLocales.IsEmpty()) { ReadRequestedLocales(mRequestedLocales); } if (mRequestedLocales.Length() > 0) { aRetVal = mRequestedLocales[0]; } return NS_OK; } NS_IMETHODIMP LocaleService::SetRequestedLocales(const nsTArray& aRequested) { MOZ_ASSERT(mIsServer, "This should only be called in the server mode."); if (!mIsServer) { return NS_ERROR_UNEXPECTED; } nsAutoCString str; for (auto& req : aRequested) { nsAutoCString locale(req); if (!CanonicalizeLanguageId(locale)) { NS_ERROR("Invalid language tag provided to SetRequestedLocales!"); return NS_ERROR_INVALID_ARG; } if (!str.IsEmpty()) { str.AppendLiteral(","); } str.Append(locale); } Preferences::SetCString(REQUESTED_LOCALES_PREF, str); return NS_OK; } NS_IMETHODIMP LocaleService::GetAvailableLocales(nsTArray& aRetVal) { MOZ_ASSERT(mIsServer, "This should only be called in the server mode."); if (!mIsServer) { return NS_ERROR_UNEXPECTED; } if (mAvailableLocales.IsEmpty()) { // If there are no available locales set, it means that L10nRegistry // did not register its locale pool yet. The best course of action // is to use packaged locales until that happens. GetPackagedLocales(mAvailableLocales); } aRetVal = mAvailableLocales.Clone(); return NS_OK; } NS_IMETHODIMP LocaleService::GetIsAppLocaleRTL(bool* aRetVal) { (*aRetVal) = IsAppLocaleRTL(); return NS_OK; } NS_IMETHODIMP LocaleService::SetAvailableLocales(const nsTArray& aAvailable) { MOZ_ASSERT(mIsServer, "This should only be called in the server mode."); if (!mIsServer) { return NS_ERROR_UNEXPECTED; } nsTArray newLocales; for (auto& avail : aAvailable) { nsAutoCString locale(avail); if (!CanonicalizeLanguageId(locale)) { NS_ERROR("Invalid language tag provided to SetAvailableLocales!"); return NS_ERROR_INVALID_ARG; } newLocales.AppendElement(locale); } if (newLocales != mAvailableLocales) { mAvailableLocales = std::move(newLocales); LocalesChanged(); } return NS_OK; } NS_IMETHODIMP LocaleService::GetPackagedLocales(nsTArray& aRetVal) { if (mPackagedLocales.IsEmpty()) { InitPackagedLocales(); } aRetVal = mPackagedLocales.Clone(); return NS_OK; }