/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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 "js/JSON.h" #include "mozilla/dom/DocumentL10n.h" #include "mozilla/dom/DocumentL10nBinding.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/PromiseNativeHandler.h" #include "nsQueryObject.h" #include "nsISupports.h" #include "nsContentUtils.h" #include "xpcprivate.h" namespace mozilla { namespace dom { 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) { JS::RootedObject sourceScope(aCx, JS::CurrentGlobalOrNull(aCx)); AutoEntryScript aes(mPromise->GetParentObject(), "Promise resolution"); JSContext* cx = aes.cx(); JS::Rooted value(cx, aValue); xpc::StackScopedCloneOptions options; StackScopedClone(cx, options, sourceScope, &value); mPromise->MaybeResolve(cx, value); mPromise = nullptr; } void PromiseResolver::RejectedCallback(JSContext* aCx, JS::Handle aValue) { JS::RootedObject sourceScope(aCx, JS::CurrentGlobalOrNull(aCx)); AutoEntryScript aes(mPromise->GetParentObject(), "Promise rejection"); JSContext* cx = aes.cx(); JS::Rooted value(cx, aValue); xpc::StackScopedCloneOptions options; StackScopedClone(cx, options, sourceScope, &value); mPromise->MaybeReject(cx, value); mPromise = nullptr; } PromiseResolver::~PromiseResolver() { mPromise = nullptr; } NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentL10n) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(DocumentL10n) NS_IMPL_CYCLE_COLLECTING_RELEASE(DocumentL10n) NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DocumentL10n, mDocument, mDOMLocalization, mReady) DocumentL10n::DocumentL10n(nsIDocument* aDocument) : mDocument(aDocument), mState(DocumentL10nState::Initialized) { } DocumentL10n::~DocumentL10n() { } bool DocumentL10n::Init(nsTArray& aResourceIds) { nsCOMPtr domL10n = do_CreateInstance("@mozilla.org/intl/domlocalization;1"); if (NS_WARN_IF(!domL10n)) { return false; } nsIGlobalObject* global = mDocument->GetScopeObject(); if (!global) { return false; } ErrorResult rv; mReady = Promise::Create(global, rv); if (rv.Failed()) { return false; } mDOMLocalization = domL10n; // The `aEager = true` here allows us to eagerly trigger // resource fetching to increase the chance that the l10n // resources will be ready by the time the document // is ready for localization. uint32_t ret; mDOMLocalization->AddResourceIds(aResourceIds, true, &ret); // Register observers for this instance of // mozDOMLocalization to allow it to retranslate // the document when locale changes or pseudolocalization // gets turned on. mDOMLocalization->RegisterObservers(); return true; } JSObject* DocumentL10n::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return DocumentL10n_Binding::Wrap(aCx, this, aGivenProto); } already_AddRefed DocumentL10n::MaybeWrapPromise(Promise* aInnerPromise) { // For system principal we don't need to wrap the // result promise at all. if (nsContentUtils::IsSystemPrincipal(mDocument->NodePrincipal())) { return RefPtr(aInnerPromise).forget(); } nsIGlobalObject* global = mDocument->GetScopeObject(); if (!global) { return nullptr; } ErrorResult result; RefPtr docPromise = Promise::Create(global, result); if (result.Failed()) { return nullptr; } RefPtr resolver = new PromiseResolver(docPromise); aInnerPromise->AppendNativeHandler(resolver); return docPromise.forget(); } NS_IMETHODIMP DocumentL10n::HandleEvent(Event* aEvent) { #ifdef DEBUG nsAutoString eventType; aEvent->GetType(eventType); MOZ_ASSERT(eventType.EqualsLiteral("MozBeforeInitialXULLayout")); #endif TriggerInitialDocumentTranslation(); return NS_OK; } uint32_t DocumentL10n::AddResourceIds(nsTArray& aResourceIds) { uint32_t ret = 0; mDOMLocalization->AddResourceIds(aResourceIds, false, &ret); return ret; } uint32_t DocumentL10n::RemoveResourceIds(nsTArray& aResourceIds) { // We need to guard against a scenario where the // mDOMLocalization has been unlinked, but the elements // are only now removed from DOM. if (!mDOMLocalization) { return 0; } uint32_t ret = 0; mDOMLocalization->RemoveResourceIds(aResourceIds, &ret); return ret; } already_AddRefed DocumentL10n::FormatMessages(JSContext* aCx, const Sequence& aKeys, ErrorResult& aRv) { nsTArray jsKeys; SequenceRooter rooter(aCx, &jsKeys); for (auto& key : aKeys) { JS::RootedValue jsKey(aCx); if (!ToJSValue(aCx, key, &jsKey)) { aRv.NoteJSContextException(aCx); return nullptr; } jsKeys.AppendElement(jsKey); } RefPtr promise; aRv = mDOMLocalization->FormatMessages(jsKeys, getter_AddRefs(promise)); if (aRv.Failed()) { return nullptr; } return MaybeWrapPromise(promise); } already_AddRefed DocumentL10n::FormatValues(JSContext* aCx, const Sequence& aKeys, ErrorResult& aRv) { nsTArray jsKeys; SequenceRooter rooter(aCx, &jsKeys); for (auto& key : aKeys) { JS::RootedValue jsKey(aCx); if (!ToJSValue(aCx, key, &jsKey)) { aRv.NoteJSContextException(aCx); return nullptr; } jsKeys.AppendElement(jsKey); } RefPtr promise; aRv = mDOMLocalization->FormatValues(jsKeys, getter_AddRefs(promise)); if (aRv.Failed()) { return nullptr; } return MaybeWrapPromise(promise); } already_AddRefed DocumentL10n::FormatValue(JSContext* aCx, const nsAString& aId, const Optional>& aArgs, ErrorResult& aRv) { JS::Rooted args(aCx); if (aArgs.WasPassed()) { args = JS::ObjectValue(*aArgs.Value()); } else { args = JS::UndefinedValue(); } RefPtr promise; nsresult rv = mDOMLocalization->FormatValue(aId, args, getter_AddRefs(promise)); if (NS_FAILED(rv)) { aRv.Throw(rv); return nullptr; } return MaybeWrapPromise(promise); } void DocumentL10n::SetAttributes(JSContext* aCx, Element& aElement, const nsAString& aId, const Optional>& aArgs, ErrorResult& aRv) { aElement.SetAttribute(NS_LITERAL_STRING("data-l10n-id"), aId, aRv); if (aRv.Failed()) { return; } if (aArgs.WasPassed()) { nsAutoString data; JS::Rooted val(aCx, JS::ObjectValue(*aArgs.Value())); if (!nsContentUtils::StringifyJSON(aCx, &val, data)) { aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); return; } aElement.SetAttribute(NS_LITERAL_STRING("data-l10n-args"), data, aRv); } else { aElement.RemoveAttribute(NS_LITERAL_STRING("data-l10n-args"), aRv); } } void DocumentL10n::GetAttributes(JSContext* aCx, Element& aElement, L10nKey& aResult, ErrorResult& aRv) { nsAutoString l10nId; nsAutoString l10nArgs; aElement.GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nid, l10nId); aResult.mId = l10nId; aElement.GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nargs, l10nArgs); if (!l10nArgs.IsEmpty()) { JS::Rooted json(aCx); if (!JS_ParseJSON(aCx, l10nArgs.get(), l10nArgs.Length(), &json)) { aRv.NoteJSContextException(aCx); return; } else if (!json.isObject()) { aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); return; } aResult.mArgs.Construct(); aResult.mArgs.Value() = &json.toObject(); } } already_AddRefed DocumentL10n::TranslateFragment(nsINode& aNode, ErrorResult& aRv) { RefPtr promise; nsresult rv = mDOMLocalization->TranslateFragment(&aNode, getter_AddRefs(promise)); if (NS_FAILED(rv)) { aRv.Throw(rv); return nullptr; } return MaybeWrapPromise(promise); } already_AddRefed DocumentL10n::TranslateElements(const Sequence>& aElements, ErrorResult& aRv) { AutoTArray, 10> elements; elements.SetCapacity(aElements.Length()); for (auto& element : aElements) { elements.AppendElement(element); } RefPtr promise; aRv = mDOMLocalization->TranslateElements( elements, getter_AddRefs(promise)); if (aRv.Failed()) { return nullptr; } return MaybeWrapPromise(promise); } class L10nReadyHandler final : public PromiseNativeHandler { public: NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS(L10nReadyHandler) explicit L10nReadyHandler(Promise* aPromise) : mPromise(aPromise) { } void ResolvedCallback(JSContext* aCx, JS::Handle aValue) override { mPromise->MaybeResolveWithUndefined(); } void RejectedCallback(JSContext* aCx, JS::Handle aValue) override { mPromise->MaybeRejectWithUndefined(); } private: ~L10nReadyHandler() = default; RefPtr mPromise; }; NS_IMPL_CYCLE_COLLECTION(L10nReadyHandler, mPromise) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nReadyHandler) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nReadyHandler) NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nReadyHandler) void DocumentL10n::TriggerInitialDocumentTranslation() { if (mState == DocumentL10nState::InitialTranslationTriggered) { return; } mState = DocumentL10nState::InitialTranslationTriggered; Element* elem = mDocument->GetDocumentElement(); if (elem) { mDOMLocalization->ConnectRoot(elem); } RefPtr promise; mDOMLocalization->TranslateRoots(getter_AddRefs(promise)); RefPtr l10nReadyHandler = new L10nReadyHandler(mReady); promise->AppendNativeHandler(l10nReadyHandler); } Promise* DocumentL10n::Ready() { return mReady; } } // namespace dom } // namespace mozilla