DocumentL10n.cpp (11105B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim:set ts=2 sw=2 sts=2 et cindent: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "DocumentL10n.h" 8 9 #include "mozilla/dom/AutoEntryScript.h" 10 #include "mozilla/dom/Document.h" 11 #include "mozilla/dom/DocumentL10nBinding.h" 12 #include "nsContentUtils.h" 13 #include "nsIContentSink.h" 14 15 using namespace mozilla; 16 using namespace mozilla::intl; 17 using namespace mozilla::dom; 18 19 NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentL10n) 20 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocumentL10n, DOMLocalization) 21 NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument) 22 NS_IMPL_CYCLE_COLLECTION_UNLINK(mReady) 23 NS_IMPL_CYCLE_COLLECTION_UNLINK(mContentSink) 24 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER 25 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 26 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocumentL10n, DOMLocalization) 27 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument) 28 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReady) 29 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContentSink) 30 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 31 32 NS_IMPL_ADDREF_INHERITED(DocumentL10n, DOMLocalization) 33 NS_IMPL_RELEASE_INHERITED(DocumentL10n, DOMLocalization) 34 35 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentL10n) 36 NS_INTERFACE_MAP_END_INHERITING(DOMLocalization) 37 38 /* static */ 39 RefPtr<DocumentL10n> DocumentL10n::Create(Document* aDocument, bool aSync) { 40 RefPtr<DocumentL10n> l10n = new DocumentL10n(aDocument, aSync); 41 42 IgnoredErrorResult rv; 43 l10n->mReady = Promise::Create(l10n->mGlobal, rv); 44 if (NS_WARN_IF(rv.Failed())) { 45 return nullptr; 46 } 47 48 return l10n.forget(); 49 } 50 51 RefPtr<DocumentL10n> DocumentL10n::Create(Document* aDocument, bool aSync, 52 const nsTArray<nsCString>& aLocales) { 53 RefPtr<DocumentL10n> l10n = new DocumentL10n(aDocument, aSync, aLocales); 54 55 IgnoredErrorResult rv; 56 l10n->mReady = Promise::Create(l10n->mGlobal, rv); 57 if (NS_WARN_IF(rv.Failed())) { 58 return nullptr; 59 } 60 61 return l10n.forget(); 62 } 63 64 DocumentL10n::DocumentL10n(Document* aDocument, bool aSync) 65 : DOMLocalization(aDocument->GetScopeObject(), aSync), 66 mDocument(aDocument), 67 mState(DocumentL10nState::Constructed) { 68 mContentSink = do_QueryInterface(aDocument->GetCurrentContentSink()); 69 } 70 71 DocumentL10n::DocumentL10n(Document* aDocument, bool aSync, 72 const nsTArray<nsCString>& aLocales) 73 : DOMLocalization(aDocument->GetScopeObject(), aSync, aLocales), 74 mDocument(aDocument), 75 mState(DocumentL10nState::Constructed) { 76 mContentSink = do_QueryInterface(aDocument->GetCurrentContentSink()); 77 } 78 79 JSObject* DocumentL10n::WrapObject(JSContext* aCx, 80 JS::Handle<JSObject*> aGivenProto) { 81 return DocumentL10n_Binding::Wrap(aCx, this, aGivenProto); 82 } 83 84 class L10nReadyHandler final : public PromiseNativeHandler { 85 public: 86 NS_DECL_CYCLE_COLLECTING_ISUPPORTS 87 NS_DECL_CYCLE_COLLECTION_CLASS(L10nReadyHandler) 88 89 explicit L10nReadyHandler(Promise* aPromise, DocumentL10n* aDocumentL10n) 90 : mPromise(aPromise), mDocumentL10n(aDocumentL10n) {} 91 92 void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, 93 ErrorResult& aRv) override { 94 mDocumentL10n->InitialTranslationCompleted(true); 95 mPromise->MaybeResolveWithUndefined(); 96 } 97 98 void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, 99 ErrorResult& aRv) override { 100 mDocumentL10n->InitialTranslationCompleted(false); 101 102 nsTArray<nsCString> errors{ 103 "[dom/l10n] Could not complete initial document translation."_ns, 104 }; 105 IgnoredErrorResult rv; 106 MaybeReportErrorsToGecko(errors, rv, mDocumentL10n->GetParentObject()); 107 108 /** 109 * We resolve the mReady here even if we encountered failures, because 110 * there is nothing actionable for the user pending on `mReady` to do here 111 * and we don't want to incentivized consumers of this API to plan the 112 * same pending operation for resolve and reject scenario. 113 * 114 * Additionally, without it, the stderr received "uncaught promise 115 * rejection" warning, which is noisy and not-actionable. 116 * 117 * So instead, we just resolve and report errors. 118 * 119 * However, in automated tests we do reject so that errors with missing 120 * messages and resources can be caught. 121 */ 122 if (xpc::IsInAutomation()) { 123 mPromise->MaybeRejectWithClone(aCx, aValue); 124 } else { 125 mPromise->MaybeResolveWithUndefined(); 126 } 127 } 128 129 private: 130 ~L10nReadyHandler() = default; 131 132 RefPtr<Promise> mPromise; 133 RefPtr<DocumentL10n> mDocumentL10n; 134 }; 135 136 NS_IMPL_CYCLE_COLLECTION(L10nReadyHandler, mPromise, mDocumentL10n) 137 138 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nReadyHandler) 139 NS_INTERFACE_MAP_ENTRY(nsISupports) 140 NS_INTERFACE_MAP_END 141 142 NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nReadyHandler) 143 NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nReadyHandler) 144 145 void DocumentL10n::TriggerInitialTranslation() { 146 MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); 147 if (mState >= DocumentL10nState::InitialTranslationTriggered) { 148 return; 149 } 150 if (!mReady) { 151 // If we don't have `mReady` it means that we are in shutdown mode. 152 // See bug 1687118 for details. 153 InitialTranslationCompleted(false); 154 return; 155 } 156 157 AutoAllowLegacyScriptExecution exemption; 158 159 nsTArray<RefPtr<Promise>> promises; 160 161 ErrorResult rv; 162 promises.AppendElement(TranslateDocument(rv)); 163 if (NS_WARN_IF(rv.Failed())) { 164 rv.SuppressException(); 165 InitialTranslationCompleted(false); 166 mReady->MaybeRejectWithUndefined(); 167 return; 168 } 169 promises.AppendElement(TranslateRoots(rv)); 170 Element* documentElement = mDocument->GetDocumentElement(); 171 if (!documentElement) { 172 InitialTranslationCompleted(false); 173 mReady->MaybeRejectWithUndefined(); 174 return; 175 } 176 177 DOMLocalization::ConnectRoot(*documentElement); 178 179 AutoEntryScript aes(mGlobal, "DocumentL10n InitialTranslation"); 180 RefPtr<Promise> promise = Promise::All(aes.cx(), promises, rv); 181 182 if (promise->State() == Promise::PromiseState::Resolved) { 183 // If the promise is already resolved, we can fast-track 184 // to initial translation completed. 185 InitialTranslationCompleted(true); 186 mReady->MaybeResolveWithUndefined(); 187 } else { 188 RefPtr<PromiseNativeHandler> l10nReadyHandler = 189 new L10nReadyHandler(mReady, this); 190 promise->AppendNativeHandler(l10nReadyHandler); 191 192 mState = DocumentL10nState::InitialTranslationTriggered; 193 } 194 } 195 196 already_AddRefed<Promise> DocumentL10n::TranslateDocument(ErrorResult& aRv) { 197 MOZ_ASSERT(mState == DocumentL10nState::Constructed, 198 "This method should be called only from Constructed state."); 199 RefPtr<Promise> promise = Promise::Create(mGlobal, aRv); 200 if (NS_WARN_IF(aRv.Failed())) { 201 return nullptr; 202 } 203 204 Element* elem = mDocument->GetDocumentElement(); 205 if (!elem) { 206 promise->MaybeRejectWithUndefined(); 207 return promise.forget(); 208 } 209 210 // 1. Collect all localizable elements. 211 Sequence<OwningNonNull<Element>> elements; 212 GetTranslatables(*elem, elements, aRv); 213 if (NS_WARN_IF(aRv.Failed())) { 214 promise->MaybeRejectWithUndefined(); 215 return promise.forget(); 216 } 217 218 RefPtr<nsXULPrototypeDocument> proto = mDocument->GetPrototype(); 219 220 // 2. Check if the document has a prototype that may cache 221 // translated elements. 222 if (proto) { 223 // 2.1. Handle the case when we have proto. 224 225 // 2.1.1. Move elements that are not in the proto to a separate 226 // array. 227 Sequence<OwningNonNull<Element>> nonProtoElements; 228 229 uint32_t i = elements.Length(); 230 while (i > 0) { 231 Element* elem = elements.ElementAt(i - 1); 232 MOZ_RELEASE_ASSERT(elem->HasAttr(nsGkAtoms::datal10nid)); 233 if (!elem->HasElementCreatedFromPrototypeAndHasUnmodifiedL10n()) { 234 if (NS_WARN_IF(!nonProtoElements.AppendElement(*elem, fallible))) { 235 promise->MaybeRejectWithUndefined(); 236 return promise.forget(); 237 } 238 elements.RemoveElement(elem); 239 } 240 i--; 241 } 242 243 // We populate the sequence in reverse order. Let's bring it 244 // back to top->bottom one. 245 nonProtoElements.Reverse(); 246 247 AutoAllowLegacyScriptExecution exemption; 248 249 nsTArray<RefPtr<Promise>> promises; 250 251 // 2.1.2. If we're not loading from cache, push the elements that 252 // are in the prototype to be translated and cached. 253 if (!proto->WasL10nCached() && !elements.IsEmpty()) { 254 RefPtr<Promise> translatePromise = 255 TranslateElements(elements, proto, aRv); 256 if (NS_WARN_IF(!translatePromise || aRv.Failed())) { 257 promise->MaybeRejectWithUndefined(); 258 return promise.forget(); 259 } 260 promises.AppendElement(translatePromise); 261 } 262 263 // 2.1.3. If there are elements that are not in the prototype, 264 // localize them without attempting to cache and 265 // independently of if we're loading from cache. 266 if (!nonProtoElements.IsEmpty()) { 267 RefPtr<Promise> nonProtoTranslatePromise = 268 TranslateElements(nonProtoElements, nullptr, aRv); 269 if (NS_WARN_IF(!nonProtoTranslatePromise || aRv.Failed())) { 270 promise->MaybeRejectWithUndefined(); 271 return promise.forget(); 272 } 273 promises.AppendElement(nonProtoTranslatePromise); 274 } 275 276 // 2.1.4. Collect promises with Promise::All (maybe empty). 277 AutoEntryScript aes(mGlobal, "DocumentL10n InitialTranslationCompleted"); 278 promise = Promise::All(aes.cx(), promises, aRv); 279 if (NS_WARN_IF(aRv.Failed())) { 280 return nullptr; 281 } 282 } else { 283 // 2.2. Handle the case when we don't have proto. 284 285 // 2.2.1. Otherwise, translate all available elements, 286 // without attempting to cache them. 287 promise = TranslateElements(elements, nullptr, aRv); 288 if (NS_WARN_IF(aRv.Failed())) { 289 return nullptr; 290 } 291 } 292 293 return promise.forget(); 294 } 295 296 void DocumentL10n::InitialTranslationCompleted(bool aL10nCached) { 297 if (mState >= DocumentL10nState::Ready) { 298 return; 299 } 300 301 Element* documentElement = mDocument->GetDocumentElement(); 302 if (documentElement) { 303 SetRootInfo(documentElement); 304 } 305 306 mState = DocumentL10nState::Ready; 307 308 RefPtr<Document> doc = mDocument; 309 doc->InitialTranslationCompleted(aL10nCached); 310 311 // In XUL scenario contentSink is nullptr. 312 if (mContentSink) { 313 nsCOMPtr<nsIContentSink> sink = mContentSink.forget(); 314 sink->InitialTranslationCompleted(); 315 } 316 317 // From now on, the state of Localization is unconditionally 318 // async. 319 SetAsync(); 320 } 321 322 void DocumentL10n::ConnectRoot(nsINode& aNode, bool aTranslate, 323 ErrorResult& aRv) { 324 if (aTranslate) { 325 if (mState >= DocumentL10nState::InitialTranslationTriggered) { 326 RefPtr<Promise> promise = TranslateFragment(aNode, aRv); 327 } 328 } 329 DOMLocalization::ConnectRoot(aNode); 330 } 331 332 Promise* DocumentL10n::Ready() { return mReady; } 333 334 void DocumentL10n::OnCreatePresShell() { mMutations->OnCreatePresShell(); }