tor-browser

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

HTMLTrackElement.cpp (18343B)


      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 "mozilla/dom/HTMLTrackElement.h"
      8 
      9 #include "mozilla/LoadInfo.h"
     10 #include "mozilla/StaticPrefs_media.h"
     11 #include "mozilla/dom/Document.h"
     12 #include "mozilla/dom/Element.h"
     13 #include "mozilla/dom/HTMLMediaElement.h"
     14 #include "mozilla/dom/HTMLTrackElementBinding.h"
     15 #include "mozilla/dom/UnbindContext.h"
     16 #include "mozilla/dom/WebVTTListener.h"
     17 #include "nsAttrValueInlines.h"
     18 #include "nsCOMPtr.h"
     19 #include "nsContentUtils.h"
     20 #include "nsCycleCollectionParticipant.h"
     21 #include "nsGenericHTMLElement.h"
     22 #include "nsGkAtoms.h"
     23 #include "nsIContentPolicy.h"
     24 #include "nsILoadGroup.h"
     25 #include "nsIObserver.h"
     26 #include "nsIObserverService.h"
     27 #include "nsIScriptError.h"
     28 #include "nsISupportsImpl.h"
     29 #include "nsISupportsPrimitives.h"
     30 #include "nsNetUtil.h"
     31 #include "nsStyleConsts.h"
     32 #include "nsThreadUtils.h"
     33 
     34 extern mozilla::LazyLogModule gTextTrackLog;
     35 #define LOG(msg, ...)                       \
     36  MOZ_LOG(gTextTrackLog, LogLevel::Verbose, \
     37          ("TextTrackElement=%p, " msg, this, ##__VA_ARGS__))
     38 
     39 // Replace the usual NS_IMPL_NS_NEW_HTML_ELEMENT(Track) so
     40 // we can return an UnknownElement instead when pref'd off.
     41 nsGenericHTMLElement* NS_NewHTMLTrackElement(
     42    already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
     43    mozilla::dom::FromParser aFromParser) {
     44  RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo);
     45  auto* nim = nodeInfo->NodeInfoManager();
     46  return new (nim) mozilla::dom::HTMLTrackElement(nodeInfo.forget());
     47 }
     48 
     49 namespace mozilla::dom {
     50 
     51 // Map html attribute string values to TextTrackKind enums.
     52 static constexpr nsAttrValue::EnumTableEntry kKindTable[] = {
     53    {"subtitles", static_cast<int16_t>(TextTrackKind::Subtitles)},
     54    {"captions", static_cast<int16_t>(TextTrackKind::Captions)},
     55    {"descriptions", static_cast<int16_t>(TextTrackKind::Descriptions)},
     56    {"chapters", static_cast<int16_t>(TextTrackKind::Chapters)},
     57    {"metadata", static_cast<int16_t>(TextTrackKind::Metadata)},
     58 };
     59 
     60 // Invalid values are treated as "metadata" in ParseAttribute, but if no value
     61 // at all is specified, it's treated as "subtitles" in GetKind
     62 static constexpr const nsAttrValue::EnumTableEntry*
     63    kKindTableInvalidValueDefault = &kKindTable[4];
     64 
     65 class WindowDestroyObserver final : public nsIObserver {
     66  NS_DECL_ISUPPORTS
     67 
     68 public:
     69  explicit WindowDestroyObserver(HTMLTrackElement* aElement, uint64_t aWinID)
     70      : mTrackElement(aElement), mInnerID(aWinID) {
     71    RegisterWindowDestroyObserver();
     72  }
     73  void RegisterWindowDestroyObserver() {
     74    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
     75    if (obs) {
     76      obs->AddObserver(this, "inner-window-destroyed", false);
     77    }
     78  }
     79  void UnRegisterWindowDestroyObserver() {
     80    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
     81    if (obs) {
     82      obs->RemoveObserver(this, "inner-window-destroyed");
     83    }
     84    mTrackElement = nullptr;
     85  }
     86  NS_IMETHODIMP Observe(nsISupports* aSubject, const char* aTopic,
     87                        const char16_t* aData) override {
     88    MOZ_ASSERT(NS_IsMainThread());
     89    if (strcmp(aTopic, "inner-window-destroyed") == 0) {
     90      nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject);
     91      NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE);
     92      uint64_t innerID;
     93      nsresult rv = wrapper->GetData(&innerID);
     94      NS_ENSURE_SUCCESS(rv, rv);
     95      if (innerID == mInnerID) {
     96        if (mTrackElement) {
     97          // Window is being destroyed, cancel without checking for RFP.
     98          mTrackElement->CancelChannelAndListener(false);
     99        }
    100        UnRegisterWindowDestroyObserver();
    101      }
    102    }
    103    return NS_OK;
    104  }
    105 
    106 private:
    107  ~WindowDestroyObserver() = default;
    108 
    109  HTMLTrackElement* mTrackElement;
    110  uint64_t mInnerID;
    111 };
    112 NS_IMPL_ISUPPORTS(WindowDestroyObserver, nsIObserver);
    113 
    114 /** HTMLTrackElement */
    115 HTMLTrackElement::HTMLTrackElement(
    116    already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
    117    : nsGenericHTMLElement(std::move(aNodeInfo)),
    118      mLoadResourceDispatched(false),
    119      mWindowDestroyObserver(nullptr) {
    120  nsISupports* parentObject = OwnerDoc()->GetParentObject();
    121  NS_ENSURE_TRUE_VOID(parentObject);
    122  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(parentObject);
    123  if (window) {
    124    mWindowDestroyObserver =
    125        new WindowDestroyObserver(this, window->WindowID());
    126  }
    127 }
    128 
    129 HTMLTrackElement::~HTMLTrackElement() {
    130  if (mWindowDestroyObserver) {
    131    mWindowDestroyObserver->UnRegisterWindowDestroyObserver();
    132  }
    133  // Track element is being destroyed, cancel without checking for RFP.
    134  CancelChannelAndListener(false);
    135 }
    136 
    137 NS_IMPL_ELEMENT_CLONE(HTMLTrackElement)
    138 
    139 NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLTrackElement, nsGenericHTMLElement,
    140                                   mTrack, mMediaParent, mListener)
    141 
    142 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLTrackElement,
    143                                               nsGenericHTMLElement)
    144 
    145 void HTMLTrackElement::GetKind(DOMString& aKind) const {
    146  GetEnumAttr(nsGkAtoms::kind, kKindTable[0].tag, aKind);
    147 }
    148 
    149 void HTMLTrackElement::OnChannelRedirect(nsIChannel* aChannel,
    150                                         nsIChannel* aNewChannel,
    151                                         uint32_t aFlags) {
    152  NS_ASSERTION(aChannel == mChannel, "Channels should match!");
    153  mChannel = aNewChannel;
    154 }
    155 
    156 JSObject* HTMLTrackElement::WrapNode(JSContext* aCx,
    157                                     JS::Handle<JSObject*> aGivenProto) {
    158  return HTMLTrackElement_Binding::Wrap(aCx, this, aGivenProto);
    159 }
    160 
    161 TextTrack* HTMLTrackElement::GetTrack() {
    162  if (!mTrack) {
    163    CreateTextTrack();
    164  }
    165  return mTrack;
    166 }
    167 
    168 void HTMLTrackElement::CreateTextTrack() {
    169  nsISupports* parentObject = OwnerDoc()->GetParentObject();
    170  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(parentObject);
    171  if (!parentObject) {
    172    nsContentUtils::ReportToConsole(
    173        nsIScriptError::errorFlag, "Media"_ns, OwnerDoc(),
    174        nsContentUtils::eDOM_PROPERTIES,
    175        "Using track element in non-window context");
    176    return;
    177  }
    178 
    179  nsString label, srcLang;
    180  GetSrclang(srcLang);
    181  GetLabel(label);
    182 
    183  TextTrackKind kind;
    184  if (const nsAttrValue* value = GetParsedAttr(nsGkAtoms::kind)) {
    185    kind = static_cast<TextTrackKind>(value->GetEnumValue());
    186  } else {
    187    kind = TextTrackKind::Subtitles;
    188  }
    189 
    190  MOZ_ASSERT(!mTrack, "No need to recreate a text track!");
    191  mTrack =
    192      new TextTrack(window, kind, label, srcLang, TextTrackMode::Disabled,
    193                    TextTrackReadyState::NotLoaded, TextTrackSource::Track);
    194  mTrack->SetTrackElement(this);
    195 }
    196 
    197 bool HTMLTrackElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
    198                                      const nsAString& aValue,
    199                                      nsIPrincipal* aMaybeScriptedPrincipal,
    200                                      nsAttrValue& aResult) {
    201  if (aNamespaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::kind) {
    202    // Case-insensitive lookup, with the first element as the default.
    203    return aResult.ParseEnumValue(aValue, kKindTable, false,
    204                                  kKindTableInvalidValueDefault);
    205  }
    206 
    207  // Otherwise call the generic implementation.
    208  return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
    209                                              aMaybeScriptedPrincipal, aResult);
    210 }
    211 
    212 void HTMLTrackElement::SetSrc(const nsAString& aSrc, ErrorResult& aError) {
    213  LOG("Set src=%s", NS_ConvertUTF16toUTF8(aSrc).get());
    214 
    215  nsAutoString src;
    216  if (GetAttr(nsGkAtoms::src, src) && src == aSrc) {
    217    LOG("No need to reload for same src url");
    218    return;
    219  }
    220 
    221  SetHTMLAttr(nsGkAtoms::src, aSrc, aError);
    222  SetReadyState(TextTrackReadyState::NotLoaded);
    223  if (!mMediaParent) {
    224    return;
    225  }
    226 
    227  // Stop WebVTTListener.
    228  mListener = nullptr;
    229  if (mChannel) {
    230    mChannel->CancelWithReason(NS_BINDING_ABORTED,
    231                               "HTMLTrackElement::SetSrc"_ns);
    232    mChannel = nullptr;
    233  }
    234 
    235  MaybeDispatchLoadResource();
    236 }
    237 
    238 void HTMLTrackElement::MaybeClearAllCues() {
    239  // Empty track's cue list whenever the track element's `src` attribute set,
    240  // changed, or removed,
    241  // https://html.spec.whatwg.org/multipage/media.html#sourcing-out-of-band-text-tracks:attr-track-src
    242  if (!mTrack) {
    243    return;
    244  }
    245  mTrack->ClearAllCues();
    246 }
    247 
    248 // This function will run partial steps from `start-the-track-processing-model`
    249 // and finish the rest of steps in `LoadResource()` during the stable state.
    250 // https://html.spec.whatwg.org/multipage/media.html#start-the-track-processing-model
    251 void HTMLTrackElement::MaybeDispatchLoadResource() {
    252  MOZ_ASSERT(mTrack, "Should have already created text track!");
    253 
    254  // step2, if the text track's text track mode is not set to one of hidden or
    255  // showing, then return.
    256  bool resistFingerprinting = ShouldResistFingerprinting(RFPTarget::WebVTT);
    257  if (mTrack->Mode() == TextTrackMode::Disabled && !resistFingerprinting) {
    258    LOG("Do not load resource for disable track");
    259    return;
    260  }
    261 
    262  // We need to do this check in order to prevent
    263  // HonorUserPreferencesForTrackSelection from loading the text track twice.
    264  if (resistFingerprinting && ReadyState() == TextTrackReadyState::Loading) {
    265    return;
    266  }
    267 
    268  // step3, if the text track's track element does not have a media element as a
    269  // parent, return.
    270  if (!mMediaParent) {
    271    LOG("Do not load resource for track without media element");
    272    return;
    273  }
    274 
    275  if (ReadyState() == TextTrackReadyState::Loaded) {
    276    LOG("Has already loaded resource");
    277    return;
    278  }
    279 
    280  // step5, await a stable state and run the rest of steps.
    281  if (!mLoadResourceDispatched) {
    282    RefPtr<WebVTTListener> listener = new WebVTTListener(this);
    283    RefPtr<Runnable> r = NewRunnableMethod<RefPtr<WebVTTListener>>(
    284        "dom::HTMLTrackElement::LoadResource", this,
    285        &HTMLTrackElement::LoadResource, std::move(listener));
    286    nsContentUtils::RunInStableState(r.forget());
    287    mLoadResourceDispatched = true;
    288  }
    289 }
    290 
    291 void HTMLTrackElement::LoadResource(RefPtr<WebVTTListener>&& aWebVTTListener) {
    292  LOG("LoadResource");
    293  mLoadResourceDispatched = false;
    294 
    295  nsAutoString src;
    296  if (!GetAttr(nsGkAtoms::src, src) || src.IsEmpty()) {
    297    LOG("Fail to load because no src");
    298    SetReadyState(TextTrackReadyState::FailedToLoad);
    299    return;
    300  }
    301 
    302  nsCOMPtr<nsIURI> uri;
    303  nsresult rv = NewURIFromString(src, getter_AddRefs(uri));
    304  NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv));
    305  LOG("Trying to load from src=%s", NS_ConvertUTF16toUTF8(src).get());
    306 
    307  // Prevent canceling the channel and listener if RFP is enabled.
    308  CancelChannelAndListener(true);
    309 
    310  // According to
    311  // https://www.w3.org/TR/html5/embedded-content-0.html#sourcing-out-of-band-text-tracks
    312  //
    313  // "8: If the track element's parent is a media element then let CORS mode
    314  // be the state of the parent media element's crossorigin content attribute.
    315  // Otherwise, let CORS mode be No CORS."
    316  //
    317  CORSMode corsMode =
    318      mMediaParent ? AttrValueToCORSMode(
    319                         mMediaParent->GetParsedAttr(nsGkAtoms::crossorigin))
    320                   : CORS_NONE;
    321 
    322  // Determine the security flag based on corsMode.
    323  nsSecurityFlags secFlags;
    324  if (CORS_NONE == corsMode) {
    325    // Same-origin is required for track element.
    326    secFlags = nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT;
    327  } else {
    328    secFlags = nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT;
    329    if (CORS_ANONYMOUS == corsMode) {
    330      secFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN;
    331    } else if (CORS_USE_CREDENTIALS == corsMode) {
    332      secFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
    333    } else {
    334      NS_WARNING("Unknown CORS mode.");
    335      secFlags = nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT;
    336    }
    337  }
    338 
    339  mListener = std::move(aWebVTTListener);
    340  // This will do 6. Set the text track readiness state to loading.
    341  rv = mListener->LoadResource();
    342  NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv));
    343 
    344  Document* doc = OwnerDoc();
    345  if (!doc) {
    346    return;
    347  }
    348 
    349  // 9. End the synchronous section, continuing the remaining steps in parallel.
    350  nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
    351      "dom::HTMLTrackElement::LoadResource",
    352      [self = RefPtr<HTMLTrackElement>(this), this, uri, secFlags]() {
    353        if (!mListener) {
    354          // Shutdown got called, abort.
    355          return;
    356        }
    357        nsCOMPtr<nsIChannel> channel;
    358        nsCOMPtr<nsILoadGroup> loadGroup = OwnerDoc()->GetDocumentLoadGroup();
    359        nsresult rv = NS_NewChannel(getter_AddRefs(channel), uri,
    360                                    static_cast<Element*>(this), secFlags,
    361                                    nsIContentPolicy::TYPE_INTERNAL_TRACK,
    362                                    nullptr,  // PerformanceStorage
    363                                    loadGroup);
    364 
    365        if (NS_FAILED(rv)) {
    366          LOG("create channel failed.");
    367          SetReadyState(TextTrackReadyState::FailedToLoad);
    368          return;
    369        }
    370 
    371        channel->SetNotificationCallbacks(mListener);
    372 
    373        LOG("opening webvtt channel");
    374        rv = channel->AsyncOpen(mListener);
    375 
    376        if (NS_FAILED(rv)) {
    377          SetReadyState(TextTrackReadyState::FailedToLoad);
    378          return;
    379        }
    380        mChannel = channel;
    381      });
    382  doc->Dispatch(runnable.forget());
    383 }
    384 
    385 nsresult HTMLTrackElement::BindToTree(BindContext& aContext, nsINode& aParent) {
    386  nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
    387  NS_ENSURE_SUCCESS(rv, rv);
    388 
    389  LOG("Track Element bound to tree.");
    390  auto* parent = HTMLMediaElement::FromNode(aParent);
    391  if (!parent) {
    392    return NS_OK;
    393  }
    394 
    395  // Store our parent so we can look up its frame for display.
    396  if (!mMediaParent) {
    397    mMediaParent = parent;
    398 
    399    // TODO: separate notification for 'alternate' tracks?
    400    mMediaParent->NotifyAddedSource();
    401    LOG("Track element sent notification to parent.");
    402 
    403    // We may already have a TextTrack at this point if GetTrack() has already
    404    // been called. This happens, for instance, if script tries to get the
    405    // TextTrack before its mTrackElement has been bound to the DOM tree.
    406    if (!mTrack) {
    407      CreateTextTrack();
    408    }
    409    // As `CreateTextTrack()` might fail, so we have to check it again.
    410    if (mTrack) {
    411      LOG("Add text track to media parent");
    412      mMediaParent->AddTextTrack(mTrack);
    413    }
    414    MaybeDispatchLoadResource();
    415  }
    416 
    417  return NS_OK;
    418 }
    419 
    420 void HTMLTrackElement::UnbindFromTree(UnbindContext& aContext) {
    421  if (mMediaParent && aContext.IsUnbindRoot(this)) {
    422    // mTrack can be null if HTMLTrackElement::LoadResource has never been
    423    // called.
    424    if (mTrack) {
    425      mMediaParent->RemoveTextTrack(mTrack);
    426      mMediaParent->UpdateReadyState();
    427    }
    428    mMediaParent = nullptr;
    429  }
    430 
    431  nsGenericHTMLElement::UnbindFromTree(aContext);
    432 }
    433 
    434 TextTrackReadyState HTMLTrackElement::ReadyState() const {
    435  if (!mTrack) {
    436    return TextTrackReadyState::NotLoaded;
    437  }
    438 
    439  return mTrack->ReadyState();
    440 }
    441 
    442 void HTMLTrackElement::SetReadyState(TextTrackReadyState aReadyState) {
    443  if (ReadyState() == aReadyState) {
    444    return;
    445  }
    446 
    447  if (mTrack) {
    448    switch (aReadyState) {
    449      case TextTrackReadyState::Loaded:
    450        LOG("dispatch 'load' event");
    451        DispatchTrackRunnable(u"load"_ns);
    452        break;
    453      case TextTrackReadyState::FailedToLoad:
    454        LOG("dispatch 'error' event");
    455        DispatchTrackRunnable(u"error"_ns);
    456        break;
    457      default:
    458        break;
    459    }
    460    mTrack->SetReadyState(aReadyState);
    461  }
    462 }
    463 
    464 void HTMLTrackElement::DispatchTrackRunnable(const nsString& aEventName) {
    465  Document* doc = OwnerDoc();
    466  if (!doc) {
    467    return;
    468  }
    469  nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod<const nsString>(
    470      "dom::HTMLTrackElement::DispatchTrustedEvent", this,
    471      &HTMLTrackElement::DispatchTrustedEvent, aEventName);
    472  doc->Dispatch(runnable.forget());
    473 }
    474 
    475 void HTMLTrackElement::DispatchTrustedEvent(const nsAString& aName) {
    476  Document* doc = OwnerDoc();
    477  if (!doc) {
    478    return;
    479  }
    480  nsContentUtils::DispatchTrustedEvent(doc, this, aName, CanBubble::eNo,
    481                                       Cancelable::eNo);
    482 }
    483 
    484 void HTMLTrackElement::CancelChannelAndListener(bool aCheckRFP) {
    485  if (aCheckRFP && ShouldResistFingerprinting(RFPTarget::WebVTT)) {
    486    return;
    487  }
    488 
    489  if (mChannel) {
    490    mChannel->CancelWithReason(NS_BINDING_ABORTED,
    491                               "HTMLTrackElement::CancelChannelAndListener"_ns);
    492    mChannel->SetNotificationCallbacks(nullptr);
    493    mChannel = nullptr;
    494  }
    495 
    496  if (mListener) {
    497    mListener->Cancel();
    498    mListener = nullptr;
    499  }
    500 }
    501 
    502 bool HTMLTrackElement::ShouldResistFingerprinting(RFPTarget aRfpTarget) {
    503  Document* doc = OwnerDoc();
    504  if (!doc) {
    505    return nsContentUtils::ShouldResistFingerprinting("Null document",
    506                                                      aRfpTarget);
    507  }
    508  return doc->ShouldResistFingerprinting(aRfpTarget);
    509 }
    510 
    511 void HTMLTrackElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
    512                                    const nsAttrValue* aValue,
    513                                    const nsAttrValue* aOldValue,
    514                                    nsIPrincipal* aMaybeScriptedPrincipal,
    515                                    bool aNotify) {
    516  if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::src) {
    517    MaybeClearAllCues();
    518    // In spec, `start the track processing model` step10, while fetching is
    519    // ongoing, if the track URL changes, then we have to set the `FailedToLoad`
    520    // state.
    521    // https://html.spec.whatwg.org/multipage/media.html#sourcing-out-of-band-text-tracks:text-track-failed-to-load-3
    522    if (ReadyState() == TextTrackReadyState::Loading && aValue != aOldValue) {
    523      SetReadyState(TextTrackReadyState::FailedToLoad);
    524    }
    525  }
    526  return nsGenericHTMLElement::AfterSetAttr(
    527      aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify);
    528 }
    529 
    530 void HTMLTrackElement::DispatchTestEvent(const nsAString& aName) {
    531  if (!StaticPrefs::media_webvtt_testing_events()) {
    532    return;
    533  }
    534  DispatchTrustedEvent(aName);
    535 }
    536 
    537 #undef LOG
    538 
    539 }  // namespace mozilla::dom