diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/prototype | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/prototype')
-rw-r--r-- | dom/prototype/PrototypeDocumentContentSink.cpp | 1137 | ||||
-rw-r--r-- | dom/prototype/PrototypeDocumentContentSink.h | 262 | ||||
-rw-r--r-- | dom/prototype/moz.build | 25 | ||||
-rw-r--r-- | dom/prototype/tests/chrome/chrome.ini | 7 | ||||
-rw-r--r-- | dom/prototype/tests/chrome/form.xhtml | 8 | ||||
-rw-r--r-- | dom/prototype/tests/chrome/no_whitespace.xhtml | 3 | ||||
-rw-r--r-- | dom/prototype/tests/chrome/test_prototype_document.xhtml | 73 | ||||
-rw-r--r-- | dom/prototype/tests/chrome/whitespace.xhtml | 8 |
8 files changed, 1523 insertions, 0 deletions
diff --git a/dom/prototype/PrototypeDocumentContentSink.cpp b/dom/prototype/PrototypeDocumentContentSink.cpp new file mode 100644 index 0000000000..2c299f6a91 --- /dev/null +++ b/dom/prototype/PrototypeDocumentContentSink.cpp @@ -0,0 +1,1137 @@ +/* -*- 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 https://mozilla.org/MPL/2.0/. */ + +#include "nsCOMPtr.h" +#include "mozilla/dom/PrototypeDocumentContentSink.h" +#include "nsIParser.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/URL.h" +#include "nsIContent.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsHTMLParts.h" +#include "nsCRT.h" +#include "mozilla/StyleSheetInlines.h" +#include "mozilla/css/Loader.h" +#include "nsGkAtoms.h" +#include "nsContentUtils.h" +#include "nsDocElementCreatedNotificationRunner.h" +#include "nsIScriptContext.h" +#include "nsNameSpaceManager.h" +#include "nsIScriptError.h" +#include "prtime.h" +#include "mozilla/Logging.h" +#include "nsRect.h" +#include "nsIScriptElement.h" +#include "nsReadableUtils.h" +#include "nsUnicharUtils.h" +#include "nsIChannel.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "nsIContentPolicy.h" +#include "nsContentPolicyUtils.h" +#include "nsError.h" +#include "nsIScriptGlobalObject.h" +#include "mozAutoDocUpdate.h" +#include "nsMimeTypes.h" +#include "nsHtml5SVGLoadDispatcher.h" +#include "nsTextNode.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/CDATASection.h" +#include "mozilla/dom/Comment.h" +#include "mozilla/dom/DocumentType.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLTemplateElement.h" +#include "mozilla/dom/ProcessingInstruction.h" +#include "mozilla/dom/XMLStylesheetProcessingInstruction.h" +#include "mozilla/dom/ScriptLoader.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/PresShell.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/RefPtr.h" + +#include "nsXULPrototypeCache.h" +#include "nsXULElement.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "js/CompilationAndEvaluation.h" +#include "js/experimental/JSStencil.h" + +using namespace mozilla; +using namespace mozilla::dom; + +LazyLogModule PrototypeDocumentContentSink::gLog("PrototypeDocument"); + +nsresult NS_NewPrototypeDocumentContentSink(nsIContentSink** aResult, + Document* aDoc, nsIURI* aURI, + nsISupports* aContainer, + nsIChannel* aChannel) { + MOZ_ASSERT(nullptr != aResult, "null ptr"); + if (nullptr == aResult) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<PrototypeDocumentContentSink> it = new PrototypeDocumentContentSink(); + + nsresult rv = it->Init(aDoc, aURI, aContainer, aChannel); + NS_ENSURE_SUCCESS(rv, rv); + + it.forget(aResult); + return NS_OK; +} + +namespace mozilla::dom { + +PrototypeDocumentContentSink::PrototypeDocumentContentSink() + : mNextSrcLoadWaiter(nullptr), + mCurrentScriptProto(nullptr), + mOffThreadCompiling(false), + mOffThreadCompileStringBuf(nullptr), + mOffThreadCompileStringLength(0), + mStillWalking(false), + mPendingSheets(0) {} + +PrototypeDocumentContentSink::~PrototypeDocumentContentSink() { + NS_ASSERTION( + mNextSrcLoadWaiter == nullptr, + "unreferenced document still waiting for script source to load?"); + + if (mOffThreadCompileStringBuf) { + js_free(mOffThreadCompileStringBuf); + } +} + +nsresult PrototypeDocumentContentSink::Init(Document* aDoc, nsIURI* aURI, + nsISupports* aContainer, + nsIChannel* aChannel) { + MOZ_ASSERT(aDoc, "null ptr"); + MOZ_ASSERT(aURI, "null ptr"); + + mDocument = aDoc; + + mDocument->SetDelayFrameLoaderInitialization(true); + mDocument->SetMayStartLayout(false); + + // Get the URI. this should match the uri used for the OnNewURI call in + // nsDocShell::CreateContentViewer. + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(mDocumentURI)); + NS_ENSURE_SUCCESS(rv, rv); + + mScriptLoader = mDocument->ScriptLoader(); + + return NS_OK; +} + +NS_IMPL_CYCLE_COLLECTION(PrototypeDocumentContentSink, mParser, mDocumentURI, + mDocument, mScriptLoader, mContextStack, + mCurrentPrototype) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PrototypeDocumentContentSink) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIContentSink) + NS_INTERFACE_MAP_ENTRY(nsIContentSink) + NS_INTERFACE_MAP_ENTRY(nsIStreamLoaderObserver) + NS_INTERFACE_MAP_ENTRY(nsICSSLoaderObserver) + NS_INTERFACE_MAP_ENTRY(nsIOffThreadScriptReceiver) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(PrototypeDocumentContentSink) +NS_IMPL_CYCLE_COLLECTING_RELEASE(PrototypeDocumentContentSink) + +//---------------------------------------------------------------------- +// +// nsIContentSink interface +// + +void PrototypeDocumentContentSink::SetDocumentCharset( + NotNull<const Encoding*> aEncoding) { + if (mDocument) { + mDocument->SetDocumentCharacterSet(aEncoding); + } +} + +nsISupports* PrototypeDocumentContentSink::GetTarget() { + return ToSupports(mDocument); +} + +bool PrototypeDocumentContentSink::IsScriptExecuting() { + return !!mScriptLoader->GetCurrentScript(); +} + +NS_IMETHODIMP +PrototypeDocumentContentSink::SetParser(nsParserBase* aParser) { + MOZ_ASSERT(aParser, "Should have a parser here!"); + mParser = aParser; + return NS_OK; +} + +nsIParser* PrototypeDocumentContentSink::GetParser() { + return static_cast<nsIParser*>(mParser.get()); +} + +void PrototypeDocumentContentSink::ContinueInterruptedParsingIfEnabled() { + if (mParser && mParser->IsParserEnabled()) { + GetParser()->ContinueInterruptedParsing(); + } +} + +void PrototypeDocumentContentSink::ContinueInterruptedParsingAsync() { + nsCOMPtr<nsIRunnable> ev = NewRunnableMethod( + "PrototypeDocumentContentSink::ContinueInterruptedParsingIfEnabled", this, + &PrototypeDocumentContentSink::ContinueInterruptedParsingIfEnabled); + + mDocument->Dispatch(mozilla::TaskCategory::Other, ev.forget()); +} + +//---------------------------------------------------------------------- +// +// PrototypeDocumentContentSink::ContextStack +// + +PrototypeDocumentContentSink::ContextStack::ContextStack() + : mTop(nullptr), mDepth(0) {} + +PrototypeDocumentContentSink::ContextStack::~ContextStack() { Clear(); } + +void PrototypeDocumentContentSink::ContextStack::Traverse( + nsCycleCollectionTraversalCallback& aCallback, const char* aName, + uint32_t aFlags) { + aFlags |= CycleCollectionEdgeNameArrayFlag; + Entry* current = mTop; + while (current) { + CycleCollectionNoteChild(aCallback, current->mElement, aName, aFlags); + current = current->mNext; + } +} + +void PrototypeDocumentContentSink::ContextStack::Clear() { + while (mTop) { + Entry* doomed = mTop; + mTop = mTop->mNext; + NS_IF_RELEASE(doomed->mElement); + delete doomed; + } + mDepth = 0; +} + +nsresult PrototypeDocumentContentSink::ContextStack::Push( + nsXULPrototypeElement* aPrototype, nsIContent* aElement) { + Entry* entry = new Entry; + entry->mPrototype = aPrototype; + entry->mElement = aElement; + NS_IF_ADDREF(entry->mElement); + entry->mIndex = 0; + + entry->mNext = mTop; + mTop = entry; + + ++mDepth; + return NS_OK; +} + +nsresult PrototypeDocumentContentSink::ContextStack::Pop() { + if (mDepth == 0) return NS_ERROR_UNEXPECTED; + + Entry* doomed = mTop; + mTop = mTop->mNext; + --mDepth; + + NS_IF_RELEASE(doomed->mElement); + delete doomed; + return NS_OK; +} + +nsresult PrototypeDocumentContentSink::ContextStack::Peek( + nsXULPrototypeElement** aPrototype, nsIContent** aElement, + int32_t* aIndex) { + if (mDepth == 0) return NS_ERROR_UNEXPECTED; + + *aPrototype = mTop->mPrototype; + *aElement = mTop->mElement; + NS_IF_ADDREF(*aElement); + *aIndex = mTop->mIndex; + + return NS_OK; +} + +nsresult PrototypeDocumentContentSink::ContextStack::SetTopIndex( + int32_t aIndex) { + if (mDepth == 0) return NS_ERROR_UNEXPECTED; + + mTop->mIndex = aIndex; + return NS_OK; +} + +//---------------------------------------------------------------------- +// +// Content model walking routines +// + +nsresult PrototypeDocumentContentSink::OnPrototypeLoadDone( + nsXULPrototypeDocument* aPrototype) { + mCurrentPrototype = aPrototype; + mDocument->SetPrototypeDocument(aPrototype); + + nsresult rv = PrepareToWalk(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ResumeWalk(); + + return rv; +} + +nsresult PrototypeDocumentContentSink::PrepareToWalk() { + MOZ_ASSERT(mCurrentPrototype); + nsresult rv; + + mStillWalking = true; + + // Notify document that the load is beginning + mDocument->BeginLoad(); + + // Get the prototype's root element and initialize the context + // stack for the prototype walk. + nsXULPrototypeElement* proto = mCurrentPrototype->GetRootElement(); + + if (!proto) { + if (MOZ_LOG_TEST(gLog, LogLevel::Error)) { + nsCOMPtr<nsIURI> url = mCurrentPrototype->GetURI(); + + nsAutoCString urlspec; + rv = url->GetSpec(urlspec); + if (NS_FAILED(rv)) return rv; + + MOZ_LOG(gLog, LogLevel::Error, + ("prototype: error parsing '%s'", urlspec.get())); + } + + return NS_OK; + } + + nsINode* nodeToInsertBefore = mDocument->GetFirstChild(); + + const nsTArray<RefPtr<nsXULPrototypePI> >& processingInstructions = + mCurrentPrototype->GetProcessingInstructions(); + + uint32_t total = processingInstructions.Length(); + for (uint32_t i = 0; i < total; ++i) { + rv = CreateAndInsertPI(processingInstructions[i], mDocument, + nodeToInsertBefore); + if (NS_FAILED(rv)) return rv; + } + + // Do one-time initialization. + RefPtr<Element> root; + + // Add the root element + rv = CreateElementFromPrototype(proto, getter_AddRefs(root), nullptr); + if (NS_FAILED(rv)) return rv; + + ErrorResult error; + mDocument->AppendChildTo(root, false, error); + if (error.Failed()) { + return error.StealNSResult(); + } + + // TODO(emilio): Should this really notify? We don't notify of appends anyhow, + // and we just appended the root so no styles can possibly depend on it. + mDocument->UpdateDocumentStates(DocumentState::RTL_LOCALE, true); + + nsContentUtils::AddScriptRunner( + new nsDocElementCreatedNotificationRunner(mDocument)); + + // There'd better not be anything on the context stack at this + // point! This is the basis case for our "induction" in + // ResumeWalk(), below, which'll assume that there's always a + // content element on the context stack if we're in the document. + NS_ASSERTION(mContextStack.Depth() == 0, + "something's on the context stack already"); + if (mContextStack.Depth() != 0) return NS_ERROR_UNEXPECTED; + + rv = mContextStack.Push(proto, root); + if (NS_FAILED(rv)) return rv; + + return NS_OK; +} + +nsresult PrototypeDocumentContentSink::CreateAndInsertPI( + const nsXULPrototypePI* aProtoPI, nsINode* aParent, nsINode* aBeforeThis) { + MOZ_ASSERT(aProtoPI, "null ptr"); + MOZ_ASSERT(aParent, "null ptr"); + + RefPtr<ProcessingInstruction> node = + NS_NewXMLProcessingInstruction(aParent->OwnerDoc()->NodeInfoManager(), + aProtoPI->mTarget, aProtoPI->mData); + + nsresult rv; + if (aProtoPI->mTarget.EqualsLiteral("xml-stylesheet")) { + MOZ_ASSERT(LinkStyle::FromNode(*node), + "XML Stylesheet node does not implement LinkStyle!"); + auto* pi = static_cast<XMLStylesheetProcessingInstruction*>(node.get()); + rv = InsertXMLStylesheetPI(aProtoPI, aParent, aBeforeThis, pi); + } else { + // No special processing, just add the PI to the document. + ErrorResult error; + aParent->InsertChildBefore(node->AsContent(), + aBeforeThis ? aBeforeThis->AsContent() : nullptr, + false, error); + rv = error.StealNSResult(); + } + + return rv; +} + +nsresult PrototypeDocumentContentSink::InsertXMLStylesheetPI( + const nsXULPrototypePI* aProtoPI, nsINode* aParent, nsINode* aBeforeThis, + XMLStylesheetProcessingInstruction* aPINode) { + // We want to be notified when the style sheet finishes loading, so + // disable style sheet loading for now. + aPINode->SetEnableUpdates(false); + aPINode->OverrideBaseURI(mCurrentPrototype->GetURI()); + + ErrorResult rv; + aParent->InsertChildBefore( + aPINode, aBeforeThis ? aBeforeThis->AsContent() : nullptr, false, rv); + if (rv.Failed()) { + return rv.StealNSResult(); + } + + aPINode->SetEnableUpdates(true); + + // load the stylesheet if necessary, passing ourselves as + // nsICSSObserver + auto result = aPINode->UpdateStyleSheet(this); + if (result.isErr()) { + // Ignore errors from UpdateStyleSheet; we don't want failure to + // do that to break the XUL document load. But do propagate out + // NS_ERROR_OUT_OF_MEMORY. + if (result.unwrapErr() == NS_ERROR_OUT_OF_MEMORY) { + return result.unwrapErr(); + } + return NS_OK; + } + + auto update = result.unwrap(); + if (update.ShouldBlock()) { + ++mPendingSheets; + } + + return NS_OK; +} + +void PrototypeDocumentContentSink::CloseElement(Element* aElement, + bool aHadChildren) { + if (nsIContent::RequiresDoneAddingChildren( + aElement->NodeInfo()->NamespaceID(), + aElement->NodeInfo()->NameAtom())) { + aElement->DoneAddingChildren(false); + } + + if (!aHadChildren) { + return; + } + + // See bug 370111 and bug 1495946. We don't cache inline styles nor module + // scripts in the prototype cache, and we don't notify on node insertion, so + // we need to do this for the stylesheet / script to be properly processed. + // This kinda sucks, but notifying was a pretty sizeable perf regression so... + if (aElement->IsHTMLElement(nsGkAtoms::script) || + aElement->IsSVGElement(nsGkAtoms::script)) { + nsCOMPtr<nsIScriptElement> sele = do_QueryInterface(aElement); + MOZ_ASSERT(sele, "Node didn't QI to script."); + if (sele->GetScriptIsModule()) { + DebugOnly<bool> block = sele->AttemptToExecute(); + MOZ_ASSERT(!block, "<script type=module> shouldn't block the parser"); + } + } + + if (aElement->IsHTMLElement(nsGkAtoms::style) || + aElement->IsSVGElement(nsGkAtoms::style)) { + auto* linkStyle = LinkStyle::FromNode(*aElement); + NS_ASSERTION(linkStyle, + "<html:style> doesn't implement " + "nsIStyleSheetLinkingElement?"); + Unused << linkStyle->UpdateStyleSheet(nullptr); + } +} + +nsresult PrototypeDocumentContentSink::ResumeWalk() { + nsresult rv = ResumeWalkInternal(); + if (NS_FAILED(rv)) { + nsContentUtils::ReportToConsoleNonLocalized( + u"Failed to load document from prototype document."_ns, + nsIScriptError::errorFlag, "Prototype Document"_ns, mDocument, + mDocumentURI); + } + return rv; +} + +nsresult PrototypeDocumentContentSink::ResumeWalkInternal() { + MOZ_ASSERT(mStillWalking); + // Walk the prototype and build the delegate content model. The + // walk is performed in a top-down, left-to-right fashion. That + // is, a parent is built before any of its children; a node is + // only built after all of its siblings to the left are fully + // constructed. + // + // It is interruptable so that transcluded documents (e.g., + // <html:script src="..." />) can be properly re-loaded if the + // cached copy of the document becomes stale. + nsresult rv; + nsCOMPtr<nsIURI> docURI = + mCurrentPrototype ? mCurrentPrototype->GetURI() : nullptr; + + while (1) { + // Begin (or resume) walking the current prototype. + + while (mContextStack.Depth() > 0) { + // Look at the top of the stack to determine what we're + // currently working on. + // This will always be a node already constructed and + // inserted to the actual document. + nsXULPrototypeElement* proto; + nsCOMPtr<nsIContent> element; + nsCOMPtr<nsIContent> nodeToPushTo; + int32_t indx; // all children of proto before indx (not + // inclusive) have already been constructed + rv = mContextStack.Peek(&proto, getter_AddRefs(element), &indx); + if (NS_FAILED(rv)) return rv; + + if (indx >= (int32_t)proto->mChildren.Length()) { + if (element) { + // We've processed all of the prototype's children. + CloseElement(element->AsElement(), /* aHadChildren = */ true); + } + // Now pop the context stack back up to the parent + // element and continue the prototype walk. + mContextStack.Pop(); + continue; + } + + nodeToPushTo = element; + // For template elements append the content to the template's document + // fragment. + if (auto* templateElement = HTMLTemplateElement::FromNode(element)) { + nodeToPushTo = templateElement->Content(); + } + + // Grab the next child, and advance the current context stack + // to the next sibling to our right. + nsXULPrototypeNode* childproto = proto->mChildren[indx]; + mContextStack.SetTopIndex(++indx); + + switch (childproto->mType) { + case nsXULPrototypeNode::eType_Element: { + // An 'element', which may contain more content. + auto* protoele = static_cast<nsXULPrototypeElement*>(childproto); + + RefPtr<Element> child; + + rv = CreateElementFromPrototype(protoele, getter_AddRefs(child), + nodeToPushTo); + if (NS_FAILED(rv)) return rv; + + // ...and append it to the content model. + ErrorResult error; + nodeToPushTo->AppendChildTo(child, false, error); + if (error.Failed()) { + return error.StealNSResult(); + } + + if (nsIContent::RequiresDoneCreatingElement( + protoele->mNodeInfo->NamespaceID(), + protoele->mNodeInfo->NameAtom())) { + child->DoneCreatingElement(); + } + + // If it has children, push the element onto the context + // stack and begin to process them. + if (protoele->mChildren.Length() > 0) { + rv = mContextStack.Push(protoele, child); + if (NS_FAILED(rv)) return rv; + } else { + // If there are no children, close the element immediately. + CloseElement(child, /* aHadChildren = */ false); + } + } break; + + case nsXULPrototypeNode::eType_Script: { + // A script reference. Execute the script immediately; + // this may have side effects in the content model. + auto* scriptproto = static_cast<nsXULPrototypeScript*>(childproto); + if (scriptproto->mSrcURI) { + // A transcluded script reference; this may + // "block" our prototype walk if the script isn't + // cached, or the cached copy of the script is + // stale and must be reloaded. + bool blocked; + rv = LoadScript(scriptproto, &blocked); + // If the script cannot be loaded, just keep going! + + if (NS_SUCCEEDED(rv) && blocked) return NS_OK; + } else if (scriptproto->HasStencil()) { + // An inline script + rv = ExecuteScript(scriptproto); + if (NS_FAILED(rv)) return rv; + } + } break; + + case nsXULPrototypeNode::eType_Text: { + nsNodeInfoManager* nim = nodeToPushTo->NodeInfo()->NodeInfoManager(); + // A simple text node. + RefPtr<nsTextNode> text = new (nim) nsTextNode(nim); + + auto* textproto = static_cast<nsXULPrototypeText*>(childproto); + text->SetText(textproto->mValue, false); + + ErrorResult error; + nodeToPushTo->AppendChildTo(text, false, error); + if (error.Failed()) { + return error.StealNSResult(); + } + } break; + + case nsXULPrototypeNode::eType_PI: { + auto* piProto = static_cast<nsXULPrototypePI*>(childproto); + + // <?xml-stylesheet?> doesn't have an effect + // outside the prolog, like it used to. Issue a warning. + + if (piProto->mTarget.EqualsLiteral("xml-stylesheet")) { + AutoTArray<nsString, 1> params = {piProto->mTarget}; + + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + "XUL Document"_ns, nullptr, + nsContentUtils::eXUL_PROPERTIES, + "PINotInProlog", params, docURI); + } + + nsIContent* parent = element.get(); + if (parent) { + // an inline script could have removed the root element + rv = CreateAndInsertPI(piProto, parent, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + } + } break; + + default: + MOZ_ASSERT_UNREACHABLE("Unexpected nsXULPrototypeNode::Type"); + } + } + + // Once we get here, the context stack will have been + // depleted. That means that the entire prototype has been + // walked and content has been constructed. + break; + } + + mStillWalking = false; + return MaybeDoneWalking(); +} + +void PrototypeDocumentContentSink::InitialTranslationCompleted() { + MaybeDoneWalking(); +} + +nsresult PrototypeDocumentContentSink::MaybeDoneWalking() { + if (mPendingSheets > 0 || mStillWalking) { + return NS_OK; + } + + if (mDocument->HasPendingInitialTranslation()) { + mDocument->OnParsingCompleted(); + return NS_OK; + } + + return DoneWalking(); +} + +nsresult PrototypeDocumentContentSink::DoneWalking() { + MOZ_ASSERT(mPendingSheets == 0, "there are sheets to be loaded"); + MOZ_ASSERT(!mStillWalking, "walk not done"); + MOZ_ASSERT(!mDocument->HasPendingInitialTranslation(), "translation pending"); + + if (mDocument) { + MOZ_ASSERT(mDocument->GetReadyStateEnum() == Document::READYSTATE_LOADING, + "Bad readyState"); + mDocument->SetReadyStateInternal(Document::READYSTATE_INTERACTIVE); + mDocument->NotifyPossibleTitleChange(false); + + nsContentUtils::DispatchEventOnlyToChrome(mDocument, ToSupports(mDocument), + u"MozBeforeInitialXULLayout"_ns, + CanBubble::eYes, Cancelable::eNo); + } + + if (mScriptLoader) { + mScriptLoader->ParsingComplete(false); + mScriptLoader->DeferCheckpointReached(); + } + + StartLayout(); + + if (IsChromeURI(mDocumentURI) && + nsXULPrototypeCache::GetInstance()->IsEnabled()) { + bool isCachedOnDisk; + nsXULPrototypeCache::GetInstance()->HasPrototype(mDocumentURI, + &isCachedOnDisk); + if (!isCachedOnDisk) { + nsXULPrototypeCache::GetInstance()->WritePrototype(mCurrentPrototype); + } + } + + mDocument->SetDelayFrameLoaderInitialization(false); + RefPtr<Document> doc = mDocument; + doc->MaybeInitializeFinalizeFrameLoaders(); + + // If the document we are loading has a reference or it is a + // frameset document, disable the scroll bars on the views. + + doc->SetScrollToRef(mDocument->GetDocumentURI()); + + doc->EndLoad(); + + return NS_OK; +} + +void PrototypeDocumentContentSink::StartLayout() { + AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING( + "PrototypeDocumentContentSink::StartLayout", LAYOUT, + mDocumentURI->GetSpecOrDefault()); + mDocument->SetMayStartLayout(true); + RefPtr<PresShell> presShell = mDocument->GetPresShell(); + if (presShell && !presShell->DidInitialize()) { + nsresult rv = presShell->Initialize(); + if (NS_FAILED(rv)) { + return; + } + } +} + +NS_IMETHODIMP +PrototypeDocumentContentSink::StyleSheetLoaded(StyleSheet* aSheet, + bool aWasDeferred, + nsresult aStatus) { + if (!aWasDeferred) { + // Don't care about when alternate sheets finish loading + MOZ_ASSERT(mPendingSheets > 0, "Unexpected StyleSheetLoaded notification"); + + --mPendingSheets; + + return MaybeDoneWalking(); + } + + return NS_OK; +} + +nsresult PrototypeDocumentContentSink::LoadScript( + nsXULPrototypeScript* aScriptProto, bool* aBlock) { + // Load a transcluded script + nsresult rv; + + bool isChromeDoc = IsChromeURI(mDocumentURI); + + if (isChromeDoc && aScriptProto->HasStencil()) { + rv = ExecuteScript(aScriptProto); + + // Ignore return value from execution, and don't block + *aBlock = false; + return NS_OK; + } + + // Try the XUL script cache, in case two XUL documents source the same + // .js file (e.g., strres.js from navigator.xul and utilityOverlay.xul). + // XXXbe the cache relies on aScriptProto's GC root! + bool useXULCache = nsXULPrototypeCache::GetInstance()->IsEnabled(); + + if (isChromeDoc && useXULCache) { + RefPtr<JS::Stencil> newStencil = + nsXULPrototypeCache::GetInstance()->GetStencil(aScriptProto->mSrcURI); + if (newStencil) { + // The script language for a proto must remain constant - we + // can't just change it for this unexpected language. + aScriptProto->Set(newStencil); + } + + if (aScriptProto->HasStencil()) { + rv = ExecuteScript(aScriptProto); + + // Ignore return value from execution, and don't block + *aBlock = false; + return NS_OK; + } + } + + // Release stencil from FastLoad since we decided against using them + aScriptProto->Set(nullptr); + + // Set the current script prototype so that OnStreamComplete can report + // the right file if there are errors in the script. + NS_ASSERTION(!mCurrentScriptProto, + "still loading a script when starting another load?"); + mCurrentScriptProto = aScriptProto; + + if (isChromeDoc && aScriptProto->mSrcLoading) { + // Another document load has started, which is still in progress. + // Remember to ResumeWalk this document when the load completes. + mNextSrcLoadWaiter = aScriptProto->mSrcLoadWaiters; + aScriptProto->mSrcLoadWaiters = this; + NS_ADDREF_THIS(); + } else { + nsCOMPtr<nsILoadGroup> group = + mDocument + ->GetDocumentLoadGroup(); // found in + // mozilla::dom::Document::SetScriptGlobalObject + + // Note: the loader will keep itself alive while it's loading. + nsCOMPtr<nsIStreamLoader> loader; + rv = NS_NewStreamLoader( + getter_AddRefs(loader), aScriptProto->mSrcURI, + this, // aObserver + mDocument, // aRequestingContext + nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT, + nsIContentPolicy::TYPE_INTERNAL_SCRIPT, group); + + if (NS_FAILED(rv)) { + mCurrentScriptProto = nullptr; + return rv; + } + + aScriptProto->mSrcLoading = true; + } + + // Block until OnStreamComplete resumes us. + *aBlock = true; + return NS_OK; +} + +NS_IMETHODIMP +PrototypeDocumentContentSink::OnStreamComplete(nsIStreamLoader* aLoader, + nsISupports* context, + nsresult aStatus, + uint32_t stringLen, + const uint8_t* string) { + nsCOMPtr<nsIRequest> request; + aLoader->GetRequest(getter_AddRefs(request)); + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request); + +#ifdef DEBUG + // print a load error on bad status + if (NS_FAILED(aStatus)) { + if (channel) { + nsCOMPtr<nsIURI> uri; + channel->GetURI(getter_AddRefs(uri)); + if (uri) { + printf("Failed to load %s\n", uri->GetSpecOrDefault().get()); + } + } + } +#endif + + // This is the completion routine that will be called when a + // transcluded script completes. Compile and execute the script + // if the load was successful, then continue building content + // from the prototype. + nsresult rv = aStatus; + + NS_ASSERTION(mCurrentScriptProto && mCurrentScriptProto->mSrcLoading, + "script source not loading on unichar stream complete?"); + if (!mCurrentScriptProto) { + // XXX Wallpaper for bug 270042 + return NS_OK; + } + + if (NS_SUCCEEDED(aStatus)) { + // If the including document is a FastLoad document, and we're + // compiling an out-of-line script (one with src=...), then we must + // be writing a new FastLoad file. If we were reading this script + // from the FastLoad file, XULContentSinkImpl::OpenScript (over in + // nsXULContentSink.cpp) would have already deserialized a non-null + // script->mStencil, causing control flow at the top of LoadScript + // not to reach here. + nsCOMPtr<nsIURI> uri = mCurrentScriptProto->mSrcURI; + + // XXX should also check nsIHttpChannel::requestSucceeded + + MOZ_ASSERT(!mOffThreadCompiling && (mOffThreadCompileStringLength == 0 && + !mOffThreadCompileStringBuf), + "PrototypeDocument can't load multiple scripts at once"); + + rv = ScriptLoader::ConvertToUTF8(channel, string, stringLen, u""_ns, + mDocument, mOffThreadCompileStringBuf, + mOffThreadCompileStringLength); + if (NS_SUCCEEDED(rv)) { + // Pass ownership of the buffer, carefully emptying the existing + // fields in the process. Note that the |Compile| function called + // below always takes ownership of the buffer. + Utf8Unit* units = nullptr; + size_t unitsLength = 0; + + std::swap(units, mOffThreadCompileStringBuf); + std::swap(unitsLength, mOffThreadCompileStringLength); + + rv = mCurrentScriptProto->Compile(units, unitsLength, + JS::SourceOwnership::TakeOwnership, uri, + 1, mDocument, this); + if (NS_SUCCEEDED(rv) && !mCurrentScriptProto->HasStencil()) { + mOffThreadCompiling = true; + mDocument->BlockOnload(); + return NS_OK; + } + } + } + + return OnScriptCompileComplete(mCurrentScriptProto->GetStencil(), rv); +} + +NS_IMETHODIMP +PrototypeDocumentContentSink::OnScriptCompileComplete(JS::Stencil* aStencil, + nsresult aStatus) { + // The mCurrentScriptProto may have been cleared out by another + // PrototypeDocumentContentSink. + if (!mCurrentScriptProto) { + return NS_OK; + } + + // When compiling off thread the script will not have been attached to the + // script proto yet. + if (aStencil && !mCurrentScriptProto->HasStencil()) { + mCurrentScriptProto->Set(aStencil); + } + + // Allow load events to be fired once off thread compilation finishes. + if (mOffThreadCompiling) { + mOffThreadCompiling = false; + mDocument->UnblockOnload(false); + } + + // After compilation finishes the script's characters are no longer needed. + if (mOffThreadCompileStringBuf) { + js_free(mOffThreadCompileStringBuf); + mOffThreadCompileStringBuf = nullptr; + mOffThreadCompileStringLength = 0; + } + + // Clear mCurrentScriptProto now, but save it first for use below in + // the execute code, and in the while loop that resumes walks of other + // documents that raced to load this script. + nsXULPrototypeScript* scriptProto = mCurrentScriptProto; + mCurrentScriptProto = nullptr; + + // Clear the prototype's loading flag before executing the script or + // resuming document walks, in case any of those control flows starts a + // new script load. + scriptProto->mSrcLoading = false; + + nsresult rv = aStatus; + if (NS_SUCCEEDED(rv)) { + rv = ExecuteScript(scriptProto); + + // If the XUL cache is enabled, save the script object there in + // case different XUL documents source the same script. + // + // But don't save the script in the cache unless the master XUL + // document URL is a chrome: URL. It is valid for a URL such as + // about:config to translate into a master document URL, whose + // prototype document nodes -- including prototype scripts that + // hold GC roots protecting their mJSObject pointers -- are not + // cached in the XUL prototype cache. See StartDocumentLoad, + // the fillXULCache logic. + // + // A document such as about:config is free to load a script via + // a URL such as chrome://global/content/config.js, and we must + // not cache that script object without a prototype cache entry + // containing a companion nsXULPrototypeScript node that owns a + // GC root protecting the script object. Otherwise, the script + // cache entry will dangle once the uncached prototype document + // is released when its owning document is unloaded. + // + // (See http://bugzilla.mozilla.org/show_bug.cgi?id=98207 for + // the true crime story.) + bool useXULCache = nsXULPrototypeCache::GetInstance()->IsEnabled(); + + if (useXULCache && IsChromeURI(mDocumentURI) && scriptProto->HasStencil()) { + nsXULPrototypeCache::GetInstance()->PutStencil(scriptProto->mSrcURI, + scriptProto->GetStencil()); + } + // ignore any evaluation errors + } + + rv = ResumeWalk(); + + // Load a pointer to the prototype-script's list of documents who + // raced to load the same script + PrototypeDocumentContentSink** docp = &scriptProto->mSrcLoadWaiters; + + // Resume walking other documents that waited for this one's load, first + // executing the script we just compiled, in each doc's script context + PrototypeDocumentContentSink* doc; + while ((doc = *docp) != nullptr) { + NS_ASSERTION(doc->mCurrentScriptProto == scriptProto, + "waiting for wrong script to load?"); + doc->mCurrentScriptProto = nullptr; + + // Unlink doc from scriptProto's list before executing and resuming + *docp = doc->mNextSrcLoadWaiter; + doc->mNextSrcLoadWaiter = nullptr; + + if (aStatus == NS_BINDING_ABORTED && !scriptProto->HasStencil()) { + // If the previous doc load was aborted, we want to try loading + // again for the next doc. Otherwise, one abort would lead to all + // subsequent waiting docs to abort as well. + bool block = false; + doc->LoadScript(scriptProto, &block); + NS_RELEASE(doc); + return rv; + } + + // Execute only if we loaded and compiled successfully, then resume + if (NS_SUCCEEDED(aStatus) && scriptProto->HasStencil()) { + doc->ExecuteScript(scriptProto); + } + doc->ResumeWalk(); + NS_RELEASE(doc); + } + + return rv; +} + +nsresult PrototypeDocumentContentSink::ExecuteScript( + nsXULPrototypeScript* aScript) { + MOZ_ASSERT(aScript != nullptr, "null ptr"); + NS_ENSURE_TRUE(aScript, NS_ERROR_NULL_POINTER); + + nsIScriptGlobalObject* scriptGlobalObject; + bool aHasHadScriptHandlingObject; + scriptGlobalObject = + mDocument->GetScriptHandlingObject(aHasHadScriptHandlingObject); + + NS_ENSURE_TRUE(scriptGlobalObject, NS_ERROR_NOT_INITIALIZED); + + nsresult rv; + rv = scriptGlobalObject->EnsureScriptEnvironment(); + NS_ENSURE_SUCCESS(rv, rv); + + // Execute the precompiled script with the given version + nsAutoMicroTask mt; + + // We're about to run script via JS_ExecuteScript, so we need an + // AutoEntryScript. This is Gecko specific and not in any spec. + AutoEntryScript aes(scriptGlobalObject, "precompiled XUL <script> element"); + JSContext* cx = aes.cx(); + + JS::Rooted<JSScript*> scriptObject(cx); + rv = aScript->InstantiateScript(cx, &scriptObject); + NS_ENSURE_SUCCESS(rv, rv); + + JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); + NS_ENSURE_TRUE(xpc::Scriptability::Get(global).Allowed(), NS_OK); + + // On failure, ~AutoScriptEntry will handle exceptions, so + // there is no need to manually check the return value. + JS::Rooted<JS::Value> rval(cx); + Unused << JS_ExecuteScript(cx, scriptObject, &rval); + + return NS_OK; +} + +nsresult PrototypeDocumentContentSink::CreateElementFromPrototype( + nsXULPrototypeElement* aPrototype, Element** aResult, nsIContent* aParent) { + // Create a content model element from a prototype element. + MOZ_ASSERT(aPrototype, "null ptr"); + if (!aPrototype) return NS_ERROR_NULL_POINTER; + + *aResult = nullptr; + nsresult rv = NS_OK; + + if (MOZ_LOG_TEST(gLog, LogLevel::Debug)) { + MOZ_LOG( + gLog, LogLevel::Debug, + ("prototype: creating <%s> from prototype", + NS_ConvertUTF16toUTF8(aPrototype->mNodeInfo->QualifiedName()).get())); + } + + RefPtr<Element> result; + + Document* doc = aParent ? aParent->OwnerDoc() : mDocument.get(); + if (aPrototype->mNodeInfo->NamespaceEquals(kNameSpaceID_XUL)) { + const bool isRoot = !aParent; + // If it's a XUL element, it'll be lightweight until somebody + // monkeys with it. + rv = nsXULElement::CreateFromPrototype(aPrototype, doc, true, isRoot, + getter_AddRefs(result)); + if (NS_FAILED(rv)) return rv; + } else { + // If it's not a XUL element, it's gonna be heavyweight no matter + // what. So we need to copy everything out of the prototype + // into the element. Get a nodeinfo from our nodeinfo manager + // for this node. + RefPtr<NodeInfo> newNodeInfo = doc->NodeInfoManager()->GetNodeInfo( + aPrototype->mNodeInfo->NameAtom(), + aPrototype->mNodeInfo->GetPrefixAtom(), + aPrototype->mNodeInfo->NamespaceID(), nsINode::ELEMENT_NODE); + if (!newNodeInfo) { + return NS_ERROR_OUT_OF_MEMORY; + } + const bool isScript = + newNodeInfo->Equals(nsGkAtoms::script, kNameSpaceID_XHTML) || + newNodeInfo->Equals(nsGkAtoms::script, kNameSpaceID_SVG); + if (aPrototype->mIsAtom && + newNodeInfo->NamespaceID() == kNameSpaceID_XHTML) { + rv = NS_NewHTMLElement(getter_AddRefs(result), newNodeInfo.forget(), + NOT_FROM_PARSER, aPrototype->mIsAtom); + } else { + rv = NS_NewElement(getter_AddRefs(result), newNodeInfo.forget(), + NOT_FROM_PARSER); + } + if (NS_FAILED(rv)) return rv; + + rv = AddAttributes(aPrototype, result); + if (NS_FAILED(rv)) return rv; + + if (isScript) { + nsCOMPtr<nsIScriptElement> sele = do_QueryInterface(result); + MOZ_ASSERT(sele, "Node didn't QI to script."); + + sele->FreezeExecutionAttrs(doc); + // Script loading is handled by the this content sink, so prevent the + // script from loading when it is bound to the document. + // + // NOTE(emilio): This is only done for non-module scripts, because we + // don't support caching modules properly yet, see the comment in + // XULContentSinkImpl::OpenScript. For non-inline scripts, this is enough, + // since we can start the load when the node is inserted. Non-inline + // scripts need another special-case in CloseElement. + if (!sele->GetScriptIsModule()) { + sele->PreventExecution(); + } + } + } + + // FIXME(bug 1627474): Is this right if this is inside an <html:template>? + if (result->HasAttr(kNameSpaceID_None, nsGkAtoms::datal10nid)) { + mDocument->mL10nProtoElements.InsertOrUpdate(result, RefPtr{aPrototype}); + result->SetElementCreatedFromPrototypeAndHasUnmodifiedL10n(); + } + result.forget(aResult); + return NS_OK; +} + +nsresult PrototypeDocumentContentSink::AddAttributes( + nsXULPrototypeElement* aPrototype, Element* aElement) { + nsresult rv; + + for (size_t i = 0; i < aPrototype->mAttributes.Length(); ++i) { + nsXULPrototypeAttribute* protoattr = &(aPrototype->mAttributes[i]); + nsAutoString valueStr; + protoattr->mValue.ToString(valueStr); + + rv = aElement->SetAttr(protoattr->mName.NamespaceID(), + protoattr->mName.LocalName(), + protoattr->mName.GetPrefix(), valueStr, false); + if (NS_FAILED(rv)) return rv; + } + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/prototype/PrototypeDocumentContentSink.h b/dom/prototype/PrototypeDocumentContentSink.h new file mode 100644 index 0000000000..c4735ed52c --- /dev/null +++ b/dom/prototype/PrototypeDocumentContentSink.h @@ -0,0 +1,262 @@ +/* -*- 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 https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PrototypeDocumentContentSink_h__ +#define mozilla_dom_PrototypeDocumentContentSink_h__ + +#include "mozilla/Attributes.h" +#include "nsIContentSink.h" +#include "nsTArray.h" +#include "nsCOMPtr.h" +#include "nsCRT.h" +#include "nsCycleCollectionNoteChild.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIDTD.h" +#include "mozilla/dom/FromParser.h" +#include "nsXULPrototypeDocument.h" +#include "nsIStreamLoader.h" +#include "nsIScriptContext.h" +#include "nsICSSLoaderObserver.h" +#include "mozilla/Logging.h" +#include "js/experimental/JSStencil.h" +#include "mozilla/RefPtr.h" + +class nsIURI; +class nsIChannel; +class nsIContent; +class nsIParser; +class nsTextNode; +class nsINode; +class nsXULPrototypeElement; +class nsXULPrototypePI; +class nsXULPrototypeScript; + +namespace mozilla::dom { +class Element; +class ScriptLoader; +class Document; +class XMLStylesheetProcessingInstruction; +} // namespace mozilla::dom + +nsresult NS_NewPrototypeDocumentContentSink(nsIContentSink** aResult, + mozilla::dom::Document* aDoc, + nsIURI* aURI, + nsISupports* aContainer, + nsIChannel* aChannel); + +namespace mozilla::dom { + +class PrototypeDocumentContentSink final : public nsIStreamLoaderObserver, + public nsIContentSink, + public nsICSSLoaderObserver, + public nsIOffThreadScriptReceiver { + public: + PrototypeDocumentContentSink(); + + nsresult Init(Document* aDoc, nsIURI* aURL, nsISupports* aContainer, + nsIChannel* aChannel); + + // nsISupports + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSISTREAMLOADEROBSERVER + + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(PrototypeDocumentContentSink, + nsIContentSink) + + // nsIContentSink + NS_IMETHOD WillParse(void) override { return NS_OK; }; + NS_IMETHOD WillInterrupt(void) override { return NS_OK; }; + void WillResume() override{}; + NS_IMETHOD SetParser(nsParserBase* aParser) override; + virtual void InitialTranslationCompleted() override; + virtual void FlushPendingNotifications(FlushType aType) override{}; + virtual void SetDocumentCharset(NotNull<const Encoding*> aEncoding) override; + virtual nsISupports* GetTarget() override; + virtual bool IsScriptExecuting() override; + virtual void ContinueInterruptedParsingAsync() override; + + // nsICSSLoaderObserver + NS_IMETHOD StyleSheetLoaded(StyleSheet* aSheet, bool aWasDeferred, + nsresult aStatus) override; + + // nsIOffThreadScriptReceiver + NS_IMETHOD OnScriptCompileComplete(JS::Stencil* aStencil, + nsresult aStatus) override; + + nsresult OnPrototypeLoadDone(nsXULPrototypeDocument* aPrototype); + + protected: + virtual ~PrototypeDocumentContentSink(); + + static LazyLogModule gLog; + + nsIParser* GetParser(); + + void ContinueInterruptedParsingIfEnabled(); + void StartLayout(); + + virtual nsresult AddAttributes(nsXULPrototypeElement* aPrototype, + Element* aElement); + + RefPtr<nsParserBase> mParser; + nsCOMPtr<nsIURI> mDocumentURI; + RefPtr<Document> mDocument; + RefPtr<ScriptLoader> mScriptLoader; + + PrototypeDocumentContentSink* mNextSrcLoadWaiter; // [OWNER] but not COMPtr + + /** + * The prototype-script of the current transcluded script that is being + * loaded. For document.write('<script src="nestedwrite.js"><\/script>') + * to work, these need to be in a stack element type, and we need to hold + * the top of stack here. + */ + nsXULPrototypeScript* mCurrentScriptProto; + + /** + * Whether the current transcluded script is being compiled off thread. + * The load event is blocked while this is in progress. + */ + bool mOffThreadCompiling; + + /** + * If the current transcluded script is being compiled off thread, the + * source for that script. + */ + Utf8Unit* mOffThreadCompileStringBuf; + size_t mOffThreadCompileStringLength; + + /** + * Wether the prototype document is still be traversed to create the DOM. + * Layout will not be started until false. + */ + bool mStillWalking; + + /** + * Number of style sheets still loading. Layout will not start until zero. + */ + uint32_t mPendingSheets; + + /** + * Context stack, which maintains the state of the Builder and allows + * it to be interrupted. + */ + class ContextStack { + protected: + struct Entry { + nsXULPrototypeElement* mPrototype; + nsIContent* mElement; + int32_t mIndex; + Entry* mNext; + }; + + Entry* mTop; + int32_t mDepth; + + public: + ContextStack(); + ~ContextStack(); + + int32_t Depth() { return mDepth; } + + nsresult Push(nsXULPrototypeElement* aPrototype, nsIContent* aElement); + nsresult Pop(); + nsresult Peek(nsXULPrototypeElement** aPrototype, nsIContent** aElement, + int32_t* aIndex); + + nsresult SetTopIndex(int32_t aIndex); + + void Traverse(nsCycleCollectionTraversalCallback& aCallback, + const char* aName, uint32_t aFlags = 0); + void Clear(); + + // Cycle collector helpers for ContextStack. + friend void ImplCycleCollectionUnlink( + PrototypeDocumentContentSink::ContextStack& aField) { + aField.Clear(); + } + + friend void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, + PrototypeDocumentContentSink::ContextStack& aField, const char* aName, + uint32_t aFlags = 0) { + aField.Traverse(aCallback, aName, aFlags); + } + }; + + friend class ContextStack; + ContextStack mContextStack; + + /** + * The current prototype that we are walking to construct the + * content model. + */ + RefPtr<nsXULPrototypeDocument> mCurrentPrototype; + nsresult CreateAndInsertPI(const nsXULPrototypePI* aProtoPI); + nsresult ExecuteScript(nsXULPrototypeScript* aScript); + nsresult LoadScript(nsXULPrototypeScript* aScriptProto, bool* aBlock); + + /** + * A wrapper around ResumeWalkInternal to report walking errors. + */ + nsresult ResumeWalk(); + + /** + * Resume (or initiate) an interrupted (or newly prepared) + * prototype walk. + */ + nsresult ResumeWalkInternal(); + + /** + * Called at the end of ResumeWalk(), from StyleSheetLoaded(), + * and from DocumentL10n. + * If walking, stylesheets and l10n are not blocking, it + * will trigger `DoneWalking()`. + */ + nsresult MaybeDoneWalking(); + + /** + * Called from `MaybeDoneWalking()`. + * Expects that both the prototype document walk is complete and + * all referenced stylesheets finished loading. + */ + nsresult DoneWalking(); + + /** + * Create a delegate content model element from a prototype. + * Note that the resulting content node is not bound to any tree + */ + nsresult CreateElementFromPrototype(nsXULPrototypeElement* aPrototype, + Element** aResult, nsIContent* aParent); + /** + * Prepare to walk the current prototype. + */ + nsresult PrepareToWalk(); + /** + * Creates a processing instruction based on aProtoPI and inserts + * it to the DOM. + */ + nsresult CreateAndInsertPI(const nsXULPrototypePI* aProtoPI, nsINode* aParent, + nsINode* aBeforeThis); + + /** + * Inserts the passed <?xml-stylesheet ?> PI at the specified + * index. Loads and applies the associated stylesheet + * asynchronously. + * The prototype document walk can happen before the stylesheets + * are loaded, but the final steps in the load process (see + * DoneWalking()) are not run before all the stylesheets are done + * loading. + */ + nsresult InsertXMLStylesheetPI(const nsXULPrototypePI* aProtoPI, + nsINode* aParent, nsINode* aBeforeThis, + XMLStylesheetProcessingInstruction* aPINode); + void CloseElement(Element* aElement, bool aHadChildren); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_PrototypeDocumentContentSink_h__ diff --git a/dom/prototype/moz.build b/dom/prototype/moz.build new file mode 100644 index 0000000000..f73b3824ac --- /dev/null +++ b/dom/prototype/moz.build @@ -0,0 +1,25 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "XUL") + +EXPORTS.mozilla.dom += [ + "PrototypeDocumentContentSink.h", +] + +SOURCES += [ + "PrototypeDocumentContentSink.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/base", + "/dom/xul", +] + +MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.ini"] + +FINAL_LIBRARY = "xul" diff --git a/dom/prototype/tests/chrome/chrome.ini b/dom/prototype/tests/chrome/chrome.ini new file mode 100644 index 0000000000..8de4e4bfb5 --- /dev/null +++ b/dom/prototype/tests/chrome/chrome.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + whitespace.xhtml + no_whitespace.xhtml + form.xhtml + +[test_prototype_document.xhtml] diff --git a/dom/prototype/tests/chrome/form.xhtml b/dom/prototype/tests/chrome/form.xhtml new file mode 100644 index 0000000000..0659215e6e --- /dev/null +++ b/dom/prototype/tests/chrome/form.xhtml @@ -0,0 +1,8 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" +"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <body> + <input type="text" id="input" value=""/> + </body> +</html> diff --git a/dom/prototype/tests/chrome/no_whitespace.xhtml b/dom/prototype/tests/chrome/no_whitespace.xhtml new file mode 100644 index 0000000000..78a8c34cff --- /dev/null +++ b/dom/prototype/tests/chrome/no_whitespace.xhtml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/strict.dtd"> +<html xmlns="http://www.w3.org/TR/xhtml1/strict"><body>Hello<p>there!</p></body></html> diff --git a/dom/prototype/tests/chrome/test_prototype_document.xhtml b/dom/prototype/tests/chrome/test_prototype_document.xhtml new file mode 100644 index 0000000000..47dff2d565 --- /dev/null +++ b/dom/prototype/tests/chrome/test_prototype_document.xhtml @@ -0,0 +1,73 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<window title="Test prototype document" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script><![CDATA[ + SimpleTest.waitForExplicitFinish(); + ]]></script> + <browser type="chrome" id="browser" flex="1"/> + <body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + + +<script><![CDATA[ + +async function load(frame, url) { + return new Promise((resolve) => { + frame.addEventListener("load", () => { + resolve(); + }, {once: true}); + + if (frame.src === url) { + frame.reload(); + } else { + frame.src = url; + } + }); +} + +// Load a file with and without the prototype document cache enabled. +async function compare(filename) { + let browser = document.getElementById("browser"); + // Load the page with no prototype document cache (the regular loading flow of + // an XHTML page). + await SpecialPowers.pushPrefEnv({ set: [["dom.prototype_document_cache.enabled", false]] }); + await load(browser, filename); + ok(!browser.contentDocument.loadedFromPrototype, `${filename} should not use prototype.`); + const contentWithoutPrototype = browser.contentDocument.documentElement.outerHTML; + + // Load the page with the prototype document cache enabled. The prototype should + // be built from the source file. + await SpecialPowers.pushPrefEnv({ set: [["dom.prototype_document_cache.enabled", true]] }); + await load(browser, filename); + ok(browser.contentDocument.loadedFromPrototype, `${filename} should load from prototype.`); + const contentWithPrototype = browser.contentDocument.documentElement.outerHTML; + is(contentWithPrototype, contentWithoutPrototype, `${filename} document contents should be the same.`); +} + +add_task(async function test_prototype_document() { + await compare("no_whitespace.xhtml"); + await compare("whitespace.xhtml"); + // TODO: Test whitespace within XUL elements, since it is handled differently + // with and without the prototype sink (bug 1544567). +}); + +add_task(async function test_prototype_document_form() { + let browser = document.getElementById("browser"); + await load(browser, "form.xhtml"); + ok(browser.contentDocument.loadedFromPrototype, `form.xhtml should load from prototype.`); + browser.contentDocument.getElementById("input").value = "test"; + await load(browser, "form.xhtml"); + is(browser.contentDocument.getElementById("input").value, "test", "input value should persist after reload"); +}); +]]></script> +</body> +</window> diff --git a/dom/prototype/tests/chrome/whitespace.xhtml b/dom/prototype/tests/chrome/whitespace.xhtml new file mode 100644 index 0000000000..0f121973c7 --- /dev/null +++ b/dom/prototype/tests/chrome/whitespace.xhtml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/strict.dtd"> +<html xmlns="http://www.w3.org/TR/xhtml1/strict"> + <body> + Hello + <p>there!</p> + </body> +</html> |