/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/*
 * Base class for the XML and HTML content sinks, which construct a
 * DOM based on information from the parser.
 */

#include "nsContentSink.h"
#include "mozilla/Components.h"
#include "mozilla/PresShell.h"
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/StaticPrefs_content.h"
#include "mozilla/StaticPrefs_network.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/LinkStyle.h"
#include "mozilla/dom/ReferrerInfo.h"
#include "mozilla/css/Loader.h"
#include "mozilla/dom/MutationObservers.h"
#include "mozilla/dom/SRILogHelper.h"
#include "mozilla/StoragePrincipalHelper.h"
#include "mozilla/net/HttpBaseChannel.h"
#include "mozilla/net/NeckoChannelParams.h"
#include "nsIDocShell.h"
#include "nsILoadContext.h"
#include "nsIPrefetchService.h"
#include "nsIURI.h"
#include "nsNetUtil.h"
#include "nsIMIMEHeaderParam.h"
#include "nsIProtocolHandler.h"
#include "nsIHttpChannel.h"
#include "nsIContent.h"
#include "nsPresContext.h"
#include "nsViewManager.h"
#include "nsAtom.h"
#include "nsGkAtoms.h"
#include "nsGlobalWindowInner.h"
#include "nsNetCID.h"
#include "nsICookieService.h"
#include "nsContentUtils.h"
#include "nsNodeInfoManager.h"
#include "nsIAppShell.h"
#include "nsIWidget.h"
#include "nsWidgetsCID.h"
#include "mozAutoDocUpdate.h"
#include "nsIWebNavigation.h"
#include "nsGenericHTMLElement.h"
#include "nsIObserverService.h"
#include "mozilla/Preferences.h"
#include "mozilla/ProfilerLabels.h"
#include "mozilla/dom/HTMLDNSPrefetch.h"
#include "mozilla/dom/ServiceWorkerDescriptor.h"
#include "mozilla/dom/ScriptLoader.h"
#include "nsParserConstants.h"
#include "nsSandboxFlags.h"
#include "Link.h"
#include "HTMLLinkElement.h"
#include "MediaList.h"
#include "nsString.h"
#include "nsStringFwd.h"
#include <stdint.h>
#include "mozilla/RefPtr.h"
#include "nsCOMPtr.h"
#include "nsLiteralString.h"
#include "nsIContentPolicy.h"
using namespace mozilla;
using namespace mozilla::css;
using namespace mozilla::dom;

LazyLogModule gContentSinkLogModuleInfo("nscontentsink");

NS_IMPL_CYCLE_COLLECTING_ADDREF(nsContentSink)
NS_IMPL_CYCLE_COLLECTING_RELEASE(nsContentSink)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsContentSink)
  NS_INTERFACE_MAP_ENTRY(nsICSSLoaderObserver)
  NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
  NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver)
  NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
  NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
  NS_INTERFACE_MAP_ENTRY(nsINamed)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDocumentObserver)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTION_CLASS(nsContentSink)

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsContentSink)
  if (tmp->mDocument) {
    tmp->mDocument->RemoveObserver(tmp);
  }
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mParser)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocShell)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mCSSLoader)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mNodeInfoManager)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mScriptLoader)
  NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsContentSink)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParser)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocShell)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCSSLoader)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNodeInfoManager)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mScriptLoader)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

nsContentSink::nsContentSink()
    : mBackoffCount(0),
      mLastNotificationTime(0),
      mLayoutStarted(0),
      mDynamicLowerValue(0),
      mParsing(0),
      mDroppedTimer(0),
      mDeferredLayoutStart(0),
      mDeferredFlushTags(0),
      mIsDocumentObserver(0),
      mRunsToCompletion(0),
      mIsBlockingOnload(false),
      mDeflectedCount(0),
      mHasPendingEvent(false),
      mCurrentParseEndTime(0),
      mBeginLoadTime(0),
      mLastSampledUserEventTime(0),
      mInMonolithicContainer(0),
      mInNotification(0),
      mUpdatesInNotification(0),
      mPendingSheetCount(0) {
  NS_ASSERTION(!mLayoutStarted, "What?");
  NS_ASSERTION(!mDynamicLowerValue, "What?");
  NS_ASSERTION(!mParsing, "What?");
  NS_ASSERTION(mLastSampledUserEventTime == 0, "What?");
  NS_ASSERTION(mDeflectedCount == 0, "What?");
  NS_ASSERTION(!mDroppedTimer, "What?");
  NS_ASSERTION(mInMonolithicContainer == 0, "What?");
  NS_ASSERTION(mInNotification == 0, "What?");
  NS_ASSERTION(!mDeferredLayoutStart, "What?");
}

nsContentSink::~nsContentSink() {
  if (mDocument) {
    // Remove ourselves just to be safe, though we really should have
    // been removed in DidBuildModel if everything worked right.
    mDocument->RemoveObserver(this);
  }
}

nsresult nsContentSink::Init(Document* aDoc, nsIURI* aURI,
                             nsISupports* aContainer, nsIChannel* aChannel) {
  MOZ_ASSERT(aDoc, "null ptr");
  MOZ_ASSERT(aURI, "null ptr");

  if (!aDoc || !aURI) {
    return NS_ERROR_NULL_POINTER;
  }

  mDocument = aDoc;

  mDocumentURI = aURI;
  mDocShell = do_QueryInterface(aContainer);
  mScriptLoader = mDocument->ScriptLoader();

  if (!mRunsToCompletion) {
    if (mDocShell) {
      uint32_t loadType = 0;
      mDocShell->GetLoadType(&loadType);
      mDocument->SetChangeScrollPosWhenScrollingToRef(
          (loadType & nsIDocShell::LOAD_CMD_HISTORY) == 0);
    }

    ProcessHTTPHeaders(aChannel);
  }

  mCSSLoader = aDoc->CSSLoader();

  mNodeInfoManager = aDoc->NodeInfoManager();

  mBackoffCount = StaticPrefs::content_notify_backoffcount();

  if (StaticPrefs::content_sink_enable_perf_mode() != 0) {
    mDynamicLowerValue = StaticPrefs::content_sink_enable_perf_mode() == 1;
  }

  return NS_OK;
}

NS_IMETHODIMP
nsContentSink::StyleSheetLoaded(StyleSheet* aSheet, bool aWasDeferred,
                                nsresult aStatus) {
  MOZ_ASSERT(!mRunsToCompletion, "How come a fragment parser observed sheets?");
  if (aWasDeferred) {
    return NS_OK;
  }
  MOZ_ASSERT(mPendingSheetCount > 0, "How'd that happen?");
  --mPendingSheetCount;

  const bool loadedAllSheets = !mPendingSheetCount;
  if (loadedAllSheets && (mDeferredLayoutStart || mDeferredFlushTags)) {
    if (mDeferredFlushTags) {
      FlushTags();
    }
    if (mDeferredLayoutStart) {
      // We might not have really started layout, since this sheet was still
      // loading.  Do it now.  Probably doesn't matter whether we do this
      // before or after we unblock scripts, but before feels saner.  Note
      // that if mDeferredLayoutStart is true, that means any subclass
      // StartLayout() stuff that needs to happen has already happened, so
      // we don't need to worry about it.
      StartLayout(false);
    }

    // Go ahead and try to scroll to our ref if we have one
    ScrollToRef();
  }

  mScriptLoader->RemoveParserBlockingScriptExecutionBlocker();

  if (loadedAllSheets &&
      mDocument->GetReadyStateEnum() >= Document::READYSTATE_INTERACTIVE) {
    mScriptLoader->DeferCheckpointReached();
  }

  return NS_OK;
}

nsresult nsContentSink::ProcessHTTPHeaders(nsIChannel* aChannel) {
  nsCOMPtr<nsIHttpChannel> httpchannel(do_QueryInterface(aChannel));

  if (!httpchannel) {
    return NS_OK;
  }

  bool gotEarlyHints = false;
  if (nsCOMPtr<mozilla::net::HttpBaseChannel> baseChannel =
          do_QueryInterface(aChannel)) {
    nsTArray<mozilla::net::EarlyHintConnectArgs> earlyHints =
        baseChannel->TakeEarlyHints();
    gotEarlyHints = !earlyHints.IsEmpty();
    mDocument->SetEarlyHints(std::move(earlyHints));
  }

  // Note that the only header we care about is the "link" header, since we
  // have all the infrastructure for kicking off stylesheet loads.

  nsAutoCString linkHeader;

  nsresult rv = httpchannel->GetResponseHeader("link"_ns, linkHeader);
  bool gotLinkHeader = NS_SUCCEEDED(rv) && !linkHeader.IsEmpty();
  if (gotLinkHeader) {
    mDocument->SetHeaderData(nsGkAtoms::link,
                             NS_ConvertASCIItoUTF16(linkHeader));
  }
  if (gotLinkHeader || gotEarlyHints) {
    NS_ASSERTION(!mProcessLinkHeaderEvent.get(),
                 "Already dispatched an event?");

    mProcessLinkHeaderEvent =
        NewNonOwningRunnableMethod("nsContentSink::DoProcessLinkHeader", this,
                                   &nsContentSink::DoProcessLinkHeader);
    rv = NS_DispatchToCurrentThread(mProcessLinkHeaderEvent.get());
    if (NS_FAILED(rv)) {
      mProcessLinkHeaderEvent.Forget();
    }
  }

  return NS_OK;
}

void nsContentSink::DoProcessLinkHeader() {
  for (const auto& earlyHint : mDocument->GetEarlyHints()) {
    ProcessLinkFromHeader(earlyHint.link(), earlyHint.earlyHintPreloaderId());
  }

  nsAutoString value;
  mDocument->GetHeaderData(nsGkAtoms::link, value);
  auto linkHeaders = net::ParseLinkHeader(value);
  for (const auto& linkHeader : linkHeaders) {
    ProcessLinkFromHeader(linkHeader, 0);
  }
}

nsresult nsContentSink::ProcessLinkFromHeader(const net::LinkHeader& aHeader,
                                              uint64_t aEarlyHintPreloaderId) {
  uint32_t linkTypes = LinkStyle::ParseLinkTypes(aHeader.mRel);

  // The link relation may apply to a different resource, specified
  // in the anchor parameter. For the link relations supported so far,
  // we simply abort if the link applies to a resource different to the
  // one we've loaded
  if (!nsContentUtils::LinkContextIsURI(aHeader.mAnchor,
                                        mDocument->GetDocumentURI())) {
    return NS_OK;
  }

  if (nsContentUtils::PrefetchPreloadEnabled(mDocShell)) {
    // prefetch href if relation is "next" or "prefetch"
    if ((linkTypes & LinkStyle::eNEXT) || (linkTypes & LinkStyle::ePREFETCH)) {
      PrefetchHref(aHeader.mHref, aHeader.mAs, aHeader.mType, aHeader.mMedia);
    }

    if (!aHeader.mHref.IsEmpty() && (linkTypes & LinkStyle::eDNS_PREFETCH)) {
      PrefetchDNS(aHeader.mHref);
    }

    if (!aHeader.mHref.IsEmpty() && (linkTypes & LinkStyle::ePRECONNECT)) {
      Preconnect(aHeader.mHref, aHeader.mCrossOrigin);
    }

    if (linkTypes & LinkStyle::ePRELOAD) {
      PreloadHref(aHeader.mHref, aHeader.mAs, aHeader.mType, aHeader.mMedia,
                  aHeader.mIntegrity, aHeader.mSrcset, aHeader.mSizes,
                  aHeader.mCrossOrigin, aHeader.mReferrerPolicy,
                  aEarlyHintPreloaderId);
    }

    if ((linkTypes & LinkStyle::eMODULE_PRELOAD) &&
        mDocument->ScriptLoader()->GetModuleLoader()) {
      PreloadModule(aHeader.mHref, aHeader.mAs, aHeader.mMedia,
                    aHeader.mIntegrity, aHeader.mCrossOrigin,
                    aHeader.mReferrerPolicy, aEarlyHintPreloaderId);
    }
  }

  // is it a stylesheet link?
  if (!(linkTypes & LinkStyle::eSTYLESHEET)) {
    return NS_OK;
  }

  bool isAlternate = linkTypes & LinkStyle::eALTERNATE;
  return ProcessStyleLinkFromHeader(aHeader.mHref, isAlternate, aHeader.mTitle,
                                    aHeader.mIntegrity, aHeader.mType,
                                    aHeader.mMedia, aHeader.mReferrerPolicy);
}

nsresult nsContentSink::ProcessStyleLinkFromHeader(
    const nsAString& aHref, bool aAlternate, const nsAString& aTitle,
    const nsAString& aIntegrity, const nsAString& aType,
    const nsAString& aMedia, const nsAString& aReferrerPolicy) {
  if (aAlternate && aTitle.IsEmpty()) {
    // alternates must have title return without error, for now
    return NS_OK;
  }

  nsAutoString mimeType;
  nsAutoString params;
  nsContentUtils::SplitMimeType(aType, mimeType, params);

  // see bug 18817
  if (!mimeType.IsEmpty() && !mimeType.LowerCaseEqualsLiteral("text/css")) {
    // Unknown stylesheet language
    return NS_OK;
  }

  nsCOMPtr<nsIURI> url;
  nsresult rv = NS_NewURI(getter_AddRefs(url), aHref, nullptr,
                          mDocument->GetDocBaseURI());

  if (NS_FAILED(rv)) {
    // The URI is bad, move along, don't propagate the error (for now)
    return NS_OK;
  }

  // Link header is working like a <link> node, so referrerPolicy attr should
  // have higher priority than referrer policy from document.
  ReferrerPolicy policy =
      ReferrerInfo::ReferrerPolicyAttributeFromString(aReferrerPolicy);
  nsCOMPtr<nsIReferrerInfo> referrerInfo =
      ReferrerInfo::CreateFromDocumentAndPolicyOverride(mDocument, policy);

  Loader::SheetInfo info{
      *mDocument,
      nullptr,
      url.forget(),
      nullptr,
      referrerInfo.forget(),
      CORS_NONE,
      aTitle,
      aMedia,
      aIntegrity,
      /* nonce = */ u""_ns,
      aAlternate ? Loader::HasAlternateRel::Yes : Loader::HasAlternateRel::No,
      Loader::IsInline::No,
      Loader::IsExplicitlyEnabled::No,
  };

  auto loadResultOrErr =
      mCSSLoader->LoadStyleLink(info, mRunsToCompletion ? nullptr : this);
  if (loadResultOrErr.isErr()) {
    return loadResultOrErr.unwrapErr();
  }

  if (loadResultOrErr.inspect().ShouldBlock() && !mRunsToCompletion) {
    ++mPendingSheetCount;
    mScriptLoader->AddParserBlockingScriptExecutionBlocker();
  }

  return NS_OK;
}

void nsContentSink::PrefetchHref(const nsAString& aHref, const nsAString& aAs,
                                 const nsAString& aType,
                                 const nsAString& aMedia) {
  nsCOMPtr<nsIPrefetchService> prefetchService(components::Prefetch::Service());
  if (prefetchService) {
    // construct URI using document charset
    auto encoding = mDocument->GetDocumentCharacterSet();
    nsCOMPtr<nsIURI> uri;
    NS_NewURI(getter_AddRefs(uri), aHref, encoding, mDocument->GetDocBaseURI());
    if (uri) {
      auto referrerInfo = MakeRefPtr<ReferrerInfo>(*mDocument);
      referrerInfo = referrerInfo->CloneWithNewOriginalReferrer(mDocumentURI);

      prefetchService->PrefetchURI(uri, referrerInfo, mDocument, true);
    }
  }
}

void nsContentSink::PreloadHref(const nsAString& aHref, const nsAString& aAs,
                                const nsAString& aType, const nsAString& aMedia,
                                const nsAString& aIntegrity,
                                const nsAString& aSrcset,
                                const nsAString& aSizes, const nsAString& aCORS,
                                const nsAString& aReferrerPolicy,
                                uint64_t aEarlyHintPreloaderId) {
  auto encoding = mDocument->GetDocumentCharacterSet();
  nsCOMPtr<nsIURI> uri;
  NS_NewURI(getter_AddRefs(uri), aHref, encoding, mDocument->GetDocBaseURI());
  if (!uri) {
    // URL parsing failed.
    return;
  }

  nsAttrValue asAttr;
  mozilla::net::ParseAsValue(aAs, asAttr);

  nsAutoString mimeType;
  nsAutoString notUsed;
  nsContentUtils::SplitMimeType(aType, mimeType, notUsed);

  auto policyType = mozilla::net::AsValueToContentPolicy(asAttr);
  if (policyType == nsIContentPolicy::TYPE_INVALID ||
      !mozilla::net::CheckPreloadAttrs(asAttr, mimeType, aMedia, mDocument)) {
    // Ignore preload wrong or empty attributes.
    mozilla::net::WarnIgnoredPreload(*mDocument, *uri);
    return;
  }

  mDocument->Preloads().PreloadLinkHeader(
      uri, aHref, policyType, aAs, aType, aIntegrity, aSrcset, aSizes, aCORS,
      aReferrerPolicy, aEarlyHintPreloaderId);
}

void nsContentSink::PreloadModule(const nsAString& aHref, const nsAString& aAs,
                                  const nsAString& aMedia,
                                  const nsAString& aIntegrity,
                                  const nsAString& aCORS,
                                  const nsAString& aReferrerPolicy,
                                  uint64_t aEarlyHintPreloaderId) {
  ModuleLoader* moduleLoader = mDocument->ScriptLoader()->GetModuleLoader();

  if (!StaticPrefs::network_modulepreload()) {
    // Keep behavior from https://phabricator.services.mozilla.com/D149371,
    // prior to main implementation of modulepreload
    moduleLoader->DisallowImportMaps();
    return;
  }

  RefPtr<mozilla::dom::MediaList> mediaList =
      mozilla::dom::MediaList::Create(NS_ConvertUTF16toUTF8(aMedia));
  if (!mediaList->Matches(*mDocument)) {
    return;
  }

  if (aHref.IsEmpty()) {
    return;
  }

  if (!net::IsScriptLikeOrInvalid(aAs)) {
    return;
  }

  auto encoding = mDocument->GetDocumentCharacterSet();
  nsCOMPtr<nsIURI> uri;
  NS_NewURI(getter_AddRefs(uri), aHref, encoding, mDocument->GetDocBaseURI());
  if (!uri) {
    return;
  }

  moduleLoader->DisallowImportMaps();

  mDocument->Preloads().PreloadLinkHeader(
      uri, aHref, nsIContentPolicy::TYPE_SCRIPT, u"script"_ns, u"module"_ns,
      aIntegrity, u""_ns, u""_ns, aCORS, aReferrerPolicy,
      aEarlyHintPreloaderId);
}

void nsContentSink::PrefetchDNS(const nsAString& aHref) {
  nsAutoString hostname;
  bool isHttps = false;

  if (StringBeginsWith(aHref, u"//"_ns)) {
    hostname = Substring(aHref, 2);
  } else {
    nsCOMPtr<nsIURI> uri;
    NS_NewURI(getter_AddRefs(uri), aHref);
    if (!uri) {
      return;
    }
    nsresult rv;
    bool isLocalResource = false;
    rv = NS_URIChainHasFlags(uri, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE,
                             &isLocalResource);
    if (NS_SUCCEEDED(rv) && !isLocalResource) {
      nsAutoCString host;
      uri->GetHost(host);
      CopyUTF8toUTF16(host, hostname);
    }
    isHttps = uri->SchemeIs("https");
  }

  if (!hostname.IsEmpty() && HTMLDNSPrefetch::IsAllowed(mDocument)) {
    OriginAttributes oa;
    StoragePrincipalHelper::GetOriginAttributesForNetworkState(mDocument, oa);

    HTMLDNSPrefetch::Prefetch(hostname, isHttps, oa,
                              mDocument->GetChannel()->GetTRRMode(),
                              HTMLDNSPrefetch::Priority::Low);
  }
}

void nsContentSink::Preconnect(const nsAString& aHref,
                               const nsAString& aCrossOrigin) {
  // construct URI using document charset
  auto encoding = mDocument->GetDocumentCharacterSet();
  nsCOMPtr<nsIURI> uri;
  NS_NewURI(getter_AddRefs(uri), aHref, encoding, mDocument->GetDocBaseURI());

  if (uri && mDocument) {
    mDocument->MaybePreconnect(uri,
                               dom::Element::StringToCORSMode(aCrossOrigin));
  }
}

void nsContentSink::ScrollToRef() {
  RefPtr<Document> document = mDocument;
  document->ScrollToRef();
}

void nsContentSink::StartLayout(bool aIgnorePendingSheets) {
  if (mLayoutStarted) {
    // Nothing to do here
    return;
  }

  mDeferredLayoutStart = true;

  if (!aIgnorePendingSheets &&
      (WaitForPendingSheets() || mDocument->HasPendingInitialTranslation())) {
    // Bail out; we'll start layout when the sheets and l10n load
    return;
  }

  AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING_RELEVANT_FOR_JS(
      "Layout", LAYOUT, mDocumentURI->GetSpecOrDefault());

  mDeferredLayoutStart = false;

  if (aIgnorePendingSheets) {
    nsContentUtils::ReportToConsole(
        nsIScriptError::warningFlag, "Layout"_ns, mDocument,
        nsContentUtils::eLAYOUT_PROPERTIES, "ForcedLayoutStart");
  }

  // Notify on all our content.  If none of our presshells have started layout
  // yet it'll be a no-op except for updating our data structures, a la
  // UpdateChildCounts() (because we don't want to double-notify on whatever we
  // have right now).  If some of them _have_ started layout, we want to make
  // sure to flush tags instead of just calling UpdateChildCounts() after we
  // loop over the shells.
  FlushTags();

  mLayoutStarted = true;
  mLastNotificationTime = PR_Now();

  mDocument->SetMayStartLayout(true);
  RefPtr<PresShell> presShell = mDocument->GetPresShell();
  // Make sure we don't call Initialize() for a shell that has
  // already called it. This can happen when the layout frame for
  // an iframe is constructed *between* the Embed() call for the
  // docshell in the iframe, and the content sink's call to OpenBody().
  // (Bug 153815)
  if (presShell && !presShell->DidInitialize()) {
    nsresult rv = presShell->Initialize();
    if (NS_FAILED(rv)) {
      return;
    }
  }

  // If the document we are loading has a reference or it is a
  // frameset document, disable the scroll bars on the views.

  mDocument->SetScrollToRef(mDocument->GetDocumentURI());
}

void nsContentSink::NotifyAppend(nsIContent* aContainer, uint32_t aStartIndex) {
  mInNotification++;

  {
    // Scope so we call EndUpdate before we decrease mInNotification
    //
    // Note that aContainer->OwnerDoc() may not be mDocument.
    MOZ_AUTO_DOC_UPDATE(aContainer->OwnerDoc(), true);
    MutationObservers::NotifyContentAppended(
        aContainer, aContainer->GetChildAt_Deprecated(aStartIndex));
    mLastNotificationTime = PR_Now();
  }

  mInNotification--;
}

NS_IMETHODIMP
nsContentSink::Notify(nsITimer* timer) {
  if (mParsing) {
    // We shouldn't interfere with our normal DidProcessAToken logic
    mDroppedTimer = true;
    return NS_OK;
  }

  if (WaitForPendingSheets()) {
    mDeferredFlushTags = true;
  } else {
    FlushTags();

    // Now try and scroll to the reference
    // XXX Should we scroll unconditionally for history loads??
    ScrollToRef();
  }

  mNotificationTimer = nullptr;
  return NS_OK;
}

bool nsContentSink::IsTimeToNotify() {
  if (!StaticPrefs::content_notify_ontimer() || !mLayoutStarted ||
      !mBackoffCount || mInMonolithicContainer) {
    return false;
  }

  if (WaitForPendingSheets()) {
    mDeferredFlushTags = true;
    return false;
  }

  PRTime now = PR_Now();

  int64_t interval = GetNotificationInterval();
  int64_t diff = now - mLastNotificationTime;

  if (diff > interval) {
    mBackoffCount--;
    return true;
  }

  return false;
}

nsresult nsContentSink::WillInterruptImpl() {
  nsresult result = NS_OK;

  SINK_TRACE(static_cast<LogModule*>(gContentSinkLogModuleInfo),
             SINK_TRACE_CALLS, ("nsContentSink::WillInterrupt: this=%p", this));
#ifndef SINK_NO_INCREMENTAL
  if (WaitForPendingSheets()) {
    mDeferredFlushTags = true;
  } else if (StaticPrefs::content_notify_ontimer() && mLayoutStarted) {
    if (mBackoffCount && !mInMonolithicContainer) {
      int64_t now = PR_Now();
      int64_t interval = GetNotificationInterval();
      int64_t diff = now - mLastNotificationTime;

      // If it's already time for us to have a notification
      if (diff > interval || mDroppedTimer) {
        mBackoffCount--;
        SINK_TRACE(static_cast<LogModule*>(gContentSinkLogModuleInfo),
                   SINK_TRACE_REFLOW,
                   ("nsContentSink::WillInterrupt: flushing tags since we've "
                    "run out time; backoff count: %d",
                    mBackoffCount));
        result = FlushTags();
        if (mDroppedTimer) {
          ScrollToRef();
          mDroppedTimer = false;
        }
      } else if (!mNotificationTimer) {
        interval -= diff;
        int32_t delay = interval;

        // Convert to milliseconds
        delay /= PR_USEC_PER_MSEC;

        NS_NewTimerWithCallback(getter_AddRefs(mNotificationTimer), this, delay,
                                nsITimer::TYPE_ONE_SHOT);
        if (mNotificationTimer) {
          SINK_TRACE(static_cast<LogModule*>(gContentSinkLogModuleInfo),
                     SINK_TRACE_REFLOW,
                     ("nsContentSink::WillInterrupt: setting up timer with "
                      "delay %d",
                      delay));
        }
      }
    }
  } else {
    SINK_TRACE(static_cast<LogModule*>(gContentSinkLogModuleInfo),
               SINK_TRACE_REFLOW,
               ("nsContentSink::WillInterrupt: flushing tags "
                "unconditionally"));
    result = FlushTags();
  }
#endif

  mParsing = false;

  return result;
}

void nsContentSink::WillResumeImpl() {
  SINK_TRACE(static_cast<LogModule*>(gContentSinkLogModuleInfo),
             SINK_TRACE_CALLS, ("nsContentSink::WillResume: this=%p", this));

  mParsing = true;
}

nsresult nsContentSink::DidProcessATokenImpl() {
  if (mRunsToCompletion || !mParser) {
    return NS_OK;
  }

  // Get the current user event time
  PresShell* presShell = mDocument->GetPresShell();
  if (!presShell) {
    // If there's no pres shell in the document, return early since
    // we're not laying anything out here.
    return NS_OK;
  }

  // Increase before comparing to gEventProbeRate
  ++mDeflectedCount;

  // Check if there's a pending event
  if (StaticPrefs::content_sink_pending_event_mode() != 0 &&
      !mHasPendingEvent &&
      (mDeflectedCount % StaticPrefs::content_sink_event_probe_rate()) == 0) {
    nsViewManager* vm = presShell->GetViewManager();
    NS_ENSURE_TRUE(vm, NS_ERROR_FAILURE);
    nsCOMPtr<nsIWidget> widget = vm->GetRootWidget();
    mHasPendingEvent = widget && widget->HasPendingInputEvent();
  }

  if (mHasPendingEvent && StaticPrefs::content_sink_pending_event_mode() == 2) {
    return NS_ERROR_HTMLPARSER_INTERRUPTED;
  }

  // Have we processed enough tokens to check time?
  if (!mHasPendingEvent &&
      mDeflectedCount <
          uint32_t(mDynamicLowerValue
                       ? StaticPrefs::content_sink_interactive_deflect_count()
                       : StaticPrefs::content_sink_perf_deflect_count())) {
    return NS_OK;
  }

  mDeflectedCount = 0;

  // Check if it's time to return to the main event loop
  if (PR_IntervalToMicroseconds(PR_IntervalNow()) > mCurrentParseEndTime) {
    return NS_ERROR_HTMLPARSER_INTERRUPTED;
  }

  return NS_OK;
}

//----------------------------------------------------------------------

void nsContentSink::BeginUpdate(Document* aDocument) {
  // Remember nested updates from updates that we started.
  if (mInNotification > 0 && mUpdatesInNotification < 2) {
    ++mUpdatesInNotification;
  }

  // If we're in a script and we didn't do the notification,
  // something else in the script processing caused the
  // notification to occur. Since this could result in frame
  // creation, make sure we've flushed everything before we
  // continue.

  if (!mInNotification++) {
    FlushTags();
  }
}

void nsContentSink::EndUpdate(Document* aDocument) {
  // If we're in a script and we didn't do the notification,
  // something else in the script processing caused the
  // notification to occur. Update our notion of how much
  // has been flushed to include any new content if ending
  // this update leaves us not inside a notification.
  if (!--mInNotification) {
    UpdateChildCounts();
  }
}

void nsContentSink::DidBuildModelImpl(bool aTerminated) {
  MOZ_ASSERT(aTerminated ||
                 mDocument->GetReadyStateEnum() == Document::READYSTATE_LOADING,
             "Bad readyState");
  mDocument->SetReadyStateInternal(Document::READYSTATE_INTERACTIVE);

  if (mScriptLoader) {
    mScriptLoader->ParsingComplete(aTerminated);
    if (!mPendingSheetCount) {
      mScriptLoader->DeferCheckpointReached();
    }
  }

  if (!mDocument->HaveFiredDOMTitleChange()) {
    mDocument->NotifyPossibleTitleChange(false);
  }

  // Cancel a timer if we had one out there
  if (mNotificationTimer) {
    SINK_TRACE(static_cast<LogModule*>(gContentSinkLogModuleInfo),
               SINK_TRACE_REFLOW,
               ("nsContentSink::DidBuildModel: canceling notification "
                "timeout"));
    mNotificationTimer->Cancel();
    mNotificationTimer = nullptr;
  }
}

void nsContentSink::DropParserAndPerfHint(void) {
  if (!mParser) {
    // Make sure we don't unblock unload too many times
    return;
  }

  // Ref. Bug 49115
  // Do this hack to make sure that the parser
  // doesn't get destroyed, accidently, before
  // the circularity, between sink & parser, is
  // actually broken.
  // Drop our reference to the parser to get rid of a circular
  // reference.
  RefPtr<nsParserBase> kungFuDeathGrip = std::move(mParser);
  mozilla::Unused << kungFuDeathGrip;

  // Call UnblockOnload only if mRunsToComletion is false and if
  // we have already started loading because it's possible that this function
  // is called (i.e. the parser is terminated) before we start loading due to
  // destroying the window inside unload event callbacks for the previous
  // document.
  if (!mRunsToCompletion && mIsBlockingOnload) {
    mDocument->UnblockOnload(true);
    mIsBlockingOnload = false;
  }
}

bool nsContentSink::IsScriptExecutingImpl() {
  return !!mScriptLoader->GetCurrentScript();
}

nsresult nsContentSink::WillParseImpl(void) {
  if (mRunsToCompletion || !mDocument) {
    return NS_OK;
  }

  PresShell* presShell = mDocument->GetPresShell();
  if (!presShell) {
    return NS_OK;
  }

  uint32_t currentTime = PR_IntervalToMicroseconds(PR_IntervalNow());

  if (StaticPrefs::content_sink_enable_perf_mode() == 0) {
    nsViewManager* vm = presShell->GetViewManager();
    NS_ENSURE_TRUE(vm, NS_ERROR_FAILURE);
    uint32_t lastEventTime;
    vm->GetLastUserEventTime(lastEventTime);

    bool newDynLower = mDocument->IsInBackgroundWindow() ||
                       ((currentTime - mBeginLoadTime) >
                            StaticPrefs::content_sink_initial_perf_time() &&
                        (currentTime - lastEventTime) <
                            StaticPrefs::content_sink_interactive_time());

    if (mDynamicLowerValue != newDynLower) {
      mDynamicLowerValue = newDynLower;
    }
  }

  mDeflectedCount = 0;
  mHasPendingEvent = false;

  mCurrentParseEndTime =
      currentTime + (mDynamicLowerValue
                         ? StaticPrefs::content_sink_interactive_parse_time()
                         : StaticPrefs::content_sink_perf_parse_time());

  return NS_OK;
}

void nsContentSink::WillBuildModelImpl() {
  if (!mRunsToCompletion) {
    mDocument->BlockOnload();
    mIsBlockingOnload = true;

    mBeginLoadTime = PR_IntervalToMicroseconds(PR_IntervalNow());
  }

  mDocument->ResetScrolledToRefAlready();

  if (mProcessLinkHeaderEvent.get()) {
    mProcessLinkHeaderEvent.Revoke();

    DoProcessLinkHeader();
  }
}

/* static */
void nsContentSink::NotifyDocElementCreated(Document* aDoc) {
  MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());

  nsCOMPtr<nsIObserverService> observerService =
      mozilla::services::GetObserverService();
  MOZ_ASSERT(observerService);

  auto* win = nsGlobalWindowInner::Cast(aDoc->GetInnerWindow());
  bool fireInitialInsertion = !win || !win->DidFireDocElemInserted();
  if (win) {
    win->SetDidFireDocElemInserted();
  }
  if (fireInitialInsertion) {
    observerService->NotifyObservers(ToSupports(aDoc),
                                     "initial-document-element-inserted", u"");
  }
  observerService->NotifyObservers(ToSupports(aDoc),
                                   "document-element-inserted", u"");

  nsContentUtils::DispatchChromeEvent(aDoc, ToSupports(aDoc),
                                      u"DOMDocElementInserted"_ns,
                                      CanBubble::eYes, Cancelable::eNo);
}

NS_IMETHODIMP
nsContentSink::GetName(nsACString& aName) {
  aName.AssignLiteral("nsContentSink_timer");
  return NS_OK;
}
