tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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(); }