/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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 "Localization.h" #include "nsIObserverService.h" #include "mozilla/BasePrincipal.h" #include "mozilla/Preferences.h" #include "mozilla/Services.h" #include "mozilla/dom/PromiseNativeHandler.h" #define INTL_APP_LOCALES_CHANGED "intl:app-locales-changed" #define L10N_PSEUDO_PREF "intl.l10n.pseudo" using namespace mozilla; using namespace mozilla::dom; using namespace mozilla::intl; static const char* kObservedPrefs[] = {L10N_PSEUDO_PREF, nullptr}; static nsTArray ConvertFromL10nKeys( const Sequence& aKeys) { nsTArray l10nKeys(aKeys.Length()); for (const auto& entry : aKeys) { if (entry.IsUTF8String()) { const auto& id = entry.GetAsUTF8String(); ffi::L10nKey* key = l10nKeys.AppendElement(); key->id = &id; } else { const auto& e = entry.GetAsL10nIdArgs(); ffi::L10nKey* key = l10nKeys.AppendElement(); key->id = &e.mId; if (!e.mArgs.IsNull()) { FluentBundle::ConvertArgs(e.mArgs.Value(), key->args); } } } return l10nKeys; } [[nodiscard]] static bool ConvertToAttributeNameValue( const nsTArray& aAttributes, FallibleTArray& aValues) { if (!aValues.SetCapacity(aAttributes.Length(), fallible)) { return false; } for (const auto& attr : aAttributes) { auto* cvtAttr = aValues.AppendElement(fallible); MOZ_ASSERT(cvtAttr, "SetCapacity didn't set enough capacity somehow?"); cvtAttr->mName = attr.name; cvtAttr->mValue = attr.value; } return true; } [[nodiscard]] static bool ConvertToL10nMessages( const nsTArray& aMessages, nsTArray>& aOut) { if (!aOut.SetCapacity(aMessages.Length(), fallible)) { return false; } for (const auto& entry : aMessages) { Nullable* msg = aOut.AppendElement(fallible); MOZ_ASSERT(msg, "SetCapacity didn't set enough capacity somehow?"); if (!entry.is_present) { continue; } L10nMessage& m = msg->SetValue(); if (!entry.message.value.IsVoid()) { m.mValue = entry.message.value; } if (!entry.message.attributes.IsEmpty()) { auto& value = m.mAttributes.SetValue(); if (!ConvertToAttributeNameValue(entry.message.attributes, value)) { return false; } } } return true; } NS_IMPL_CYCLE_COLLECTING_ADDREF(Localization) NS_IMPL_CYCLE_COLLECTING_RELEASE(Localization) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Localization) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WEAK(Localization, mGlobal) /* static */ already_AddRefed Localization::Create( const nsTArray& aResourceIds, bool aIsSync) { return MakeAndAddRef(aResourceIds, aIsSync); } /* static */ already_AddRefed Localization::Create( const nsTArray& aResourceIds, bool aIsSync) { return MakeAndAddRef(aResourceIds, aIsSync); } Localization::Localization(const nsTArray& aResIds, bool aIsSync) { auto ffiResourceIds{L10nRegistry::ResourceIdsToFFI(aResIds)}; ffi::localization_new(&ffiResourceIds, aIsSync, nullptr, getter_AddRefs(mRaw)); RegisterObservers(); } Localization::Localization(const nsTArray& aResIds, bool aIsSync) { ffi::localization_new(&aResIds, aIsSync, nullptr, getter_AddRefs(mRaw)); RegisterObservers(); } Localization::Localization(nsIGlobalObject* aGlobal, const nsTArray& aResIds, bool aIsSync) : mGlobal(aGlobal) { nsTArray resourceIds{ L10nRegistry::ResourceIdsToFFI(aResIds)}; ffi::localization_new(&resourceIds, aIsSync, nullptr, getter_AddRefs(mRaw)); RegisterObservers(); } Localization::Localization(nsIGlobalObject* aGlobal, bool aIsSync) : mGlobal(aGlobal) { nsTArray resIds; ffi::localization_new(&resIds, aIsSync, nullptr, getter_AddRefs(mRaw)); RegisterObservers(); } Localization::Localization(nsIGlobalObject* aGlobal, bool aIsSync, const ffi::LocalizationRc* aRaw) : mGlobal(aGlobal), mRaw(aRaw) { RegisterObservers(); } already_AddRefed Localization::Constructor( const GlobalObject& aGlobal, const Sequence& aResourceIds, bool aIsSync, const Optional>& aRegistry, const Optional>& aLocales, ErrorResult& aRv) { auto ffiResourceIds{L10nRegistry::ResourceIdsToFFI(aResourceIds)}; Maybe> locales; if (aLocales.WasPassed()) { locales.emplace(); locales->SetCapacity(aLocales.Value().Length()); for (const auto& locale : aLocales.Value()) { locales->AppendElement(locale); } } RefPtr raw; bool result = ffi::localization_new_with_locales( &ffiResourceIds, aIsSync, aRegistry.WasPassed() ? aRegistry.Value().Raw() : nullptr, locales.ptrOr(nullptr), getter_AddRefs(raw)); if (!result) { aRv.ThrowInvalidStateError( "Failed to create the Localization. Check the locales arguments."); return nullptr; } nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); return do_AddRef(new Localization(global, aIsSync, raw)); } JSObject* Localization::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return Localization_Binding::Wrap(aCx, this, aGivenProto); } Localization::~Localization() = default; NS_IMETHODIMP Localization::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (!strcmp(aTopic, INTL_APP_LOCALES_CHANGED)) { OnChange(); } else { MOZ_ASSERT(!strcmp("nsPref:changed", aTopic)); nsDependentString pref(aData); if (pref.EqualsLiteral(L10N_PSEUDO_PREF)) { OnChange(); } } return NS_OK; } void Localization::RegisterObservers() { DebugOnly rv = Preferences::AddWeakObservers(this, kObservedPrefs); MOZ_ASSERT(NS_SUCCEEDED(rv), "Adding observers failed."); nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->AddObserver(this, INTL_APP_LOCALES_CHANGED, true); } } void Localization::OnChange() { ffi::localization_on_change(mRaw.get()); } void Localization::AddResourceId(const ffi::GeckoResourceId& aResourceId) { ffi::localization_add_res_id(mRaw.get(), &aResourceId); } void Localization::AddResourceId(const nsCString& aResourceId) { auto ffiResourceId{L10nRegistry::ResourceIdToFFI(aResourceId)}; AddResourceId(ffiResourceId); } void Localization::AddResourceId( const dom::OwningUTF8StringOrResourceId& aResourceId) { auto ffiResourceId{L10nRegistry::ResourceIdToFFI(aResourceId)}; AddResourceId(ffiResourceId); } uint32_t Localization::RemoveResourceId( const ffi::GeckoResourceId& aResourceId) { return ffi::localization_remove_res_id(mRaw.get(), &aResourceId); } uint32_t Localization::RemoveResourceId(const nsCString& aResourceId) { auto ffiResourceId{L10nRegistry::ResourceIdToFFI(aResourceId)}; return RemoveResourceId(ffiResourceId); } uint32_t Localization::RemoveResourceId( const dom::OwningUTF8StringOrResourceId& aResourceId) { auto ffiResourceId{L10nRegistry::ResourceIdToFFI(aResourceId)}; return RemoveResourceId(ffiResourceId); } void Localization::AddResourceIds( const nsTArray& aResourceIds) { auto ffiResourceIds{L10nRegistry::ResourceIdsToFFI(aResourceIds)}; ffi::localization_add_res_ids(mRaw.get(), &ffiResourceIds); } uint32_t Localization::RemoveResourceIds( const nsTArray& aResourceIds) { auto ffiResourceIds{L10nRegistry::ResourceIdsToFFI(aResourceIds)}; return ffi::localization_remove_res_ids(mRaw.get(), &ffiResourceIds); } already_AddRefed Localization::FormatValue( const nsACString& aId, const Optional& aArgs, ErrorResult& aRv) { nsTArray l10nArgs; nsTArray errors; if (aArgs.WasPassed()) { const L10nArgs& args = aArgs.Value(); FluentBundle::ConvertArgs(args, l10nArgs); } RefPtr promise = Promise::Create(mGlobal, aRv); ffi::localization_format_value( mRaw.get(), &aId, &l10nArgs, promise, [](const Promise* aPromise, const nsACString* aValue, const nsTArray* aErrors) { Promise* promise = const_cast(aPromise); ErrorResult rv; if (MaybeReportErrorsToGecko(*aErrors, rv, promise->GetParentObject())) { promise->MaybeReject(std::move(rv)); } else { promise->MaybeResolve(aValue); } }); return MaybeWrapPromise(promise); } already_AddRefed Localization::FormatValues( const Sequence& aKeys, ErrorResult& aRv) { nsTArray l10nKeys = ConvertFromL10nKeys(aKeys); RefPtr promise = Promise::Create(mGlobal, aRv); if (aRv.Failed()) { return nullptr; } ffi::localization_format_values( mRaw.get(), &l10nKeys, promise, // callback function which will be invoked by the rust code, passing the // promise back in. [](const Promise* aPromise, const nsTArray* aValues, const nsTArray* aErrors) { Promise* promise = const_cast(aPromise); ErrorResult rv; if (MaybeReportErrorsToGecko(*aErrors, rv, promise->GetParentObject())) { promise->MaybeReject(std::move(rv)); } else { promise->MaybeResolve(*aValues); } }); return MaybeWrapPromise(promise); } already_AddRefed Localization::FormatMessages( const Sequence& aKeys, ErrorResult& aRv) { auto l10nKeys = ConvertFromL10nKeys(aKeys); RefPtr promise = Promise::Create(mGlobal, aRv); if (aRv.Failed()) { return nullptr; } ffi::localization_format_messages( mRaw.get(), &l10nKeys, promise, // callback function which will be invoked by the rust code, passing the // promise back in. [](const Promise* aPromise, const nsTArray* aRaw, const nsTArray* aErrors) { Promise* promise = const_cast(aPromise); ErrorResult rv; if (MaybeReportErrorsToGecko(*aErrors, rv, promise->GetParentObject())) { promise->MaybeReject(std::move(rv)); } else { nsTArray> messages; if (!ConvertToL10nMessages(*aRaw, messages)) { promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY); } else { promise->MaybeResolve(std::move(messages)); } } }); return MaybeWrapPromise(promise); } void Localization::FormatValueSync(const nsACString& aId, const Optional& aArgs, nsACString& aRetVal, ErrorResult& aRv) { nsTArray l10nArgs; nsTArray errors; if (aArgs.WasPassed()) { const L10nArgs& args = aArgs.Value(); FluentBundle::ConvertArgs(args, l10nArgs); } bool rv = ffi::localization_format_value_sync(mRaw.get(), &aId, &l10nArgs, &aRetVal, &errors); if (rv) { MaybeReportErrorsToGecko(errors, aRv, GetParentObject()); } else { aRv.ThrowInvalidStateError( "Can't use formatValueSync when state is async."); } } void Localization::FormatValuesSync( const Sequence& aKeys, nsTArray& aRetVal, ErrorResult& aRv) { nsTArray l10nKeys(aKeys.Length()); nsTArray errors; for (const auto& entry : aKeys) { if (entry.IsUTF8String()) { const auto& id = entry.GetAsUTF8String(); nsTArray l10nArgs; ffi::L10nKey* key = l10nKeys.AppendElement(); key->id = &id; } else { const auto& e = entry.GetAsL10nIdArgs(); nsTArray l10nArgs; ffi::L10nKey* key = l10nKeys.AppendElement(); key->id = &e.mId; if (!e.mArgs.IsNull()) { FluentBundle::ConvertArgs(e.mArgs.Value(), key->args); } } } bool rv = ffi::localization_format_values_sync(mRaw.get(), &l10nKeys, &aRetVal, &errors); if (rv) { MaybeReportErrorsToGecko(errors, aRv, GetParentObject()); } else { aRv.ThrowInvalidStateError( "Can't use formatValuesSync when state is async."); } } void Localization::FormatMessagesSync( const Sequence& aKeys, nsTArray>& aRetVal, ErrorResult& aRv) { nsTArray l10nKeys(aKeys.Length()); nsTArray errors; for (const auto& entry : aKeys) { if (entry.IsUTF8String()) { const auto& id = entry.GetAsUTF8String(); nsTArray l10nArgs; ffi::L10nKey* key = l10nKeys.AppendElement(); key->id = &id; } else { const auto& e = entry.GetAsL10nIdArgs(); nsTArray l10nArgs; ffi::L10nKey* key = l10nKeys.AppendElement(); key->id = &e.mId; if (!e.mArgs.IsNull()) { FluentBundle::ConvertArgs(e.mArgs.Value(), key->args); } } } nsTArray result(l10nKeys.Length()); bool rv = ffi::localization_format_messages_sync(mRaw.get(), &l10nKeys, &result, &errors); if (!rv) { return aRv.ThrowInvalidStateError( "Can't use formatMessagesSync when state is async."); } MaybeReportErrorsToGecko(errors, aRv, GetParentObject()); if (aRv.Failed()) { return; } if (!ConvertToL10nMessages(result, aRetVal)) { return aRv.Throw(NS_ERROR_OUT_OF_MEMORY); } } void Localization::SetAsync() { ffi::localization_set_async(mRaw.get()); } bool Localization::IsSync() { return ffi::localization_is_sync(mRaw.get()); } /** * PromiseResolver is a PromiseNativeHandler used * by MaybeWrapPromise method. */ class PromiseResolver final : public PromiseNativeHandler { public: NS_DECL_ISUPPORTS explicit PromiseResolver(Promise* aPromise); void ResolvedCallback(JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) override; void RejectedCallback(JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) override; protected: virtual ~PromiseResolver(); RefPtr mPromise; }; NS_INTERFACE_MAP_BEGIN(PromiseResolver) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_ADDREF(PromiseResolver) NS_IMPL_RELEASE(PromiseResolver) PromiseResolver::PromiseResolver(Promise* aPromise) { mPromise = aPromise; } void PromiseResolver::ResolvedCallback(JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { mPromise->MaybeResolveWithClone(aCx, aValue); } void PromiseResolver::RejectedCallback(JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { mPromise->MaybeRejectWithClone(aCx, aValue); } PromiseResolver::~PromiseResolver() { mPromise = nullptr; } /** * MaybeWrapPromise is a helper method used by Localization * API methods to clone the value returned by a promise * into a new context. * * This allows for a promise from a privileged context * to be returned into an unprivileged document. * * This method is only used for promises that carry values. */ already_AddRefed Localization::MaybeWrapPromise( Promise* aInnerPromise) { // For system principal we don't need to wrap the // result promise at all. nsIPrincipal* principal = mGlobal->PrincipalOrNull(); if (principal && principal->IsSystemPrincipal()) { return RefPtr(aInnerPromise).forget(); } ErrorResult result; RefPtr docPromise = Promise::Create(mGlobal, result); if (NS_WARN_IF(result.Failed())) { return nullptr; } RefPtr resolver = new PromiseResolver(docPromise); aInnerPromise->AppendNativeHandler(resolver); return docPromise.forget(); }