/* 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 "ErrorList.h" #include "nsError.h" #include "nsHtml5AttributeName.h" #include "nsHtml5HtmlAttributes.h" #include "nsHtml5String.h" #include "nsNetUtil.h" #include "mozilla/dom/FetchPriority.h" #include "mozilla/dom/ShadowRoot.h" #include "mozilla/dom/ShadowRootBinding.h" #include "mozilla/CheckedInt.h" #include "mozilla/Likely.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/StaticPrefs_network.h" #include "mozilla/UniquePtr.h" #include "mozilla/UniquePtrExtensions.h" #include "mozilla/dom/Sanitizer.h" #include "nsTHashMap.h" // The spec's "parser sanitizer configuration", "remove javascript navigation // URLs" and "insertion target redirection map", as of whatwg/html#12756. // // The spec sanitizes an element in "insert an element at the adjusted // insertion location"; we sanitize it in createElement() instead, so that an // element can never reach the tree unsanitized, and remember here what the // insertion hooks have to do with it. The attributes are sanitized on the // start tag token, before they are applied to the new element, so that the // element creation process never sees a disallowed "is". // // Main thread only: sanitizing while parsing only happens on the op-less // builder, which the main thread drives from beginning to end. class nsHtml5TreeBuilder::SanitizerState { public: // A resolved "adjusted insertion location": append to mParent, or insert // before mBefore when it is non-null. struct Location { nsIContent* mParent = nullptr; nsIContent* mBefore = nullptr; }; // The "appropriate place for inserting a node" when foster parenting: // before the table in the table's parent, or the end of the stack parent // when the table has no parent to insert into. static Location FosterLocation(nsIContent* aStackParent, nsIContent* aTable) { if (nsIContent* foster = nsHtml5TreeOperation::GetFosterParentForInsertBefore(aTable)) { return {foster, aTable}; } return {aStackParent, nullptr}; } SanitizerState(const mozilla::dom::Sanitizer* aSanitizer, bool aSafe) : mSanitizer(aSanitizer), mSafe(aSafe) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aSanitizer); } bool CommentsAllowed() const { return mSanitizer->CommentsAllowed(); } // Runs the configuration against a start tag token, once per token: // removes the disallowed attributes from aAttributes in place and remembers // what the configuration does with the element the token creates, for the // RecordNewElement() call that follows once the element exists. The tree // builder sanitizes the token itself when it has to consult the sanitized // attributes before the element exists, so this runs before createElement() // on those paths and from createElement() on all the others. void EnsureTokenSanitized(int32_t aNamespace, nsAtom* aLocalName, nsHtml5HtmlAttributes* aAttributes) { MOZ_ASSERT(NS_IsMainThread()); if (mTokenSanitized) { MOZ_ASSERT(mTokenNamespace == aNamespace && mTokenLocalName == aLocalName, "Sanitized a token other than the one being created."); return; } mTokenSanitized = true; #ifdef DEBUG mTokenNamespace = aNamespace; mTokenLocalName = aLocalName; #endif mozilla::dom::SanitizerElementMatch match = mSanitizer->MatchElement(aLocalName, aNamespace, mSafe); mTokenAction = match.Action(); if (mTokenAction == mozilla::dom::SanitizerElementAction::ReplaceWithChildren && aNamespace == kNameSpaceID_XHTML && aLocalName == nsGkAtoms::_template) { mTokenAction = mozilla::dom::SanitizerElementAction::Remove; } if (mTokenAction != mozilla::dom::SanitizerElementAction::Keep) { return; } for (int32_t i = aAttributes->getLength(); i > 0; --i) { nsHtml5AttributeEntry& entry = aAttributes->entryAt(i - 1); nsHtml5NameTriple name = AttributeName(entry, aNamespace); if (mSanitizer->ShouldRemoveAttribute(match, name.mLocal, name.mNamespace, [&entry](nsAString& aValue) { entry.Value().ToString(aValue); })) { aAttributes->removeAttributeAt(i - 1); } } } // What the configuration does with the element the sanitized token creates. mozilla::dom::SanitizerElementAction TokenAction() const { MOZ_ASSERT(mTokenSanitized); return mTokenAction; } // Remembers the element the last sanitized token created, if the // configuration does not keep it. Such an element is never inserted, not // even when it is replaced with its children, so it gives up the form // association that element creation gave it, like an element the tree // builder itself fails to insert. void RecordNewElement(nsIContent* aElement) { using mozilla::dom::SanitizerElementAction; MOZ_ASSERT(mTokenSanitized); mTokenSanitized = false; if (mTokenAction != SanitizerElementAction::Keep) { nsHtml5TreeOperation::AbortNodeInsertion(aElement); mDropped.InsertOrUpdate( aElement, Dropped{mTokenAction == SanitizerElementAction::ReplaceWithChildren}); } } // Sanitizes the attributes a second start tag merged onto the html or body // element. The action is discarded: whatwg/html#12756 asserts it is "Keep" // instead, which does not hold for a configuration that replaces body with // its children. void SanitizeMergedAttributes(nsIContent* aElement) { mSanitizer->SanitizeElement(aElement->AsElement(), mSafe); } bool IsDropped(nsIContent* aElement) const { return mDropped.Contains(aElement); } bool IsReplacedWithChildren(nsIContent* aElement) const { auto entry = mDropped.Lookup(aElement); return entry && entry.Data().mReplaceWithChildren; } // Redirects the content of an element the configuration replaces with its // children to where the content of aParent goes. The adoption agency // algorithm never inserts the clones it makes, so a clone that is replaced // with its children gets its redirection here instead of from // ResolveLocationForChild(). Returns whether the clone was replaced with // its children, which is when the algorithm leaves lastNode where it is. bool RedirectClone(nsIContent* aClone, nsIContent* aParent) { auto entry = mDropped.Lookup(aClone); if (!entry || !entry.Data().mReplaceWithChildren) { return false; } entry.Data().mLocationResolved = true; entry.Data().mLocation = LocationFor(aParent); return true; } // Moves the redirection of an element that has one already, which the // adoption agency algorithm does for a furthest block once it has moved the // block's content to (aParent, aBefore). void MoveRedirection(nsIContent* aElement, nsIContent* aParent, nsIContent* aBefore = nullptr) { auto entry = mDropped.Lookup(aElement); if (!entry || !entry.Data().mLocation.mParent) { return; } entry.Data().mLocation = LocationFor(aParent, aBefore); } // The location content whose tree construction parent is aParent goes to. // An element the sanitizer removed is not in the tree, but it still is the // parent its content is inserted into, like in the spec, where the content // ends up in the removed element's detached subtree. The adoption agency // algorithm can move nodes back out of such a subtree, so dropping the // content here instead would lose nodes the spec keeps. Location LocationFor(nsIContent* aParent, nsIContent* aBefore = nullptr) const { auto entry = mDropped.Lookup(aParent); // Only a "Replace with children" element that reached an insertion point // redirects its content; everything else takes its content with it. if (!entry || !entry.Data().mReplaceWithChildren || !entry.Data().mLocation.mParent) { return Location{aParent, aBefore}; } Location location = entry.Data().mLocation; // The tree can have moved since the location was recorded. Inserting // before a node that is no longer a child of mParent would corrupt // mParent's child list, so fall back to appending. if (location.mBefore && location.mBefore->GetParent() != location.mParent) { location.mBefore = nullptr; } if (aBefore && aBefore->GetParent() == location.mParent) { location.mBefore = aBefore; } return location; } // The location aChild is inserted at, or Nothing() when the sanitizer // dropped aChild itself, which the spec never inserts anywhere. Records // where the children of a "Replace with children" element go, which is only // known now that the element itself would be inserted. Only the first // attempt counts: the adoption agency algorithm re-inserts elements it has // already placed, which must not move the recorded location. mozilla::Maybe ResolveLocationForChild( nsIContent* aChild, nsIContent* aParent, nsIContent* aBefore = nullptr) { Location location = LocationFor(aParent, aBefore); auto entry = mDropped.Lookup(aChild); if (!entry) { return mozilla::Some(location); } if (!entry.Data().mLocationResolved) { entry.Data().mLocationResolved = true; // The spec checks whether the element can be inserted at all before it // sanitizes it, so an element that cannot be inserted never redirects // its children either. if (entry.Data().mReplaceWithChildren && nsHtml5TreeOperation::CanInsert(aChild, location.mParent)) { entry.Data().mLocation = location; } } return mozilla::Nothing(); } private: // The name an attribute of a token in aNamespace ends up with on the // element, which is what the configuration matches against. static nsHtml5NameTriple AttributeName(const nsHtml5AttributeEntry& aEntry, int32_t aNamespace) { if (aNamespace == kNameSpaceID_SVG) { return aEntry.NameSVG(); } if (aNamespace == kNameSpaceID_MathML) { return aEntry.NameMathML(); } return {kNameSpaceID_None, nullptr, aEntry.NameHTML()}; } struct Dropped { bool mReplaceWithChildren = false; // Whether the element has already reached the insertion point that decides // mLocation, whether or not there was a location to record. bool mLocationResolved = false; // Where the children of a "Replace with children" element go. mParent is // null until the element would have been inserted. Location mLocation; }; // Raw pointers: every element the tree builder creates is kept alive for the // whole parse by nsHtml5DocumentBuilder::HoldElement(), so an entry can // neither dangle nor be matched by a later element at the same address. nsTHashMap, Dropped> mDropped; const mozilla::dom::Sanitizer* const mSanitizer; const bool mSafe; // What the configuration does with the element the last sanitized start tag // token creates. Set by EnsureTokenSanitized(), consumed by // RecordNewElement(), so that createElement() does not have to keep it live // across the element creation on the path that does not sanitize. mozilla::dom::SanitizerElementAction mTokenAction = mozilla::dom::SanitizerElementAction::Keep; // Whether mTokenAction belongs to the token being processed, i.e. whether // the token was already sanitized before its element was created. bool mTokenSanitized = false; #ifdef DEBUG int32_t mTokenNamespace = kNameSpaceID_None; nsAtom* mTokenLocalName = nullptr; #endif }; void nsHtml5TreeBuilder::SetSanitizer(mozilla::dom::Sanitizer* aSanitizer, bool aSafe) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(!aSanitizer || mBuilder, "Sanitizing while parsing needs the op-less builder."); if (aSanitizer) { aSanitizer->RecordSanitizeUse(); mSanitizerState = mozilla::MakeUnique(aSanitizer, aSafe); } else { mSanitizerState = nullptr; } } bool nsHtml5TreeBuilder::SanitizerRedirectsCloneImpl( nsIContent* aClone, nsIContent* aCommonAncestor) { return mSanitizerState->RedirectClone(aClone, aCommonAncestor); } void nsHtml5TreeBuilder::SanitizerRedirectFurthestBlockImpl( nsIContent* aFurthestBlock, nsIContent* aParent) { mSanitizerState->MoveRedirection(aFurthestBlock, aParent); } void nsHtml5TreeBuilder::SanitizerRedirectFurthestBlockToFosterParentImpl( nsIContent* aFurthestBlock, nsIContent* aTable, nsIContent* aStackParent) { SanitizerState::Location foster = SanitizerState::FosterLocation(aStackParent, aTable); mSanitizerState->MoveRedirection(aFurthestBlock, foster.mParent, foster.mBefore); } bool nsHtml5TreeBuilder::SanitizerDropsTemplateTokenImpl( nsHtml5HtmlAttributes* aAttributes) { mSanitizerState->EnsureTokenSanitized(kNameSpaceID_XHTML, nsGkAtoms::_template, aAttributes); return mSanitizerState->TokenAction() != mozilla::dom::SanitizerElementAction::Keep; } nsHtml5TreeBuilder::nsHtml5TreeBuilder(nsHtml5OplessBuilder* aBuilder) : mode(0), originalMode(0), framesetOk(false), tokenizer(nullptr), scriptingEnabled(false), needToDropLF(false), fragment(false), contextName(nullptr), contextNamespace(kNameSpaceID_None), contextNode(nullptr), templateModePtr(0), stackNodesIdx(0), numStackNodes(0), currentPtr(0), listPtr(0), formPointer(nullptr), headPointer(nullptr), charBufferLen(0), quirks(false), forceNoQuirks(false), allowDeclarativeShadowRoots(false), keepBuffer(false), mBuilder(aBuilder), mViewSource(nullptr), mOpSink(nullptr), mHandles(nullptr), mHandlesUsed(0), mSpeculativeLoadStage(nullptr), mBroken(NS_OK), mCurrentHtmlScriptCannotDocumentWriteOrBlock(false), mPreventScriptExecution(false), mGenerateSpeculativeLoads(false), mHasSeenImportMap(false) #ifdef DEBUG , mActive(false) #endif { MOZ_COUNT_CTOR(nsHtml5TreeBuilder); } nsHtml5TreeBuilder::nsHtml5TreeBuilder(nsAHtml5TreeOpSink* aOpSink, nsHtml5TreeOpStage* aStage, bool aGenerateSpeculativeLoads) : mode(0), originalMode(0), framesetOk(false), tokenizer(nullptr), scriptingEnabled(false), needToDropLF(false), fragment(false), contextName(nullptr), contextNamespace(kNameSpaceID_None), contextNode(nullptr), templateModePtr(0), stackNodesIdx(0), numStackNodes(0), currentPtr(0), listPtr(0), formPointer(nullptr), headPointer(nullptr), charBufferLen(0), quirks(false), forceNoQuirks(false), allowDeclarativeShadowRoots(false), keepBuffer(false), mBuilder(nullptr), mViewSource(nullptr), mOpSink(aOpSink), mHandles(new nsIContent*[NS_HTML5_TREE_BUILDER_HANDLE_ARRAY_LENGTH]), mHandlesUsed(0), mSpeculativeLoadStage(aStage), mBroken(NS_OK), mCurrentHtmlScriptCannotDocumentWriteOrBlock(false), mPreventScriptExecution(false), mGenerateSpeculativeLoads(aGenerateSpeculativeLoads), mHasSeenImportMap(false) #ifdef DEBUG , mActive(false) #endif { MOZ_ASSERT(!(!aStage && aGenerateSpeculativeLoads), "Must not generate speculative loads without a stage"); MOZ_COUNT_CTOR(nsHtml5TreeBuilder); } nsHtml5TreeBuilder::~nsHtml5TreeBuilder() { MOZ_COUNT_DTOR(nsHtml5TreeBuilder); NS_ASSERTION(!mActive, "nsHtml5TreeBuilder deleted without ever calling end() on it!"); mOpQueue.Clear(); } static void getTypeString(nsHtml5String& aType, nsAString& aTypeString) { aType.ToString(aTypeString); // Since `typeString` after trimming and lowercasing is only checked // for "module" and " importmap", we don't need to remember // pre-trimming emptiness here. // ASCII whitespace https://infra.spec.whatwg.org/#ascii-whitespace: // U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE. static const char kASCIIWhitespace[] = "\t\n\f\r "; aTypeString.Trim(kASCIIWhitespace); } nsIContentHandle* nsHtml5TreeBuilder::createElement( int32_t aNamespace, nsAtom* aName, nsHtml5HtmlAttributes* aAttributes, nsIContentHandle* aIntendedParent, nsHtml5ContentCreatorFunction aCreator) { MOZ_ASSERT(aAttributes, "Got null attributes."); MOZ_ASSERT(aName, "Got null name."); MOZ_ASSERT(aNamespace == kNameSpaceID_XHTML || aNamespace == kNameSpaceID_SVG || aNamespace == kNameSpaceID_MathML, "Bogus namespace."); if (mBuilder) { nsIContent* intendedParent = aIntendedParent ? static_cast(aIntendedParent) : nullptr; // intendedParent == nullptr is a special case where the // intended parent is the document. FIXME(emilio): Seems using // mBuilder->GetNodeInfoManager() should always be fine? nsNodeInfoManager* nodeInfoManager = intendedParent ? intendedParent->NodeInfoManager() : mBuilder->GetNodeInfoManager(); if (MOZ_UNLIKELY(mSanitizerState)) { mSanitizerState->EnsureTokenSanitized(aNamespace, aName, aAttributes); } nsIContent* elem; if (aNamespace == kNameSpaceID_XHTML) { elem = nsHtml5TreeOperation::CreateHTMLElement( aName, aAttributes, mozilla::dom::FROM_PARSER_FRAGMENT, nodeInfoManager, mBuilder, aCreator.html, intendedParent, mCustomElementRegistry); } else if (aNamespace == kNameSpaceID_SVG) { elem = nsHtml5TreeOperation::CreateSVGElement( aName, aAttributes, mozilla::dom::FROM_PARSER_FRAGMENT, nodeInfoManager, mBuilder, aCreator.svg); } else { MOZ_ASSERT(aNamespace == kNameSpaceID_MathML); elem = nsHtml5TreeOperation::CreateMathMLElement( aName, aAttributes, nodeInfoManager, mBuilder); } if (MOZ_UNLIKELY(aAttributes != tokenizer->GetAttributes() && aAttributes != nsHtml5HtmlAttributes::EMPTY_ATTRIBUTES)) { delete aAttributes; } if (MOZ_UNLIKELY(mSanitizerState)) { mSanitizerState->RecordNewElement(elem); } return elem; } nsIContentHandle* content = AllocateContentHandle(); nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement(mozilla::fallible); if (MOZ_UNLIKELY(!treeOp)) { MarkAsBrokenAndRequestSuspensionWithoutBuilder(NS_ERROR_OUT_OF_MEMORY); return nullptr; } if (aNamespace == kNameSpaceID_XHTML) { opCreateHTMLElement opeation( content, aName, aAttributes, aCreator.html, aIntendedParent, (!!mSpeculativeLoadStage) ? mozilla::dom::FROM_PARSER_NETWORK : mozilla::dom::FROM_PARSER_DOCUMENT_WRITE); treeOp->Init(mozilla::AsVariant(opeation)); } else if (aNamespace == kNameSpaceID_SVG) { opCreateSVGElement operation( content, aName, aAttributes, aCreator.svg, aIntendedParent, (!!mSpeculativeLoadStage) ? mozilla::dom::FROM_PARSER_NETWORK : mozilla::dom::FROM_PARSER_DOCUMENT_WRITE); treeOp->Init(mozilla::AsVariant(operation)); } else { // kNameSpaceID_MathML opCreateMathMLElement operation(content, aName, aAttributes, aIntendedParent); treeOp->Init(mozilla::AsVariant(operation)); } // mSpeculativeLoadStage is non-null only in the off-the-main-thread // tree builder, which handles the network stream // Start wall of code for speculative loading and line numbers if (mGenerateSpeculativeLoads && mode != IN_TEMPLATE) { switch (aNamespace) { case kNameSpaceID_XHTML: if (nsGkAtoms::img == aName) { nsHtml5String loading = aAttributes->getValue(nsHtml5AttributeName::ATTR_LOADING); if (!loading.LowerCaseEqualsASCII("lazy")) { nsHtml5String url = aAttributes->getValue(nsHtml5AttributeName::ATTR_SRC); nsHtml5String srcset = aAttributes->getValue(nsHtml5AttributeName::ATTR_SRCSET); nsHtml5String crossOrigin = aAttributes->getValue(nsHtml5AttributeName::ATTR_CROSSORIGIN); nsHtml5String referrerPolicy = aAttributes->getValue( nsHtml5AttributeName::ATTR_REFERRERPOLICY); nsHtml5String sizes = aAttributes->getValue(nsHtml5AttributeName::ATTR_SIZES); nsHtml5String fetchPriority = aAttributes->getValue(nsHtml5AttributeName::ATTR_FETCHPRIORITY); nsHtml5String type = aAttributes->getValue(nsHtml5AttributeName::ATTR_TYPE); mSpeculativeLoadQueue.AppendElement()->InitImage( url, crossOrigin, /* aMedia = */ nullptr, referrerPolicy, srcset, sizes, false, fetchPriority, type); } } else if (nsGkAtoms::source == aName) { nsHtml5String srcset = aAttributes->getValue(nsHtml5AttributeName::ATTR_SRCSET); // Sources without srcset cannot be selected. The source could also be // for a media element, but in that context doesn't use srcset. See // comments in nsHtml5SpeculativeLoad.h about preloading if (srcset) { nsHtml5String sizes = aAttributes->getValue(nsHtml5AttributeName::ATTR_SIZES); nsHtml5String type = aAttributes->getValue(nsHtml5AttributeName::ATTR_TYPE); nsHtml5String media = aAttributes->getValue(nsHtml5AttributeName::ATTR_MEDIA); mSpeculativeLoadQueue.AppendElement()->InitPictureSource( srcset, sizes, type, media); } } else if (nsGkAtoms::script == aName) { nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement(mozilla::fallible); if (MOZ_UNLIKELY(!treeOp)) { MarkAsBrokenAndRequestSuspensionWithoutBuilder( NS_ERROR_OUT_OF_MEMORY); return nullptr; } opSetScriptLineAndColumnNumberAndFreeze operation( content, tokenizer->getLineNumber(), // NOTE: tokenizer->getColumnNumber() points '>'. tokenizer->getColumnNumber() + 1); treeOp->Init(mozilla::AsVariant(operation)); nsHtml5String type = aAttributes->getValue(nsHtml5AttributeName::ATTR_TYPE); nsAutoString typeString; getTypeString(type, typeString); bool isModule = typeString.LowerCaseEqualsASCII("module"); bool importmap = typeString.LowerCaseEqualsASCII("importmap"); bool async = false; bool defer = false; bool nomodule = aAttributes->contains(nsHtml5AttributeName::ATTR_NOMODULE); // For microtask semantics, we need to queue either // `opRunScriptThatMayDocumentWriteOrBlock` or // `opRunScriptThatCannotDocumentWriteOrBlock` for every script // element--even ones that we already know won't run. // `mCurrentHtmlScriptCannotDocumentWriteOrBlock` controls which // kind of operation is used for an HTML script, and this is the // place where `mCurrentHtmlScriptCannotDocumentWriteOrBlock` // needs to be set correctly. // // Non-async, non-defer classic scripts that will run MUST use // `opRunScriptThatMayDocumentWriteOrBlock` in order to run // the more complex code that // 1. is able to resume the HTML parse after a parser-blocking // scripts no longer blocks the parser // 2. is able to receive more content to parse on the main thread // via document.write // 3. is able to throw away off-the-main-thread parsing results // if what's document.written on the main thread invalidates // the speculation. // // Async and defer classic scripts as well as module scripts and // importmaps MUST use `opRunScriptThatCannotDocumentWriteOrBlock`. // This is necessary particularly because the relevant main-thread // code assumes it doesn't need to deal with resuming the HTML // parse some time afterwards, so using a tree operation with // mismatching expectations regarding that responsibility may // cause the HTML parse to stall. // // Various scripts that won't actually run work with either type // of tree op in the sense that the HTML parse won't stall. // However, in the case where a script cannot block or insert // data to the HTML parser via document.write, unnecessary use // of `opRunScriptThatMayDocumentWriteOrBlock` instead of // `opRunScriptThatCannotDocumentWriteOrBlock` causes the HTML // parser to enter the speculative mode when doing so isn't // actually required. // // Ideally, we would check for `type`/`language` attribute // combinations that are known to cause non-execution as well as // ScriptLoader::IsScriptEventHandler equivalent. That way, we // wouldn't unnecessarily speculate after scripts that won't // execute. https://bugzilla.mozilla.org/show_bug.cgi?id=1848311 if (importmap) { // If we see an importmap, we don't want to later start speculative // loads for modulepreloads, since such load might finish before // the importmap is created. This also applies to module scripts so // that any modulepreload integrity checks can be performed before // the modules scripts are loaded. // This state is not part of speculation rollback: If an importmap // is seen speculatively and the speculation is rolled back, the // importmap is still considered seen. // TODO: Sync importmap seenness between the main thread and the // parser thread. // https://bugzilla.mozilla.org/show_bug.cgi?id=1848312 mHasSeenImportMap = true; } nsHtml5String url = aAttributes->getValue(nsHtml5AttributeName::ATTR_SRC); if (url) { async = aAttributes->contains(nsHtml5AttributeName::ATTR_ASYNC); defer = aAttributes->contains(nsHtml5AttributeName::ATTR_DEFER); if ((isModule && !mHasSeenImportMap) || (!isModule && !importmap && !nomodule)) { nsHtml5String charset = aAttributes->getValue(nsHtml5AttributeName::ATTR_CHARSET); nsHtml5String crossOrigin = aAttributes->getValue(nsHtml5AttributeName::ATTR_CROSSORIGIN); nsHtml5String nonce = aAttributes->getValue(nsHtml5AttributeName::ATTR_NONCE); nsHtml5String fetchPriority = aAttributes->getValue( nsHtml5AttributeName::ATTR_FETCHPRIORITY); nsHtml5String integrity = aAttributes->getValue(nsHtml5AttributeName::ATTR_INTEGRITY); nsHtml5String referrerPolicy = aAttributes->getValue( nsHtml5AttributeName::ATTR_REFERRERPOLICY); mSpeculativeLoadQueue.AppendElement()->InitScript( url, charset, type, crossOrigin, /* aMedia = */ nullptr, nonce, fetchPriority, integrity, referrerPolicy, mode == nsHtml5TreeBuilder::IN_HEAD, async, defer, false); } } // `mCurrentHtmlScriptCannotDocumentWriteOrBlock` MUST be computed to // match the ScriptLoader-perceived kind of the script regardless of // enqueuing a speculative load. Scripts with the `nomodule` attribute // never block or document.write: Either the attribute prevents a // classic script execution or is ignored on a module script or // importmap. mCurrentHtmlScriptCannotDocumentWriteOrBlock = isModule || importmap || async || defer || nomodule; } else if (nsGkAtoms::link == aName) { nsHtml5String rel = aAttributes->getValue(nsHtml5AttributeName::ATTR_REL); // Not splitting on space here is bogus but the old parser didn't even // do a case-insensitive check. if (rel) { if (rel.LowerCaseEqualsASCII("stylesheet")) { nsHtml5String url = aAttributes->getValue(nsHtml5AttributeName::ATTR_HREF); if (url && !aAttributes->getValue(nsHtml5AttributeName::ATTR_DISABLED)) { nsHtml5String charset = aAttributes->getValue(nsHtml5AttributeName::ATTR_CHARSET); nsHtml5String crossOrigin = aAttributes->getValue( nsHtml5AttributeName::ATTR_CROSSORIGIN); nsHtml5String nonce = aAttributes->getValue(nsHtml5AttributeName::ATTR_NONCE); nsHtml5String integrity = aAttributes->getValue(nsHtml5AttributeName::ATTR_INTEGRITY); nsHtml5String referrerPolicy = aAttributes->getValue( nsHtml5AttributeName::ATTR_REFERRERPOLICY); nsHtml5String media = aAttributes->getValue(nsHtml5AttributeName::ATTR_MEDIA); nsHtml5String fetchPriority = aAttributes->getValue( nsHtml5AttributeName::ATTR_FETCHPRIORITY); mSpeculativeLoadQueue.AppendElement()->InitStyle( url, charset, crossOrigin, media, referrerPolicy, nonce, integrity, false, fetchPriority); } } else if (rel.LowerCaseEqualsASCII("preconnect")) { nsHtml5String url = aAttributes->getValue(nsHtml5AttributeName::ATTR_HREF); if (url) { nsHtml5String crossOrigin = aAttributes->getValue( nsHtml5AttributeName::ATTR_CROSSORIGIN); mSpeculativeLoadQueue.AppendElement()->InitPreconnect( url, crossOrigin); } } else if (rel.LowerCaseEqualsASCII("preload")) { nsHtml5String url = aAttributes->getValue(nsHtml5AttributeName::ATTR_HREF); nsHtml5String as = aAttributes->getValue(nsHtml5AttributeName::ATTR_AS); bool isImage = as.LowerCaseEqualsASCII("image"); nsHtml5String srcset; if (isImage) { srcset = aAttributes->getValue( nsHtml5AttributeName::ATTR_IMAGESRCSET); } if (url || (isImage && srcset)) { nsHtml5String charset = aAttributes->getValue(nsHtml5AttributeName::ATTR_CHARSET); nsHtml5String crossOrigin = aAttributes->getValue( nsHtml5AttributeName::ATTR_CROSSORIGIN); nsHtml5String nonce = aAttributes->getValue(nsHtml5AttributeName::ATTR_NONCE); nsHtml5String integrity = aAttributes->getValue(nsHtml5AttributeName::ATTR_INTEGRITY); nsHtml5String referrerPolicy = aAttributes->getValue( nsHtml5AttributeName::ATTR_REFERRERPOLICY); nsHtml5String media = aAttributes->getValue(nsHtml5AttributeName::ATTR_MEDIA); nsHtml5String fetchPriority = aAttributes->getValue( nsHtml5AttributeName::ATTR_FETCHPRIORITY); // Note that respective speculative loaders for scripts and // styles check all additional attributes to be equal to use the // speculative load. So, if any of them is specified and the // preload has to take the expected effect, those attributes // must also be specified on the actual tag to use the preload. // Omitting an attribute on both will make the values equal // (empty) and thus use the preload. if (as.LowerCaseEqualsASCII("script")) { nsHtml5String type = aAttributes->getValue(nsHtml5AttributeName::ATTR_TYPE); mSpeculativeLoadQueue.AppendElement()->InitScript( url, charset, type, crossOrigin, media, nonce, /* aFetchPriority */ fetchPriority, integrity, referrerPolicy, mode == nsHtml5TreeBuilder::IN_HEAD, false, false, true); } else if (as.LowerCaseEqualsASCII("style")) { mSpeculativeLoadQueue.AppendElement()->InitStyle( url, charset, crossOrigin, media, referrerPolicy, nonce, integrity, true, fetchPriority); } else if (as.LowerCaseEqualsASCII("image")) { nsHtml5String sizes = aAttributes->getValue( nsHtml5AttributeName::ATTR_IMAGESIZES); nsHtml5String type = aAttributes->getValue(nsHtml5AttributeName::ATTR_TYPE); mSpeculativeLoadQueue.AppendElement()->InitImage( url ? url : nsHtml5String::EmptyString(), crossOrigin, media, referrerPolicy, srcset, sizes, true, fetchPriority, type); } else if (as.LowerCaseEqualsASCII("font")) { mSpeculativeLoadQueue.AppendElement()->InitFont( url, crossOrigin, media, referrerPolicy, fetchPriority); } else if (as.LowerCaseEqualsASCII("fetch")) { mSpeculativeLoadQueue.AppendElement()->InitFetch( url, crossOrigin, media, referrerPolicy, fetchPriority); } // Other "as" values will be supported later. } } else if (mozilla::StaticPrefs::network_modulepreload() && rel.LowerCaseEqualsASCII("modulepreload") && !mHasSeenImportMap) { nsHtml5String url = aAttributes->getValue(nsHtml5AttributeName::ATTR_HREF); if (url && url.Length() != 0) { nsHtml5String as = aAttributes->getValue(nsHtml5AttributeName::ATTR_AS); nsAutoString asString; as.ToString(asString); if (mozilla::net::IsScriptLikeOrInvalid(asString)) { nsHtml5String charset = aAttributes->getValue(nsHtml5AttributeName::ATTR_CHARSET); RefPtr moduleType = nsGkAtoms::_module; nsHtml5String type = nsHtml5String::FromAtom(moduleType.forget()); nsHtml5String crossOrigin = aAttributes->getValue( nsHtml5AttributeName::ATTR_CROSSORIGIN); nsHtml5String media = aAttributes->getValue(nsHtml5AttributeName::ATTR_MEDIA); nsHtml5String nonce = aAttributes->getValue(nsHtml5AttributeName::ATTR_NONCE); nsHtml5String integrity = aAttributes->getValue( nsHtml5AttributeName::ATTR_INTEGRITY); nsHtml5String referrerPolicy = aAttributes->getValue( nsHtml5AttributeName::ATTR_REFERRERPOLICY); nsHtml5String fetchPriority = aAttributes->getValue( nsHtml5AttributeName::ATTR_FETCHPRIORITY); mSpeculativeLoadQueue.AppendElement()->InitScript( url, charset, type, crossOrigin, media, nonce, /* aFetchPriority */ fetchPriority, integrity, referrerPolicy, mode == nsHtml5TreeBuilder::IN_HEAD, false, false, true); } } } } } else if (nsGkAtoms::video == aName) { nsHtml5String url = aAttributes->getValue(nsHtml5AttributeName::ATTR_POSTER); if (url) { // Fetch priority is not supported for video. Nullptr will map to // the auto state // (https://html.spec.whatwg.org/#fetch-priority-attribute). auto fetchPriority = nullptr; mSpeculativeLoadQueue.AppendElement()->InitImage( url, nullptr, nullptr, nullptr, nullptr, nullptr, false, fetchPriority, nullptr); } } else if (nsGkAtoms::style == aName) { mImportScanner.Start(); nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement(mozilla::fallible); if (MOZ_UNLIKELY(!treeOp)) { MarkAsBrokenAndRequestSuspensionWithoutBuilder( NS_ERROR_OUT_OF_MEMORY); return nullptr; } opSetStyleLineNumber operation(content, tokenizer->getLineNumber()); treeOp->Init(mozilla::AsVariant(operation)); } else if (nsGkAtoms::html == aName) { nsHtml5String url = aAttributes->getValue(nsHtml5AttributeName::ATTR_MANIFEST); mSpeculativeLoadQueue.AppendElement()->InitManifest(url); } else if (nsGkAtoms::base == aName) { nsHtml5String url = aAttributes->getValue(nsHtml5AttributeName::ATTR_HREF); if (url) { mSpeculativeLoadQueue.AppendElement()->InitBase(url); } } else if (nsGkAtoms::meta == aName) { if (nsHtml5Portability::lowerCaseLiteralEqualsIgnoreAsciiCaseString( "content-security-policy", aAttributes->getValue( nsHtml5AttributeName::ATTR_HTTP_EQUIV))) { nsHtml5String csp = aAttributes->getValue(nsHtml5AttributeName::ATTR_CONTENT); if (csp) { mSpeculativeLoadQueue.AppendElement()->InitMetaCSP(csp); } } else if (nsHtml5Portability:: lowerCaseLiteralEqualsIgnoreAsciiCaseString( "referrer", aAttributes->getValue( nsHtml5AttributeName::ATTR_NAME))) { nsHtml5String referrerPolicy = aAttributes->getValue(nsHtml5AttributeName::ATTR_CONTENT); if (referrerPolicy) { mSpeculativeLoadQueue.AppendElement()->InitMetaReferrerPolicy( referrerPolicy); } } } break; case kNameSpaceID_SVG: if (nsGkAtoms::image == aName || nsGkAtoms::feImage == aName) { nsHtml5String url = aAttributes->getValue(nsHtml5AttributeName::ATTR_HREF); if (!url) { url = aAttributes->getValue(nsHtml5AttributeName::ATTR_XLINK_HREF); } if (url) { nsHtml5String crossOrigin = aAttributes->getValue(nsHtml5AttributeName::ATTR_CROSSORIGIN); nsHtml5String fetchPriority = aAttributes->getValue(nsHtml5AttributeName::ATTR_FETCHPRIORITY); mSpeculativeLoadQueue.AppendElement()->InitImage( url, crossOrigin, /* aMedia = */ nullptr, nullptr, nullptr, nullptr, false, fetchPriority, nullptr); } } else if (nsGkAtoms::script == aName) { nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement(mozilla::fallible); if (MOZ_UNLIKELY(!treeOp)) { MarkAsBrokenAndRequestSuspensionWithoutBuilder( NS_ERROR_OUT_OF_MEMORY); return nullptr; } opSetScriptLineAndColumnNumberAndFreeze operation( content, tokenizer->getLineNumber(), // NOTE: tokenizer->getColumnNumber() points '>'. tokenizer->getColumnNumber() + 1); treeOp->Init(mozilla::AsVariant(operation)); nsHtml5String type = aAttributes->getValue(nsHtml5AttributeName::ATTR_TYPE); nsAutoString typeString; getTypeString(type, typeString); bool isModule = typeString.LowerCaseEqualsASCII("module"); bool importmap = typeString.LowerCaseEqualsASCII("importmap"); bool async = false; bool defer = false; if (importmap) { // If we see an importmap, we don't want to later start speculative // loads for modulepreloads, since such load might finish before // the importmap is created. This also applies to module scripts so // that any modulepreload integrity checks can be performed before // the modules scripts are loaded. // This state is not part of speculation rollback: If an importmap // is seen speculatively and the speculation is rolled back, the // importmap is still considered seen. // TODO: Sync importmap seenness between the main thread and the // parser thread. // https://bugzilla.mozilla.org/show_bug.cgi?id=1848312 mHasSeenImportMap = true; } nsHtml5String url = aAttributes->getValue(nsHtml5AttributeName::ATTR_HREF); if (!url) { url = aAttributes->getValue(nsHtml5AttributeName::ATTR_XLINK_HREF); } if (url) { async = aAttributes->contains(nsHtml5AttributeName::ATTR_ASYNC); defer = aAttributes->contains(nsHtml5AttributeName::ATTR_DEFER); if ((isModule && !mHasSeenImportMap) || (!isModule && !importmap)) { nsHtml5String type = aAttributes->getValue(nsHtml5AttributeName::ATTR_TYPE); nsHtml5String crossOrigin = aAttributes->getValue(nsHtml5AttributeName::ATTR_CROSSORIGIN); nsHtml5String nonce = aAttributes->getValue(nsHtml5AttributeName::ATTR_NONCE); nsHtml5String integrity = aAttributes->getValue(nsHtml5AttributeName::ATTR_INTEGRITY); nsHtml5String referrerPolicy = aAttributes->getValue( nsHtml5AttributeName::ATTR_REFERRERPOLICY); nsHtml5String fetchPriority = aAttributes->getValue( nsHtml5AttributeName::ATTR_FETCHPRIORITY); mSpeculativeLoadQueue.AppendElement()->InitScript( url, nullptr, type, crossOrigin, /* aMedia = */ nullptr, nonce, fetchPriority, integrity, referrerPolicy, mode == nsHtml5TreeBuilder::IN_HEAD, async, defer, false); } } // `mCurrentHtmlScriptCannotDocumentWriteOrBlock` MUST be computed to // match the ScriptLoader-perceived kind of the script regardless of // enqueuing a speculative load. Either the attribute prevents a // classic script execution or is ignored on a module script. mCurrentHtmlScriptCannotDocumentWriteOrBlock = isModule || importmap || async || defer; } else if (nsGkAtoms::style == aName) { mImportScanner.Start(); nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement(mozilla::fallible); if (MOZ_UNLIKELY(!treeOp)) { MarkAsBrokenAndRequestSuspensionWithoutBuilder( NS_ERROR_OUT_OF_MEMORY); return nullptr; } opSetStyleLineNumber operation(content, tokenizer->getLineNumber()); treeOp->Init(mozilla::AsVariant(operation)); } break; } } else if (aNamespace != kNameSpaceID_MathML) { // No speculative loader--just line numbers and defer/async check if (nsGkAtoms::style == aName) { nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement(mozilla::fallible); if (MOZ_UNLIKELY(!treeOp)) { MarkAsBrokenAndRequestSuspensionWithoutBuilder(NS_ERROR_OUT_OF_MEMORY); return nullptr; } opSetStyleLineNumber operation(content, tokenizer->getLineNumber()); treeOp->Init(mozilla::AsVariant(operation)); } else if (nsGkAtoms::script == aName) { nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement(mozilla::fallible); if (MOZ_UNLIKELY(!treeOp)) { MarkAsBrokenAndRequestSuspensionWithoutBuilder(NS_ERROR_OUT_OF_MEMORY); return nullptr; } opSetScriptLineAndColumnNumberAndFreeze operation( content, tokenizer->getLineNumber(), // NOTE: tokenizer->getColumnNumber() points '>'. tokenizer->getColumnNumber() + 1); treeOp->Init(mozilla::AsVariant(operation)); if (aNamespace == kNameSpaceID_XHTML) { // Although we come here in cases where the value of // `mCurrentHtmlScriptCannotDocumentWriteOrBlock` doesn't actually // matter, we also come here when parsing document.written content on // the main thread. In that case, IT MATTERS that // `mCurrentHtmlScriptCannotDocumentWriteOrBlock` is set correctly, // so let's just always set it correctly even if it a bit of wasted work // in the scenarios where no scripts execute and it doesn't matter. // // See the comments around generating speculative loads for HTML scripts // elements for the details of when // `mCurrentHtmlScriptCannotDocumentWriteOrBlock` needs to be set to // `true` and when to `false`. nsHtml5String type = aAttributes->getValue(nsHtml5AttributeName::ATTR_TYPE); nsAutoString typeString; getTypeString(type, typeString); mCurrentHtmlScriptCannotDocumentWriteOrBlock = typeString.LowerCaseEqualsASCII("module") || typeString.LowerCaseEqualsASCII("nomodule") || typeString.LowerCaseEqualsASCII("importmap"); if (!mCurrentHtmlScriptCannotDocumentWriteOrBlock && aAttributes->contains(nsHtml5AttributeName::ATTR_SRC)) { mCurrentHtmlScriptCannotDocumentWriteOrBlock = (aAttributes->contains(nsHtml5AttributeName::ATTR_ASYNC) || aAttributes->contains(nsHtml5AttributeName::ATTR_DEFER)); } } } else if (aNamespace == kNameSpaceID_XHTML) { if (nsGkAtoms::html == aName) { nsHtml5String url = aAttributes->getValue(nsHtml5AttributeName::ATTR_MANIFEST); nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement(mozilla::fallible); if (MOZ_UNLIKELY(!treeOp)) { MarkAsBrokenAndRequestSuspensionWithoutBuilder( NS_ERROR_OUT_OF_MEMORY); return nullptr; } if (url) { nsString urlString; // Not Auto, because using it to hold nsStringBuffer* url.ToString(urlString); opProcessOfflineManifest operation(ToNewUnicode(urlString)); treeOp->Init(mozilla::AsVariant(operation)); } else { opProcessOfflineManifest operation(ToNewUnicode(u""_ns)); treeOp->Init(mozilla::AsVariant(operation)); } } else if (nsGkAtoms::base == aName && mViewSource) { nsHtml5String url = aAttributes->getValue(nsHtml5AttributeName::ATTR_HREF); if (url) { mViewSource->AddBase(url); } } } } // End wall of code for speculative loading return content; } nsIContentHandle* nsHtml5TreeBuilder::createElement( int32_t aNamespace, nsAtom* aName, nsHtml5HtmlAttributes* aAttributes, nsIContentHandle* aFormElement, nsIContentHandle* aIntendedParent, nsHtml5ContentCreatorFunction aCreator) { nsIContentHandle* content = createElement(aNamespace, aName, aAttributes, aIntendedParent, aCreator); if (aFormElement) { if (mBuilder) { nsHtml5TreeOperation::SetFormElement( static_cast(content), static_cast(aFormElement), static_cast(aIntendedParent)); } else { nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement(mozilla::fallible); if (MOZ_UNLIKELY(!treeOp)) { MarkAsBrokenAndRequestSuspensionWithoutBuilder(NS_ERROR_OUT_OF_MEMORY); return nullptr; } opSetFormElement operation(content, aFormElement, aIntendedParent); treeOp->Init(mozilla::AsVariant(operation)); } } return content; } nsIContentHandle* nsHtml5TreeBuilder::createHtmlElementSetAsRoot( nsHtml5HtmlAttributes* aAttributes) { nsHtml5ContentCreatorFunction creator; // uses NS_NewHTMLSharedElement creator creator.html = NS_NewHTMLSharedElement; nsIContentHandle* content = createElement(kNameSpaceID_XHTML, nsGkAtoms::html, aAttributes, nullptr, creator); if (mBuilder) { nsIContent* elem = static_cast(content); // The html element is on the built-in non-replaceable elements list, so // the sanitizer either keeps or removes it. if (MOZ_UNLIKELY(mSanitizerState) && mSanitizerState->IsDropped(elem)) { return content; } nsresult rv = nsHtml5TreeOperation::AppendToDocument(elem, mBuilder); if (NS_FAILED(rv)) { MarkAsBrokenAndRequestSuspensionWithBuilder(rv); } } else { nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement(mozilla::fallible); if (MOZ_UNLIKELY(!treeOp)) { MarkAsBrokenAndRequestSuspensionWithoutBuilder(NS_ERROR_OUT_OF_MEMORY); return nullptr; } opAppendToDocument operation(content); treeOp->Init(mozilla::AsVariant(operation)); } return content; } nsIContentHandle* nsHtml5TreeBuilder::createAndInsertFosterParentedElement( int32_t aNamespace, nsAtom* aName, nsHtml5HtmlAttributes* aAttributes, nsIContentHandle* aFormElement, nsIContentHandle* aTable, nsIContentHandle* aStackParent, nsHtml5ContentCreatorFunction aCreator) { MOZ_ASSERT(aTable, "Null table"); MOZ_ASSERT(aStackParent, "Null stack parent"); if (mBuilder) { // Get the foster parent to use as the intended parent when creating // the child element. nsIContent* fosterParent = nsHtml5TreeOperation::GetFosterParent( static_cast(aTable), static_cast(aStackParent)); nsIContentHandle* child = createElement( aNamespace, aName, aAttributes, aFormElement, fosterParent, aCreator); insertFosterParentedChild(child, aTable, aStackParent); return child; } // Tree op to get the foster parent that we use as the intended parent // when creating the child element. nsHtml5TreeOperation* fosterParentTreeOp = mOpQueue.AppendElement(); NS_ASSERTION(fosterParentTreeOp, "Tree op allocation failed."); nsIContentHandle* fosterParentHandle = AllocateContentHandle(); opGetFosterParent operation(aTable, aStackParent, fosterParentHandle); fosterParentTreeOp->Init(mozilla::AsVariant(operation)); // Create the element with the correct intended parent. nsIContentHandle* child = createElement(aNamespace, aName, aAttributes, aFormElement, fosterParentHandle, aCreator); // Insert the child into the foster parent. insertFosterParentedChild(child, aTable, aStackParent); return child; } void nsHtml5TreeBuilder::optionElementPopped(nsIContentHandle* aOption) { // TODO: Implement "maybe clone an option into selectedcontent" for // customizable