tor-browser

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

L10nMutations.cpp (10596B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
      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 "L10nMutations.h"
      8 
      9 #include "DOMLocalization.h"
     10 #include "mozilla/dom/DocumentInlines.h"
     11 #include "mozilla/intl/Localization.h"
     12 #include "nsRefreshDriver.h"
     13 #include "nsThreadManager.h"
     14 
     15 using namespace mozilla;
     16 using namespace mozilla::intl;
     17 using namespace mozilla::dom;
     18 
     19 NS_IMPL_CYCLE_COLLECTION_CLASS(L10nMutations)
     20 
     21 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(L10nMutations)
     22  NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingElements)
     23  NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingElementsHash)
     24 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
     25 
     26 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(L10nMutations)
     27  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingElements)
     28  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingElementsHash)
     29 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
     30 
     31 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nMutations)
     32  NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
     33  NS_INTERFACE_MAP_ENTRY(nsISupports)
     34 NS_INTERFACE_MAP_END
     35 
     36 NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nMutations)
     37 NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nMutations)
     38 
     39 L10nMutations::L10nMutations(DOMLocalization* aDOMLocalization)
     40    : mDOMLocalization(aDOMLocalization) {
     41  mObserving = true;
     42 }
     43 
     44 L10nMutations::~L10nMutations() {
     45  StopRefreshObserver();
     46  MOZ_ASSERT(!mDOMLocalization,
     47             "DOMLocalization<-->L10nMutations cycle should be broken.");
     48 }
     49 
     50 void L10nMutations::AttributeChanged(Element* aElement, int32_t aNameSpaceID,
     51                                     nsAtom* aAttribute, AttrModType,
     52                                     const nsAttrValue* aOldValue) {
     53  if (!mObserving) {
     54    return;
     55  }
     56 
     57  if (aNameSpaceID == kNameSpaceID_None &&
     58      (aAttribute == nsGkAtoms::datal10nid ||
     59       aAttribute == nsGkAtoms::datal10nargs)) {
     60    if (IsInRoots(aElement)) {
     61      L10nElementChanged(aElement);
     62    }
     63  }
     64 }
     65 
     66 void L10nMutations::ContentAppended(nsIContent* aChild,
     67                                    const ContentAppendInfo&) {
     68  if (!mObserving) {
     69    return;
     70  }
     71 
     72  if (!IsInRoots(aChild)) {
     73    return;
     74  }
     75 
     76  Sequence<OwningNonNull<Element>> elements;
     77  for (nsIContent* node = aChild; node; node = node->GetNextSibling()) {
     78    if (node->IsElement()) {
     79      DOMLocalization::GetTranslatables(*node, elements, IgnoreErrors());
     80    }
     81  }
     82 
     83  for (auto& elem : elements) {
     84    L10nElementChanged(elem);
     85  }
     86 }
     87 
     88 void L10nMutations::ContentInserted(nsIContent* aChild,
     89                                    const ContentInsertInfo&) {
     90  if (!mObserving) {
     91    return;
     92  }
     93 
     94  if (!aChild->IsElement()) {
     95    return;
     96  }
     97  Element* elem = aChild->AsElement();
     98 
     99  if (!IsInRoots(elem)) {
    100    return;
    101  }
    102 
    103  Sequence<OwningNonNull<Element>> elements;
    104  DOMLocalization::GetTranslatables(*aChild, elements, IgnoreErrors());
    105 
    106  for (auto& elem : elements) {
    107    L10nElementChanged(elem);
    108  }
    109 }
    110 
    111 void L10nMutations::ContentWillBeRemoved(nsIContent* aChild,
    112                                         const ContentRemoveInfo&) {
    113  if (!mObserving || mPendingElements.IsEmpty()) {
    114    return;
    115  }
    116 
    117  Element* elem = Element::FromNode(*aChild);
    118  if (!elem || !IsInRoots(elem)) {
    119    return;
    120  }
    121 
    122  Sequence<OwningNonNull<Element>> elements;
    123  DOMLocalization::GetTranslatables(*aChild, elements, IgnoreErrors());
    124 
    125  for (auto& elem : elements) {
    126    if (mPendingElementsHash.EnsureRemoved(elem)) {
    127      mPendingElements.RemoveElement(elem);
    128    }
    129  }
    130 
    131  if (!HasPendingMutations()) {
    132    nsContentUtils::AddScriptRunner(NewRunnableMethod(
    133        "MaybeFirePendingTranslationsFinished", this,
    134        &L10nMutations::MaybeFirePendingTranslationsFinished));
    135  }
    136 }
    137 
    138 void L10nMutations::L10nElementChanged(Element* aElement) {
    139  const bool wasEmpty = mPendingElements.IsEmpty();
    140 
    141  if (mPendingElementsHash.EnsureInserted(aElement)) {
    142    mPendingElements.AppendElement(aElement);
    143  }
    144 
    145  if (!wasEmpty) {
    146    return;
    147  }
    148 
    149  if (!mRefreshDriver) {
    150    StartRefreshObserver();
    151  }
    152 
    153  if (!mBlockingLoad) {
    154    Document* doc = GetDocument();
    155    if (doc && doc->GetReadyStateEnum() != Document::READYSTATE_COMPLETE) {
    156      doc->BlockOnload();
    157      mBlockingLoad = true;
    158    }
    159  }
    160 
    161  if (mBlockingLoad && !mPendingBlockingLoadFlush) {
    162    // We want to make sure we flush translations and don't block the load
    163    // indefinitely (and, in fact, that we do it rather soon, even if the
    164    // refresh driver is not ticking yet).
    165    //
    166    // In some platforms (mainly Wayland) the load of the main document
    167    // causes vsync to start running and start ticking the refresh driver,
    168    // so we can't rely on the refresh driver ticking yet.
    169    RefPtr<nsIRunnable> task =
    170        NewRunnableMethod("FlushPendingTranslationsBeforeLoad", this,
    171                          &L10nMutations::FlushPendingTranslationsBeforeLoad);
    172    nsThreadManager::get().DispatchDirectTaskToCurrentThread(task);
    173    mPendingBlockingLoadFlush = true;
    174  }
    175 }
    176 
    177 void L10nMutations::PauseObserving() { mObserving = false; }
    178 
    179 void L10nMutations::ResumeObserving() { mObserving = true; }
    180 
    181 void L10nMutations::WillRefresh(mozilla::TimeStamp aTime) {
    182  StopRefreshObserver();
    183  FlushPendingTranslations();
    184 }
    185 
    186 /**
    187 * The handler for the `TranslateElements` promise used to turn
    188 * a potential rejection into a console warning.
    189 **/
    190 class L10nMutationFinalizationHandler final : public PromiseNativeHandler {
    191 public:
    192  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
    193  NS_DECL_CYCLE_COLLECTION_CLASS(L10nMutationFinalizationHandler)
    194 
    195  explicit L10nMutationFinalizationHandler(L10nMutations* aMutations,
    196                                           nsIGlobalObject* aGlobal)
    197      : mMutations(aMutations), mGlobal(aGlobal) {}
    198 
    199  MOZ_CAN_RUN_SCRIPT void Settled() {
    200    if (RefPtr mutations = mMutations) {
    201      mutations->PendingPromiseSettled();
    202    }
    203  }
    204 
    205  MOZ_CAN_RUN_SCRIPT void ResolvedCallback(JSContext* aCx,
    206                                           JS::Handle<JS::Value> aValue,
    207                                           ErrorResult& aRv) override {
    208    Settled();
    209  }
    210 
    211  MOZ_CAN_RUN_SCRIPT void RejectedCallback(JSContext* aCx,
    212                                           JS::Handle<JS::Value> aValue,
    213                                           ErrorResult& aRv) override {
    214    nsTArray<nsCString> errors{
    215        "[dom/l10n] Errors during l10n mutation frame."_ns,
    216    };
    217    MaybeReportErrorsToGecko(errors, IgnoreErrors(), mGlobal);
    218    Settled();
    219  }
    220 
    221 private:
    222  ~L10nMutationFinalizationHandler() = default;
    223 
    224  RefPtr<L10nMutations> mMutations;
    225  nsCOMPtr<nsIGlobalObject> mGlobal;
    226 };
    227 
    228 NS_IMPL_CYCLE_COLLECTION(L10nMutationFinalizationHandler, mGlobal, mMutations)
    229 
    230 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nMutationFinalizationHandler)
    231  NS_INTERFACE_MAP_ENTRY(nsISupports)
    232 NS_INTERFACE_MAP_END
    233 
    234 NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nMutationFinalizationHandler)
    235 NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nMutationFinalizationHandler)
    236 
    237 void L10nMutations::FlushPendingTranslationsBeforeLoad() {
    238  MOZ_ASSERT(mPendingBlockingLoadFlush);
    239  mPendingBlockingLoadFlush = false;
    240  FlushPendingTranslations();
    241 }
    242 
    243 void L10nMutations::FlushPendingTranslations() {
    244  if (!mDOMLocalization) {
    245    return;
    246  }
    247 
    248  nsTArray<OwningNonNull<Element>> elements;
    249  for (auto& elem : mPendingElements) {
    250    if (elem->HasAttr(nsGkAtoms::datal10nid)) {
    251      elements.AppendElement(*elem);
    252    }
    253  }
    254 
    255  mPendingElementsHash.Clear();
    256  mPendingElements.Clear();
    257 
    258  RefPtr<Promise> promise =
    259      mDOMLocalization->TranslateElements(elements, IgnoreErrors());
    260  if (promise && promise->State() == Promise::PromiseState::Pending) {
    261    mPendingPromises++;
    262    auto l10nMutationFinalizationHandler =
    263        MakeRefPtr<L10nMutationFinalizationHandler>(
    264            this, mDOMLocalization->GetParentObject());
    265    promise->AppendNativeHandler(l10nMutationFinalizationHandler);
    266  }
    267 
    268  MaybeFirePendingTranslationsFinished();
    269 }
    270 
    271 void L10nMutations::PendingPromiseSettled() {
    272  MOZ_DIAGNOSTIC_ASSERT(mPendingPromises);
    273  mPendingPromises--;
    274  MaybeFirePendingTranslationsFinished();
    275 }
    276 
    277 void L10nMutations::MaybeFirePendingTranslationsFinished() {
    278  if (HasPendingMutations()) {
    279    return;
    280  }
    281 
    282  RefPtr doc = GetDocument();
    283  if (NS_WARN_IF(!doc)) {
    284    return;
    285  }
    286 
    287  if (mBlockingLoad) {
    288    mBlockingLoad = false;
    289    doc->UnblockOnload(false);
    290  }
    291  nsContentUtils::DispatchEventOnlyToChrome(
    292      doc, doc, u"L10nMutationsFinished"_ns, CanBubble::eNo, Cancelable::eNo,
    293      Composed::eNo, nullptr);
    294 }
    295 
    296 void L10nMutations::Disconnect() {
    297  StopRefreshObserver();
    298  mDOMLocalization = nullptr;
    299 }
    300 
    301 Document* L10nMutations::GetDocument() const {
    302  if (!mDOMLocalization) {
    303    return nullptr;
    304  }
    305  auto* innerWindow = mDOMLocalization->GetParentObject()->GetAsInnerWindow();
    306  if (!innerWindow) {
    307    return nullptr;
    308  }
    309  return innerWindow->GetExtantDoc();
    310 }
    311 
    312 void L10nMutations::StartRefreshObserver() {
    313  if (!mDOMLocalization || mRefreshDriver) {
    314    return;
    315  }
    316  if (Document* doc = GetDocument()) {
    317    if (nsPresContext* ctx = doc->GetPresContext()) {
    318      mRefreshDriver = ctx->RefreshDriver();
    319    }
    320  }
    321 
    322  // If we can't start the refresh driver, it means
    323  // that the presContext is not available yet.
    324  // In that case, we'll trigger the flush of pending
    325  // elements in Document::CreatePresShell.
    326  if (mRefreshDriver) {
    327    mRefreshDriver->AddRefreshObserver(this, FlushType::Style,
    328                                       "L10n mutations");
    329  } else {
    330    NS_WARNING("[l10n][mutations] Failed to start a refresh observer.");
    331  }
    332 }
    333 
    334 void L10nMutations::StopRefreshObserver() {
    335  if (mRefreshDriver) {
    336    mRefreshDriver->RemoveRefreshObserver(this, FlushType::Style);
    337    mRefreshDriver = nullptr;
    338  }
    339 }
    340 
    341 void L10nMutations::OnCreatePresShell() {
    342  StopRefreshObserver();
    343  if (!mPendingElements.IsEmpty()) {
    344    StartRefreshObserver();
    345  }
    346 }
    347 
    348 bool L10nMutations::IsInRoots(nsINode* aNode) {
    349  // If the root of the mutated element is in the light DOM,
    350  // we know it must be covered by our observer directly.
    351  //
    352  // Otherwise, we need to check if its subtree root is the same
    353  // as any of the `DOMLocalization::mRoots` subtree roots.
    354  nsINode* root = aNode->SubtreeRoot();
    355 
    356  // If element is in light DOM, it must be covered by one of
    357  // the DOMLocalization roots to end up here.
    358  MOZ_ASSERT_IF(!root->IsShadowRoot(),
    359                mDOMLocalization->SubtreeRootInRoots(root));
    360 
    361  return !root->IsShadowRoot() || mDOMLocalization->SubtreeRootInRoots(root);
    362 }