tor-browser

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

FragmentDirective.cpp (19225B)


      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 "FragmentDirective.h"
      8 
      9 #include <cstdint>
     10 
     11 #include "BasePrincipal.h"
     12 #include "Document.h"
     13 #include "RangeBoundary.h"
     14 #include "TextDirectiveCreator.h"
     15 #include "TextDirectiveFinder.h"
     16 #include "TextDirectiveUtil.h"
     17 #include "mozilla/Assertions.h"
     18 #include "mozilla/CycleCollectedUniquePtr.h"
     19 #include "mozilla/PresShell.h"
     20 #include "mozilla/ResultVariant.h"
     21 #include "mozilla/dom/BrowsingContext.h"
     22 #include "mozilla/dom/BrowsingContextGroup.h"
     23 #include "mozilla/dom/FragmentDirectiveBinding.h"
     24 #include "mozilla/dom/FragmentOrElement.h"
     25 #include "mozilla/dom/Promise.h"
     26 #include "mozilla/dom/Selection.h"
     27 #include "mozilla/glean/DomMetrics.h"
     28 #include "nsContentUtils.h"
     29 #include "nsDocShell.h"
     30 #include "nsICSSDeclaration.h"
     31 #include "nsIFrame.h"
     32 #include "nsINode.h"
     33 #include "nsIURIMutator.h"
     34 #include "nsRange.h"
     35 #include "nsString.h"
     36 
     37 namespace mozilla::dom {
     38 
     39 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FragmentDirective, mDocument, mFinder)
     40 
     41 NS_IMPL_CYCLE_COLLECTING_ADDREF(FragmentDirective)
     42 NS_IMPL_CYCLE_COLLECTING_RELEASE(FragmentDirective)
     43 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FragmentDirective)
     44  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
     45  NS_INTERFACE_MAP_ENTRY(nsISupports)
     46 NS_INTERFACE_MAP_END
     47 
     48 FragmentDirective::FragmentDirective(Document* aDocument)
     49    : mDocument(aDocument) {}
     50 
     51 FragmentDirective::~FragmentDirective() = default;
     52 
     53 JSObject* FragmentDirective::WrapObject(JSContext* aCx,
     54                                        JS::Handle<JSObject*> aGivenProto) {
     55  return FragmentDirective_Binding::Wrap(aCx, this, aGivenProto);
     56 }
     57 
     58 void FragmentDirective::SetTextDirectives(
     59    nsTArray<TextDirective>&& aTextDirectives) {
     60  MOZ_ASSERT(mDocument);
     61  if (!aTextDirectives.IsEmpty()) {
     62    mFinder.reset(
     63        new TextDirectiveFinder(mDocument, std::move(aTextDirectives)));
     64  } else {
     65    mFinder = nullptr;
     66  }
     67 }
     68 
     69 void FragmentDirective::ClearUninvokedDirectives() { mFinder = nullptr; }
     70 bool FragmentDirective::HasUninvokedDirectives() const { return !!mFinder; };
     71 
     72 bool FragmentDirective::ParseAndRemoveFragmentDirectiveFromFragmentString(
     73    nsCString& aFragment, nsTArray<TextDirective>* aTextDirectives,
     74    nsIURI* aURI) {
     75  auto uri = TextDirectiveUtil::ShouldLog() && aURI ? aURI->GetSpecOrDefault()
     76                                                    : nsCString();
     77  if (aFragment.IsEmpty()) {
     78    TEXT_FRAGMENT_LOG("URL '{}' has no fragment.", uri);
     79    return false;
     80  }
     81  TEXT_FRAGMENT_LOG(
     82      "Trying to extract a fragment directive from fragment '{}' of URL '{}'.",
     83      aFragment, uri);
     84  ParsedFragmentDirectiveResult fragmentDirective;
     85  const bool hasRemovedFragmentDirective =
     86      StaticPrefs::dom_text_fragments_enabled() &&
     87      parse_fragment_directive(&aFragment, &fragmentDirective);
     88  if (hasRemovedFragmentDirective) {
     89    TEXT_FRAGMENT_LOG(
     90        "Found a fragment directive '{}', which was removed from the fragment. "
     91        "New fragment is '{}'.",
     92        fragmentDirective.fragment_directive,
     93        fragmentDirective.hash_without_fragment_directive);
     94    if (TextDirectiveUtil::ShouldLog()) {
     95      if (fragmentDirective.text_directives.IsEmpty()) {
     96        TEXT_FRAGMENT_LOG(
     97            "Found no valid text directives in fragment directive '{}'.",
     98            fragmentDirective.fragment_directive);
     99      } else {
    100        TEXT_FRAGMENT_LOG(
    101            "Found {} valid text directives in fragment directive '{}':",
    102            fragmentDirective.text_directives.Length(),
    103            fragmentDirective.fragment_directive);
    104        for (size_t index = 0;
    105             index < fragmentDirective.text_directives.Length(); ++index) {
    106          const auto& textDirective = fragmentDirective.text_directives[index];
    107          TEXT_FRAGMENT_LOG(" [{}]: {}", index, ToString(textDirective));
    108        }
    109      }
    110    }
    111    aFragment = fragmentDirective.hash_without_fragment_directive;
    112    if (aTextDirectives) {
    113      aTextDirectives->SwapElements(fragmentDirective.text_directives);
    114    }
    115  } else {
    116    TEXT_FRAGMENT_LOG(
    117        "Fragment '{}' of URL '{}' did not contain a fragment directive.",
    118        aFragment, uri);
    119  }
    120  return hasRemovedFragmentDirective;
    121 }
    122 
    123 void FragmentDirective::ParseAndRemoveFragmentDirectiveFromFragment(
    124    nsCOMPtr<nsIURI>& aURI, nsTArray<TextDirective>* aTextDirectives) {
    125  if (!aURI || !StaticPrefs::dom_text_fragments_enabled()) {
    126    return;
    127  }
    128  bool hasRef = false;
    129  aURI->GetHasRef(&hasRef);
    130 
    131  nsAutoCString hash;
    132  aURI->GetRef(hash);
    133  if (!hasRef || hash.IsEmpty()) {
    134    TEXT_FRAGMENT_LOG("URL '{}' has no fragment. Exiting.",
    135                      aURI->GetSpecOrDefault());
    136  }
    137 
    138  const bool hasRemovedFragmentDirective =
    139      ParseAndRemoveFragmentDirectiveFromFragmentString(hash, aTextDirectives,
    140                                                        aURI);
    141  if (!hasRemovedFragmentDirective) {
    142    return;
    143  }
    144  (void)NS_MutateURI(aURI).SetRef(hash).Finalize(aURI);
    145  TEXT_FRAGMENT_LOG("Updated hash of the URL. New URL: {}",
    146                    aURI->GetSpecOrDefault());
    147 }
    148 
    149 nsTArray<RefPtr<nsRange>> FragmentDirective::FindTextFragmentsInDocument() {
    150  MOZ_ASSERT(mDocument);
    151  if (!mFinder) {
    152    auto uri = TextDirectiveUtil::ShouldLog() && mDocument->GetDocumentURI()
    153                   ? mDocument->GetDocumentURI()->GetSpecOrDefault()
    154                   : nsCString();
    155    TEXT_FRAGMENT_LOG("No uninvoked text directives in document '{}'. Exiting.",
    156                      uri);
    157    return {};
    158  }
    159  auto textDirectives = mFinder->FindTextDirectivesInDocument();
    160  if (!mFinder->HasUninvokedDirectives()) {
    161    mFinder = nullptr;
    162  }
    163  return textDirectives;
    164 }
    165 
    166 /* static */ nsresult FragmentDirective::GetSpecIgnoringFragmentDirective(
    167    nsCOMPtr<nsIURI>& aURI, nsACString& aSpecIgnoringFragmentDirective) {
    168  bool hasRef = false;
    169  if (aURI->GetHasRef(&hasRef); !hasRef) {
    170    return aURI->GetSpec(aSpecIgnoringFragmentDirective);
    171  }
    172 
    173  nsAutoCString ref;
    174  nsresult rv = aURI->GetRef(ref);
    175  if (NS_FAILED(rv)) {
    176    return rv;
    177  }
    178 
    179  rv = aURI->GetSpecIgnoringRef(aSpecIgnoringFragmentDirective);
    180  if (NS_FAILED(rv)) {
    181    return rv;
    182  }
    183 
    184  ParseAndRemoveFragmentDirectiveFromFragmentString(ref);
    185 
    186  if (!ref.IsEmpty()) {
    187    aSpecIgnoringFragmentDirective.Append('#');
    188    aSpecIgnoringFragmentDirective.Append(ref);
    189  }
    190 
    191  return NS_OK;
    192 }
    193 
    194 bool FragmentDirective::IsTextDirectiveAllowedToBeScrolledTo() {
    195  // This method follows
    196  // https://wicg.github.io/scroll-to-text-fragment/#check-if-a-text-directive-can-be-scrolled
    197  // However, there are some spec issues
    198  // (https://github.com/WICG/scroll-to-text-fragment/issues/240).
    199  // The web-platform tests currently seem more up-to-date. Therefore,
    200  // this method is adapted slightly to make sure all tests pass.
    201  // Comments are added to explain changes.
    202 
    203  MOZ_ASSERT(mDocument);
    204  auto uri = TextDirectiveUtil::ShouldLog() && mDocument->GetDocumentURI()
    205                 ? mDocument->GetDocumentURI()->GetSpecOrDefault()
    206                 : nsCString();
    207  TEXT_FRAGMENT_LOG(
    208      "Trying to find out if the load of URL '{}' is allowed to scroll to the "
    209      "text fragment",
    210      uri);
    211  // It seems the spec does not cover same-document navigation in particular,
    212  // or Gecko needs to deal with this in a different way due to the
    213  // implementation not following the spec step-by-step.
    214  // Therefore, the following algorithm needs some adaptions to deal with
    215  // same-document navigations correctly.
    216 
    217  nsCOMPtr<nsILoadInfo> loadInfo =
    218      mDocument->GetChannel() ? mDocument->GetChannel()->LoadInfo() : nullptr;
    219  const bool isSameDocumentNavigation =
    220      loadInfo && loadInfo->GetIsSameDocumentNavigation();
    221 
    222  TEXT_FRAGMENT_LOG("Current load is{} a same-document navigation.",
    223                    isSameDocumentNavigation ? "" : " not");
    224 
    225  // 1. If document's pending text directives field is null or empty, return
    226  // false.
    227  // ---
    228  // we don't store the *pending* text directives in this class, only the
    229  // *uninvoked* text directives (uninvoked = `TextDirective`, pending =
    230  // `nsRange`).
    231  // Uninvoked text directives are typically already processed into pending text
    232  // directives when this code is called. Pending text directives are handled by
    233  // the caller when this code runs; therefore, the caller should decide if this
    234  // method should be called or not.
    235 
    236  // 2. Let is user involved be true if: document's text directive user
    237  // activation is true, or user involvement is one of "activation" or "browser
    238  // UI"; false otherwise.
    239  // 3. Set document's text directive user activation to false.
    240  const bool textDirectiveUserActivation =
    241      mDocument->ConsumeTextDirectiveUserActivation();
    242  TEXT_FRAGMENT_LOG(
    243      "Consumed Document's TextDirectiveUserActivation flag (value={})",
    244      textDirectiveUserActivation ? "true" : "false");
    245 
    246  // 4. If document's content type is not a text directive allowing MIME type,
    247  // return false.
    248  const bool isAllowedMIMEType = [doc = this->mDocument, func = __FUNCTION__] {
    249    nsAutoString contentType;
    250    doc->GetContentType(contentType);
    251    TEXT_FRAGMENT_LOG_FN("Got document MIME type: {}", func,
    252                         NS_ConvertUTF16toUTF8(contentType));
    253    return contentType == u"text/html" || contentType == u"text/plain";
    254  }();
    255 
    256  if (!isAllowedMIMEType) {
    257    TEXT_FRAGMENT_LOG("Invalid document MIME type. Scrolling not allowed.");
    258    return false;
    259  }
    260 
    261  // 5. If user involvement is "browser UI", return true.
    262  //
    263  // If a navigation originates from browser UI, it's always ok to allow it
    264  // since it'll be user triggered and the page/script isn't providing the text
    265  // snippet.
    266  //
    267  // Note: The intent in this item is to distinguish cases where the app/page is
    268  // able to control the URL from those that are fully under the user's
    269  // control. In the former we want to prevent scrolling of the text fragment
    270  // unless the destination is loaded in a separate browsing context group (so
    271  // that the source cannot both control the text snippet and observe
    272  // side-effects in the navigation). There are some cases where "browser UI"
    273  // may be a grey area in this regard. E.g. an "open in new window" context
    274  // menu item when right clicking on a link.
    275  //
    276  // See sec-fetch-site [0] for a related discussion on how this applies.
    277  // [0] https://w3c.github.io/webappsec-fetch-metadata/#directly-user-initiated
    278  // ---
    279  // Gecko does not implement user involvement as defined in the spec.
    280  // However, if the triggering principal is the system principal, the load
    281  // has been triggered from browser chrome. This should be good enough for now.
    282  auto* triggeringPrincipal =
    283      loadInfo ? loadInfo->TriggeringPrincipal() : nullptr;
    284  const bool isTriggeredFromBrowserUI =
    285      triggeringPrincipal && triggeringPrincipal->IsSystemPrincipal();
    286 
    287  if (isTriggeredFromBrowserUI) {
    288    TEXT_FRAGMENT_LOG(
    289        "The load is triggered from browser UI. Scrolling allowed.");
    290    return true;
    291  }
    292  TEXT_FRAGMENT_LOG("The load is not triggered from browser UI.");
    293  // 6. If is user involved is false, return false.
    294  // ---
    295  // same-document navigation is not mentioned in the spec. However, we run this
    296  // code also in same-document navigation cases.
    297  // Same-document navigation is allowed even without any user interaction.
    298  if (!textDirectiveUserActivation && !isSameDocumentNavigation) {
    299    TEXT_FRAGMENT_LOG(
    300        "User involvement is false and not same-document navigation. Scrolling "
    301        "not allowed.");
    302    return false;
    303  }
    304  // 7. If document's node navigable has a parent, return false.
    305  // ---
    306  // this is extended to ignore this rule if this is a same-document navigation
    307  // in an iframe, which is allowed when the document's origin matches the
    308  // initiator's origin (which is checked in step 8).
    309  nsDocShell* docShell = nsDocShell::Cast(mDocument->GetDocShell());
    310  if (!isSameDocumentNavigation &&
    311      (!docShell || !docShell->GetIsTopLevelContentDocShell())) {
    312    TEXT_FRAGMENT_LOG(
    313        "Document's node navigable has a parent and this is not a "
    314        "same-document navigation. Scrolling not allowed.");
    315    return false;
    316  }
    317  // 8. If initiator origin is non-null and document's origin is same origin
    318  // with initiator origin, return true.
    319  const bool isSameOrigin = [doc = this->mDocument, triggeringPrincipal] {
    320    auto* docPrincipal = doc->GetPrincipal();
    321    return triggeringPrincipal && docPrincipal &&
    322           docPrincipal->Equals(triggeringPrincipal);
    323  }();
    324 
    325  if (isSameOrigin) {
    326    TEXT_FRAGMENT_LOG("Same origin. Scrolling allowed.");
    327    return true;
    328  }
    329  TEXT_FRAGMENT_LOG("Not same origin.");
    330 
    331  // 9. If document's browsing context's group's browsing context set has length
    332  // 1, return true.
    333  //
    334  // i.e. Only allow navigation from a cross-origin element/script if the
    335  // document is loaded in a noopener context. That is, a new top level browsing
    336  // context group to which the navigator does not have script access and which
    337  // can be placed into a separate process.
    338  if (BrowsingContextGroup* group =
    339          mDocument->GetBrowsingContext()
    340              ? mDocument->GetBrowsingContext()->Group()
    341              : nullptr) {
    342    const bool isNoOpenerContext = group->Toplevels().Length() == 1;
    343    if (!isNoOpenerContext) {
    344      TEXT_FRAGMENT_LOG(
    345          "Cross-origin + noopener=false. Scrolling not allowed.");
    346    }
    347    return isNoOpenerContext;
    348  }
    349 
    350  // 10.Otherwise, return false.
    351  TEXT_FRAGMENT_LOG("Scrolling not allowed.");
    352  return false;
    353 }
    354 
    355 void FragmentDirective::HighlightTextDirectives(
    356    const nsTArray<RefPtr<nsRange>>& aTextDirectiveRanges) {
    357  MOZ_ASSERT(mDocument);
    358  if (!StaticPrefs::dom_text_fragments_enabled()) {
    359    return;
    360  }
    361  auto uri = TextDirectiveUtil::ShouldLog() && mDocument->GetDocumentURI()
    362                 ? mDocument->GetDocumentURI()->GetSpecOrDefault()
    363                 : nsCString();
    364  if (aTextDirectiveRanges.IsEmpty()) {
    365    TEXT_FRAGMENT_LOG(
    366        "No text directive ranges to highlight for document '{}'. Exiting.",
    367        uri);
    368    return;
    369  }
    370 
    371  TEXT_FRAGMENT_LOG(
    372      "Highlighting text directives for document '{}' ({} ranges).", uri,
    373      aTextDirectiveRanges.Length());
    374 
    375  const RefPtr<Selection> targetTextSelection =
    376      [doc = this->mDocument]() -> Selection* {
    377    if (auto* presShell = doc->GetPresShell()) {
    378      return presShell->GetCurrentSelection(SelectionType::eTargetText);
    379    }
    380    return nullptr;
    381  }();
    382  if (!targetTextSelection) {
    383    return;
    384  }
    385  for (const RefPtr<nsRange>& range : aTextDirectiveRanges) {
    386    // Script won't be able to manipulate `aTextDirectiveRanges`,
    387    // therefore we can mark `range` as known live.
    388    targetTextSelection->AddRangeAndSelectFramesAndNotifyListeners(
    389        MOZ_KnownLive(*range), IgnoreErrors());
    390  }
    391 }
    392 
    393 void FragmentDirective::GetTextDirectiveRanges(
    394    nsTArray<RefPtr<nsRange>>& aRanges) const {
    395  if (!StaticPrefs::dom_text_fragments_enabled()) {
    396    return;
    397  }
    398  auto* presShell = mDocument ? mDocument->GetPresShell() : nullptr;
    399  if (!presShell) {
    400    return;
    401  }
    402  RefPtr<Selection> targetTextSelection =
    403      presShell->GetCurrentSelection(SelectionType::eTargetText);
    404  if (!targetTextSelection) {
    405    return;
    406  }
    407 
    408  aRanges.Clear();
    409  for (uint32_t rangeIndex = 0; rangeIndex < targetTextSelection->RangeCount();
    410       ++rangeIndex) {
    411    nsRange* range = targetTextSelection->GetRangeAt(rangeIndex);
    412    MOZ_ASSERT(range);
    413    aRanges.AppendElement(range);
    414  }
    415 }
    416 void FragmentDirective::RemoveAllTextDirectives(ErrorResult& aRv) {
    417  if (!StaticPrefs::dom_text_fragments_enabled()) {
    418    return;
    419  }
    420  auto* presShell = mDocument ? mDocument->GetPresShell() : nullptr;
    421  if (!presShell) {
    422    return;
    423  }
    424  RefPtr<Selection> targetTextSelection =
    425      presShell->GetCurrentSelection(SelectionType::eTargetText);
    426  if (!targetTextSelection) {
    427    return;
    428  }
    429  targetTextSelection->RemoveAllRanges(aRv);
    430 }
    431 
    432 already_AddRefed<Promise> FragmentDirective::CreateTextDirectiveForRanges(
    433    const Sequence<OwningNonNull<nsRange>>& aRanges) {
    434  RefPtr<Promise> resultPromise =
    435      Promise::Create(mDocument->GetOwnerGlobal(), IgnoreErrors());
    436  if (!resultPromise) {
    437    return nullptr;
    438  }
    439  if (!StaticPrefs::dom_text_fragments_enabled()) {
    440    TEXT_FRAGMENT_LOG("Creating text fragments is disabled.");
    441    resultPromise->MaybeResolve(JS::NullHandleValue);
    442    return resultPromise.forget();
    443  }
    444  if (aRanges.IsEmpty()) {
    445    TEXT_FRAGMENT_LOG("No ranges. Nothing to do here...");
    446    resultPromise->MaybeResolve(JS::NullHandleValue);
    447    return resultPromise.forget();
    448  }
    449  TEXT_FRAGMENT_LOG("Creating text directive for {} ranges.", aRanges.Length());
    450 
    451  nsTArray<nsCString> textDirectives;
    452  textDirectives.SetCapacity(aRanges.Length());
    453 
    454  const TimeStamp start = TimeStamp::Now();
    455  RefPtr<TimeoutWatchdog> watchdog = new TimeoutWatchdog();
    456  uint32_t rangeIndex = 0;
    457  for (const auto& range : aRanges) {
    458    ++rangeIndex;
    459 
    460    if (range->Collapsed()) {
    461      TEXT_FRAGMENT_LOG("Skipping collapsed range {}.", rangeIndex);
    462      continue;
    463    }
    464    Result<nsCString, ErrorResult> maybeTextDirective =
    465        TextDirectiveCreator::CreateTextDirectiveFromRange(mDocument, range,
    466                                                           watchdog);
    467    if (MOZ_UNLIKELY(maybeTextDirective.isErr())) {
    468      TEXT_FRAGMENT_LOG("Failed to create text directive for range {}.",
    469                        rangeIndex);
    470      resultPromise->MaybeReject(maybeTextDirective.unwrapErr());
    471      return resultPromise.forget();
    472    }
    473    nsCString textDirective = maybeTextDirective.unwrap();
    474    if (textDirective.IsEmpty() || textDirective.IsVoid()) {
    475      TEXT_FRAGMENT_LOG("Skipping empty text directive for range {}.",
    476                        rangeIndex);
    477      continue;
    478    }
    479    textDirectives.AppendElement(std::move(textDirective));
    480    TEXT_FRAGMENT_LOG("Created text directive for range {}: {}", rangeIndex,
    481                      textDirectives.LastElement());
    482  }
    483 
    484  if (watchdog->IsDone()) {
    485    TEXT_FRAGMENT_LOG("Hitting timeout while creating text directives.");
    486    resultPromise->MaybeResolve(JS::NullHandleValue);
    487  } else if (textDirectives.IsEmpty()) {
    488    TEXT_FRAGMENT_LOG("No text directives created.");
    489    mDocument->SetUseCounter(eUseCounter_custom_TextDirectiveNotCreated);
    490    resultPromise->MaybeResolve(JS::NullHandleValue);
    491  } else {
    492    TEXT_FRAGMENT_LOG("Created {} text directives in total.",
    493                      textDirectives.Length());
    494    nsAutoCString textDirectivesString;
    495    StringJoinAppend(textDirectivesString, "&"_ns, textDirectives);
    496    TEXT_FRAGMENT_LOG("Created text directive string: '{}'.",
    497                      textDirectivesString);
    498    resultPromise->MaybeResolve(textDirectivesString);
    499  }
    500 
    501  glean::dom_textfragment::create_directive.AccumulateRawDuration(
    502      TimeStamp::Now() - start);
    503 
    504  return resultPromise.forget();
    505 }
    506 
    507 }  // namespace mozilla::dom