diff options
author | wolfbeast <mcwerewolf@wolfbeast.com> | 2019-12-22 19:57:30 +0100 |
---|---|---|
committer | wolfbeast <mcwerewolf@wolfbeast.com> | 2019-12-22 19:57:30 +0100 |
commit | dfa7af70ce4cd662add88f5e2a881e1054d91ef2 (patch) | |
tree | cde1088a23371942359540a12a0c70bf8a85ac50 | |
parent | 54091ecab46c93c2e1b2c689e9179a980beaabe6 (diff) | |
download | UXP-dfa7af70ce4cd662add88f5e2a881e1054d91ef2.tar UXP-dfa7af70ce4cd662add88f5e2a881e1054d91ef2.tar.gz UXP-dfa7af70ce4cd662add88f5e2a881e1054d91ef2.tar.lz UXP-dfa7af70ce4cd662add88f5e2a881e1054d91ef2.tar.xz UXP-dfa7af70ce4cd662add88f5e2a881e1054d91ef2.zip |
Issue #1118 - Part 5: Change the way document.open() works
This changes the work we do for document.open() in the following ways:
- We no longer create a new Window when doing document.open().
We use the same Window but remove all the event listeners on the
existing DOM tree and Window before removing the document's existing
children to provide a clean slate document to use for .write().
- We no longer create a session history entry (previously would be a
wyciwyg URI). We now replace the current one, effectively losing the
entry for the original document.
- We now support document.open() on windowless documents.
-rw-r--r-- | dom/base/SimpleTreeIterator.h | 71 | ||||
-rwxr-xr-x | dom/base/moz.build | 1 | ||||
-rw-r--r-- | dom/base/nsDocument.cpp | 50 | ||||
-rw-r--r-- | dom/base/nsDocument.h | 33 | ||||
-rw-r--r-- | dom/base/nsIDocument.h | 15 | ||||
-rw-r--r-- | dom/html/nsHTMLDocument.cpp | 397 | ||||
-rw-r--r-- | layout/base/nsDocumentViewer.cpp | 5 | ||||
-rw-r--r-- | uriloader/base/nsDocLoader.cpp | 102 | ||||
-rw-r--r-- | uriloader/base/nsDocLoader.h | 23 |
9 files changed, 400 insertions, 297 deletions
diff --git a/dom/base/SimpleTreeIterator.h b/dom/base/SimpleTreeIterator.h new file mode 100644 index 000000000..7ca504082 --- /dev/null +++ b/dom/base/SimpleTreeIterator.h @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This is a light-weight tree iterator for `for` loops when full iterator + * functionality isn't required. + */ + +#ifndef mozilla_dom_SimpleTreeIterator_h +#define mozilla_dom_SimpleTreeIterator_h + +#include "nsINode.h" +#include "nsTArray.h" +#include "mozilla/dom/Element.h" + +namespace mozilla { +namespace dom { + +class SimpleTreeIterator { +public: + /** + * Initialize an iterator with aRoot. After that it can be iterated with a + * range-based for loop. At the moment, that's the only supported form of use + * for this iterator. + */ + explicit SimpleTreeIterator(nsINode& aRoot) + : mCurrent(&aRoot) + { + mTree.AppendElement(&aRoot); + } + + // Basic support for range-based for loops. + // This will modify the iterator as it goes. + SimpleTreeIterator& begin() { return *this; } + + SimpleTreeIterator end() { return SimpleTreeIterator(); } + + bool operator!=(const SimpleTreeIterator& aOther) { + return mCurrent != aOther.mCurrent; + } + + void operator++() { Next(); } + + nsINode* operator*() { return mCurrent; } + +private: + // Constructor used only for end() to represent a drained iterator. + SimpleTreeIterator() + : mCurrent(nullptr) + {} + + void Next() { + MOZ_ASSERT(mCurrent, "Don't call Next() when we have no current node"); + + mCurrent = mCurrent->GetNextNode(mTree.LastElement()); + } + + // The current node. + nsINode* mCurrent; + + // The DOM tree that we're inside of right now. + AutoTArray<nsINode*, 1> mTree; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_SimpleTreeIterator_h diff --git a/dom/base/moz.build b/dom/base/moz.build index ebb76d617..75ddefded 100755 --- a/dom/base/moz.build +++ b/dom/base/moz.build @@ -210,6 +210,7 @@ EXPORTS.mozilla.dom += [ 'ScreenOrientation.h', 'ScriptSettings.h', 'ShadowRoot.h', + 'SimpleTreeIterator.h', 'StructuredCloneHolder.h', 'StructuredCloneTags.h', 'StyleSheetList.h', diff --git a/dom/base/nsDocument.cpp b/dom/base/nsDocument.cpp index e2be6b664..0f663636f 100644 --- a/dom/base/nsDocument.cpp +++ b/dom/base/nsDocument.cpp @@ -1968,22 +1968,10 @@ nsDocument::Reset(nsIChannel* aChannel, nsILoadGroup* aLoadGroup) } void -nsDocument::ResetToURI(nsIURI *aURI, nsILoadGroup *aLoadGroup, - nsIPrincipal* aPrincipal) -{ - NS_PRECONDITION(aURI, "Null URI passed to ResetToURI"); - - if (gDocumentLeakPRLog && MOZ_LOG_TEST(gDocumentLeakPRLog, LogLevel::Debug)) { - PR_LogPrint("DOCUMENT %p ResetToURI %s", this, - aURI->GetSpecOrDefault().get()); - } - - mSecurityInfo = nullptr; - - mDocumentLoadGroup = nullptr; - +nsDocument::DisconnectNodeTree() { // Delete references to sub-documents and kill the subdocument map, - // if any. It holds strong references + // if any. This is not strictly needed, but makes the node tree + // teardown a bit faster. delete mSubDocuments; mSubDocuments = nullptr; @@ -2019,6 +2007,22 @@ nsDocument::ResetToURI(nsIURI *aURI, nsILoadGroup *aLoadGroup, "After removing all children, there should be no root elem"); } mInUnlinkOrDeletion = oldVal; +} + +void +nsDocument::ResetToURI(nsIURI *aURI, nsILoadGroup *aLoadGroup, + nsIPrincipal* aPrincipal) +{ + NS_PRECONDITION(aURI, "Null URI passed to ResetToURI"); + + if (gDocumentLeakPRLog && MOZ_LOG_TEST(gDocumentLeakPRLog, LogLevel::Debug)) { + PR_LogPrint("DOCUMENT %p ResetToURI %s", this, + aURI->GetSpecOrDefault().get()); + } + + mSecurityInfo = nullptr; + + mDocumentLoadGroup = nullptr; // Reset our stylesheets ResetStylesheetsToURI(aURI); @@ -2029,6 +2033,8 @@ nsDocument::ResetToURI(nsIURI *aURI, nsILoadGroup *aLoadGroup, mListenerManager = nullptr; } + DisconnectNodeTree(); + // Release the stylesheets list. mDOMStyleSheets = nullptr; @@ -9077,7 +9083,8 @@ nsDocument::CloneDocHelper(nsDocument* clone) const } void -nsDocument::SetReadyStateInternal(ReadyState rs) +nsDocument::SetReadyStateInternal(ReadyState rs, + bool updateTimingInformation) { mReadyState = rs; if (rs == READYSTATE_UNINITIALIZED) { @@ -9086,7 +9093,12 @@ nsDocument::SetReadyStateInternal(ReadyState rs) // transition undetectable by Web content. return; } - if (mTiming) { + + if (updateTimingInformation && READYSTATE_LOADING == rs) { + mLoadingTimeStamp = mozilla::TimeStamp::Now(); + } + + if (updateTimingInformation && mTiming) { switch (rs) { case READYSTATE_LOADING: mTiming->NotifyDOMLoading(nsIDocument::GetDocumentURI()); @@ -9102,10 +9114,6 @@ nsDocument::SetReadyStateInternal(ReadyState rs) break; } } - // At the time of loading start, we don't have timing object, record time. - if (READYSTATE_LOADING == rs) { - mLoadingTimeStamp = mozilla::TimeStamp::Now(); - } RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher(this, NS_LITERAL_STRING("readystatechange"), diff --git a/dom/base/nsDocument.h b/dom/base/nsDocument.h index ac600eb43..c2b1d3cdb 100644 --- a/dom/base/nsDocument.h +++ b/dom/base/nsDocument.h @@ -704,7 +704,11 @@ public: virtual void BeginLoad() override; virtual void EndLoad() override; - virtual void SetReadyStateInternal(ReadyState rs) override; + // Set the readystate of the document. If updateTimingInformation is true, + // this will record relevant timestamps in the document's performance timing. + // Some consumers like document.open() don't want to do that. + virtual void SetReadyStateInternal(ReadyState rs, + bool updateTimingInformation = true) override; virtual void ContentStateChanged(nsIContent* aContent, mozilla::EventStates aStateMask) @@ -916,6 +920,14 @@ public: UpdateFrameRequestCallbackSchedulingState(); } + void SetLoadEventFiring(bool aFiring) override { mLoadEventFiring = aFiring; } + + bool SkipLoadEventAfterClose() override { + bool skip = mSkipLoadEventAfterClose; + mSkipLoadEventAfterClose = false; + return skip; + } + virtual nsIDocument* GetTemplateContentsOwner() override; NS_DECL_CYCLE_COLLECTION_SKIPPABLE_SCRIPT_HOLDER_CLASS_AMBIGUOUS(nsDocument, @@ -1255,6 +1267,11 @@ protected: */ Element* GetTitleElement(); + /** + * Perform tree disconnection needed by ResetToURI and document.open() + */ + void DisconnectNodeTree(); + public: // Get our title virtual void GetTitle(nsString& aTitle) override; @@ -1458,6 +1475,20 @@ public: // additional sheets and sheets from the nsStyleSheetService. bool mStyleSetFilled:1; + // The HTML spec has a "iframe load in progress" flag, but that doesn't seem + // to have the right semantics. See <https://github.com/whatwg/html/issues/4292>. + // What we have instead is a flag that is set while the window's 'load' event is + // firing if this document is the window's document. + bool mLoadEventFiring : 1; + + // The HTML spec has a "mute iframe load" flag, but that doesn't seem to have + // the right semantics. See <https://github.com/whatwg/html/issues/4292>. + // What we have instead is a flag that is set if completion of our document + // via document.close() should skip firing the load event. Note that this + // flag is only relevant for HTML documents, but lives here for reasons that + // are documented above on SkipLoadEventAfterClose(). + bool mSkipLoadEventAfterClose : 1; + uint8_t mPendingFullscreenRequests; uint8_t mXMLDeclarationBits; diff --git a/dom/base/nsIDocument.h b/dom/base/nsIDocument.h index d76a12d71..e2b34f7e3 100644 --- a/dom/base/nsIDocument.h +++ b/dom/base/nsIDocument.h @@ -1448,7 +1448,7 @@ public: virtual void EndLoad() = 0; enum ReadyState { READYSTATE_UNINITIALIZED = 0, READYSTATE_LOADING = 1, READYSTATE_INTERACTIVE = 3, READYSTATE_COMPLETE = 4}; - virtual void SetReadyStateInternal(ReadyState rs) = 0; + virtual void SetReadyStateInternal(ReadyState rs, bool updateTimingInformation = true) = 0; ReadyState GetReadyStateEnum() { return mReadyState; @@ -2187,6 +2187,19 @@ public: } /** + * Flag whether we're about to fire the window's load event for this document. + */ + virtual void SetLoadEventFiring(bool aFiring) = 0; + + /** + * Test whether we should be firing a load event for this document after a + * document.close(). + * This method should only be called at the point when the load event is about + * to be fired, since it resets `skip`. + */ + virtual bool SkipLoadEventAfterClose() = 0; + + /** * Returns the template content owner document that owns the content of * HTMLTemplateElement. */ diff --git a/dom/html/nsHTMLDocument.cpp b/dom/html/nsHTMLDocument.cpp index d64c27727..112e957cb 100644 --- a/dom/html/nsHTMLDocument.cpp +++ b/dom/html/nsHTMLDocument.cpp @@ -15,6 +15,7 @@ #include "nsPrintfCString.h" #include "nsReadableUtils.h" #include "nsUnicharUtils.h" +#include "nsIDocumentLoader.h" #include "nsIHTMLContentSink.h" #include "nsIXMLContentSink.h" #include "nsHTMLParts.h" @@ -84,6 +85,7 @@ #include "mozilla/dom/EncodingUtils.h" #include "mozilla/dom/FallbackEncoding.h" +#include "mozilla/EventListenerManager.h" #include "mozilla/LoadInfo.h" #include "nsIEditingSession.h" #include "nsIEditor.h" @@ -107,12 +109,14 @@ #include "nsIImageDocument.h" #include "mozilla/dom/HTMLBodyElement.h" #include "mozilla/dom/HTMLDocumentBinding.h" +#include "mozilla/dom/SimpleTreeIterator.h" #include "nsCharsetSource.h" #include "nsIStringBundle.h" #include "nsDOMClassInfo.h" #include "nsFocusManager.h" #include "nsIFrame.h" #include "nsIContent.h" +#include "nsIStructuredCloneContainer.h" #include "nsLayoutStylesheetCache.h" #include "mozilla/StyleSheet.h" #include "mozilla/StyleSheetInlines.h" @@ -842,6 +846,24 @@ nsHTMLDocument::EndLoad() if (turnOnEditing) { EditingStateChanged(); } + + if (!GetWindow()) { + // This is a document that's not in a window. For example, this could be an + // XMLHttpRequest responseXML document, or a document created via DOMParser + // or DOMImplementation. We don't reach this code normally for such + // documents (which is not obviously correct), but can reach it via + // document.open()/document.close(). + // + // Such documents don't fire load events, but per spec should set their + // readyState to "complete" when parsing and all loading of subresources is + // done. Parsing is done now, and documents not in a window don't load + // subresources, so just go ahead and mark ourselves as complete. + SetReadyStateInternal(nsIDocument::READYSTATE_COMPLETE, + /* updateTimingInformation = */ false); + + // Reset mSkipLoadEventAfterClose just in case. + mSkipLoadEventAfterClose = false; + } } void @@ -1410,19 +1432,21 @@ already_AddRefed<nsIDocument> nsHTMLDocument::Open(JSContext* cx, const nsAString& aType, const nsAString& aReplace, - ErrorResult& rv) + ErrorResult& aError) { - // Implements the "When called with two arguments (or fewer)" steps here: - // https://html.spec.whatwg.org/multipage/webappapis.html#opening-the-input-stream + // Implements + // <https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#document-open-steps> NS_ASSERTION(nsContentUtils::CanCallerAccess(static_cast<nsIDOMHTMLDocument*>(this)), "XOW should have caught this!"); + + // Step 1 - Throw if we're the wrong type of document. if (!IsHTMLDocument() || mDisableDocWrite || !IsMasterDocument()) { - // No calling document.open() on XHTML - rv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return nullptr; } + // Set up the content type for insertion nsAutoCString contentType; contentType.AssignLiteral("text/html"); @@ -1435,51 +1459,7 @@ nsHTMLDocument::Open(JSContext* cx, contentType.AssignLiteral("text/plain"); } - // If we already have a parser we ignore the document.open call. - if (mParser || mParserAborted) { - // The WHATWG spec says: "If the document has an active parser that isn't - // a script-created parser, and the insertion point associated with that - // parser's input stream is not undefined (that is, it does point to - // somewhere in the input stream), then the method does nothing. Abort - // these steps and return the Document object on which the method was - // invoked." - // Note that aborting a parser leaves the parser "active" with its - // insertion point "not undefined". We track this using mParserAborted, - // because aborting a parser nulls out mParser. - nsCOMPtr<nsIDocument> ret = this; - return ret.forget(); - } - - // No calling document.open() without a script global object - if (!mScriptGlobalObject) { - nsCOMPtr<nsIDocument> ret = this; - return ret.forget(); - } - - nsPIDOMWindowOuter* outer = GetWindow(); - if (!outer || (GetInnerWindow() != outer->GetCurrentInnerWindow())) { - nsCOMPtr<nsIDocument> ret = this; - return ret.forget(); - } - - // check whether we're in the middle of unload. If so, ignore this call. - nsCOMPtr<nsIDocShell> shell(mDocumentContainer); - if (!shell) { - // We won't be able to create a parser anyway. - nsCOMPtr<nsIDocument> ret = this; - return ret.forget(); - } - - bool inUnload; - shell->GetIsInUnload(&inUnload); - if (inUnload) { - nsCOMPtr<nsIDocument> ret = this; - return ret.forget(); - } - - // Note: We want to use GetEntryDocument here because this document - // should inherit the security information of the document that's opening us, - // (since if it's secure, then it's presumably trusted). + // Step 3 - Get the entryDocument for security checks nsCOMPtr<nsIDocument> callerDoc = GetEntryDocument(); if (!callerDoc) { // If we're called from C++ or in some other way without an originating @@ -1489,67 +1469,39 @@ nsHTMLDocument::Open(JSContext* cx, // change the principals of a document for security reasons we'll have to // refuse to go ahead with this call. - rv.Throw(NS_ERROR_DOM_SECURITY_ERR); + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); return nullptr; } - // Grab a reference to the calling documents security info (if any) - // and URIs as they may be lost in the call to Reset(). - nsCOMPtr<nsISupports> securityInfo = callerDoc->GetSecurityInfo(); - nsCOMPtr<nsIURI> uri = callerDoc->GetDocumentURI(); - nsCOMPtr<nsIURI> baseURI = callerDoc->GetBaseURI(); - nsCOMPtr<nsIPrincipal> callerPrincipal = callerDoc->NodePrincipal(); - nsCOMPtr<nsIChannel> callerChannel = callerDoc->GetChannel(); - - // We're called from script. Make sure the script is from the same - // origin, not just that the caller can access the document. This is - // needed to keep document principals from ever changing, which is - // needed because of the way we use our XOW code, and is a sane - // thing to do anyways. - - bool equals = false; - if (NS_FAILED(callerPrincipal->Equals(NodePrincipal(), &equals)) || - !equals) { - -#ifdef DEBUG - nsCOMPtr<nsIURI> callerDocURI = callerDoc->GetDocumentURI(); - nsCOMPtr<nsIURI> thisURI = nsIDocument::GetDocumentURI(); - printf("nsHTMLDocument::Open callerDoc %s this %s\n", - callerDocURI ? callerDocURI->GetSpecOrDefault().get() : "", - thisURI ? thisURI->GetSpecOrDefault().get() : ""); -#endif - - rv.Throw(NS_ERROR_DOM_SECURITY_ERR); + // Step 4 - Throw if we're not same-origin + if (!callerDoc->NodePrincipal()->Equals(NodePrincipal())) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); return nullptr; } - // Stop current loads targeted at the window this document is in. - if (mScriptGlobalObject) { - nsCOMPtr<nsIContentViewer> cv; - shell->GetContentViewer(getter_AddRefs(cv)); - - if (cv) { - bool okToUnload; - if (NS_SUCCEEDED(cv->PermitUnload(&okToUnload)) && !okToUnload) { - // We don't want to unload, so stop here, but don't throw an - // exception. - nsCOMPtr<nsIDocument> ret = this; - return ret.forget(); - } - - // Now double-check that our invariants still hold. - if (!mScriptGlobalObject) { - nsCOMPtr<nsIDocument> ret = this; - return ret.forget(); - } - - nsPIDOMWindowOuter* outer = GetWindow(); - if (!outer || (GetInnerWindow() != outer->GetCurrentInnerWindow())) { - nsCOMPtr<nsIDocument> ret = this; - return ret.forget(); - } + // Step 5 - If we have an active parser, abort with no-op + if (mParser || mParserAborted) { + nsCOMPtr<nsIDocument> ret = this; + return ret.forget(); + } + + // Step 6 - Check if document.open() is called during unload + nsCOMPtr<nsIDocShell> shell(mDocumentContainer); + if (shell) { + bool inUnload; + shell->GetIsInUnload(&inUnload); + if (inUnload) { + nsCOMPtr<nsIDocument> ret = this; + return ret.forget(); } + } + // Step 7 - Stop existing navigation of our browsing context (and all + // other loads it's doing) if we're the active document of our browsing + // context. If there's no existing navigation, we don't want to stop + // anything. + if (shell && IsCurrentActiveDocument() && + mScriptGlobalObject) { nsCOMPtr<nsIWebNavigation> webnav(do_QueryInterface(shell)); webnav->Stop(nsIWebNavigation::STOP_NETWORK); @@ -1560,189 +1512,123 @@ nsHTMLDocument::Open(JSContext* cx, EnsureOnloadBlocker(); } - // The open occurred after the document finished loading. - // So we reset the document and then reinitialize it. - nsCOMPtr<nsIChannel> channel; - nsCOMPtr<nsILoadGroup> group = do_QueryReferent(mDocumentLoadGroup); - rv = NS_NewChannel(getter_AddRefs(channel), - uri, - callerDoc, - nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL, - nsIContentPolicy::TYPE_OTHER, - group); - - if (rv.Failed()) { - return nullptr; - } - - if (callerChannel) { - nsLoadFlags callerLoadFlags; - rv = callerChannel->GetLoadFlags(&callerLoadFlags); - if (rv.Failed()) { - return nullptr; - } - - nsLoadFlags loadFlags; - rv = channel->GetLoadFlags(&loadFlags); - if (rv.Failed()) { - return nullptr; - } - - loadFlags |= callerLoadFlags & nsIRequest::INHIBIT_PERSISTENT_CACHING; - - rv = channel->SetLoadFlags(loadFlags); - if (rv.Failed()) { - return nullptr; + // Step 8 - Clear all event listeners out of our DOM tree + for (nsINode* node : SimpleTreeIterator(*this)) { + if (EventListenerManager* elm = node->GetExistingListenerManager()) { + elm->RemoveAllListeners(); } + } - // If the user has allowed mixed content on the rootDoc, then we should propogate it - // down to the new document channel. - bool rootHasSecureConnection = false; - bool allowMixedContent = false; - bool isDocShellRoot = false; - nsresult rvalue = shell->GetAllowMixedContentAndConnectionData(&rootHasSecureConnection, &allowMixedContent, &isDocShellRoot); - if (NS_SUCCEEDED(rvalue) && allowMixedContent && isDocShellRoot) { - shell->SetMixedContentChannel(channel); + // Step 9 - Clear event listeners from our window, if we have one. + // + // Note that we explicitly want the inner window, and only if we're its + // document. We want to do this (per spec) even when we're not the "active + // document", so we can't go through GetWindow(), because it might forward to + // the wrong inner. + if (nsPIDOMWindowInner* win = GetInnerWindow()) { + if (win->GetExtantDoc() == this) { + if (EventListenerManager* elm = + nsGlobalWindow::Cast(win)->GetExistingListenerManager()) { + elm->RemoveAllListeners(); + } } } - // Before we reset the doc notify the globalwindow of the change, - // but only if we still have a window (i.e. our window object the - // current inner window in our outer window). + // Step 10 - Remove all of our DOM children without firing any mutation events. + DisconnectNodeTree(); - // Hold onto ourselves on the offchance that we're down to one ref - nsCOMPtr<nsIDocument> kungFuDeathGrip = this; + // --- At this point our tree is clean and we can switch to the new URI --- - if (nsPIDOMWindowInner *window = GetInnerWindow()) { - // Remember the old scope in case the call to SetNewDocument changes it. - nsCOMPtr<nsIScriptGlobalObject> oldScope(do_QueryReferent(mScopeObject)); - -#ifdef DEBUG - bool willReparent = mWillReparent; - mWillReparent = true; - - nsDocument* templateContentsOwner = - static_cast<nsDocument*>(mTemplateContentsOwner.get()); - - if (templateContentsOwner) { - templateContentsOwner->mWillReparent = true; - } -#endif + // Step 11 - If we're the current document in our docshell, do the + // equivalent of pushState() with the new URL we should have. + if (shell && IsCurrentActiveDocument()) { + nsCOMPtr<nsIURI> newURI = callerDoc->GetDocumentURI(); - // Per spec, we pass false here so that a new Window is created. - rv = window->SetNewDocument(this, nullptr, - /* aForceReuseInnerWindow */ false); - if (rv.Failed()) { + // UpdateURLAndHistory might do various member-setting, so make sure we're + // holding strong refs to all the refcounted args on the stack. We can + // assume that our caller is holding on to "this" already. + nsCOMPtr<nsIURI> currentURI = nsIDocument::GetDocumentURI(); + bool equalURIs; + nsresult rv = currentURI->Equals(newURI, &equalURIs); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); return nullptr; } - -#ifdef DEBUG - if (templateContentsOwner) { - templateContentsOwner->mWillReparent = willReparent; + nsCOMPtr<nsIStructuredCloneContainer> stateContainer(mStateObjectContainer); + rv = shell->UpdateURLAndHistory(this, newURI, stateContainer, EmptyString(), + /* aReplace = */ true, currentURI, + equalURIs); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return nullptr; } - mWillReparent = willReparent; -#endif + // And use the security info of the caller document as well, since + // it's the thing providing our data. + mSecurityInfo = callerDoc->GetSecurityInfo(); - // Now make sure we're not flagged as the initial document anymore, now - // that we've had stuff done to us. From now on, if anyone tries to - // document.open() us, they get a new inner window. + // This is not mentioned in the spec, but that's probably a spec bug. + // See <https://github.com/whatwg/html/issues/4299>. + // Since our URL may be changing away from about:blank here, we really want + // to unset this flag on any document.open(), since only about:blank can be + // an initial document. SetIsInitialDocument(false); - nsCOMPtr<nsIScriptGlobalObject> newScope(do_QueryReferent(mScopeObject)); - JS::Rooted<JSObject*> wrapper(cx, GetWrapper()); - if (oldScope && newScope != oldScope && wrapper) { - JSAutoCompartment ac(cx, wrapper); - rv = mozilla::dom::ReparentWrapper(cx, wrapper); - if (rv.Failed()) { - return nullptr; - } + // And let our docloader know that it will need to track our load event. + nsDocShell::Cast(shell)->SetDocumentOpenedButNotLoaded(); + } - // Also reparent the template contents owner document - // because its global is set to the same as this document. - if (mTemplateContentsOwner) { - JS::Rooted<JSObject*> contentsOwnerWrapper(cx, - mTemplateContentsOwner->GetWrapper()); - if (contentsOwnerWrapper) { - rv = mozilla::dom::ReparentWrapper(cx, contentsOwnerWrapper); - if (rv.Failed()) { - return nullptr; - } - } - } - } - } + // Step 12 + mSkipLoadEventAfterClose = mLoadEventFiring; - mDidDocumentOpen = true; + // Preliminary to steps 13-16. Set our ready state to uninitialized before + // we do anything else, so we can then proceed to later ready state levels. + SetReadyStateInternal(READYSTATE_UNINITIALIZED, + /* updateTimingInformation = */ false); - // Call Reset(), this will now do the full reset - Reset(channel, group); - if (baseURI) { - mDocumentBaseURI = baseURI; - } + mDidDocumentOpen = true; - // Store the security info of the caller now that we're done - // resetting the document. - mSecurityInfo = securityInfo; + // Step 13 - Set our compatibility mode to standards. + SetCompatibilityMode(eCompatibility_FullStandards); + // Step 14 - Create a new parser associated with document. + // This also does step 16 implicitly. mParserAborted = false; mParser = nsHtml5Module::NewHtml5Parser(); - nsHtml5Module::Initialize(mParser, this, uri, shell, channel); + nsHtml5Module::Initialize(mParser, this, nsIDocument::GetDocumentURI(), shell, nullptr); if (mReferrerPolicySet) { // CSP may have set the referrer policy, so a speculative parser should // start with the new referrer policy. nsHtml5TreeOpExecutor* executor = nullptr; - executor = static_cast<nsHtml5TreeOpExecutor*> (mParser->GetContentSink()); + executor = static_cast<nsHtml5TreeOpExecutor*>(mParser->GetContentSink()); if (executor && mReferrerPolicySet) { - executor->SetSpeculationReferrerPolicy(static_cast<ReferrerPolicy>(mReferrerPolicy)); + executor->SetSpeculationReferrerPolicy( + static_cast<ReferrerPolicy>(mReferrerPolicy)); } } - // This will be propagated to the parser when someone actually calls write() - SetContentTypeInternal(contentType); - - // Prepare the docshell and the document viewer for the impending - // out of band document.write() - shell->PrepareForNewContentModel(); - - // Now check whether we were opened with a "replace" argument. If - // so, we need to tell the docshell to not create a new history - // entry for this load. Otherwise, make sure that we're doing a normal load, - // not whatever type of load was previously done on this docshell. - shell->SetLoadType(aReplace.LowerCaseEqualsLiteral("replace") ? - LOAD_NORMAL_REPLACE : LOAD_NORMAL); + if (shell) { + // Prepare the docshell and the document viewer for the impending + // out-of-band document.write() + shell->PrepareForNewContentModel(); - nsCOMPtr<nsIContentViewer> cv; - shell->GetContentViewer(getter_AddRefs(cv)); - if (cv) { - cv->LoadStart(this); + nsCOMPtr<nsIContentViewer> cv; + shell->GetContentViewer(getter_AddRefs(cv)); + if (cv) { + cv->LoadStart(this); + } } - // Add a wyciwyg channel request into the document load group - NS_ASSERTION(!mWyciwygChannel, "nsHTMLDocument::Open(): wyciwyg " - "channel already exists!"); - - // In case the editor is listening and will see the new channel - // being added, make sure mWriteLevel is non-zero so that the editor - // knows that document.open/write/close() is being called on this - // document. - ++mWriteLevel; - - CreateAndAddWyciwygChannel(); + // Step 15. + SetReadyStateInternal(nsIDocument::READYSTATE_LOADING, + /* updateTimingInformation = */ false); - --mWriteLevel; - - SetReadyStateInternal(nsIDocument::READYSTATE_LOADING); + // Step 16 happened with step 14 above. - // After changing everything around, make sure that the principal on the - // document's compartment exactly matches NodePrincipal(). - DebugOnly<JSObject*> wrapper = GetWrapperPreserveColor(); - MOZ_ASSERT_IF(wrapper, - JS_GetCompartmentPrincipals(js::GetObjectCompartment(wrapper)) == - nsJSPrincipals::get(NodePrincipal())); - - return kungFuDeathGrip.forget(); -} + // Step 17. + nsCOMPtr<nsIDocument> ret = this; + return ret.forget(); +} NS_IMETHODIMP nsHTMLDocument::Clear() @@ -1806,15 +1692,6 @@ nsHTMLDocument::Close(ErrorResult& rv) if (GetShell()) { FlushPendingNotifications(Flush_Layout); } - - // Removing the wyciwygChannel here is wrong when document.close() is - // called from within the document itself. However, legacy requires the - // channel to be removed here. Otherwise, the load event never fires. - NS_ASSERTION(mWyciwygChannel, "nsHTMLDocument::Close(): Trying to remove " - "nonexistent wyciwyg channel!"); - RemoveWyciwygChannel(); - NS_ASSERTION(!mWyciwygChannel, "nsHTMLDocument::Close(): " - "nsIWyciwygChannel could not be removed!"); } void diff --git a/layout/base/nsDocumentViewer.cpp b/layout/base/nsDocumentViewer.cpp index 5478c61b0..8baf1a464 100644 --- a/layout/base/nsDocumentViewer.cpp +++ b/layout/base/nsDocumentViewer.cpp @@ -998,6 +998,9 @@ nsDocumentViewer::LoadComplete(nsresult aStatus) // will depend on whether it's cached! if(window && (NS_SUCCEEDED(aStatus) || aStatus == NS_ERROR_PARSED_DATA_CACHED)) { + // If this code changes, the code in nsDocLoader::DocLoaderIsEmpty + // that fires load events for document.open() cases might need to + // be updated too. nsEventStatus status = nsEventStatus_eIgnore; WidgetEvent event(true, eLoad); event.mFlags.mBubbles = false; @@ -1063,7 +1066,9 @@ nsDocumentViewer::LoadComplete(nsresult aStatus) MakeUnique<DocLoadingTimelineMarker>("document::Load")); } + d->SetLoadEventFiring(true); EventDispatcher::Dispatch(window, mPresContext, &event, nullptr, &status); + d->SetLoadEventFiring(false); if (timing) { timing->NotifyLoadEventEnd(); } diff --git a/uriloader/base/nsDocLoader.cpp b/uriloader/base/nsDocLoader.cpp index 69885b93f..a4beb4827 100644 --- a/uriloader/base/nsDocLoader.cpp +++ b/uriloader/base/nsDocLoader.cpp @@ -4,6 +4,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nspr.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/EventDispatcher.h" #include "mozilla/Logging.h" #include "nsDocLoader.h" @@ -23,6 +25,7 @@ #include "nsQueryObject.h" #include "nsIDOMWindow.h" +#include "nsPIDOMWindow.h" #include "nsIStringBundle.h" #include "nsIScriptSecurityManager.h" @@ -36,7 +39,10 @@ #include "nsIAsyncVerifyRedirectCallback.h" using mozilla::DebugOnly; +using mozilla::eLoad; +using mozilla::EventDispatcher; using mozilla::LogLevel; +using mozilla::WidgetEvent; static NS_DEFINE_CID(kThisImplCID, NS_THIS_DOCLOADER_IMPL_CID); @@ -114,7 +120,8 @@ nsDocLoader::nsDocLoader() mIsLoadingDocument(false), mIsRestoringDocument(false), mDontFlushLayout(false), - mIsFlushingLayout(false) + mIsFlushingLayout(false), + mDocumentOpenedButNotLoaded(false) { ClearInternalProgress(); @@ -289,7 +296,7 @@ nsDocLoader::IsBusy() } /* Is this document loader busy? */ - if (!mIsLoadingDocument) { + if (!IsBlockingLoadEvent()) { return false; } @@ -414,6 +421,7 @@ nsDocLoader::OnStartRequest(nsIRequest *request, nsISupports *aCtxt) if (!mIsLoadingDocument && (loadFlags & nsIChannel::LOAD_DOCUMENT_URI)) { bJustStartedLoading = true; mIsLoadingDocument = true; + mDocumentOpenedButNotLoaded = false; ClearInternalProgress(); // only clear our progress if we are starting a new load.... } @@ -480,10 +488,11 @@ nsDocLoader::OnStopRequest(nsIRequest *aRequest, mLoadGroup->GetActiveCount(&count); MOZ_LOG(gDocLoaderLog, LogLevel::Debug, - ("DocLoader:%p: OnStopRequest[%p](%s) status=%x mIsLoadingDocument=%s, %u active URLs", + ("DocLoader:%p: OnStopRequest[%p](%s) status=%x" + "mIsLoadingDocument=%s, mDocumentOpenedButNotLoaded=%s, %u active URLs", this, aRequest, name.get(), aStatus, (mIsLoadingDocument ? "true" : "false"), - count)); + (mDocumentOpenedButNotLoaded ? "true" : "false"), count)); } bool bFireTransferring = false; @@ -598,10 +607,9 @@ nsDocLoader::OnStopRequest(nsIRequest *aRequest, RemoveRequestInfo(aRequest); // - // Only fire the DocLoaderIsEmpty(...) if the document loader has initiated a - // load. This will handle removing the request from our hashtable as needed. + // Only fire the DocLoaderIsEmpty(...) if we may need to fire onload. // - if (mIsLoadingDocument) { + if (IsBlockingLoadEvent()) { nsCOMPtr<nsIDocShell> ds = do_QueryInterface(static_cast<nsIRequestObserver*>(this)); bool doNotFlushLayout = false; if (ds) { @@ -647,7 +655,7 @@ NS_IMETHODIMP nsDocLoader::GetDocumentChannel(nsIChannel ** aChannel) void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout) { - if (mIsLoadingDocument) { + if (IsBlockingLoadEvent()) { /* In the unimagineably rude circumstance that onload event handlers triggered by this function actually kill the window ... ok, it's not unimagineable; it's happened ... this deathgrip keeps this object @@ -660,7 +668,9 @@ void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout) } NS_ASSERTION(!mIsFlushingLayout, "Someone screwed up"); - NS_ASSERTION(mDocumentRequest, "No Document Request!"); + // We may not have a document request if we are in a document.open() situation. + NS_ASSERTION(mDocumentRequest || mDocumentOpenedButNotLoaded, + "No Document Request!"); // The load group for this DocumentLoader is idle. Flush if we need to. if (aFlushLayout && !mDontFlushLayout) { @@ -687,9 +697,14 @@ void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout) // And now check whether we're really busy; that might have changed with // the layout flush. - // Note, mDocumentRequest can be null if the flushing above re-entered this - // method. - if (!IsBusy() && mDocumentRequest) { + // + // Note, mDocumentRequest can be null while mDocumentOpenedButNotLoaded is + // false if the flushing above re-entered this method. Exit in that case. + if (IsBusy() || (!mDocumentRequest && !mDocumentOpenedButNotLoaded)) { + return; + } + + if (mDocumentRequest) { // Clear out our request info hash, now that our load really is done and // we don't need it anymore to CalculateMaxProgress(). ClearInternalProgress(); @@ -715,7 +730,7 @@ void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout) // mLoadGroup->SetDefaultLoadRequest(nullptr); - // Take a ref to our parent now so that we can call DocLoaderIsEmpty() on + // Take a ref to our parent now so that we can call ChildDoneWithOnload() on // it even if our onload handler removes us from the docloader tree. RefPtr<nsDocLoader> parent = mParent; @@ -733,6 +748,67 @@ void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout) parent->ChildDoneWithOnload(this); } } + } else { + MOZ_ASSERT(mDocumentOpenedButNotLoaded); + mDocumentOpenedButNotLoaded = false; + + // Make sure we do the ChildEnteringOnload/ChildDoneWithOnload even if we + // plan to skip firing our own load event, because otherwise we might + // never end up firing our parent's load event. + RefPtr<nsDocLoader> parent = mParent; + if (!parent || parent->ChildEnteringOnload(this)) { + nsresult loadGroupStatus = NS_OK; + mLoadGroup->GetStatus(&loadGroupStatus); + // Make sure we're not canceling the loadgroup. If we are, then we should + // not fire a load event just like in the normal navigation case. + if (NS_SUCCEEDED(loadGroupStatus) || + loadGroupStatus == NS_ERROR_PARSED_DATA_CACHED) { + // Can "doc" or "window" ever come back null here? Our state machine + // is complicated enough, so I wouldn't bet against it... + nsCOMPtr<nsIDocument> doc = do_GetInterface(GetAsSupports(this)); + if (doc) { + doc->SetReadyStateInternal(nsIDocument::READYSTATE_COMPLETE, + /* updateTimingInformation = */ false); + + nsCOMPtr<nsPIDOMWindowOuter> window = doc->GetWindow(); + if (window && !doc->SkipLoadEventAfterClose()) { + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: Firing load event for document.open\n", + this)); + + // This is a very cut-down version of + // nsDocumentViewer::LoadComplete that doesn't do various things + // that are not relevant here because this wasn't an actual + // navigation. + WidgetEvent event(true, eLoad); + event.mFlags.mBubbles = false; + event.mFlags.mCancelable = false; + // Dispatching to |window|, but using |document| as the target, + // per spec. + event.mTarget = doc; + nsEventStatus unused = nsEventStatus_eIgnore; + doc->SetLoadEventFiring(true); + EventDispatcher::Dispatch(window, nullptr, &event, nullptr, + &unused); + doc->SetLoadEventFiring(false); + + // Now unsuppress painting on the presshell, if we + // haven't done that yet. + nsCOMPtr<nsIPresShell> shell = doc->GetShell(); + if (shell && !shell->IsDestroying()) { + shell->UnsuppressPainting(); + + if (!shell->IsDestroying()) { + shell->LoadComplete(); + } + } + } + } + } + if (parent) { + parent->ChildDoneWithOnload(this); + } + } } } } diff --git a/uriloader/base/nsDocLoader.h b/uriloader/base/nsDocLoader.h index 481b1397b..b469b8e07 100644 --- a/uriloader/base/nsDocLoader.h +++ b/uriloader/base/nsDocLoader.h @@ -109,6 +109,8 @@ public: unsigned long mNotifyMask; }; + void SetDocumentOpenedButNotLoaded() { mDocumentOpenedButNotLoaded = true; } + protected: virtual ~nsDocLoader(); @@ -302,6 +304,15 @@ protected: bool mIsFlushingLayout; private: + /** + * This flag indicates that the loader is waiting for completion of + * a document.open-triggered "document load". This is set when + * document.open() happens and sets up a new parser and cleared out + * when we go to fire our load event or end up with a new document + * channel. + */ + bool mDocumentOpenedButNotLoaded; + static const PLDHashTableOps sRequestInfoHashOps; // A list of kids that are in the middle of their onload calls and will let @@ -324,10 +335,20 @@ private: nsRequestInfo *GetRequestInfo(nsIRequest* aRequest); void ClearRequestInfoHash(); int64_t CalculateMaxProgress(); -/// void DumpChannelInfo(void); + /// void DumpChannelInfo(void); // used to clear our internal progress state between loads... void ClearInternalProgress(); + + /** + * Used to test whether we might need to fire a load event. This + * can happen when we have a document load going on, or when we've + * had document.open() called and haven't fired the corresponding + * load event yet. + */ + bool IsBlockingLoadEvent() const { + return mIsLoadingDocument || mDocumentOpenedButNotLoaded; + } }; NS_DEFINE_STATIC_IID_ACCESSOR(nsDocLoader, NS_THIS_DOCLOADER_IMPL_CID) |