fune/parser/html/nsHtml5TreeOpExecutor.cpp
Frédéric Wang b005b82248 Bug 1872657 - Add fetchpriority support for <link rel=preload as=image>. r=valentin,smaug,manuel
This patch adds fetchpriority support for `<link rel=preload as=image>`
and equivalent HTTP Link header. The fetchpriority value is passed from
where the link is parsed down to `NewImageChannel` where the priority
is initially set. Currently, the default equals PRIORITY_LOW, but is
decreased a bit if LOAD_BACKGROUND flag is set (this is always the case
for link preload images, see `imgLoader::LoadImage`). Later, the
priority can be increased again depending on the category (see
`imgRequest::BoostPriority`).

In order to minimize the changes, the new calculation is to keep the
initial setting to PRIORITY_LOW, adjust it using a new
`network.fetchpriority.adjustments.*` preference depending on the
fetchpriority attributes, and then preserve further adjustments for
LOAD_BACKGROUND and `BoostPriority`.

For the default value `fetchpriority=auto`, there is no adjustment
i.e. we continue to start with PRIORITY_LOW. `fetchpriority=low/high`
are respectively mapped to PRIORITY_LOW/PRIORITY_HIGH which is simple
and consistent with the "Image" cases from Google's web.dev article
https://web.dev/articles/fetch-priority. These values could of course
be revised in the future after more experiments.

This change is covered by the following tests below. The expectations
is modified to match what is described above (i.e. map to PRIORITY_LOW
or PRIORITY_HIGH with adjustment due to LOAD_BACKGROUND):
- `link-initial-preload-image.h2.html`
- `link-dynamic-preload-image.h2.html`
- `kPipeHeaderPreloadImageLinks`

Based on a patch by Mirko Brodesser (mbrodesser@igalia.com)

Differential Revision: https://phabricator.services.mozilla.com/D197493
2024-02-27 06:33:48 +00:00

1415 lines
48 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set sw=2 ts=2 et 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/. */
#include "mozilla/DebugOnly.h"
#include "mozilla/Likely.h"
#include "mozilla/dom/BrowsingContext.h"
#include "mozilla/dom/MediaList.h"
#include "mozilla/dom/ScriptLoader.h"
#include "mozilla/dom/nsCSPContext.h"
#include "mozilla/dom/nsCSPService.h"
#include "mozAutoDocUpdate.h"
#include "mozilla/IdleTaskRunner.h"
#include "mozilla/Preferences.h"
#include "mozilla/ProfilerLabels.h"
#include "mozilla/ProfilerMarkers.h"
#include "mozilla/StaticPrefs_content.h"
#include "mozilla/StaticPrefs_security.h"
#include "mozilla/StaticPrefs_view_source.h"
#include "mozilla/Telemetry.h"
#include "mozilla/css/Loader.h"
#include "mozilla/fallible.h"
#include "nsContentUtils.h"
#include "nsDocShell.h"
#include "nsError.h"
#include "nsHTMLDocument.h"
#include "nsHtml5AutoPauseUpdate.h"
#include "nsHtml5Parser.h"
#include "nsHtml5StreamParser.h"
#include "nsHtml5Tokenizer.h"
#include "nsHtml5TreeBuilder.h"
#include "nsHtml5TreeOpExecutor.h"
#include "nsIContentSecurityPolicy.h"
#include "nsIDocShell.h"
#include "nsIDocShellTreeItem.h"
#include "nsINestedURI.h"
#include "nsIHttpChannel.h"
#include "nsIScriptContext.h"
#include "nsIScriptError.h"
#include "nsIScriptGlobalObject.h"
#include "nsIViewSourceChannel.h"
#include "nsNetUtil.h"
#include "xpcpublic.h"
using namespace mozilla;
#ifdef DEBUG
static LazyLogModule gHtml5TreeOpExecutorLog("Html5TreeOpExecutor");
#endif // DEBUG
static LazyLogModule gCharsetMenuLog("Chardetng");
#define LOG(args) MOZ_LOG(gHtml5TreeOpExecutorLog, LogLevel::Debug, args)
#define LOGCHARDETNG(args) MOZ_LOG(gCharsetMenuLog, LogLevel::Debug, args)
NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(nsHtml5TreeOpExecutor,
nsHtml5DocumentBuilder,
nsIContentSink)
class nsHtml5ExecutorReflusher : public Runnable {
private:
RefPtr<nsHtml5TreeOpExecutor> mExecutor;
public:
explicit nsHtml5ExecutorReflusher(nsHtml5TreeOpExecutor* aExecutor)
: Runnable("nsHtml5ExecutorReflusher"), mExecutor(aExecutor) {}
NS_IMETHOD Run() override {
dom::Document* doc = mExecutor->GetDocument();
if (XRE_IsContentProcess() &&
nsContentUtils::
HighPriorityEventPendingForTopLevelDocumentBeforeContentfulPaint(
doc)) {
// Possible early paint pending, reuse the runnable and try to
// call RunFlushLoop later.
nsCOMPtr<nsIRunnable> flusher = this;
if (NS_SUCCEEDED(doc->Dispatch(flusher.forget()))) {
PROFILER_MARKER_UNTYPED("HighPrio blocking parser flushing(2)", DOM);
return NS_OK;
}
}
mExecutor->RunFlushLoop();
return NS_OK;
}
};
class MOZ_RAII nsHtml5AutoFlush final {
private:
RefPtr<nsHtml5TreeOpExecutor> mExecutor;
size_t mOpsToRemove;
public:
explicit nsHtml5AutoFlush(nsHtml5TreeOpExecutor* aExecutor)
: mExecutor(aExecutor), mOpsToRemove(aExecutor->OpQueueLength()) {
mExecutor->BeginFlush();
mExecutor->BeginDocUpdate();
}
~nsHtml5AutoFlush() {
if (mExecutor->IsInDocUpdate()) {
mExecutor->EndDocUpdate();
} else {
// We aren't in an update if nsHtml5AutoPauseUpdate
// caused something to terminate the parser.
MOZ_RELEASE_ASSERT(
mExecutor->IsComplete(),
"How do we have mParser but the doc update isn't open?");
}
mExecutor->EndFlush();
mExecutor->RemoveFromStartOfOpQueue(mOpsToRemove);
}
void SetNumberOfOpsToRemove(size_t aOpsToRemove) {
MOZ_ASSERT(aOpsToRemove < mOpsToRemove,
"Requested partial clearing of op queue but the number to clear "
"wasn't less than the length of the queue.");
mOpsToRemove = aOpsToRemove;
}
};
static LinkedList<nsHtml5TreeOpExecutor>* gBackgroundFlushList = nullptr;
StaticRefPtr<IdleTaskRunner> gBackgroundFlushRunner;
nsHtml5TreeOpExecutor::nsHtml5TreeOpExecutor()
: nsHtml5DocumentBuilder(false),
mSuppressEOF(false),
mReadingFromStage(false),
mStreamParser(nullptr),
mPreloadedURLs(23), // Mean # of preloadable resources per page on dmoz
mStarted(false),
mRunFlushLoopOnStack(false),
mCallContinueInterruptedParsingIfEnabled(false),
mAlreadyComplainedAboutCharset(false),
mAlreadyComplainedAboutDeepTree(false) {}
nsHtml5TreeOpExecutor::~nsHtml5TreeOpExecutor() {
if (gBackgroundFlushList && isInList()) {
ClearOpQueue();
removeFrom(*gBackgroundFlushList);
if (gBackgroundFlushList->isEmpty()) {
delete gBackgroundFlushList;
gBackgroundFlushList = nullptr;
if (gBackgroundFlushRunner) {
gBackgroundFlushRunner->Cancel();
gBackgroundFlushRunner = nullptr;
}
}
}
MOZ_ASSERT(NS_FAILED(mBroken) || mOpQueue.IsEmpty(),
"Somehow there's stuff in the op queue.");
}
// nsIContentSink
NS_IMETHODIMP
nsHtml5TreeOpExecutor::WillParse() {
MOZ_ASSERT_UNREACHABLE("No one should call this");
return NS_ERROR_NOT_IMPLEMENTED;
}
nsresult nsHtml5TreeOpExecutor::WillBuildModel() {
mDocument->AddObserver(this);
WillBuildModelImpl();
GetDocument()->BeginLoad();
if (mDocShell && !GetDocument()->GetWindow() && !IsExternalViewSource()) {
// Not loading as data but script global object not ready
return MarkAsBroken(NS_ERROR_DOM_INVALID_STATE_ERR);
}
return NS_OK;
}
// This is called when the tree construction has ended
NS_IMETHODIMP
nsHtml5TreeOpExecutor::DidBuildModel(bool aTerminated) {
if (mRunsToCompletion) {
return NS_OK;
}
MOZ_RELEASE_ASSERT(!IsInDocUpdate(),
"DidBuildModel from inside a doc update.");
RefPtr<nsHtml5TreeOpExecutor> pin(this);
auto queueClearer = MakeScopeExit([&] {
if (aTerminated && (mFlushState == eNotFlushing)) {
ClearOpQueue(); // clear in order to be able to assert in destructor
}
});
// This comes from nsXMLContentSink and nsHTMLContentSink
// If this parser has been marked as broken, treat the end of parse as
// forced termination.
DidBuildModelImpl(aTerminated || NS_FAILED(IsBroken()));
bool destroying = true;
if (mDocShell) {
mDocShell->IsBeingDestroyed(&destroying);
}
if (!destroying) {
mDocument->OnParsingCompleted();
if (!mLayoutStarted) {
// We never saw the body, and layout never got started. Force
// layout *now*, to get an initial reflow.
// NOTE: only force the layout if we are NOT destroying the
// docshell. If we are destroying it, then starting layout will
// likely cause us to crash, or at best waste a lot of time as we
// are just going to tear it down anyway.
nsContentSink::StartLayout(false);
}
}
ScrollToRef();
mDocument->RemoveObserver(this);
if (!mParser) {
// DidBuildModelImpl may cause mParser to be nulled out
// Return early to avoid unblocking the onload event too many times.
return NS_OK;
}
// We may not have called BeginLoad() if loading is terminated before
// OnStartRequest call.
if (mStarted) {
mDocument->EndLoad();
// Gather telemetry only for top-level content navigations in order to
// avoid noise from ad iframes.
bool topLevel = false;
if (mozilla::dom::BrowsingContext* bc = mDocument->GetBrowsingContext()) {
topLevel = bc->IsTopContent();
}
// Gather telemetry only for text/html and text/plain (excluding CSS, JS,
// etc. being viewed as text.)
nsAutoString contentType;
mDocument->GetContentType(contentType);
bool htmlOrPlain = contentType.EqualsLiteral(u"text/html") ||
contentType.EqualsLiteral(u"text/plain");
// Gather telemetry only for HTTP status code 200 in order to exclude
// error pages.
bool httpOk = false;
nsCOMPtr<nsIChannel> channel;
nsresult rv = GetParser()->GetChannel(getter_AddRefs(channel));
if (NS_SUCCEEDED(rv) && channel) {
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel);
if (httpChannel) {
uint32_t httpStatus;
rv = httpChannel->GetResponseStatus(&httpStatus);
if (NS_SUCCEEDED(rv) && httpStatus == 200) {
httpOk = true;
}
}
}
// Gather chardetng telemetry
MOZ_ASSERT(mDocument->IsHTMLDocument());
if (httpOk && htmlOrPlain && topLevel && !aTerminated &&
!mDocument->AsHTMLDocument()->IsViewSource()) {
// We deliberately measure only normally-completed (non-aborted) loads
// that are not View Source loads. This seems like a better place for
// checking normal completion than anything in nsHtml5StreamParser.
bool plain = mDocument->AsHTMLDocument()->IsPlainText();
int32_t charsetSource = mDocument->GetDocumentCharacterSetSource();
switch (charsetSource) {
case kCharsetFromInitialAutoDetectionWouldHaveBeenUTF8:
if (plain) {
LOGCHARDETNG(("TEXT::UtfInitial"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_TEXT::UtfInitial);
} else {
LOGCHARDETNG(("HTML::UtfInitial"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_HTML::UtfInitial);
}
break;
case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8Generic:
if (plain) {
LOGCHARDETNG(("TEXT::GenericInitial"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_TEXT::
GenericInitial);
} else {
LOGCHARDETNG(("HTML::GenericInitial"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_HTML::
GenericInitial);
}
break;
case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8Content:
if (plain) {
LOGCHARDETNG(("TEXT::ContentInitial"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_TEXT::
ContentInitial);
} else {
LOGCHARDETNG(("HTML::ContentInitial"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_HTML::
ContentInitial);
}
break;
case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8DependedOnTLD:
if (plain) {
LOGCHARDETNG(("TEXT::TldInitial"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_TEXT::TldInitial);
} else {
LOGCHARDETNG(("HTML::TldInitial"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_HTML::TldInitial);
}
break;
case kCharsetFromFinalAutoDetectionWouldHaveBeenUTF8InitialWasASCII:
if (plain) {
LOGCHARDETNG(("TEXT::UtfFinal"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_TEXT::UtfFinal);
} else {
LOGCHARDETNG(("HTML::UtfFinal"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_HTML::UtfFinal);
}
break;
case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8Generic:
if (plain) {
LOGCHARDETNG(("TEXT::GenericFinal"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_TEXT::
GenericFinal);
} else {
LOGCHARDETNG(("HTML::GenericFinal"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_HTML::
GenericFinal);
}
break;
case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8GenericInitialWasASCII:
if (plain) {
LOGCHARDETNG(("TEXT::GenericFinalA"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_TEXT::
GenericFinalA);
} else {
LOGCHARDETNG(("HTML::GenericFinalA"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_HTML::
GenericFinalA);
}
break;
case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8Content:
if (plain) {
LOGCHARDETNG(("TEXT::ContentFinal"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_TEXT::
ContentFinal);
} else {
LOGCHARDETNG(("HTML::ContentFinal"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_HTML::
ContentFinal);
}
break;
case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8ContentInitialWasASCII:
if (plain) {
LOGCHARDETNG(("TEXT::ContentFinalA"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_TEXT::
ContentFinalA);
} else {
LOGCHARDETNG(("HTML::ContentFinalA"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_HTML::
ContentFinalA);
}
break;
case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8DependedOnTLD:
if (plain) {
LOGCHARDETNG(("TEXT::TldFinal"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_TEXT::TldFinal);
} else {
LOGCHARDETNG(("HTML::TldFinal"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_HTML::TldFinal);
}
break;
case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8DependedOnTLDInitialWasASCII:
if (plain) {
LOGCHARDETNG(("TEXT::TldFinalA"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_TEXT::TldFinalA);
} else {
LOGCHARDETNG(("HTML::TldFinalA"));
Telemetry::AccumulateCategorical(
Telemetry::LABELS_ENCODING_DETECTION_OUTCOME_HTML::TldFinalA);
}
break;
default:
// Chardetng didn't run automatically or the input was all ASCII.
break;
}
}
}
// Dropping the stream parser changes the parser's apparent
// script-createdness, which is why the stream parser must not be dropped
// before this executor's nsHtml5Parser has been made unreachable from its
// nsHTMLDocument. (mDocument->EndLoad() above drops the parser from the
// document.)
GetParser()->DropStreamParser();
DropParserAndPerfHint();
#ifdef GATHER_DOCWRITE_STATISTICS
printf("UNSAFE SCRIPTS: %d\n", sUnsafeDocWrites);
printf("TOKENIZER-SAFE SCRIPTS: %d\n", sTokenSafeDocWrites);
printf("TREEBUILDER-SAFE SCRIPTS: %d\n", sTreeSafeDocWrites);
#endif
#ifdef DEBUG
LOG(("MAX NOTIFICATION BATCH LEN: %d\n", sAppendBatchMaxSize));
if (sAppendBatchExaminations != 0) {
LOG(("AVERAGE SLOTS EXAMINED: %d\n",
sAppendBatchSlotsExamined / sAppendBatchExaminations));
}
#endif
return NS_OK;
}
NS_IMETHODIMP
nsHtml5TreeOpExecutor::WillInterrupt() {
MOZ_ASSERT_UNREACHABLE("Don't call. For interface compat only.");
return NS_ERROR_NOT_IMPLEMENTED;
}
void nsHtml5TreeOpExecutor::WillResume() {
MOZ_ASSERT_UNREACHABLE("Don't call. For interface compat only.");
}
NS_IMETHODIMP
nsHtml5TreeOpExecutor::SetParser(nsParserBase* aParser) {
mParser = aParser;
return NS_OK;
}
void nsHtml5TreeOpExecutor::InitialTranslationCompleted() {
nsContentSink::StartLayout(false);
}
void nsHtml5TreeOpExecutor::FlushPendingNotifications(FlushType aType) {
if (aType >= FlushType::EnsurePresShellInitAndFrames) {
// Bug 577508 / 253951
nsContentSink::StartLayout(true);
}
}
nsISupports* nsHtml5TreeOpExecutor::GetTarget() {
return ToSupports(mDocument);
}
nsresult nsHtml5TreeOpExecutor::MarkAsBroken(nsresult aReason) {
MOZ_ASSERT(NS_IsMainThread(), "Wrong thread!");
mBroken = aReason;
if (mStreamParser) {
mStreamParser->Terminate();
}
// We are under memory pressure, but let's hope the following allocation
// works out so that we get to terminate and clean up the parser from
// a safer point.
if (mParser && mDocument) { // can mParser ever be null here?
nsCOMPtr<nsIRunnable> terminator = NewRunnableMethod(
"nsHtml5Parser::Terminate", GetParser(), &nsHtml5Parser::Terminate);
if (NS_FAILED(mDocument->Dispatch(terminator.forget()))) {
NS_WARNING("failed to dispatch executor flush event");
}
}
return aReason;
}
static bool BackgroundFlushCallback(TimeStamp /*aDeadline*/) {
RefPtr<nsHtml5TreeOpExecutor> ex = gBackgroundFlushList->popFirst();
if (ex) {
ex->RunFlushLoop();
}
if (gBackgroundFlushList && gBackgroundFlushList->isEmpty()) {
delete gBackgroundFlushList;
gBackgroundFlushList = nullptr;
gBackgroundFlushRunner->Cancel();
gBackgroundFlushRunner = nullptr;
return true;
}
return true;
}
void nsHtml5TreeOpExecutor::ContinueInterruptedParsingAsync() {
if (mDocument && !mDocument->IsInBackgroundWindow()) {
nsCOMPtr<nsIRunnable> flusher = new nsHtml5ExecutorReflusher(this);
if (NS_FAILED(mDocument->Dispatch(flusher.forget()))) {
NS_WARNING("failed to dispatch executor flush event");
}
} else {
if (!gBackgroundFlushList) {
gBackgroundFlushList = new LinkedList<nsHtml5TreeOpExecutor>();
}
if (!isInList()) {
gBackgroundFlushList->insertBack(this);
}
if (gBackgroundFlushRunner) {
return;
}
// Now we set up a repetitive idle scheduler for flushing background list.
gBackgroundFlushRunner = IdleTaskRunner::Create(
&BackgroundFlushCallback,
"nsHtml5TreeOpExecutor::BackgroundFlushCallback",
0, // Start looking for idle time immediately.
TimeDuration::FromMilliseconds(250), // The hard deadline.
TimeDuration::FromMicroseconds(
StaticPrefs::content_sink_interactive_parse_time()), // Required
// budget.
true, // repeating
[] { return false; }); // MayStopProcessing
}
}
void nsHtml5TreeOpExecutor::FlushSpeculativeLoads() {
nsTArray<nsHtml5SpeculativeLoad> speculativeLoadQueue;
mStage.MoveSpeculativeLoadsTo(speculativeLoadQueue);
nsHtml5SpeculativeLoad* start = speculativeLoadQueue.Elements();
nsHtml5SpeculativeLoad* end = start + speculativeLoadQueue.Length();
for (nsHtml5SpeculativeLoad* iter = start; iter < end; ++iter) {
if (MOZ_UNLIKELY(!mParser)) {
// An extension terminated the parser from a HTTP observer.
return;
}
iter->Perform(this);
}
}
class nsHtml5FlushLoopGuard {
private:
RefPtr<nsHtml5TreeOpExecutor> mExecutor;
#ifdef DEBUG
uint32_t mStartTime;
#endif
public:
explicit nsHtml5FlushLoopGuard(nsHtml5TreeOpExecutor* aExecutor)
: mExecutor(aExecutor)
#ifdef DEBUG
,
mStartTime(PR_IntervalToMilliseconds(PR_IntervalNow()))
#endif
{
mExecutor->mRunFlushLoopOnStack = true;
}
~nsHtml5FlushLoopGuard() {
#ifdef DEBUG
uint32_t timeOffTheEventLoop =
PR_IntervalToMilliseconds(PR_IntervalNow()) - mStartTime;
if (timeOffTheEventLoop >
nsHtml5TreeOpExecutor::sLongestTimeOffTheEventLoop) {
nsHtml5TreeOpExecutor::sLongestTimeOffTheEventLoop = timeOffTheEventLoop;
}
LOG(("Longest time off the event loop: %d\n",
nsHtml5TreeOpExecutor::sLongestTimeOffTheEventLoop));
#endif
mExecutor->mRunFlushLoopOnStack = false;
}
};
/**
* The purpose of the loop here is to avoid returning to the main event loop
*/
void nsHtml5TreeOpExecutor::RunFlushLoop() {
AUTO_PROFILER_LABEL("nsHtml5TreeOpExecutor::RunFlushLoop", OTHER);
if (mRunFlushLoopOnStack) {
// There's already a RunFlushLoop() on the call stack.
return;
}
nsHtml5FlushLoopGuard guard(this); // this is also the self-kungfu!
RefPtr<nsParserBase> parserKungFuDeathGrip(mParser);
RefPtr<nsHtml5StreamParser> streamParserGrip;
if (mParser) {
streamParserGrip = GetParser()->GetStreamParser();
}
Unused << streamParserGrip; // Intentionally not used within function
// Remember the entry time
(void)nsContentSink::WillParseImpl();
for (;;) {
if (!mParser) {
// Parse has terminated.
ClearOpQueue(); // clear in order to be able to assert in destructor
return;
}
if (NS_FAILED(IsBroken())) {
return;
}
if (!parserKungFuDeathGrip->IsParserEnabled()) {
// The parser is blocked.
return;
}
if (mFlushState != eNotFlushing) {
// XXX Can this happen? In case it can, let's avoid crashing.
return;
}
// If there are scripts executing, then the content sink is jumping the gun
// (probably due to a synchronous XMLHttpRequest) and will re-enable us
// later, see bug 460706.
if (IsScriptExecuting()) {
return;
}
if (mReadingFromStage) {
nsTArray<nsHtml5SpeculativeLoad> speculativeLoadQueue;
MOZ_RELEASE_ASSERT(mFlushState == eNotFlushing,
"mOpQueue modified during flush.");
if (!mStage.MoveOpsAndSpeculativeLoadsTo(mOpQueue,
speculativeLoadQueue)) {
MarkAsBroken(nsresult::NS_ERROR_OUT_OF_MEMORY);
return;
}
// Make sure speculative loads never start after the corresponding
// normal loads for the same URLs.
nsHtml5SpeculativeLoad* start = speculativeLoadQueue.Elements();
nsHtml5SpeculativeLoad* end = start + speculativeLoadQueue.Length();
for (nsHtml5SpeculativeLoad* iter = start; iter < end; ++iter) {
iter->Perform(this);
if (MOZ_UNLIKELY(!mParser)) {
// An extension terminated the parser from a HTTP observer.
ClearOpQueue(); // clear in order to be able to assert in destructor
return;
}
}
} else {
FlushSpeculativeLoads(); // Make sure speculative loads never start after
// the corresponding normal loads for the same
// URLs.
if (MOZ_UNLIKELY(!mParser)) {
// An extension terminated the parser from a HTTP observer.
ClearOpQueue(); // clear in order to be able to assert in destructor
return;
}
// Now parse content left in the document.write() buffer queue if any.
// This may generate tree ops on its own or dequeue a speculation.
nsresult rv = GetParser()->ParseUntilBlocked();
// ParseUntilBlocked flushes operations from the stage to the OpQueue.
// Those operations may have accompanying speculative operations.
// If so, we have to flush those speculative loads so that we maintain
// the invariant that no speculative load starts after the corresponding
// normal load for the same URL. See
// https://bugzilla.mozilla.org/show_bug.cgi?id=1513292#c80
// for a more detailed explanation of why this is necessary.
FlushSpeculativeLoads();
if (NS_FAILED(rv)) {
MarkAsBroken(rv);
return;
}
}
if (mOpQueue.IsEmpty()) {
// Avoid bothering the rest of the engine with a doc update if there's
// nothing to do.
return;
}
nsIContent* scriptElement = nullptr;
bool interrupted = false;
bool streamEnded = false;
{
// autoFlush clears mOpQueue in its destructor unless
// SetNumberOfOpsToRemove is called first, in which case only
// some ops from the start of the queue are cleared.
nsHtml5AutoFlush autoFlush(this);
nsHtml5TreeOperation* first = mOpQueue.Elements();
nsHtml5TreeOperation* last = first + mOpQueue.Length() - 1;
for (nsHtml5TreeOperation* iter = first;; ++iter) {
if (MOZ_UNLIKELY(!mParser)) {
// The previous tree op caused a call to nsIParser::Terminate().
return;
}
MOZ_ASSERT(IsInDocUpdate(),
"Tried to perform tree op outside update batch.");
nsresult rv =
iter->Perform(this, &scriptElement, &interrupted, &streamEnded);
if (NS_FAILED(rv)) {
MarkAsBroken(rv);
break;
}
// Be sure not to check the deadline if the last op was just performed.
if (MOZ_UNLIKELY(iter == last)) {
break;
} else if (MOZ_UNLIKELY(interrupted) ||
MOZ_UNLIKELY(nsContentSink::DidProcessATokenImpl() ==
NS_ERROR_HTMLPARSER_INTERRUPTED)) {
autoFlush.SetNumberOfOpsToRemove((iter - first) + 1);
nsHtml5TreeOpExecutor::ContinueInterruptedParsingAsync();
return;
}
}
if (MOZ_UNLIKELY(!mParser)) {
// The parse ended during an update pause.
return;
}
if (streamEnded) {
GetParser()->PermanentlyUndefineInsertionPoint();
}
} // end autoFlush
if (MOZ_UNLIKELY(!mParser)) {
// Ending the doc update caused a call to nsIParser::Terminate().
return;
}
if (streamEnded) {
DidBuildModel(false);
#ifdef DEBUG
if (scriptElement) {
nsCOMPtr<nsIScriptElement> sele = do_QueryInterface(scriptElement);
if (!sele) {
MOZ_ASSERT(nsNameSpaceManager::GetInstance()->mSVGDisabled,
"Node didn't QI to script, but SVG wasn't disabled.");
}
MOZ_ASSERT(sele->IsMalformed(), "Script wasn't marked as malformed.");
}
#endif
} else if (scriptElement) {
// must be tail call when mFlushState is eNotFlushing
RunScript(scriptElement, true);
// Always check the clock in nsContentSink right after a script
StopDeflecting();
if (nsContentSink::DidProcessATokenImpl() ==
NS_ERROR_HTMLPARSER_INTERRUPTED) {
#ifdef DEBUG
LOG(("REFLUSH SCHEDULED (after script): %d\n",
++sTimesFlushLoopInterrupted));
#endif
nsHtml5TreeOpExecutor::ContinueInterruptedParsingAsync();
return;
}
}
}
}
nsresult nsHtml5TreeOpExecutor::FlushDocumentWrite() {
nsresult rv = IsBroken();
NS_ENSURE_SUCCESS(rv, rv);
FlushSpeculativeLoads(); // Make sure speculative loads never start after the
// corresponding normal loads for the same URLs.
if (MOZ_UNLIKELY(!mParser)) {
// The parse has ended.
ClearOpQueue(); // clear in order to be able to assert in destructor
return rv;
}
if (mFlushState != eNotFlushing) {
// XXX Can this happen? In case it can, let's avoid crashing.
return rv;
}
// avoid crashing near EOF
RefPtr<nsHtml5TreeOpExecutor> kungFuDeathGrip(this);
RefPtr<nsParserBase> parserKungFuDeathGrip(mParser);
Unused << parserKungFuDeathGrip; // Intentionally not used within function
RefPtr<nsHtml5StreamParser> streamParserGrip;
if (mParser) {
streamParserGrip = GetParser()->GetStreamParser();
}
Unused << streamParserGrip; // Intentionally not used within function
MOZ_RELEASE_ASSERT(!mReadingFromStage,
"Got doc write flush when reading from stage");
#ifdef DEBUG
mStage.AssertEmpty();
#endif
nsIContent* scriptElement = nullptr;
bool interrupted = false;
bool streamEnded = false;
{
// autoFlush clears mOpQueue in its destructor.
nsHtml5AutoFlush autoFlush(this);
nsHtml5TreeOperation* start = mOpQueue.Elements();
nsHtml5TreeOperation* end = start + mOpQueue.Length();
for (nsHtml5TreeOperation* iter = start; iter < end; ++iter) {
if (MOZ_UNLIKELY(!mParser)) {
// The previous tree op caused a call to nsIParser::Terminate().
return rv;
}
NS_ASSERTION(IsInDocUpdate(),
"Tried to perform tree op outside update batch.");
rv = iter->Perform(this, &scriptElement, &interrupted, &streamEnded);
if (NS_FAILED(rv)) {
MarkAsBroken(rv);
break;
}
}
if (MOZ_UNLIKELY(!mParser)) {
// The parse ended during an update pause.
return rv;
}
if (streamEnded) {
// This should be redundant but let's do it just in case.
GetParser()->PermanentlyUndefineInsertionPoint();
}
} // autoFlush
if (MOZ_UNLIKELY(!mParser)) {
// Ending the doc update caused a call to nsIParser::Terminate().
return rv;
}
if (streamEnded) {
DidBuildModel(false);
#ifdef DEBUG
if (scriptElement) {
nsCOMPtr<nsIScriptElement> sele = do_QueryInterface(scriptElement);
if (!sele) {
MOZ_ASSERT(nsNameSpaceManager::GetInstance()->mSVGDisabled,
"Node didn't QI to script, but SVG wasn't disabled.");
}
MOZ_ASSERT(sele->IsMalformed(), "Script wasn't marked as malformed.");
}
#endif
} else if (scriptElement) {
// must be tail call when mFlushState is eNotFlushing
RunScript(scriptElement, true);
}
return rv;
}
void nsHtml5TreeOpExecutor::CommitToInternalEncoding() {
if (MOZ_UNLIKELY(!mParser || !mStreamParser)) {
// An extension terminated the parser from a HTTP observer.
ClearOpQueue(); // clear in order to be able to assert in destructor
return;
}
mStreamParser->ContinueAfterScriptsOrEncodingCommitment(nullptr, nullptr,
false);
}
[[nodiscard]] bool nsHtml5TreeOpExecutor::TakeOpsFromStage() {
return mStage.MoveOpsTo(mOpQueue);
}
// copied from HTML content sink
bool nsHtml5TreeOpExecutor::IsScriptEnabled() {
// Note that if we have no document or no docshell or no global or whatnot we
// want to claim script _is_ enabled, so we don't parse the contents of
// <noscript> tags!
if (!mDocument || !mDocShell) {
return true;
}
return mDocument->IsScriptEnabled();
}
void nsHtml5TreeOpExecutor::StartLayout(bool* aInterrupted) {
if (mLayoutStarted || !mDocument) {
return;
}
nsHtml5AutoPauseUpdate autoPause(this);
if (MOZ_UNLIKELY(!mParser)) {
// got terminate
return;
}
nsContentSink::StartLayout(false);
if (mParser) {
*aInterrupted = !GetParser()->IsParserEnabled();
}
}
void nsHtml5TreeOpExecutor::PauseDocUpdate(bool* aInterrupted) {
// Pausing the document update allows JS to run, and potentially block
// further parsing.
nsHtml5AutoPauseUpdate autoPause(this);
if (MOZ_LIKELY(mParser)) {
*aInterrupted = !GetParser()->IsParserEnabled();
}
}
/**
* The reason why this code is here and not in the tree builder even in the
* main-thread case is to allow the control to return from the tokenizer
* before scripts run. This way, the tokenizer is not invoked re-entrantly
* although the parser is.
*
* The reason why this is called with `aMayDocumentWriteOrBlock=true` as a
* tail call when `mFlushState` is set to `eNotFlushing` is to allow re-entry
* to `Flush()` but only after the current `Flush()` has cleared the op queue
* and is otherwise done cleaning up after itself.
*/
void nsHtml5TreeOpExecutor::RunScript(nsIContent* aScriptElement,
bool aMayDocumentWriteOrBlock) {
if (mRunsToCompletion) {
// We are in createContextualFragment() or in the upcoming document.parse().
// Do nothing. Let's not even mark scripts malformed here, because that
// could cause serialization weirdness later.
return;
}
MOZ_ASSERT(mParser, "Trying to run script with a terminated parser.");
MOZ_ASSERT(aScriptElement, "No script to run");
nsCOMPtr<nsIScriptElement> sele = do_QueryInterface(aScriptElement);
if (!sele) {
MOZ_ASSERT(nsNameSpaceManager::GetInstance()->mSVGDisabled,
"Node didn't QI to script, but SVG wasn't disabled.");
return;
}
sele->SetCreatorParser(GetParser());
if (!aMayDocumentWriteOrBlock) {
MOZ_ASSERT(sele->GetScriptDeferred() || sele->GetScriptAsync() ||
sele->GetScriptIsModule() || sele->GetScriptIsImportMap() ||
aScriptElement->AsElement()->HasAttr(nsGkAtoms::nomodule));
DebugOnly<bool> block = sele->AttemptToExecute();
MOZ_ASSERT(!block,
"Defer, async, module, importmap, or nomodule tried to block.");
return;
}
MOZ_RELEASE_ASSERT(
mFlushState == eNotFlushing,
"Tried to run a potentially-blocking script while flushing.");
mReadingFromStage = false;
// Copied from nsXMLContentSink
// Now tell the script that it's ready to go. This may execute the script
// or return true, or neither if the script doesn't need executing.
bool block = sele->AttemptToExecute();
// If the act of insertion evaluated the script, we're fine.
// Else, block the parser till the script has loaded.
if (block) {
if (mParser) {
GetParser()->BlockParser();
}
} else {
// mParser may have been nulled out by now, but the flusher deals
// If this event isn't needed, it doesn't do anything. It is sometimes
// necessary for the parse to continue after complex situations.
nsHtml5TreeOpExecutor::ContinueInterruptedParsingAsync();
}
}
void nsHtml5TreeOpExecutor::Start() {
MOZ_ASSERT(!mStarted, "Tried to start when already started.");
mStarted = true;
}
void nsHtml5TreeOpExecutor::UpdateCharsetSource(
nsCharsetSource aCharsetSource) {
if (mDocument) {
mDocument->SetDocumentCharacterSetSource(aCharsetSource);
}
}
void nsHtml5TreeOpExecutor::SetDocumentCharsetAndSource(
NotNull<const Encoding*> aEncoding, nsCharsetSource aCharsetSource) {
if (mDocument) {
mDocument->SetDocumentCharacterSetSource(aCharsetSource);
mDocument->SetDocumentCharacterSet(aEncoding);
}
}
void nsHtml5TreeOpExecutor::NeedsCharsetSwitchTo(
NotNull<const Encoding*> aEncoding, int32_t aSource, uint32_t aLineNumber) {
nsHtml5AutoPauseUpdate autoPause(this);
if (MOZ_UNLIKELY(!mParser)) {
// got terminate
return;
}
if (!mDocShell) {
return;
}
RefPtr<nsDocShell> docShell = static_cast<nsDocShell*>(mDocShell.get());
if (NS_SUCCEEDED(docShell->CharsetChangeStopDocumentLoad())) {
docShell->CharsetChangeReloadDocument(aEncoding, aSource);
}
// if the charset switch was accepted, mDocShell has called Terminate() on the
// parser by now
if (!mParser) {
return;
}
GetParser()->ContinueAfterFailedCharsetSwitch();
}
void nsHtml5TreeOpExecutor::MaybeComplainAboutCharset(const char* aMsgId,
bool aError,
uint32_t aLineNumber) {
// Encoding errors don't count towards already complaining
if (!(!strcmp(aMsgId, "EncError") || !strcmp(aMsgId, "EncErrorFrame") ||
!strcmp(aMsgId, "EncErrorFramePlain"))) {
if (mAlreadyComplainedAboutCharset) {
return;
}
mAlreadyComplainedAboutCharset = true;
}
nsContentUtils::ReportToConsole(
aError ? nsIScriptError::errorFlag : nsIScriptError::warningFlag,
"HTML parser"_ns, mDocument, nsContentUtils::eHTMLPARSER_PROPERTIES,
aMsgId, nsTArray<nsString>(), nullptr, u""_ns, aLineNumber);
}
void nsHtml5TreeOpExecutor::ComplainAboutBogusProtocolCharset(
Document* aDoc, bool aUnrecognized) {
NS_ASSERTION(!mAlreadyComplainedAboutCharset,
"How come we already managed to complain?");
mAlreadyComplainedAboutCharset = true;
nsContentUtils::ReportToConsole(
nsIScriptError::errorFlag, "HTML parser"_ns, aDoc,
nsContentUtils::eHTMLPARSER_PROPERTIES,
aUnrecognized ? "EncProtocolUnsupported" : "EncProtocolReplacement");
}
void nsHtml5TreeOpExecutor::MaybeComplainAboutDeepTree(uint32_t aLineNumber) {
if (mAlreadyComplainedAboutDeepTree) {
return;
}
mAlreadyComplainedAboutDeepTree = true;
nsContentUtils::ReportToConsole(
nsIScriptError::errorFlag, "HTML parser"_ns, mDocument,
nsContentUtils::eHTMLPARSER_PROPERTIES, "errDeepTree",
nsTArray<nsString>(), nullptr, u""_ns, aLineNumber);
}
nsHtml5Parser* nsHtml5TreeOpExecutor::GetParser() {
MOZ_ASSERT(!mRunsToCompletion);
return static_cast<nsHtml5Parser*>(mParser.get());
}
[[nodiscard]] bool nsHtml5TreeOpExecutor::MoveOpsFrom(
nsTArray<nsHtml5TreeOperation>& aOpQueue) {
MOZ_RELEASE_ASSERT(mFlushState == eNotFlushing,
"Ops added to mOpQueue during tree op execution.");
return !!mOpQueue.AppendElements(std::move(aOpQueue), mozilla::fallible_t());
}
void nsHtml5TreeOpExecutor::ClearOpQueue() {
MOZ_RELEASE_ASSERT(mFlushState == eNotFlushing,
"mOpQueue cleared during tree op execution.");
mOpQueue.Clear();
}
void nsHtml5TreeOpExecutor::RemoveFromStartOfOpQueue(
size_t aNumberOfOpsToRemove) {
MOZ_RELEASE_ASSERT(mFlushState == eNotFlushing,
"Ops removed from mOpQueue during tree op execution.");
mOpQueue.RemoveElementsAt(0, aNumberOfOpsToRemove);
}
void nsHtml5TreeOpExecutor::InitializeDocWriteParserState(
nsAHtml5TreeBuilderState* aState, int32_t aLine) {
GetParser()->InitializeDocWriteParserState(aState, aLine);
}
nsIURI* nsHtml5TreeOpExecutor::GetViewSourceBaseURI() {
if (!mViewSourceBaseURI) {
// We query the channel for the baseURI because in certain situations it
// cannot otherwise be determined. If this process fails, fall back to the
// standard method.
nsCOMPtr<nsIViewSourceChannel> vsc =
do_QueryInterface(mDocument->GetChannel());
if (vsc) {
nsresult rv = vsc->GetBaseURI(getter_AddRefs(mViewSourceBaseURI));
if (NS_SUCCEEDED(rv) && mViewSourceBaseURI) {
return mViewSourceBaseURI;
}
}
nsCOMPtr<nsIURI> orig = mDocument->GetOriginalURI();
if (orig->SchemeIs("view-source")) {
nsCOMPtr<nsINestedURI> nested = do_QueryInterface(orig);
NS_ASSERTION(nested, "URI with scheme view-source didn't QI to nested!");
nested->GetInnerURI(getter_AddRefs(mViewSourceBaseURI));
} else {
// Fail gracefully if the base URL isn't a view-source: URL.
// Not sure if this can ever happen.
mViewSourceBaseURI = orig;
}
}
return mViewSourceBaseURI;
}
bool nsHtml5TreeOpExecutor::IsExternalViewSource() {
if (!StaticPrefs::view_source_editor_external()) {
return false;
}
if (mDocumentURI) {
return mDocumentURI->SchemeIs("view-source");
}
return false;
}
// Speculative loading
nsIURI* nsHtml5TreeOpExecutor::BaseURIForPreload() {
// The URL of the document without <base>
nsIURI* documentURI = mDocument->GetDocumentURI();
// The URL of the document with non-speculative <base>
nsIURI* documentBaseURI = mDocument->GetDocBaseURI();
// If the two above are different, use documentBaseURI. If they are the same,
// the document object isn't aware of a <base>, so attempt to use the
// mSpeculationBaseURI or, failing, that, documentURI.
return (documentURI == documentBaseURI)
? (mSpeculationBaseURI ? mSpeculationBaseURI.get() : documentURI)
: documentBaseURI;
}
already_AddRefed<nsIURI>
nsHtml5TreeOpExecutor::ConvertIfNotPreloadedYetAndMediaApplies(
const nsAString& aURL, const nsAString& aMedia) {
nsCOMPtr<nsIURI> uri = ConvertIfNotPreloadedYet(aURL);
if (!uri) {
return nullptr;
}
if (!MediaApplies(aMedia)) {
return nullptr;
}
return uri.forget();
}
bool nsHtml5TreeOpExecutor::MediaApplies(const nsAString& aMedia) {
using dom::MediaList;
if (aMedia.IsEmpty()) {
return true;
}
RefPtr<MediaList> media = MediaList::Create(NS_ConvertUTF16toUTF8(aMedia));
return media->Matches(*mDocument);
}
already_AddRefed<nsIURI> nsHtml5TreeOpExecutor::ConvertIfNotPreloadedYet(
const nsAString& aURL) {
if (aURL.IsEmpty()) {
return nullptr;
}
nsIURI* base = BaseURIForPreload();
auto encoding = mDocument->GetDocumentCharacterSet();
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, encoding, base);
if (NS_FAILED(rv)) {
NS_WARNING("Failed to create a URI");
return nullptr;
}
if (ShouldPreloadURI(uri)) {
return uri.forget();
}
return nullptr;
}
bool nsHtml5TreeOpExecutor::ShouldPreloadURI(nsIURI* aURI) {
nsAutoCString spec;
nsresult rv = aURI->GetSpec(spec);
NS_ENSURE_SUCCESS(rv, false);
return mPreloadedURLs.EnsureInserted(spec);
}
dom::ReferrerPolicy nsHtml5TreeOpExecutor::GetPreloadReferrerPolicy(
const nsAString& aReferrerPolicy) {
dom::ReferrerPolicy referrerPolicy =
dom::ReferrerInfo::ReferrerPolicyAttributeFromString(aReferrerPolicy);
return GetPreloadReferrerPolicy(referrerPolicy);
}
dom::ReferrerPolicy nsHtml5TreeOpExecutor::GetPreloadReferrerPolicy(
ReferrerPolicy aReferrerPolicy) {
if (aReferrerPolicy != dom::ReferrerPolicy::_empty) {
return aReferrerPolicy;
}
return mDocument->GetPreloadReferrerInfo()->ReferrerPolicy();
}
void nsHtml5TreeOpExecutor::PreloadScript(
const nsAString& aURL, const nsAString& aCharset, const nsAString& aType,
const nsAString& aCrossOrigin, const nsAString& aMedia,
const nsAString& aNonce, const nsAString& aFetchPriority,
const nsAString& aIntegrity, dom::ReferrerPolicy aReferrerPolicy,
bool aScriptFromHead, bool aAsync, bool aDefer, bool aLinkPreload) {
nsCOMPtr<nsIURI> uri = ConvertIfNotPreloadedYetAndMediaApplies(aURL, aMedia);
if (!uri) {
return;
}
auto key = PreloadHashKey::CreateAsScript(uri, aCrossOrigin, aType);
if (mDocument->Preloads().PreloadExists(key)) {
return;
}
mDocument->ScriptLoader()->PreloadURI(
uri, aCharset, aType, aCrossOrigin, aNonce, aFetchPriority, aIntegrity,
aScriptFromHead, aAsync, aDefer, aLinkPreload,
GetPreloadReferrerPolicy(aReferrerPolicy), 0);
}
void nsHtml5TreeOpExecutor::PreloadStyle(
const nsAString& aURL, const nsAString& aCharset,
const nsAString& aCrossOrigin, const nsAString& aMedia,
const nsAString& aReferrerPolicy, const nsAString& aNonce,
const nsAString& aIntegrity, bool aLinkPreload,
const nsAString& aFetchPriority) {
nsCOMPtr<nsIURI> uri = ConvertIfNotPreloadedYetAndMediaApplies(aURL, aMedia);
if (!uri) {
return;
}
if (aLinkPreload) {
auto hashKey = PreloadHashKey::CreateAsStyle(
uri, mDocument->NodePrincipal(),
dom::Element::StringToCORSMode(aCrossOrigin),
css::eAuthorSheetFeatures);
if (mDocument->Preloads().PreloadExists(hashKey)) {
return;
}
}
mDocument->PreloadStyle(
uri, Encoding::ForLabel(aCharset), aCrossOrigin,
GetPreloadReferrerPolicy(aReferrerPolicy), aNonce, aIntegrity,
aLinkPreload ? css::StylePreloadKind::FromLinkRelPreloadElement
: css::StylePreloadKind::FromParser,
0, aFetchPriority);
}
void nsHtml5TreeOpExecutor::PreloadImage(
const nsAString& aURL, const nsAString& aCrossOrigin,
const nsAString& aMedia, const nsAString& aSrcset, const nsAString& aSizes,
const nsAString& aImageReferrerPolicy, bool aLinkPreload,
const nsAString& aFetchPriority) {
nsCOMPtr<nsIURI> baseURI = BaseURIForPreload();
bool isImgSet = false;
nsCOMPtr<nsIURI> uri =
mDocument->ResolvePreloadImage(baseURI, aURL, aSrcset, aSizes, &isImgSet);
if (uri && ShouldPreloadURI(uri) && MediaApplies(aMedia)) {
// use document wide referrer policy
mDocument->MaybePreLoadImage(uri, aCrossOrigin,
GetPreloadReferrerPolicy(aImageReferrerPolicy),
isImgSet, aLinkPreload, aFetchPriority);
}
}
// These calls inform the document of picture state and seen sources, such that
// it can use them to inform ResolvePreLoadImage as necessary
void nsHtml5TreeOpExecutor::PreloadPictureSource(const nsAString& aSrcset,
const nsAString& aSizes,
const nsAString& aType,
const nsAString& aMedia) {
mDocument->PreloadPictureImageSource(aSrcset, aSizes, aType, aMedia);
}
void nsHtml5TreeOpExecutor::PreloadFont(const nsAString& aURL,
const nsAString& aCrossOrigin,
const nsAString& aMedia,
const nsAString& aReferrerPolicy,
const nsAString& aFetchPriority) {
nsCOMPtr<nsIURI> uri = ConvertIfNotPreloadedYetAndMediaApplies(aURL, aMedia);
if (!uri) {
return;
}
mDocument->Preloads().PreloadFont(uri, aCrossOrigin, aReferrerPolicy, 0,
aFetchPriority);
}
void nsHtml5TreeOpExecutor::PreloadFetch(const nsAString& aURL,
const nsAString& aCrossOrigin,
const nsAString& aMedia,
const nsAString& aReferrerPolicy,
const nsAString& aFetchPriority) {
nsCOMPtr<nsIURI> uri = ConvertIfNotPreloadedYetAndMediaApplies(aURL, aMedia);
if (!uri) {
return;
}
mDocument->Preloads().PreloadFetch(uri, aCrossOrigin, aReferrerPolicy, 0,
aFetchPriority);
}
void nsHtml5TreeOpExecutor::PreloadOpenPicture() {
mDocument->PreloadPictureOpened();
}
void nsHtml5TreeOpExecutor::PreloadEndPicture() {
mDocument->PreloadPictureClosed();
}
void nsHtml5TreeOpExecutor::AddBase(const nsAString& aURL) {
auto encoding = mDocument->GetDocumentCharacterSet();
nsresult rv = NS_NewURI(getter_AddRefs(mViewSourceBaseURI), aURL, encoding,
GetViewSourceBaseURI());
if (NS_FAILED(rv)) {
mViewSourceBaseURI = nullptr;
}
}
void nsHtml5TreeOpExecutor::SetSpeculationBase(const nsAString& aURL) {
if (mSpeculationBaseURI) {
// the first one wins
return;
}
auto encoding = mDocument->GetDocumentCharacterSet();
nsCOMPtr<nsIURI> newBaseURI;
DebugOnly<nsresult> rv = NS_NewURI(getter_AddRefs(newBaseURI), aURL, encoding,
mDocument->GetDocumentURI());
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to create a URI");
if (!newBaseURI) {
return;
}
// Check the document's CSP usually delivered via the CSP header.
if (nsCOMPtr<nsIContentSecurityPolicy> csp = mDocument->GetCsp()) {
// base-uri should not fallback to the default-src and preloads should not
// trigger violation reports.
bool cspPermitsBaseURI = true;
nsresult rv = csp->Permits(
nullptr, nullptr, newBaseURI,
nsIContentSecurityPolicy::BASE_URI_DIRECTIVE, true /* aSpecific */,
false /* aSendViolationReports */, &cspPermitsBaseURI);
if (NS_FAILED(rv) || !cspPermitsBaseURI) {
return;
}
}
// Also check the CSP discovered from the <meta> tag during speculative
// parsing.
if (nsCOMPtr<nsIContentSecurityPolicy> csp = mDocument->GetPreloadCsp()) {
bool cspPermitsBaseURI = true;
nsresult rv = csp->Permits(
nullptr, nullptr, newBaseURI,
nsIContentSecurityPolicy::BASE_URI_DIRECTIVE, true /* aSpecific */,
false /* aSendViolationReports */, &cspPermitsBaseURI);
if (NS_FAILED(rv) || !cspPermitsBaseURI) {
return;
}
}
mSpeculationBaseURI = newBaseURI;
mDocument->Preloads().SetSpeculationBase(mSpeculationBaseURI);
}
void nsHtml5TreeOpExecutor::UpdateReferrerInfoFromMeta(
const nsAString& aMetaReferrer) {
mDocument->UpdateReferrerInfoFromMeta(aMetaReferrer, true);
}
void nsHtml5TreeOpExecutor::AddSpeculationCSP(const nsAString& aCSP) {
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
nsresult rv = NS_OK;
nsCOMPtr<nsIContentSecurityPolicy> preloadCsp = mDocument->GetPreloadCsp();
if (!preloadCsp) {
RefPtr<nsCSPContext> csp = new nsCSPContext();
csp->SuppressParserLogMessages();
preloadCsp = csp;
rv = preloadCsp->SetRequestContextWithDocument(mDocument);
NS_ENSURE_SUCCESS_VOID(rv);
}
// Please note that multiple meta CSPs need to be joined together.
rv = preloadCsp->AppendPolicy(
aCSP,
false, // csp via meta tag can not be report only
true); // delivered through the meta tag
NS_ENSURE_SUCCESS_VOID(rv);
nsPIDOMWindowInner* inner = mDocument->GetInnerWindow();
if (inner) {
inner->SetPreloadCsp(preloadCsp);
}
mDocument->ApplySettingsFromCSP(true);
}
#ifdef DEBUG
uint32_t nsHtml5TreeOpExecutor::sAppendBatchMaxSize = 0;
uint32_t nsHtml5TreeOpExecutor::sAppendBatchSlotsExamined = 0;
uint32_t nsHtml5TreeOpExecutor::sAppendBatchExaminations = 0;
uint32_t nsHtml5TreeOpExecutor::sLongestTimeOffTheEventLoop = 0;
uint32_t nsHtml5TreeOpExecutor::sTimesFlushLoopInterrupted = 0;
#endif